package net.ranides.assira.generic;

import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
import lombok.experimental.UtilityClass;

import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Supplier;

@UtilityClass
public class LazyCallers {

    /**
     * Initialization is unsynchronized. There is no guarantee about thread-safety.
     * @param <T> t
     * @return function
     */
    public static <T> LazyCaller<T> unique() {
        return new Unique<>();
    }

    /**
     * Initialization is done once per thread. Every thread will have different thread-local object.
     * @param <T> T
     * @return function
     */
    public static <T> LazyCaller<T> local() {
        return new Local<>();
    }

    /**
     * Initialization is synchronized using correctly implemented "double checked locking".
     * Requires JDK 1.5+ because uses new "volatile semantics".
     * @param <T> T
     * @return function
     */
    public static <T> LazyCaller<T> sharedVolatile() {
        return new SharedVolatile<>();
    }

    /**
     * Initialization is synchronized using correctly implemented "double checked locking".
     * It uses wrapper object with "final" attribute instead of "volatile". I don't
     * know anything about compatibility of that with JDK &lt; 1.5.
     * @param <T> T
     * @return function
     */
    public static <T> LazyCaller<T> shared() {
        return new Shared<>();
    }

    /**
     * Initialization is lock-free. There is a chance of spurious "double initialization".
     * @param <T> T
     * @return function
     */
    public static <T> LazyCaller<T> concurrent() {
        return new Concurrent<>();
    }

    private static class Unique<T> implements LazyCaller<T> {

        private T ret;

        @Override
        public T computeIfAbsent(Supplier<T> supplier) {
            if( ret == null) {
                ret = supplier.get();
            }
            return ret;
        }

    }

    private static class SharedVolatile<T> implements LazyCaller<T> {

        // works in JDK5 +
        // http://www.cs.umd.edu/~pugh/java/memoryModel/DoubleCheckedLocking.html

        private volatile T ref = null;

        @Override
        public T computeIfAbsent(Supplier<T> supplier) {
            T temp = ref;
            if(null == temp) {
                synchronized(this) {
                    temp = ref;
                    if(temp == null) {
                        ref = temp = supplier.get();
                    }
                }
            }
            return temp;
        }

    }

    private static final class SharedWrapper<T> {

        public final T value;

        public SharedWrapper(T value) {
            this.value = value;
        }

    }

    @SuppressWarnings({"DoubleCheckedLocking"})
    @SuppressFBWarnings({"DC_DOUBLECHECK","IS2_INCONSISTENT_SYNC"})
    private static final class Shared<T> implements LazyCaller<T> {

        private SharedWrapper<T> ref = null;

        @Override
        public T computeIfAbsent(Supplier<T> supplier) {
            SharedWrapper<T> temp = ref;
            if (temp == null) {
                synchronized(this) {
                    temp = ref;
                    if (temp == null) {
                        ref = temp = new SharedWrapper<>(supplier.get());
                    }
                }
            }
            return temp.value;
        }

    }

    private static class Local<T> implements LazyCaller<T> {

        private final ThreadLocal<T> cmp = new ThreadLocal<>();

        @Override
        public T computeIfAbsent(Supplier<T> supplier) {
            T out = cmp.get();
            if(out == null) {
                cmp.set(out = supplier.get());
            }
            return out;
        }
    }

    private static class Concurrent<T> implements LazyCaller<T> {

        private final AtomicReference<T> ref = new AtomicReference<>(null);

        @Override
        public T computeIfAbsent(Supplier<T> supplier) {
            T temp = ref.get();
            if (temp == null) {
                temp = supplier.get();
                if (ref.compareAndSet(null, temp)) {
                    return temp;
                } else {
                    return ref.get();
                }
            } else {
                return temp;
            }
        }
    }
}
