/*
 *  Licensed to the Apache Software Foundation (ASF) under one
 *  or more contributor license agreements.  See the NOTICE file
 *  distributed with this work for additional information
 *  regarding copyright ownership.  The ASF licenses this file
 *  to you under the Apache License, Version 2.0 (the
 *  "License"); you may not use this file except in compliance
 *  with the License.  You may obtain a copy of the License at
 *
 *        http://www.apache.org/licenses/LICENSE-2.0
 *
 *  Unless required by applicable law or agreed to in writing,
 *  software distributed under the License is distributed on an
 *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 *  KIND, either express or implied.  See the License for the
 *  specific language governing permissions and limitations
 *  under the License.
 */
package org.apache.isis.applib.services.bookmark;

import java.util.Objects;
import java.util.Optional;
import java.util.StringTokenizer;

import javax.inject.Named;

import org.springframework.lang.Nullable;

import org.apache.isis.applib.IsisModuleApplib;
import org.apache.isis.applib.id.LogicalType;
import org.apache.isis.commons.internal.base._Strings;
import org.apache.isis.commons.internal.codec._UrlDecoderUtil;
import org.apache.isis.commons.internal.exceptions._Exceptions;
import org.apache.isis.schema.common.v2.OidDto;

import lombok.AccessLevel;
import lombok.Getter;
import lombok.NonNull;
import lombok.RequiredArgsConstructor;
import lombok.val;

/**
 * String representation of any persistable or re-creatable object managed by the framework.
 *
 * @since 1.x revised for 2.0 {@index}
 */
@Named(IsisModuleApplib.NAMESPACE + ".Bookmark")
@org.apache.isis.applib.annotation.Value
@RequiredArgsConstructor(access = AccessLevel.PRIVATE)
public final class Bookmark implements Oid {

    private static final long serialVersionUID = 3L;

    @Getter(onMethod_ = {@Override}) private final String logicalTypeName;
    @Getter(onMethod_ = {@Override}) private final String identifier;
    @Getter private final @Nullable String hintId;
    private final int hashCode;

    // -- FACTORIES

    public static Bookmark forLogicalTypeNameAndIdentifier(
            final @NonNull String logicalTypeName,
            final @NonNull String identifier) {
        return new Bookmark(
                logicalTypeName,
                identifier,
                /*hintId*/null);
    }

    public static Bookmark forLogicalTypeAndIdentifier(
            final @NonNull LogicalType logicalType,
            final @NonNull String identifier) {
        return Bookmark.forLogicalTypeNameAndIdentifier(
                logicalType.getLogicalTypeName(),
                identifier);
    }

    public static Bookmark forOidDto(final @NonNull OidDto oidDto) {
        return Bookmark.forLogicalTypeNameAndIdentifier(
                oidDto.getType(),
                oidDto.getId());
    }

    public Bookmark withHintId(final @Nullable String hintId) {
        return new Bookmark(this.getLogicalTypeName(), this.getIdentifier(), hintId);
    }

    // -- CONSTRUCTOR

    private Bookmark(
            final String logicalTypeName,
            final String identifier,
            final String hintId) {

        this.logicalTypeName = logicalTypeName;
        this.identifier = identifier;
        this.hintId = hintId;
        this.hashCode = Objects.hash(logicalTypeName, identifier);
    }

    // -- PARSE

    /**
     * Round-trip with {@link #stringify()} representation.
     */
    public static Optional<Bookmark> parse(final @Nullable String str) {

        if(str==null) {
            return Optional.empty();
        }
        val tokenizer = new StringTokenizer(str, SEPARATOR);
        int tokenCount = tokenizer.countTokens();
        if(tokenCount==2) {
            return Optional.of(Bookmark.forLogicalTypeNameAndIdentifier(
                    tokenizer.nextToken(),
                    tokenizer.nextToken()));
        }
        if(tokenCount>2) {
            return Optional.of(Bookmark.forLogicalTypeNameAndIdentifier(
                    tokenizer.nextToken(),
                    tokenizer.nextToken("").substring(1)));
        }
        return Optional.empty();
    }


    public static Bookmark parseElseFail(final @Nullable String input) {
        return parse(input)
                .orElseThrow(()->_Exceptions.illegalArgument("cannot parse Bookmark %s", input));
    }

    public static Optional<Bookmark> parseUrlEncoded(@Nullable final String urlEncodedStr) {
        return _Strings.isEmpty(urlEncodedStr)
                ? Optional.empty()
                : parse(_UrlDecoderUtil.urlDecode(urlEncodedStr));
    }

    // -- TO DTO

    public OidDto toOidDto() {
        val oidDto = new OidDto();
        oidDto.setType(getLogicalTypeName());
        oidDto.setId(getIdentifier());
        return oidDto;
    }

    // -- STRINGIFY

    @Override
    public String stringify() {
        return stringify(identifier);
    }

    // -- OBJECT CONTRACT // not considering any hintId

    @Override
    public boolean equals(final Object other) {
        if (other == null) {
            return false;
        }
        if (other == this) {
            return true;
        }
        if (getClass() != other.getClass()) {
            return false;
        }
        return equals((Bookmark) other);
    }

    public boolean equals(final Bookmark other) {
        return Objects.equals(logicalTypeName, other.getLogicalTypeName())
                && Objects.equals(identifier, other.getIdentifier());
    }

    @Override
    public int hashCode() {
        return hashCode;
    }

    @Override
    public String toString() {
        return stringify();
    }

    /**
     * Analogous to {@link #stringify()}, but replaces the {@code identifier} string with
     * the {@code hintId} if present and not empty.
     */
    public String stringifyHonoringHintIfAny() {
        return _Strings.isNotEmpty(hintId)
                ? stringify(hintId)
                : stringify(identifier);
    }

    // -- HELPER

    private String stringify(final String id) {
        return logicalTypeName + SEPARATOR + id;
    }



}

@@ -2123,16 +2123,428 @@
 TORIES%0A%0A
+    public static Bookmark empty(%0A            final @NonNull LogicalType logicalType) %7B%0A        return emptyForLogicalTypeName(logicalType.getLogicalTypeName());%0A    %7D%0A%0A    public static Bookmark emptyForLogicalTypeName(%0A            final @NonNull String logicalTypeName) %7B%0A        return new Bookmark(%0A                logicalTypeName,%0A                /*identifier*/null,%0A                /*hintId*/null);%0A    %7D%0A%0A
     publ
@@ -4175,24 +4175,172 @@
 ntTokens();%0A
+        if(tokenCount==1) %7B%0A            return Optional.of(Bookmark.emptyForLogicalTypeName(%0A                    tokenizer.nextToken()));%0A        %7D%0A
         if(t
@@ -6606,32 +6606,159 @@
 tifier);%0A    %7D%0A%0A
+    /**%0A     * Whether represents %7B@code null%7D.%0A     */%0A    public boolean isEmpty() %7B%0A        return identifier==null;%0A    %7D%0A%0A
     // -- HELPER
@@ -6821,16 +6821,45 @@
   return
+ !isEmpty()%0A                ?
  logical
@@ -6883,16 +6883,50 @@
 TOR + id
+%0A                : logicalTypeName
 ;%0A    %7D%0A
@@ -6926,12 +6926,10 @@
 %0A    %7D%0A%0A
-%0A%0A
 %7D%0A

allow Bookmarks to represent null (empty) objects