/*
 * Copyright 2012-2018 Chronicle Map Contributors
 *
 * Licensed 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 net.openhft.chronicle.hash.impl.stage.hash;

import net.openhft.chronicle.core.Memory;
import net.openhft.chronicle.core.OS;
import net.openhft.chronicle.hash.ChronicleHash;
import net.openhft.chronicle.hash.ChronicleHashClosedException;

import java.lang.reflect.Field;
import java.util.concurrent.TimeUnit;

import static net.openhft.chronicle.hash.impl.BigSegmentHeader.LOCK_TIMEOUT_SECONDS;

public abstract class ThreadLocalState {

    private static final Memory MEMORY = OS.memory();
    private static final long CONTEXT_LOCK_OFFSET;
    private static final int CONTEXT_UNLOCKED = 0;
    private static final int CONTEXT_LOCKED_LOCALLY = 1;
    private static final int CONTEXT_CLOSED = 2;

    static {
        try {
            Field contextLockField =
                    ThreadLocalState.class.getDeclaredField("contextLock");
            contextLockField.setAccessible(true);
            CONTEXT_LOCK_OFFSET = MEMORY.getFieldOffset(contextLockField);
        } catch (NoSuchFieldException e) {
            throw new AssertionError(e);
        }
    }

    public boolean iterationContextLockedInThisThread;
    private volatile int contextLock = CONTEXT_UNLOCKED;

    /**
     * Returns {@code true} if this is the outer context lock in this thread, {@code false} if this
     * is a nested context.
     */
    public boolean lockContextLocally(ChronicleHash<?, ?, ?, ?> hash) {
        // hash().isOpen() check guarantees no starvation of a thread calling chMap.close() and
        // trying to close this context by closeContext() method below, while the thread owning this
        // context frequently locks and unlocks it (e. g. in a loop). This is also the only check
        // for chMap openness during the whole context usage lifecycle.
        if (hash.isOpen() && MEMORY.compareAndSwapInt(this, CONTEXT_LOCK_OFFSET,
                CONTEXT_UNLOCKED, CONTEXT_LOCKED_LOCALLY)) {
            return true;
        } else {
            if (contextLock == CONTEXT_LOCKED_LOCALLY)
                return false;
            // Don't extract this hash().isOpen() and the one above, because they could return
            // different results: the first (above) could return true, the second (below) - false.
            if (contextLock == CONTEXT_CLOSED || !hash.isOpen())
                throw new ChronicleHashClosedException(hash);
            throw new AssertionError("Unknown context lock state: " + contextLock);
        }
    }

    public void unlockContextLocally() {
        // Ensure all reads from mapped memory are done before thread calling chronicleMap.close()
        // frees resources potentially unmapping some memory from where those reads are performed.
        MEMORY.loadFence();
        // Avoid volatile write to avoid expensive store-load barrier
        MEMORY.writeOrderedInt(this, CONTEXT_LOCK_OFFSET, CONTEXT_UNLOCKED);
    }

    public void closeContext(String chronicleHashIdentityString) {
        if (tryCloseContext())
            return;
        // Unless there are bugs in this codebase, it could happen that
        // contextLock == CONTEXT_CLOSED here only if closeContext() has succeed, and the subsequent
        // contextHolder.clear() has failed in ChronicleHashResources.closeContext(), though this is
        // hardly imaginable: contextHolder.clear() couldn't fail with OutOfMemoryError (because
        // there are no allocations in this method) and StackOverflowError (because in this case
        // closeContext() would fail with StackOverflowError before). But anyway it's probably
        // a good idea to make this check rather than not to make.
        if (contextLock == CONTEXT_CLOSED)
            return;
        // If first attempt of closing a context (i. e. moving from unused to closed state) failed,
        // it means that the context is still in use. If this context belongs to the current thread,
        // this is a bug, because we cannot "wait" until context is unused in the same thread:
        if (owner() == Thread.currentThread()) {
            throw new IllegalStateException(chronicleHashIdentityString +
                    ": Attempt to close a Chronicle Hash in the context " +
                    "of not yet finished query or iteration");
        }
        // If the context belongs to a different thread, wait until that thread finishes it's work
        // with the context:

        // Double the current timeout for segment locks "without timeout", that effectively
        // specifies maximum lock (hence context) holding time
        long timeoutMillis = TimeUnit.SECONDS.toMillis(LOCK_TIMEOUT_SECONDS) * 2;
        long lastTime = System.currentTimeMillis();
        do {
            if (tryCloseContext())
                return;
            // Unless there are bugs in this codebase, this should never happen. But anyway it's
            // probably a good idea to make this check rather than not to make.
            if (contextLock == CONTEXT_CLOSED)
                return;
            Thread.yield();
            long now = System.currentTimeMillis();
            if (now != lastTime) {
                lastTime = now;
                timeoutMillis--;
            }
        } while (timeoutMillis >= 0);
        throw new RuntimeException(chronicleHashIdentityString +
                ": Failed to close a context, belonging to the thread\n" +
                owner() + ", in the state: " + owner().getState() + "\n" +
                "Possible reasons:\n" +
                "- The context owner thread exited before closing this context. Ensure that you\n" +
                "always close opened Chronicle Map's contexts, the best way to do this is to use\n" +
                "try-with-resources blocks." +
                "- The context owner thread runs some context operation (e. g. a query) for\n" +
                "unexpectedly long time (at least " + LOCK_TIMEOUT_SECONDS + " seconds).\n" +
                "You should either redesign your logic to spend less time in Chronicle Map\n" +
                "contexts (recommended) or synchronize map.close() with queries externally,\n" +
                "so that close() is called only after all query operations finished.\n" +
                "- Iteration over a large Chronicle Map takes more than " + LOCK_TIMEOUT_SECONDS +
                " seconds.\n" +
                "In this case you should synchronize map.close() with iterations over the map\n" +
                "externally, so that close() is called only after all iterations are finished.\n" +
                "- This is a dead lock involving the context owner thread and this thread (from\n" +
                "which map.close() method is called. Make sure you always close Chronicle Map\n" +
                "contexts, preferably using try-with-resources blocks.");
    }

    private boolean tryCloseContext() {
        return MEMORY.compareAndSwapInt(this, CONTEXT_LOCK_OFFSET,
                CONTEXT_UNLOCKED, CONTEXT_CLOSED);
    }

    public abstract Thread owner();

    /**
     * Returns if this ThreadLocalState prevents a Map from being closed (which may be an asynchronous snapshot operation).
     * <p>
     * This method returns a snapshot value from a potentially volatile source that may change at any time.
     *
     * @return if prevents closing
     */
    public boolean preventClose() {
        return isLocked();
    }

    /**
     * Returns if this ThreadLocalState is locked (asynchronous snapshot operation).
     * <p>
     * This method returns a snapshot value from a volatile source that may change at any time.
     *
     * @return if locked
     */
    protected boolean isLocked() {
        return contextLock == CONTEXT_LOCKED_LOCALLY;
    }

}
