package org.multiverse.stms.gamma.transactions.fat;

import org.multiverse.api.lifecycle.TxnEvent;
import org.multiverse.stms.gamma.GammaStm;
import org.multiverse.stms.gamma.Listeners;
import org.multiverse.stms.gamma.transactionalobjects.BaseGammaTxnRef;
import org.multiverse.stms.gamma.transactionalobjects.GammaObject;
import org.multiverse.stms.gamma.transactionalobjects.Tranlocal;
import org.multiverse.stms.gamma.transactions.GammaTxn;
import org.multiverse.stms.gamma.transactions.GammaTxnConfig;
import org.multiverse.stms.gamma.transactions.SpeculativeGammaConfiguration;

import static org.multiverse.utils.Bugshaker.shakeBugs;

@SuppressWarnings({"OverlyComplexClass"})
public final class FatVariableLengthGammaTxn extends GammaTxn {

    public Tranlocal[] array;
    public int size = 0;
    public boolean hasReads = false;
    public long localConflictCount;

    public FatVariableLengthGammaTxn(GammaStm stm) {
        this(new GammaTxnConfig(stm));
    }

    public FatVariableLengthGammaTxn(GammaTxnConfig config) {
        super(config, TRANSACTIONTYPE_FAT_VARIABLE_LENGTH);
        this.array = new Tranlocal[config.minimalArrayTreeSize];
    }

    @Override
    public final void commit() {
        if (status == TX_COMMITTED) {
            return;
        }

        if (status != TX_ACTIVE && status != TX_PREPARED) {
            throw abortCommitOnBadStatus();
        }

        if (abortOnly) {
            throw abortCommitOnAbortOnly();
        }

        if (status == TX_ACTIVE) {
            notifyListeners(TxnEvent.PrePrepare);
        }

        if (size > 0) {
            if (hasWrites) {
                if (status == TX_ACTIVE) {
                    GammaObject conflictingObject = doPrepare();
                    if (conflictingObject != null) {
                        throw abortOnReadWriteConflict(conflictingObject);
                    }
                }

                if (commitConflict) {
                    config.globalConflictCounter.signalConflict();
                }

                Listeners[] listenersArray = commitArray();

                if (listenersArray != null) {
                    Listeners.openAll(listenersArray, pool);
                    pool.putListenersArray(listenersArray);
                }
            } else {
                releaseArray(true);
            }
        }

        status = TX_COMMITTED;
        notifyListeners(TxnEvent.PostCommit);
    }

    private Listeners[] commitArray() {
        Listeners[] listenersArray = null;

        int listenersIndex = 0;
        int itemCount = 0;
        //first write everything without releasing
        for (int k = 0; k < array.length; k++) {
            if (SHAKE_BUGS) shakeBugs();

            final Tranlocal tranlocal = array[k];

            if (tranlocal == null) {
                continue;
            }

            final BaseGammaTxnRef owner = tranlocal.owner;
            final Listeners listeners = owner.commit(tranlocal, pool);

            if (listeners != null) {
                if (listenersArray == null) {
                    listenersArray = pool.takeListenersArray(size - itemCount);
                }

                listenersArray[listenersIndex] = listeners;
                listenersIndex++;
            }
            pool.put(tranlocal);
            itemCount++;
        }
        return listenersArray;
    }

    private void releaseArray(boolean success) {
        for (int k = 0; k < array.length; k++) {

            final Tranlocal tranlocal = array[k];

            if (tranlocal != null) {
                if (SHAKE_BUGS) shakeBugs();

                array[k] = null;
                if (success) {
                    tranlocal.owner.releaseAfterReading(tranlocal, pool);
                } else {
                    tranlocal.owner.releaseAfterFailure(tranlocal, pool);
                }
                pool.put(tranlocal);
            }
        }
    }

