package org.multiverse.stms.gamma;

import org.multiverse.api.BackoffPolicy;
import org.multiverse.api.DefaultBackoffPolicy;
import org.multiverse.api.IsolationLevel;
import org.multiverse.api.LockMode;
import org.multiverse.api.PropagationLevel;
import org.multiverse.api.TraceLevel;
import org.multiverse.api.lifecycle.TxnListener;

import java.util.LinkedList;
import java.util.List;

import static java.lang.String.format;

/**
 * Contains the default configuration for all transactions created by the GammaStm. With the TxnFactoryBuilder,
 * this behavior can be overridden.
 * <p/>
 * Once the GammaStm has been created, changes on this structure are ignored because the content of this configuration
 * is copied.
 *
 * @author Peter Veentjer.
 */
@SuppressWarnings({"ClassWithTooManyFields"})
public final class GammaStmConfig {

    /**
     * Contains the permanent TxnListeners that should always be executed. Null references are not allowed.
     */
    public List<TxnListener> permanentListeners = new LinkedList<TxnListener>();

    /**
     * The default propagation level for all transactions executed by the Stm.
     */
    public PropagationLevel propagationLevel = PropagationLevel.Requires;

    /**
     * The default isolation level for all transactions executed by the GammaStm.
     */
    public IsolationLevel isolationLevel = IsolationLevel.Snapshot;

    /**
     * The default isolation level for all reads. Putting this to a level higher than LockMode.None has the same
     * effect as putting the isolationLevel to IsolationLevel.Serialized although the former approach will be
     * pessimistic and the later be optimistic.
     */
    public LockMode readLockMode = LockMode.None;

    /**
     * The default isolation level for all writes. For a write also a read is acquired, so the highest one wins.
     * <p/>
     * Putting it the LockMode.Write gives the same behavior as you have with Oracle. Reads are still allowed (even
     * though a WriteLock is allowed) only other transactions are not able to acquire a lock on it (whatever the lock
     * should be).
     */
    public LockMode writeLockMode = LockMode.None;

    /**
     * The default behavior if blocking transactions are allowed.
     */
    public boolean blockingAllowed = true;

    /**
     * The default behavior for blocking transactions if they are allowed to be interrupted.
     */
    public boolean interruptible = false;

    /**
     * The default timeout for a transaction if it blocks. A Long.MAX_VALUE indicates that there is no timeout.
     */
    public long timeoutNs = Long.MAX_VALUE;

    /**
     * The default readonly behavior. Setting this to true would be quite useless.
     */
    public boolean readonly = false;

    /**
     * The default number of spins a transaction is allowed for a read/write/commit if something is locked.
     */
    public int spinCount = 64;

    /**
     * The default behavior for writing 'dirty' changes for an update transaction. If it is set to true, a change needs to
     * be made. If there is no change, it will not be written (and essentially be seen as a read).
     */
    public boolean dirtyCheck = true;

    /**
     * The minimal size for the internal array for a variable length transaction. A variable length transaction internally uses
     * an array to store its content and when the transaction grows, the array will grow accordingly.
     */
    public int minimalVariableLengthTransactionSize = 4;

    /**
     * If reads should be tracked by the transaction (this is something else than (semi)visible reads). The advantage of readtracking
     * is there is less overhead once it has been read, but the disadvantage is that the transaction needs to track more changes.
     */
    public boolean trackReads = true;

    /**
     * The default number of retries a transaction is allowed to do if a transaction fails for a read/write conflict. The GammaStm also
     * uses a speculative configuration mechanism that can consume some, so setting it to a very low value in combination with
     * speculativeConfigEnabled is not recommended.
     */
    public int maxRetries = 1000;

    /**
     * The GammaStm makes use of a speculative mechanism to select the optimal transaction settings/implementation for executing a transactional closure.
     * An TxnExecutor will start cheap and grow to use more expensive transaction implementations (see the Lean/Fat transactions) and
     * use the automatic retry mechanism of the STM to rescue itself from a situation where the speculation was incorrect. Setting it to false
     * reduces the number of unwanted conflicts (once the TxnExecutor has learned it will not make the same mistakes again, so you only need
     * to pay the price of speculation in the beginning) at the cost of overhead.
     */
    public boolean speculativeConfigEnabled = true;

    /**
     * The maximum size size of a fixed length transaction. A fixed length transaction is very cheap compared to a variable length, but
     * the big problem of the fixed length is that it needs to do a full transaction scan to see if the desired data is there. So there is
     * a point where the full scan gets more expensive (O(N) vs O(log n)) expensive.
     * <p/>
     * Atm there has not been put much research in finding the optimal size and it could differ from machine to machine.
     * <p/>
     * It also is important that the fixed length transactions are able to put a frequently read ref in a hot spot, making the overhead
     * of searching lower.
     */
    public int maxFixedLengthTransactionSize = 20;

    /**
     * If a transaction fails for a read/write conflict it should not hammer the system by trying again and running in the same conflict
     * The default backoff policy helps to back threads of by sleeping/yielding.
     */
    public BackoffPolicy backoffPolicy = DefaultBackoffPolicy.MAX_100_MS;

    /**
     * With the trace level you have control if you get output of transactions executing. It helps with debugging. If the
     * org.multiverse.MultiverseConstants.___TracingEnabled is not set to true, this value is ignored and the whole profiling
     * stuff is removed by the JIT since it effectively has become dead code.
     */
    public TraceLevel traceLevel = TraceLevel.None;

