package tc.oc.commons.core.util;

import java.time.Duration;
import java.time.Instant;
import java.util.function.Supplier;
import javax.inject.Inject;
import javax.inject.Provider;

/**
 * General-purpose container for a lazily calculated value.
 * Both thread-safe and thread-unsafe variants are available.
 *
 *     Lazy<String> text = Lazy.from(() -> "Expensive string");
 *     text.get();
 *
 * There is also an injectable constructor that injects a Provider<T>,
 * so you can simply self-bind this class to get a cached provider
 * of anything.
 */
public class Lazy<T> implements Supplier<T>, Provider<T> {

    private final Supplier<T> supplier;
    private boolean got;
    private T instance;

    @Inject private Lazy(Provider<T> provider) {
        this.supplier = provider::get;
    }

    private Lazy(Supplier<T> supplier) {
        this.supplier = supplier;
    }

    @Override
    public T get() {
        if(!got) {
            instance = supplier.get();
            got = true;
        }
        return instance;
    }

    public void clear() {
        got = false;
        instance = null;
    }

    /**
     * Create a lazy value calculated by the given {@link Supplier}.
     *
     * The lazy container is not thread-safe. If the value is
     * retrieved concurrently from multiple threads, it may be
     * calculated more than once.
     */
    public static <T> Lazy<T> from(Supplier<T> supplier) {
        return new Lazy<>(supplier);
    }

    /**
     * Create a thread-safe lazy value calculated by the given {@link Supplier}.
     */
    public static <T> Lazy<T> sync(Supplier<T> supplier) {
        return new SyncLazy<>(supplier);
    }

    private static class SyncLazy<T> extends Lazy<T> {
        private SyncLazy(Supplier<T> supplier) {
            super(supplier);
        }

        @Override
        synchronized public T get() {
            return super.get();
        }
    }

    public static <T> Lazy<T> expiring(Duration maxAge, Supplier<T> supplier) {
        return new Expiring<>(maxAge, supplier);
    }

    private static class Expiring<T> extends Lazy<T> {
        private final Duration max;
        private Instant last = TimeUtils.INF_PAST;

        private Expiring(Duration max, Supplier<T> supplier) {
            super(supplier);
            this.max = max;
        }

        @Override
        public T get() {
            if(Comparables.greaterThan(Duration.between(last, Instant.now()), max)) {
                clear();
            }
            return super.get();
        }
    }
}