    @Override
    public final void prepare() {
        if (status == TX_PREPARED) {
            return;
        }

        if (status != TX_ACTIVE) {
            throw abortPrepareOnBadStatus();
        }

        if (abortOnly) {
            throw abortPrepareOnAbortOnly();
        }

        notifyListeners(TxnEvent.PrePrepare);

        if (hasWrites) {
            final GammaObject conflictingObject = doPrepare();
            if (conflictingObject != null) {
                throw abortOnReadWriteConflict(conflictingObject);
            }
        }

        status = TX_PREPARED;
    }

    @SuppressWarnings({"BooleanMethodIsAlwaysInverted"})
    private GammaObject doPrepare() {
        if (skipPrepare()) {
            return null;
        }

        for (int k = 0; k < array.length; k++) {
            if (SHAKE_BUGS) shakeBugs();

            final Tranlocal tranlocal = array[k];

            if (tranlocal == null) {
                continue;
            }

            final BaseGammaTxnRef owner = tranlocal.owner;

            if (!owner.prepare(this, tranlocal)) {
                return owner;
            }
        }

        return null;
    }

    @Override
    public final void abort() {
        if (status == TX_ABORTED) {
            return;
        }

        if (status == TX_COMMITTED) {
            throw failAbortOnAlreadyCommitted();
        }

        if (size > 0) {
            releaseArray(false);
        }

        status = TX_ABORTED;

        notifyListeners(TxnEvent.PostAbort);
    }

    @Override
    public final Tranlocal locate(BaseGammaTxnRef o) {
        if (status != TX_ACTIVE) {
            throw abortLocateOnBadStatus(o);
        }

        if (o == null) {
            throw abortLocateOnNullArgument();
        }

        return getRefTranlocal(o);
    }

    @Override
    public final Tranlocal getRefTranlocal(BaseGammaTxnRef ref) {
        int indexOf = indexOf(ref, ref.identityHashCode());
        return indexOf == -1 ? null : array[indexOf];
    }

    @Override
    public final void retry() {
        if (status != TX_ACTIVE) {
            throw abortRetryOnBadStatus();
        }

        if (!config.isBlockingAllowed()) {
            throw abortRetryOnNoBlockingAllowed();
        }

        if (size == 0) {
            throw abortRetryOnNoRetryPossible();
        }

        retryListener.reset();
        final long listenerEra = retryListener.getEra();

        boolean furtherRegistrationNeeded = true;
        boolean atLeastOneRegistration = false;

        for (int k = 0; k < array.length; k++) {
            final Tranlocal tranlocal = array[k];
            if (tranlocal == null) {
                continue;
            }

            array[k] = null;

            final BaseGammaTxnRef owner = tranlocal.owner;

            if (furtherRegistrationNeeded) {
                switch (owner.registerChangeListener(retryListener, tranlocal, pool, listenerEra)) {
                    case REGISTRATION_DONE:
                        atLeastOneRegistration = true;
                        break;
                    case REGISTRATION_NOT_NEEDED:
                        furtherRegistrationNeeded = false;
                        atLeastOneRegistration = true;
                        break;
                    case REGISTRATION_NONE:
                        break;
                    default:
                        throw new IllegalStateException();
                }
            }

            owner.releaseAfterFailure(tranlocal, pool);
            pool.put(tranlocal);
        }

        status = TX_ABORTED;

        if (!atLeastOneRegistration) {
            throw abortRetryOnNoRetryPossible();
        }

        throw newRetryError();
    }

    @Override
    public final boolean softReset() {
        if (attempt >= config.getMaxRetries()) {
            return false;
        }

        status = TX_ACTIVE;
        hasReads = false;
        hasWrites = false;
        size = 0;
        abortOnly = false;
        attempt++;
        commitConflict = false;
        evaluatingCommute = false;
        if (listeners != null) {
            listeners.clear();
            pool.putArrayList(listeners);
            listeners = null;
        }

        return true;
    }

