package net.ranides.assira.collection.query.support;

import lombok.experimental.UtilityClass;

import net.ranides.assira.collection.query.CQuery;

import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;

/**
 * Utility class with methods for management state of iteration.
 *
 * Some query operations must uses helper counters, termination conditions or result containers.
 * Those helper objects should work in efficient way both in sequential and parallel context.
 *
 * Generally: sequential state objects are not-thread-safe and parallel use non-blocking, atomic operations.
 * There is no guarantee about synchronization and order, but in exchange, those state management does not
 * degenerate parallel operations into serialized sequential ones.
 */
@UtilityClass
public class BaseState {

    /**
     * Creates new Index object initialized with 0.
     * Supports both sequential and parallel stream.
     *
     * @param query query
     * @return index
     */
    public static Index newIndex(CQuery<?> query) {
        return newIndex(query.features().isParallel());
    }

    /**
     * Creates new Index object initialized with "offset"
     * Supports both sequential and parallel stream.
     *
     * @param query query
     * @param offset offset
     * @return index
     */
    public static Index newIndex(CQuery<?> query, int offset) {
        return newIndex(query.features().isParallel(), offset);
    }

    /**
     * Creates new Index object initialized with 0.
     * Supports both sequential and parallel stream.
     *
     * @param paralell paralell
     * @return index
     */
    public static Index newIndex(boolean paralell) {
        return newIndex(paralell, 0);
    }

    /**
     * Creates new Index object initialized with "offset".
     * Supports both sequential and parallel stream.
     *
     * @param paralell paralell
     * @param offset offset
     * @return index
     */
    public static Index newIndex(boolean paralell, int offset) {
        return paralell ? new PIndex(offset) : new SIndex(offset);
    }

    /**
     * Creates new Terminator object.
     * Supports both sequential and parallel stream.
     *
     * @param query query
     * @return terminator
     */
    public static Terminator newTerminator(CQuery<?> query) {
        return newTerminator(query.features().isParallel());
    }

    /**
     * Creates new Terminator object.
     * Supports both sequential and parallel stream.
     *
     * @param paralell paralell
     * @return terminator
     */
    public static Terminator newTerminator(boolean paralell) {
        return paralell ? new PTerminator() : new STerminator();
    }

    /**
     * Creates new Holder object initialized with null.
     * Supports both sequential and parallel stream.
     *
     * @param query query
     * @param <T> T
     * @return holder
     */
    public static <T> Holder<T> newHolder(CQuery<?> query) {
        return newHolder(query, null);
    }

    /**
     * Creates new Terminator object initialized with null.
     * Supports both sequential and parallel stream.
     *
     * @param paralell paralell
     * @param <T> T
     * @return holder
     */
    public static <T> Holder<T> newHolder(boolean paralell) {
        return newHolder(paralell, null);
    }

    /**
     * Creates new Holder object initialized with "value".
     * Supports both sequential and parallel stream.
     *
     * @param query query
     * @param value value
     * @param <T> T
     * @return holder
     */
    public static <T> Holder<T> newHolder(CQuery<?> query, T value) {
        return newHolder(query.features().isParallel(), value);
    }

    /**
     * Creates new Holder object initialized with "value".
     * Supports both sequential and parallel stream.
     *
     * @param paralell paralell
     * @param value value
     * @param <T> T
     * @return holder
     */
    public static <T> Holder<T> newHolder(boolean paralell, T value) {
        return paralell ? new PHolder<>(value) : new SHolder<>(value);
    }

    /**
     * Terminator object is designed to use inside loops to signal "termination condition".
     * It could be passed across multiple looping methods, preserving state of the loop.
     * Every user should inspect its state on every step.
     *
     * There is no guarantee, that different threads will notice termination instantly.
     * Instead, iteration should stop "reasonably soon".
     */
    public interface Terminator {

        /**
         * Stops iteration
         */
        void stop();

        /**
         * Checks if iteration was stopped by some code.
         * @return boolean
         */
        boolean stopped();

    }

    /**
     * Index object is designed to use inside loops as a counter, to estimate index of processed element.
     * It could be passed across multiple looping methods, preserving index of recently processed item.
     * Every user should call "next" method on every step, even if it doesn't care about "index".
     *
     * There is no guarantee, that different threads will create "correct" index for elements.
     * Instead, there is guarantee, that returned values will be strictly monotonic, won't repeat and won't miss any value.
     *
     * "value" will always return largest processed index.
     */
    public interface Index {

        /**
         * Returns current index and increments its value.
         * No other thread will see the same index once it was returned anywhere.
         *
         * @return int
         */
        int next();

        /**
         * Returns current index.
         * If all worker threads finished iteration, it is will contain largest processed value.
         *
         * @return int
         */
        int value();

    }

    /**
     * Holder object is designed to use inside loops as a result storage.
     * It could be passed across multiple looping methods, allowing to store result without using "return" instruction.
     *
     * There is no guarantee, that different threads will see the same "result".
     * Especially, please note, that "result" stored by one thread could be overwritten by others.
     *
     * Value returned by "get" is extremally race-condition sensitive
     * There is only guarantee, that it will return non-empty value if any thread stored some value.
     *
     * @param <T> T
     */
    public interface Holder<T> {

        /**
         * Stores result, overwrites previous one
         *
         * @param value value
         */
        void set(T value);

        /**
         * Returns result stored by any thread
         *
         * @return result
         */
        T get();

        /**
         * Checks if there is no result yet.
         * Extremally sensitive to race conditions.
         * Should be used only when all worker threads finished iteration.
         *
         * @return boolean
         */
        default boolean isEmpty() {
            return null == get();
        }

    }

    private static class SIndex implements Index {

        private int value;

        public SIndex(int value) {
            this.value = value;
        }

        @Override
        public int next() {
            return value++;
        }

        @Override
        public int value() {
            return value;
        }
    }

    private static class PIndex implements Index {

        private final AtomicInteger value;

        public PIndex(int value) {
            this.value = new AtomicInteger(value);
        }

        @Override
        public int next() {
            return value.getAndIncrement();
        }

        @Override
        public int value() {
            return value.intValue();
        }
    }

    private static class STerminator implements Terminator {

        private boolean value;

        @Override
        public void stop() {
            value = true;
        }

        @Override
        public boolean stopped() {
            return value;
        }
    }

    private static class PTerminator implements Terminator {

        private final AtomicBoolean value = new AtomicBoolean(false);

        @Override
        public void stop() {
            value.set(true);
        }

        @Override
        public boolean stopped() {
            return value.get();
        }
    }

    private static class SHolder<T> implements Holder<T> {

        private T value;

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

        @Override
        public void set(T value) {
            this.value = value;
        }

        @Override
        public T get() {
            return value;
        }
    }

    private static class PHolder<T> implements Holder<T> {

        private final AtomicReference<T> value;

        public PHolder(T value) {
            this.value = new AtomicReference<>(value);
        }

        @Override
        public void set(T value) {
            this.value.set(value);
        }

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


}