    /**
     * If control flow errors should be reused. Normally exception reuse would be a very very very bad thing to do. But if they are
     * used to regulate control flow, they can be thrown thousands of times a second and this puts a lot of pressure on the gc. The most
     * expensive part is building the StackTrace.
     * <p/>
     * For more info about the control flow errors see the subclasses of the {@link org.multiverse.api.exceptions.ControlFlowError} like
     * the {@link org.multiverse.api.exceptions.ReadWriteConflict}, {@link org.multiverse.api.exceptions.RetryError} and the
     * {@link org.multiverse.api.exceptions.SpeculativeConfigurationError}.
     */
    public boolean controlFlowErrorsReused = true;

    /**
     * Should only be used internally to select fat instead of lean transactions. Normally the speculative configuration takes care of this
     * but for testing purposes you want to control it manually.
     */
    public boolean isFat = false;

    /**
     * The maximum size of a transaction that is allowed to do a full conflict scan instead of arrive/depart operations. Arrive/depart
     * is more expensive since it increased the pressure on refs that a full conflict scan for short transactions, but at a certain length
     * of the transaction only needing to do a full conflict scan when the global conflict counter increases, becomes cheaper.
     */
    public int maximumPoorMansConflictScanLength = 20;

    /**
     * The number of times a transactional object is only read before becoming readbiased. The advantage of a readBiased transactional object
     * is that you don't need to arrive/depart (richmans conflict scan), but the disadvantage is that it could cause transactions to do
     * full conflict scans even though they are not required.
     */
    public int readBiasedThreshold = 128;

    /**
     * Checks if the configuration is valid.
     *
     * @throws IllegalStateException if the configuration isn't valid.
     */
    public void validate() {
        if (timeoutNs < 0) {
            throw new IllegalStateException(
                    "[GammaStmConfig] timeoutNs can't be smaller than 0, " +
                            "timeoutNs was " + timeoutNs);
        }

        if (readBiasedThreshold < 0) {
            throw new IllegalStateException(
                    "[GammaStmConfig] readBiasedThreshold can't be smaller than 0, " +
                            "readBiasedThreshold was " + readBiasedThreshold);
        }

        if (readBiasedThreshold > 1023) {
            throw new IllegalStateException(
                    "[GammaStmConfig] readBiasedThreshold can't be larger than 1023, " +
                            "readBiasedThreshold was " + readBiasedThreshold);
        }

        if (maximumPoorMansConflictScanLength < 0) {
            throw new IllegalStateException(
                    "[GammaStmConfig] maximumFullConflictScanSize can't be smaller than 0, " +
                            "maximumFullConflictScanSize was " + maxFixedLengthTransactionSize);
        }

        if (readLockMode == null) {
            throw new IllegalStateException(
                    "[GammaStmConfig] readLockMode can't be null");
        }

        if (writeLockMode == null) {
            throw new IllegalStateException(
                    "[GammaStmConfig] writeLockMode can't be null");
        }

        if (writeLockMode.asInt() < readLockMode.asInt()) {
            throw new IllegalStateException(
                    format("[GammaStmConfig] writeLockMode [%s] can't be lower than readLockMode [%s]",
                            writeLockMode, readLockMode));
        }

        if (isolationLevel == null) {
            throw new IllegalStateException("[GammaStmConfig] isolationLevel can't be null");
        }

        if (isolationLevel.doesAllowWriteSkew() && !trackReads) {
            throw new IllegalStateException(
                    format("[GammaStmConfig] isolation level '%s' can't be combined with readtracking" +
                            "is false since it is needed to prevent the writeskew problem", isolationLevel));
        }

        if (blockingAllowed && !trackReads) {
            throw new IllegalStateException(
                    "[GammaStmConfig] blockingAllowed can't be true if trackReads is false");
        }

        if (spinCount < 0) {
            throw new IllegalStateException(
                    "[GammaStmConfig] spinCount can't be smaller than 0, but was " + spinCount);
        }

        if (minimalVariableLengthTransactionSize < 1) {
            throw new IllegalStateException(
                    "[GammaStmConfig] minimalVariableLengthTransactionSize can't be smaller than 1, but was "
                            + minimalVariableLengthTransactionSize);
        }

        if (maxRetries < 0) {
            throw new IllegalStateException(
                    "[GammaStmConfig] maxRetries can't be smaller than 0, but was " + maxRetries);
        }

        if (maxFixedLengthTransactionSize < 2) {
            throw new IllegalStateException(
                    "[GammaStmConfig] maxFixedLengthTransactionSize can't be smaller than 2, but was "
                            + maxFixedLengthTransactionSize);
        }

        if (backoffPolicy == null) {
            throw new IllegalStateException("[GammaStmConfig] backoffPolicy can't be null");
        }

        if (traceLevel == null) {
            throw new IllegalStateException("[GammaStmConfig] traceLevel can't be null");
        }

        if (propagationLevel == null) {
            throw new IllegalStateException("[GammaStmConfig] propagationLevel can't be null");
        }

        if (permanentListeners == null) {
            throw new IllegalStateException("[GammaStmConfig] permanentListeners can't be null");
        }

        for (int k = 0; k < permanentListeners.size(); k++) {
            TxnListener listener = permanentListeners.get(k);
            if (listener == null) {
                throw new IllegalStateException(
                        format("[GammaStmConfig] permanentListener at index %s can't be null", k));
            }
        }
    }
}