    @Override
    public final void hardReset() {
        status = TX_ACTIVE;
        hasReads = false;
        hasWrites = false;
        size = 0;
        abortOnly = false;

        attempt = 1;
        remainingTimeoutNs = config.timeoutNs;
        //todo: only change when the array size is different.
        if (array != null) {
            pool.putTranlocalArray(array);
        }
        array = pool.takeTranlocalArray(config.minimalArrayTreeSize);
        final SpeculativeGammaConfiguration speculativeConfig = config.speculativeConfiguration.get();
        richmansMansConflictScan = speculativeConfig.richMansConflictScanRequired;
        commitConflict = false;
        evaluatingCommute = false;
        if (listeners != null) {
            listeners.clear();
            pool.putArrayList(listeners);
            listeners = null;
        }

    }

    @Override
    public void initLocalConflictCounter() {
        if (richmansMansConflictScan && !hasReads) {
            localConflictCount = config.globalConflictCounter.count();
        }
    }

    @Override
    public final boolean isReadConsistent(Tranlocal justAdded) {
        if (!hasReads) {
            return true;
        }

        if (config.readLockModeAsInt > LOCKMODE_NONE) {
            return true;
        }

        if (config.inconsistentReadAllowed) {
            return true;
        }

        if (richmansMansConflictScan) {
            if (SHAKE_BUGS) shakeBugs();

            final long conflictCount = config.globalConflictCounter.count();

            if (localConflictCount == conflictCount) {
                return true;
            }

            localConflictCount = conflictCount;
            //we are going to fall through to do a full conflict scan
        } else if (size > config.maximumPoorMansConflictScanLength) {
            throw abortOnRichmanConflictScanDetected();
        }

        //doing a full conflict scan
        for (int k = 0; k < array.length; k++) {
            if (SHAKE_BUGS) shakeBugs();

            final Tranlocal tranlocal = array[k];

            //noinspection ObjectEquality
            final boolean skip = tranlocal == null || (!richmansMansConflictScan && justAdded == tranlocal);

            if (!skip && tranlocal.owner.hasReadConflict(tranlocal)) {
                return false;
            }
        }

        return true;
    }

    public final float getUsage() {
        return (size * 1.0f) / array.length;
    }

    public final int size() {
        return size;
    }

    public final int indexOf(final BaseGammaTxnRef ref, final int hash) {
        int jump = 0;
        boolean goLeft = true;

        do {
            final int offset = goLeft ? -jump : jump;
            final int index = (hash + offset) % array.length;

            final Tranlocal current = array[index];
            if (current == null || current.owner == null) {
                return -1;
            }

            //noinspection ObjectEquality
            if (current.owner == ref) {
                return index;
            }

            final int currentHash = current.owner.identityHashCode();
            goLeft = currentHash > hash;
            jump = jump == 0 ? 1 : jump * 2;
        } while (jump < array.length);

        return -1;
    }

    public final void attach(final Tranlocal tranlocal, final int hash) {
        int jump = 0;
        boolean goLeft = true;

        do {
            final int offset = goLeft ? -jump : jump;
            final int index = (hash + offset) % array.length;

            Tranlocal current = array[index];
            if (current == null) {
                array[index] = tranlocal;
                return;
            }

            final int currentHash = current.owner.identityHashCode();
            goLeft = currentHash > hash;
            jump = jump == 0 ? 1 : jump * 2;
        } while (jump < array.length);

        expand();
        attach(tranlocal, hash);
    }

    private void expand() {
        Tranlocal[] oldArray = array;
        int newSize = oldArray.length * 2;
        array = pool.takeTranlocalArray(newSize);

        for (int k = 0; k < oldArray.length; k++) {
            final Tranlocal tranlocal = oldArray[k];

            if (tranlocal == null) {
                continue;
            }

            oldArray[k] = null;
            attach(tranlocal, tranlocal.owner.identityHashCode());
        }

        pool.putTranlocalArray(oldArray);
    }
}
