/*
 * 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.impl.progresslog;

import javax.annotation.Nullable;

import accord.api.ProgressLog.BlockedUntil;
import accord.coordinate.Infer;
import accord.local.SafeCommandStore;
import accord.primitives.TxnId;
import accord.utils.Invariants;
import accord.utils.LogGroupTimers;

import static accord.coordinate.Infer.InvalidIf.IfUncommitted;
import static accord.coordinate.Infer.InvalidIf.NotKnownToBeInvalid;
import static accord.impl.progresslog.TxnStateKind.Home;
import static accord.impl.progresslog.TxnStateKind.Waiting;

/**
 * We have a slightly odd class hierarchy here to cleanly group the logic, though TxnState can logically
 * be read as a single class.
 * <p>
 * We have:
 * - BaseTxnState defines the state that each of the child classes operate on and provides shared
 *   methods for updating it, particularly with relation to tracking timer scheduling
 * - WaitingState defines the methods for updating the WaitingState state machine (i.e. for local commands that are waiting on a dependency's outcome)
 * - HomeState defines the methods for updating the HomeState state machine (i.e. for coordinators ensuring a transaction completes)
 * <p>
 * TODO (testing): unit test all bit fiddling methods
 */
abstract class BaseTxnState extends LogGroupTimers.Timer implements Comparable<BaseTxnState>
{
    private static final int RESTORED_SHIFT = 63;
    static final long RESTORED_BIT = 1L << RESTORED_SHIFT;
    private static final int CONTACT_ALL_SHIFT = RESTORED_SHIFT - 1;
    private static final long CONTACT_ALL_BIT = 1L << CONTACT_ALL_SHIFT; // TODO (desired): have separate contact all flags for recovery and waiting states
    private static final int SCHEDULED_TIMER_SHIFT = CONTACT_ALL_SHIFT - 1;
    private static final int INVALID_IF_UNCOMMITTED_SHIFT = SCHEDULED_TIMER_SHIFT - 1;
    private static final int PENDING_TIMER_BITS = 9;
    private static final int PENDING_TIMER_SHIFT = INVALID_IF_UNCOMMITTED_SHIFT - PENDING_TIMER_BITS;
    private static final int PENDING_TIMER_LOW_BITS = PackedLogLinearInteger.validateLowBits(4, PENDING_TIMER_BITS);
    private static final long PENDING_TIMER_MASK = (1L << PENDING_TIMER_BITS) - 1;
    private static final long PENDING_TIMER_CLEAR_MASK = ~(PENDING_TIMER_MASK << PENDING_TIMER_SHIFT);
    static final long BASE_STATE_START_SHIFT = PENDING_TIMER_SHIFT;

    public final TxnId txnId;

    /**
     * bits [0..8) encode HomeState
     * 2 bits for Progress
     * 3 bits for CoordinatePhase
     * 3 bits for retry counter
     * <p>
     * bits [8..51) encode WaitingState
     * 2 bits for Progress
     * 2 bits for BlockedUntil target
     * 2 bits for BlockedUntil querying (<= target)
     * 2 bits for BlockedUntil that home shard can satisfy
     * 1 bit to query non-home shards (as home shard has been truncated)
     * 24+5 bits for remote key progress tracking [note: if we need to in future we can safely and easily reclaim bits here]
     * 3 bits for retry counter
     * <p>
     * bits [51..60) for pending timer delay
     * bit 60 indicates if this transaction can be inferred invalid if a later quorum finds it not committed on any shard
     * bit 61 for which kind of timer is scheduled
     * bit 62 for whether we should contact all candidate replicas (rather than just our preferred group)
     * bit 63 for whether the item was restored from a snapshot (so can modify certain invariants)
     */
    long encodedState;

    BaseTxnState(TxnId txnId)
    {
        this.txnId = txnId;
    }

    @Override
    public int compareTo(BaseTxnState that)
    {
        return this.txnId.compareTo(that.txnId);
    }

    boolean isRestored()
    {
        return (encodedState & RESTORED_BIT) != 0L;
    }

    boolean contactEveryone()
    {
        return (encodedState & CONTACT_ALL_BIT) != 0L;
    }

    void setContactEveryone(boolean newContactEveryone)
    {
        encodedState &= encodedState & ~(1L << CONTACT_ALL_SHIFT);
        encodedState |= (newContactEveryone ? 1L : 0) << CONTACT_ALL_SHIFT;
    }

    /**
     * The state that has a timer actively scheduled, if any
     */
    TxnStateKind scheduledTimer()
    {
        if (!isScheduled())
            return null;
        return wasScheduledTimer();
    }

    /**
     * The state that last had a timer actively scheduled
     */
    TxnStateKind wasScheduledTimer()
    {
        return ((encodedState >>> SCHEDULED_TIMER_SHIFT) & 1) == 0 ? Waiting : Home;
    }

    /**
     * Set the state that has a timer actively scheduled
     */
    void setScheduledTimer(TxnStateKind kind)
    {
        encodedState &= ~(1L << SCHEDULED_TIMER_SHIFT);
        encodedState |= (long) kind.ordinal() << SCHEDULED_TIMER_SHIFT;
        Invariants.require(wasScheduledTimer() == kind);
    }

    /**
     * The state that has a timer pending, if any
     */
    TxnStateKind pendingTimer()
    {
        int pendingDelay = pendingTimerDelay();
        if (pendingDelay == 0)
            return null;
        return ((encodedState >>> SCHEDULED_TIMER_SHIFT) & 1) == 0 ? Home : Waiting;
    }

    void clearPendingTimerDelay()
    {
        encodedState &= PENDING_TIMER_CLEAR_MASK;
    }

    void setPendingTimerDelay(int deltaMicros)
    {
        clearPendingTimerDelay();
        encodedState |= (long)PackedLogLinearInteger.encode(deltaMicros, PENDING_TIMER_LOW_BITS, PENDING_TIMER_BITS) << PENDING_TIMER_SHIFT;
    }

    int pendingTimerDelay()
    {
        return PackedLogLinearInteger.decode((int)((encodedState >>> PENDING_TIMER_SHIFT) & PENDING_TIMER_MASK), PENDING_TIMER_LOW_BITS);
    }

    long pendingTimerDeadline(long scheduledDeadline)
    {
        long pendingTimerDelay = pendingTimerDelay();
        return pendingTimerDelay == 0 ? 0 : scheduledDeadline + pendingTimerDelay;
    }

    long pendingTimerDeadline()
    {
        long pendingTimerDelay = pendingTimerDelay();
        return pendingTimerDelay == 0 ? 0 : deadline() + pendingTimerDelay;
    }

    void setInvalidIfUncommitted()
    {
        encodedState |= 1L << INVALID_IF_UNCOMMITTED_SHIFT;
    }

    Infer.InvalidIf invalidIf()
    {
        return 0 == (encodedState & (1L << INVALID_IF_UNCOMMITTED_SHIFT)) ? NotKnownToBeInvalid : IfUncommitted;
    }

    long scheduledTimerDeadline()
    {
        return deadline();
    }

    boolean isScheduled()
    {
        return isInHeap();
    }

    abstract void updateScheduling(SafeCommandStore safeStore, DefaultProgressLog instance, TxnStateKind updated, @Nullable BlockedUntil awaiting, Progress newProgress);

    abstract boolean maybeRemove(DefaultProgressLog instance);
}
