/*
 * 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.map.VanillaChronicleMap;
import net.openhft.sg.Stage;
import net.openhft.sg.Staged;

import java.util.ArrayList;
import java.util.List;
import java.util.function.BiFunction;

@Staged
public abstract class Chaining extends ChainingInterface {

    public final List<ChainingInterface> contextChain;
    public final int indexInContextChain;
    /**
     * First context, ever created in this thread. rootContextInThisThread === contextChain.get(0).
     */
    public final ChainingInterface rootContextInThisThread;
    @Stage("Used")
    public boolean used;
    @Stage("Used")
    private boolean firstContextLockedInThisThread;

    public Chaining(VanillaChronicleMap map) {
        contextChain = new ArrayList<>();
        contextChain.add(this);
        indexInContextChain = 0;
        rootContextInThisThread = this;
        initMap(map);
    }

    public Chaining(ChainingInterface rootContextInThisThread, VanillaChronicleMap map) {
        contextChain = rootContextInThisThread.getContextChain();
        indexInContextChain = contextChain.size();
        contextChain.add(this);
        this.rootContextInThisThread = rootContextInThisThread;
        initMap(map);
    }

    private static <T extends ChainingInterface> T initUsedAndReturn(
            VanillaChronicleMap map, ChainingInterface context) {
        try {
            context.initUsed(true, map);
            //noinspection unchecked
            return (T) context;
        } catch (Throwable throwable) {
            try {
                ((AutoCloseable) context).close();
            } catch (Throwable t) {
                throwable.addSuppressed(t);
            }
            throw throwable;
        }
    }

    @Override
    public List<ChainingInterface> getContextChain() {
        return contextChain;
    }

    public <T> T contextAtIndexInChain(int index) {
        //noinspection unchecked
        return (T) contextChain.get(index);
    }

    /**
     * This method stores a reference to the context's owner ChronicleMap into a field of the
     * context in the beginning of each usage of the context. Previously, this field was final and
     * set only once during context creation. It was preventing ChronicleMap objects from becoming
     * dead and collected by the GC, while any thread, from which the ChronicleMap was accessed
     * (hence a thread local context created), is alive.
     * <p>
     * <p>The chain of strong references:
     * 1) Thread ->
     * 2) ThreadLocalMap ->
     * 3) Entry with ThreadLocal {@link net.openhft.chronicle.map.VanillaChronicleMap#cxt} as weak
     * referent and a context (e. g. {@link net.openhft.chronicle.map.impl.CompiledMapQueryContext})
     * as value (a simple field, not a weak reference!) ->
     * 4) final reference to the owner {@link VanillaChronicleMap} ->
     * 5) ThreadLocal {@link net.openhft.chronicle.map.VanillaChronicleMap#cxt} (a strong reference
     * this time! note that this ThreadLocal is an instance field of VanillaChronicleMap)
     * <p>
     * <p>So in order to break this chain at step 4), contexts store references to their owner
     * ChronicleMaps only when contexts are used.
     */
    public abstract void initMap(VanillaChronicleMap map);

    @Override
    public boolean usedInit() {
        return used;
    }

    /**
     * Init method parameterized to avoid automatic initialization. {@code used} argument should
     * always be {@code true}.
     */
    @Override
    public void initUsed(boolean used, VanillaChronicleMap map) {
        assert used;
        firstContextLockedInThisThread = rootContextInThisThread.lockContextLocally(map);
        initMap(map);
        this.used = true;
    }

    @SuppressWarnings("unused")
    void closeUsed() {
        used = false;
        if (firstContextLockedInThisThread)
            rootContextInThisThread.unlockContextLocally();
    }

    @Override
    public <T extends ChainingInterface> T getContext(
            Class<? extends T> contextClass, BiFunction<ChainingInterface,
            VanillaChronicleMap, T> createChaining,
            VanillaChronicleMap map) {
        for (int i = 0; i < contextChain.size(); i++) {
            ChainingInterface context = contextChain.get(i);
            if (context.getClass() == contextClass && !context.usedInit()) {
                return initUsedAndReturn(map, context);
            }
        }
        int maxNestedContexts = 1 << 10;
        if (contextChain.size() > maxNestedContexts) {
            throw new IllegalStateException(map.toIdentityString() +
                    ": More than " + maxNestedContexts + " nested ChronicleHash contexts\n" +
                    "are not supported. Very probable that you simply forgot to close context\n" +
                    "somewhere (recommended to use try-with-resources statement).\n" +
                    "Otherwise this is a bug, please report with this\n" +
                    "stack trace on https://github.com/OpenHFT/Chronicle-Map/issues");
        }
        //noinspection unchecked
        T context = createChaining.apply(this, map);
        return initUsedAndReturn(map, context);
    }
}
