/*
 * 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
 *
 *   https://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.
 *
 *   SPDX-License-Identifier: Apache-2.0
 */

package org.apache.jena.tdb2.store;

import static org.apache.jena.tdb2.store.NodeIdType.*;

import org.apache.jena.atlas.lib.BitsLong;
import org.apache.jena.atlas.logging.FmtLog;
import org.apache.jena.dboe.sys.SysDB;
import org.apache.jena.graph.Node;
import org.apache.jena.tdb2.sys.SystemTDB;

final
public class NodeId implements Comparable<NodeId>
{
    public static final int SIZE = SysDB.SizeOfLong;

    public static final NodeId NodeIdInvalid        = makeSpecial(0xA1);
    public static final NodeId NodeDoesNotExist     = makeSpecial(0xA3);
    public static final NodeId NodeIdAny            = makeSpecial(0xA4);
    public static final NodeId NodeIdDefined        = makeSpecial(0xA5);
    public static final NodeId NodeIdUndefined      = makeSpecial(0xA6);

    private static NodeId makeSpecial(long v) {
        // Careful of an initializer loop (create(type, v1, v2) looks at specials).
        return NodeIdFactory.createValue(SPECIAL, v);
    }

    /*package*/ static final boolean enableInlineLiterals = SystemTDB.enableInlineLiterals;

    // Internal consistency checks.
    private static final boolean CHECKING = true;

    // Code search:
    // [64 bits]

    // Encoding:
    // Long:
    //   8 bits type, except PTR which is 0 high bit.
    //   24 bits int
    //   64 bit value
    // The high byte of value1==type as integer value
    // Short:
    //  64 bit value

    // Long:
    //   type PTR high bit zero, then 31+64 bits of value.
    //  Inlines:
    //   type   is 8 bits
    //   value1 is 24 bits
    //   value2 is 64 bits
    //   type is a type and is OR'ed into value1 to store.

    // Short: 64 bit:
    //   See NodeIdInline
    //   High bit 0 => PTR => 63 bits
    //   Bit 63 = 1, bit 62 = 1 => double in 62 bits.
    //   Bit 63 = 1, bit 62 = 0 => 6 bits of type, 56 bits of value

    final NodeIdType type;
    final int  value1;
    final long value2;

    private NodeId(NodeIdType type, int v1, long v2) {
        if ( CHECKING ) check(type, v1, v2);
        this.type = type;
        value1 = v1;    // Zero for 64 bit.
        value2 = v2;
    }

    private final void check(NodeIdType type, int v1, long v2) {
        if ( type == PTR )
            return;
        if ( type == SPECIAL )
            return;
        // 64 bit NodeId.
        int x = ( type == XSD_DOUBLE )
            ? (int)BitsLong.unpack(v2, 62, 64)   // High two bits
            : (int)BitsLong.unpack(v2, 56, 64);  // else
        if ( x != 0 )
            FmtLog.warn(getClass(), "Type set in long: type=%s value=%016X", type, v2);
    }

    public boolean isPtr() { return type == PTR; }

//    public long getPtrLocation() { return value2; }
//    public long getPtrLo() { return value2; }
//    public int  getPtrHi() { return value1 & 0x00FFFFFF; }

    /** The pointer part of a NodeId reference. */
    public long getPtrLocation()    { return value2; }

    // 96 bit
//    public long getPtrLo()          { return value2; }
//    public int  getPtrHi()          { return value1; }

    // 64 bit.
    public int getTypeValue() {
        return type.type();
    }

    /** The value (encoding) part of an inline literal (56 bits) */
    public long getValue56() {
        return value2;
    }

    public boolean isInline()       {
        return isInline(this);
    }

    public static boolean isInline(NodeId nodeId) {
        return NodeIdType.isInline(nodeId.type);
    }

    public boolean isValue() {
        return type != PTR && NodeIdType.isStorable(type);
    }

    // Migration
    public static NodeId inline(Node node) { return NodeIdInline.inline(node); }
    public static boolean hasInlineDatatype(Node node) { return NodeIdInline.hasInlineDatatype(node); }
    public static Node extract(NodeId nodeId) { return NodeIdInline.extract(nodeId); }

    // XXX Later.
//    //Static forms only?
//    public boolean isAny()          { return isAny(this); }
//    public boolean isDoesNotExist() { return isDoesNotExist(this); }
//    public boolean isDefined()      { return isDefined(this); }
//    public boolean isUndefined()    { return isDefined(this); }
//
    public static final boolean isAny(NodeId nodeId)           { return nodeId == NodeIdAny || nodeId == null; }
    public static final boolean isDoesNotExist(NodeId nodeId)  { return nodeId == NodeDoesNotExist; }
//    public static boolean isDefined(NodeId nodeId)             { return nodeId == NodeIdDefined; }
//    public static boolean isUndefined(NodeId nodeId)           { return nodeId == NodeIdUndefined; }

    /**
     * Create from a long-encoded value.
     * Caution: an illegal value for the long argument will cause serious problems.
     */
    public static NodeId createRaw(NodeIdType type, long value) {
        return new NodeId(type, 0, value);
    }

    /** Create from a (int,long)-encoded value */
    /*package*/ static NodeId createRaw(NodeIdType type, int value1, long value2) {
        return new NodeId(type, value1, value2);
    }

    public NodeIdType type() { return type; }

    /*package*/ int  getValue1() { return value1; }
    /*package*/ long getValue2() { return value2; }

    @Override
    public int hashCode() {
        // Ensure all parts have an effect on the 32 bit hash value.
        return value1 ^ ((int)value2) ^ ((int)(value2 >> 32));
    }

    @Override
    public boolean equals(Object other) {
        if ( this == other ) return true;
        if ( other == null ) return false;
        if ( !(other instanceof NodeId nOther) ) return false;
        return equals(nOther, value1, value2);
    }

    public boolean equals(NodeId nodeIdOther) {
        if ( nodeIdOther == null ) return false;
        if ( this == nodeIdOther ) return true;
        return equals(nodeIdOther, value1, value2);
    }

    /*package*/ static boolean equals(NodeId nodeId, int v1, long v2) {
        return v2 == nodeId.value2 && v1 == nodeId.value1;
    }
    @Override
    public String toString() {
        if ( this == NodeDoesNotExist ) return "[DoesNotExist]";
        if ( this == NodeIdAny ) return "[Any]";
        if ( this == NodeIdInvalid ) return "[Invalid]";
        if ( this == NodeIdDefined ) return "[Defined]";
        if ( this == NodeIdUndefined ) return "[Undefined]";

        if ( this.isInline() ) {
            String displayName = this.type().toString();
            return String.format("[%s 0x%014X]", displayName, value2);
        }
        // [64 bits]
        //return String.format("[%08X-%016X]", value1, value2);
        return String.format("[0x%16X]", value2);
    }

    // ---- Encoding special - inlines.

   /** Compare - provides an ordering of {@code NodeIds}. */
    @Override
    public int compareTo(NodeId other) {
        return compare(this, other);
    }

    /** Compare - provides an ordering of {@code NodeIds}. */
    public static int compare(NodeId n1, NodeId n2) {
        int x = Integer.compare(n1.value1, n2.value1);
        if ( x != 0 )
            return x;
        return Long.compare(n1.value2, n2.value2);
    }

    public final boolean isConcrete() { return isConcrete(this); }

    public static final boolean isConcrete(NodeId nodeId) {
        return ! NodeIdType.isSpecial(nodeId.type);
    }
}
