/*
 * 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 accord.primitives;

import java.util.regex.Matcher;
import java.util.regex.Pattern;

import javax.annotation.Nullable;

import accord.local.Node.Id;
import accord.primitives.Routable.Domain;
import accord.primitives.Txn.Kind;
import accord.primitives.Txn.Kind.Kinds;
import accord.topology.SelectShards;
import accord.utils.Invariants;
import accord.utils.TinyEnumSet;

import static accord.primitives.Timestamp.Flag.UNSTABLE;
import static accord.primitives.Txn.Kind.EphemeralRead;
import static accord.primitives.Txn.Kind.Read;
import static accord.primitives.Txn.Kind.Write;
import static accord.primitives.TxnId.Cardinality.Any;
import static accord.primitives.TxnId.MediumPath.NoMediumPath;
import static accord.utils.Invariants.illegalArgument;

public class TxnId extends Timestamp
{
    public enum FastPath
    {
        Unoptimised,
        PrivilegedCoordinatorWithoutDeps,
        PrivilegedCoordinatorWithDeps;

        private static final int SHIFT = 5;
        private static final FastPath[] VALUES = values();

        public final int bits;

        FastPath()
        {
            this.bits = ordinal() << SHIFT;
        }

        final boolean is(int flagsUnmasked)
        {
            return is(flagsUnmasked, this);
        }

        private static boolean is(int flagsUnmasked, FastPath test)
        {
            return ((flagsUnmasked >>> SHIFT) & 3) == test.ordinal();
        }

        private static FastPath forFlags(int flagsUnmasked)
        {
            return forOrdinal(ordinalFromFlags(flagsUnmasked));
        }

        private static int ordinalFromFlags(int flagsUnmasked)
        {
            return (flagsUnmasked >>> SHIFT) & 3;
        }

        public static FastPath forOrdinal(int ordinal)
        {
            return VALUES[ordinal];
        }

        static boolean hasPrivilegedCoordinator(int flagsUnmasked)
        {
            return 0 != ordinalFromFlags(flagsUnmasked);
        }

        public FastPath toPermitted(FastPaths permitted)
        {
            if (permitted.contains(this))
                return this;
            if (this == Unoptimised)
                return this;
            FastPath alt = this == PrivilegedCoordinatorWithDeps ? PrivilegedCoordinatorWithoutDeps : PrivilegedCoordinatorWithDeps;
            if (permitted.contains(alt))
                return alt;
            return Unoptimised;
        }
    }

    public static class FastPaths extends TinyEnumSet<FastPath>
    {
        public FastPaths(FastPath... values) { super(values); }

        public boolean hasPrivilegedCoordinator() { return bitset > 1; }
    }

    public enum MediumPath
    {
        NoMediumPath,
        TrackStable;

        private static final int SHIFT = 7;
        public final int bit() { return ordinal() << SHIFT; }

        final boolean is(int flagsUnmasked)
        {
            return is(flagsUnmasked, this);
        }

        private static boolean is(int flagsUnmasked, MediumPath test)
        {
            return ((flagsUnmasked >>> SHIFT) & 1) == test.ordinal();
        }

        public static MediumPath forFlags(int flagsUnmasked)
        {
            return (flagsUnmasked & (1 << SHIFT)) == 0 ? NoMediumPath : TrackStable;
        }

        public static MediumPath forOrdinal(int ordinal)
        {
            return ordinal == 0 ? NoMediumPath : TrackStable;
        }
    }

    public enum Cardinality
    {
        SingleKey,
        Any;

        private static final int SHIFT = 4;
        public final int bit()
        {
            return ordinal() << SHIFT;
        }

        final boolean is(int flagsUnmasked)
        {
            return is(flagsUnmasked, this);
        }

        public static Cardinality cardinality(Routables<?> routables)
        {
            return routables.size() == 1 && routables.domain() == Domain.Key ? SingleKey : Any;
        }

        public static Cardinality cardinality(Domain domain, Routables<?> routables)
        {
            return domain == Domain.Key && routables.size() == 1 ? SingleKey : Any;
        }

        private static boolean is(int flagsUnmasked, Cardinality test)
        {
            return ((flagsUnmasked >>> SHIFT) & 1) == test.ordinal();
        }

        private static Cardinality forFlags(int flagsUnmasked)
        {
            return (flagsUnmasked & (1 << SHIFT)) == 0 ? SingleKey : Any;
        }

        public static Cardinality forOrdinal(int ordinal)
        {
            return ordinal == 0 ? SingleKey : Any;
        }
    }

    private static final int DOMAIN_AND_KIND_AND_CARDINALITY_MASK = 0x1f;
    public static final TxnId[] NO_TXNIDS = new TxnId[0];

    public static final TxnId NONE = new TxnId(0, 0, Id.NONE);
    public static final TxnId MAX;
    static
    {
        Kind[] kinds = Kind.values();
        Kind maxKind = kinds[kinds.length - 1];
        Domain[] domains = Domain.values();
        Domain maxDomain = domains[domains.length - 1];
        Cardinality[] cardinalities = Cardinality.values();
        Cardinality maxCardinality = cardinalities[cardinalities.length - 1];
        MAX = new TxnId(MAX_EPOCH, Long.MAX_VALUE, MAX_FLAGS & ~KIND_AND_DOMAIN_FLAGS & ~maxCardinality.bit(),
                        maxKind, maxDomain, maxCardinality, Id.MAX);
    }

    public static TxnId fromBits(long msb, long lsb, Id node)
    {
        return new TxnId(msb, lsb, node);
    }

    public static TxnId fromValues(long epoch, long hlc, Id node)
    {
        return new TxnId(epoch, hlc, 0, node);
    }

    public static TxnId fromValues(long epoch, long hlc, int flags, Id node)
    {
        return new TxnId(epoch, hlc, flags, node);
    }

    public static TxnId fromValues(long epoch, long hlc, int flags, int node)
    {
        return new TxnId(epoch, hlc, flags, new Id(node));
    }

    public TxnId(Timestamp timestamp, Kind rw, Domain domain)
    {
        this(timestamp, rw, domain, Any);
    }

    public TxnId(Timestamp timestamp, Kind rw, Domain domain, Cardinality cardinality)
    {
        this(timestamp, 0, rw, domain, cardinality);
    }

    public TxnId(Timestamp timestamp, int flags, Kind rw, Domain domain, Cardinality cardinality)
    {
        super(timestamp, flags(flags, rw, domain, cardinality));
    }

    public TxnId(TxnId copy)
    {
        super(copy.msb, copy.lsb, copy.node);
    }

    public TxnId(long epoch, long hlc, Kind rw, Domain domain, Id node)
    {
        this(epoch, hlc, rw, domain, Any, node);
    }

    public TxnId(long epoch, long hlc, Kind rw, Domain domain, Cardinality cardinality, Id node)
    {
        this(epoch, hlc, 0, rw, domain, cardinality, node);
    }

    public TxnId(long epoch, long hlc, int flags, Kind rw, Domain domain, Id node)
    {
        this(epoch, hlc, flags, rw, domain, Any, node);
    }

    public TxnId(long epoch, long hlc, int flags, Kind rw, Domain domain, Cardinality cardinality, Id node)
    {
        this(epoch, hlc, flags(flags, rw, domain, cardinality), node);
    }

    private TxnId(long epoch, long hlc, int flags, Id node)
    {
        super(epoch, hlc, flags, node);
    }

    private TxnId(long msb, long lsb, Id node)
    {
        super(msb, lsb, node);
    }

    public final boolean isWrite()
    {
        return is(Write);
    }

    public final boolean isSomeRead()
    {
        return is(Read) || is(EphemeralRead);
    }

    public final Kind kind()
    {
        return kind(flagsUnmasked());
    }

    public final boolean is(Kinds kinds)
    {
        return kinds.testOrdinal(kindOrdinal(flagsUnmasked()));
    }

    public final boolean is(Kind kind)
    {
        return kindOrdinal(flagsUnmasked()) == kind.ordinal();
    }

    public final boolean is(FastPath fastPath)
    {
        return fastPath.is(flagsUnmasked());
    }

    public final boolean is(MediumPath mediumPath)
    {
        return mediumPath.is(flagsUnmasked());
    }

    public final boolean is(Cardinality cardinality)
    {
        return cardinality.is(flagsUnmasked());
    }

    @Override
    public final TxnId withoutNonIdentityFlags()
    {
        return removeFlags(this, NON_IDENTITY_FLAGS, TxnId::fromBits);
    }

    public final int nonIdentityFlags()
    {
        return flags() >>> NON_IDENTITY_FLAGS_SHIFT;
    }

    public final boolean hasPrivilegedCoordinator()
    {
        return FastPath.hasPrivilegedCoordinator(flagsUnmasked());
    }

    public final boolean hasMediumPath()
    {
        return !MediumPath.is(flagsUnmasked(), NoMediumPath);
    }

    public final boolean isVisible()
    {
        return Kind.isVisible(kindOrdinal(flagsUnmasked()));
    }

    public final boolean hasFastPath()
    {
        return !isSyncPoint();
    }

    public final boolean isSyncPoint()
    {
        return Kind.isSyncPoint(kindOrdinal(flagsUnmasked()));
    }

    public final boolean isSystemTxn()
    {
        return Kind.isSystemTxn(kindOrdinal(flagsUnmasked()));
    }

    public final boolean awaitsOnlyDeps()
    {
        return Kind.awaitsOnlyDeps(kindOrdinal(flagsUnmasked()));
    }

    public final boolean awaitsPreviouslyOwned()
    {
        return Kind.awaitsPreviouslyOwned(kindOrdinal(flagsUnmasked()));
    }

    public final boolean is(Domain domain)
    {
        return domainOrdinal(flagsUnmasked()) == domain.ordinal();
    }

    public final Kinds witnesses()
    {
        return kind().witnessesKinds();
    }

    public final boolean witnesses(TxnId txnId)
    {
        return Kind.witnesses(kindOrdinal(flagsUnmasked()), kindOrdinal(txnId.flagsUnmasked()));
    }

    public final boolean witnessedBy(TxnId txnId)
    {
        return txnId.witnesses(this);
    }

    public final Kinds witnessedBy()
    {
        return kind().witnessedByKinds();
    }

    public final Domain domain()
    {
        return domain(flagsUnmasked());
    }

    public TxnId as(Kind kind)
    {
        return as(kind, domain());
    }

    public TxnId as(Domain domain)
    {
        return as(kind(), domain);
    }

    public TxnId as(Kind kind, Domain domain)
    {
        return new TxnId(epoch(), hlc(), flagsWithoutDomainOrKindOrCardinality(), kind, domain, cardinality(), node);
    }

    private int flagsWithoutDomainOrKindOrCardinality()
    {
        return flags() & ~DOMAIN_AND_KIND_AND_CARDINALITY_MASK;
    }

    public TxnId addFlags(int flags)
    {
        Invariants.requireArgument(flags <= MAX_FLAGS);
        return addFlags(this, flags, TxnId::new);
    }

    public TxnId addFlag(Flag flag)
    {
        return addFlags(flag.bit);
    }

    public TxnId propagateUnstable(@Nullable TxnId ifNullOrUnstable)
    {
        return ifNullOrUnstable == null || ifNullOrUnstable.is(UNSTABLE) ? addFlag(UNSTABLE) : this;
    }

    public final TxnId addFlags(TxnId merge)
    {
        return addFlags(this, merge, TxnId::new);
    }

    public TxnId withEpoch(long epoch)
    {
        return epoch == epoch() ? this : new TxnId(epoch, hlc(), flags(), node);
    }

    @Override
    public TxnId merge(Timestamp that)
    {
        return merge(this, that, TxnId::fromBits);
    }

    @Override
    public String toString()
    {
        return "[" + epoch() + ',' + hlc() + ',' + flags() + '(' + domain().shortName() + kind().shortName() + ')' + ',' + node + ']';
    }

    private static int flags(int flags, Kind rw, Domain domain, Cardinality cardinality)
    {
        Invariants.require(cardinality == Any || domain == Domain.Key);
        int paramFlags = flags(rw) | flags(domain) | cardinality.bit();
        Invariants.require((flags & paramFlags) == 0);
        return paramFlags | flags;
    }

    private static int flags(Kind rw)
    {
        return rw.ordinal() << 1;
    }

    private static int flags(Domain domain)
    {
        return domain.ordinal();
    }

    private static Kind kind(int flags)
    {
        return Kind.forOrdinal(kindOrdinal(flags));
    }

    private static Domain domain(int flags)
    {
        return Domain.forOrdinal(domainOrdinal(flags));
    }

    public static int kindOrdinal(int flags)
    {
        return (flags >>> 1) & 7;
    }

    public final int kindOrdinal()
    {
        return kindOrdinal(flagsUnmasked());
    }

    public FastPath fastPath()
    {
        return FastPath.forFlags(flagsUnmasked());
    }

    public MediumPath mediumPath()
    {
        return MediumPath.forFlags(flagsUnmasked());
    }

    public Cardinality cardinality()
    {
        return Cardinality.forFlags(flagsUnmasked());
    }

    public SelectShards selectsShards()
    {
        return isSyncPoint() ? SelectShards.ALL : SelectShards.LIVE;
    }

    public boolean hasOnlyIdentityFlags()
    {
        return (flags() & ~IDENTITY_FLAGS) == 0;
    }

    public static int domainOrdinal(int flags)
    {
        return flags & 1;
    }

    public static TxnId maxForEpoch(long epoch)
    {
        return maxForEpoch(epoch, TxnId::fromBits);
    }

    public static TxnId minForEpoch(long epoch)
    {
        return minForEpoch(epoch, TxnId::fromBits);
    }

    public static TxnId noneIfNull(TxnId id)
    {
        return id == null ? NONE : id;
    }

    public static TxnId maxIfNull(TxnId id)
    {
        return id == null ? MAX : id;
    }

    public static TxnId atLeast(Timestamp timestamp)
    {
        if (timestamp.flags() != 0)
            timestamp = timestamp.next();
        return new TxnId(timestamp, 0, Read, Domain.Key, Any);
    }

    private static final Pattern PARSE = Pattern.compile("\\[(?<epoch>[0-9]+),(?<hlc>[0-9]+),(?<flags>[0-9]+)\\([KR][REWXV]\\),(?<node>[0-9]+)]");
    public static TxnId parse(String txnIdString)
    {
        Matcher m = PARSE.matcher(txnIdString);
        if (!m.matches())
            throw illegalArgument("Invalid TxnId string: " + txnIdString);
        return fromMatcher(txnIdString, m, true);
    }

    public static TxnId tryParse(String txnIdString)
    {
        Matcher m = PARSE.matcher(txnIdString);
        if (!m.matches())
            return null;
        return fromMatcher(txnIdString, m, false);
    }

    private static TxnId fromMatcher(String txnIdString, Matcher m, boolean failIfInvalid)
    {
        long epoch = Long.parseLong(m.group("epoch"));
        long hlc = Long.parseLong(m.group("hlc"));
        int flags = Integer.parseInt(m.group("flags"));
        if (flags > 0xffff)
            return failOrNull(txnIdString, failIfInvalid);

        Id node = new Id(Integer.parseInt(m.group("node")));
        // TODO (expected): validate kind vs flags
        return TxnId.fromValues(epoch, hlc, flags, node);
    }

    private static <T extends Timestamp> T failOrNull(String timestampString, boolean fail)
    {
        if (fail)
            throw illegalArgument("Invalid TxnId string: " + timestampString);
        return null;
    }

    public static boolean equalsStrict(TxnId[] a, TxnId[] b)
    {
        if (a.length != b.length)
            return false;

        for (int i = 0 ; i < a.length ; ++i)
        {
            if (!a[i].equalsStrict(b[i]))
                return false;
        }
        return true;
    }
}
