/*
 * Copyright (c) Huawei Technologies Co., Ltd. 2021-2021. All rights reserved.
 */

package net.ranides.assira.functional;

import lombok.RequiredArgsConstructor;
import lombok.experimental.UtilityClass;

import net.ranides.assira.generic.Ref;
import net.ranides.assira.generic.TypeToken;
import net.ranides.assira.reflection.IClass;
import net.ranides.assira.trace.PreparedException;

import java.util.NoSuchElementException;
import java.util.function.Function;
import java.util.function.Supplier;

/**
 * Class contains functional implementation of switch-case construct.
 * Useful if we want to switch over values, which are not constant at compile-time.
 *
 * @author Ranides Atterwim {@literal <ranides@gmail.com>}
 */
@UtilityClass
public class Branch {

    private static final Function<Ref<?>, NoSuchElementException> NO_RESULT = PreparedException
        .runtime(NoSuchElementException.class)
        .hideHelpers()
        .message("No result. Selector input: {0}")
        .params()
        .withParam(new TypeToken<Ref<?>>() {})
        .function();

    private static final Functions.Function3<Ref<?>, Ref<?>, Ref<?>, NoSuchElementException> MANY_RESULTS = PreparedException
        .runtime(NoSuchElementException.class)
        .hideHelpers()
        .message("Ambiguous result. Selector input: {0}. First result: {1}. Second result is {2}.")
        .params()
        .withParam(new TypeToken<Ref<?>>() {})
        .withParam(new TypeToken<Ref<?>>() {})
        .withParam(new TypeToken<Ref<?>>() {})
        .function();

    /**
     * Creates new selector, which will return first non-null value.
     *
     * @return new selector
     */
    public static BranchSelector<Object> first() {
        return new BranchChain<>(true, null);
    }

    /**
     * Creates new selector, which stores specified input value, and examines all conditions declared
     * in chain, to return result matching specified input.
     *
     * Effectively, it is more flexible, functional version of switch-case construct.
     *
     * @param value input
     * @return new selector
     */
    public static BranchSelector<Object> first(Object value) {
        return new BranchChain<>(true, value);
    }

    /**
     * Creates new selector, which will return first non-null value.
     * It checks if there is exactly one unambiguous non-null result
     *
     * @return new selector
     */
    public static BranchSelector<Object> single() {
        return new BranchChain<>(false, null);
    }

    /**
     * Creates new selector, which stores specified input value, and examines all conditions declared
     * in chain, to return result matching specified input.
     * It checks if there is exactly one unambiguous non-null result
     *
     * Effectively, it is more flexible, non-ambiguous, functional version of switch-case construct.
     *
     * @param value input
     * @return new selector
     */
    public static BranchSelector<Object> single(Object value) {
        return new BranchChain<>(false, value);
    }

    /**
     * Branch selector, which allows you to declare switch-case construct by chaining conditions one by one.
     * Every branch selector is initially constructed by one of factory methods, which defines basic contract.
     * Then, you can add specific clauses into it, and finally query for result.
     *
     * Please note, that every clause can declare narrowed result type, although if you shouldn't abuse
     * this functionality, because class cast exception will be thrown at the end. Ideally, you should use
     * this functionality to define result type in first clause and then do not narrow it in next clauses.
     *
     * @param <F> type of result
     */
    public interface BranchSelector<F> {
        /**
         * Returns computed result as optional, possibly empty if none condition is met.
         *
         * @return optional result
         */
        Ref<F> optional();

        /**
         * Returns computed result.
         * If there is no result, it throws exception.
         *
         * @return result
         * @throws NoSuchElementException if there is no result
         */
        F get();

        /**
         * Returns computed result.
         * Returns default value, if there is no result.
         *
         * @param other default result
         * @return result
         */
        F orElse(F other);

        /**
         * BRANCH CLAUSE: return const value if input has specified type.
         *
         * @param type type of input
         * @param result result
         * @param <T> type of input
         * @param <R> type of result, every clause can narrow it
         * @return selector
         */
        <T, R extends F> BranchSelector<R> forType(Class<T> type, R result);

        /**
         * BRANCH CLAUSE: return const value if input has specified type.
         *
         * @param type type of input
         * @param result result
         * @param <T> type of input
         * @param <R> type of result, every clause can narrow it
         * @return selector
         */
        <T, R extends F> BranchSelector<R> forType(IClass<T> type, R result);

        /**
         * BRANCH CLAUSE: return const value if input has specified value.
         *
         * @param condition expected value
         * @param result result
         * @param <T> type of input
         * @param <R> type of result, every clause can narrow it
         * @return selector
         */
        <T, R extends F> BranchSelector<R> forValue(T condition, R result);

        /**
         * BRANCH CLAUSE: return const value if input has specified value generated by supplier.
         * Expected value is computed lazily.
         *
         * @param condition expected value
         * @param result result
         * @param <T> type of input
         * @param <R> type of result, every clause can narrow it
         * @return selector
         */
        <T, R extends F> BranchSelector<R> forExpression(Supplier<T> condition, R result);

        /**
         * BRANCH CLAUSE: return const value if input has specified value.
         *
         * @param condition expected value
         * @param result result
         * @param <T> type of input
         * @param <R> type of result, every clause can narrow it
         * @return selector
         */
        <T, R extends F> BranchSelector<R> forOptional(Ref<T> condition, R result);

        /**
         * BRANCH CLAUSE: return generated value if input has value generated by specified supplier.
         * Result is computed lazily.
         *
         * @param condition expected value
         * @param supplier result
         * @param <T> type of input
         * @param <R> type of result, every clause can narrow it
         * @return selector
         */
        <T, R extends F> BranchSelector<R> forOptional(Supplier<Ref<T>> condition, R supplier);

        /**
         * BRANCH CLAUSE: return generated value if input has specified type.
         * Expected value is computed lazily.
         * Result is computed lazily.
         *
         * @param type type
         * @param supplier supplier
         * @param <T> type of input
         * @param <R> type of result, every clause can narrow it
         * @return selector
         */
        <T, R extends F> BranchSelector<R> computeForType(Class<T> type, Supplier<R> supplier);

        /**
         * BRANCH CLAUSE: return generated value if input has specified type.
         * Result is computed lazily.
         *
         * @param type type
         * @param supplier supplier
         * @param <T> type of input
         * @param <R> type of result, every clause can narrow it
         * @return selector
         */
        <T, R extends F> BranchSelector<R> computeForType(IClass<T> type, Supplier<R> supplier);

        /**
         * BRANCH CLAUSE: return generated value if input has specified value.
         * Result is computed lazily.
         *
         * @param condition condition
         * @param supplier supplier
         * @param <T> type of input
         * @param <R> type of result, every clause can narrow it
         * @return selector
         */
        <T, R extends F> BranchSelector<R> computeForValue(T condition, Supplier<R> supplier);

        /**
         * BRANCH CLAUSE: return generated value if input has the same value as generated by specified supplier.
         * Expected value is computed lazily.
         * Result is computed lazily.
         *
         * @param condition condition
         * @param supplier supplier
         * @param <T> type of input
         * @param <R> type of result, every clause can narrow it
         * @return selector
         */
        <T, R extends F> BranchSelector<R> computeForExpression(Supplier<T> condition, Supplier<R> supplier);

        /**
         * BRANCH CLAUSE: return generated value if input has specified value.
         * Result is computed lazily.
         *
         * @param condition condition
         * @param supplier supplier
         * @param <T> type of input
         * @param <R> type of result, every clause can narrow it
         * @return selector
         */
        <T, R extends F> BranchSelector<R> computeForOptional(Ref<T> condition, Supplier<R> supplier);

        /**
         * BRANCH CLAUSE: return generated value if input has the same value as generated by specified supplier.
         * Expected value is computed lazily.
         * Result is computed lazily.
         *
         * @param condition condition
         * @param supplier supplier
         * @param <T> type of input
         * @param <R> type of result, every clause can narrow it
         * @return selector
         */
        <T, R extends F> BranchSelector<R> computeForOptional(Supplier<Ref<T>> condition, Supplier<R> supplier);

        /**
         * BRANCH CLAUSE: map input into result if input has specified type
         * Result is computed lazily.
         *
         * @param type type
         * @param function function
         * @param <T> type of input
         * @param <R> type of result, every clause can narrow it
         * @return selector
         */
        <T, R extends F> BranchSelector<R> mapType(Class<T> type, Function<T, R> function);

        /**
         * BRANCH CLAUSE: map input into result if input has specified type
         * Result is computed lazily.
         *
         * @param type type
         * @param function function
         * @param <T> type of input
         * @param <R> type of result, every clause can narrow it
         * @return selector
         */
        <T, R extends F> BranchSelector<R> mapType(IClass<T> type, Function<T, R> function);

        /**
         * BRANCH CLAUSE: map input into result if input has specified value
         * Result is computed lazily.
         *
         * @param condition condition
         * @param function function
         * @param <T> type of input
         * @param <R> type of result, every clause can narrow it
         * @return selector
         */
        <T, R extends F> BranchSelector<R> mapValue(T condition, Function<T, R> function);

        /**
         * BRANCH CLAUSE: map input into result if input has the same value as generated by specified supplier.
         * Expected value is computed lazily.
         * Result is computed lazily.
         *
         * @param condition condition
         * @param function function
         * @param <T> type of input
         * @param <R> type of result, every clause can narrow it
         * @return selector
         */
        <T, R extends F> BranchSelector<R> mapExpression(Supplier<T> condition, Function<T, R> function);

        /**
         * BRANCH CLAUSE: map input into result if input has specified value
         * Result is computed lazily.
         *
         * @param condition condition
         * @param function function
         * @param <T> type of input
         * @param <R> type of result, every clause can narrow it
         * @return selector
         */
        <T, R extends F> BranchSelector<R> mapOptional(Ref<T> condition, Function<T, R> function);

        /**
         * BRANCH CLAUSE: map input into result if input has the same value as generated by specified supplier.
         * Expected value is computed lazily.
         * Result is computed lazily.
         *
         * @param condition condition
         * @param function function
         * @param <T> type of input
         * @param <R> type of result, every clause can narrow it
         * @return selector
         */
        <T, R extends F> BranchSelector<R> mapOptional(Supplier<Ref<T>> condition, Function<T, R> function);
    }

    @RequiredArgsConstructor
    private static class BranchChain<F> implements BranchSelector<F> {

        protected final boolean first;

        protected final Ref<Object> input;

        protected final Ref<F> output;

        public BranchChain(boolean first, Object input) {
            this(first, Ref.ofNullable(input), Ref.empty());
        }

        @Override
        public final <T, R extends F> BranchSelector<R> forType(Class<T> type, R result) {
            return mapType(type, v -> result);
        }

        @Override
        public final <T, R extends F> BranchSelector<R> forType(IClass<T> type, R result) {
            return mapType(type, v -> result);
        }

        @Override
        public final <T, R extends F> BranchSelector<R> forValue(T condition, R result) {
            return forOptional(() -> Ref.ofNullable(condition), result);
        }

        @Override
        public final <T, R extends F> BranchSelector<R> forOptional(Ref<T> condition, R result) {
            return forOptional(() -> condition, result);
        }

        @Override
        public final <T, R extends F> BranchSelector<R> forExpression(Supplier<T> condition, R result) {
            return forOptional(() -> Ref.ofNullable(condition.get()), result);
        }

        @Override
        public final <T, R extends F> BranchSelector<R> forOptional(Supplier<Ref<T>> condition, R supplier) {
            return computeForOptional(condition, () -> supplier);
        }

        @Override
        public final <T, R extends F> BranchSelector<R> computeForType(Class<T> type, Supplier<R> supplier) {
            return mapType(type, v -> supplier.get());
        }

        @Override
        public final <T, R extends F> BranchSelector<R> computeForType(IClass<T> type, Supplier<R> supplier) {
            return mapType(type, v -> supplier.get());
        }

        @Override
        public final <T, R extends F> BranchSelector<R> computeForValue(T condition, Supplier<R> supplier) {
            return computeForOptional(() -> Ref.ofNullable(condition), supplier);
        }

        @Override
        public final <T, R extends F> BranchSelector<R> computeForOptional(Ref<T> condition, Supplier<R> supplier) {
            return computeForOptional(() -> condition, supplier);
        }

        @Override
        public final <T, R extends F> BranchSelector<R> computeForExpression(Supplier<T> condition, Supplier<R> supplier) {
            return computeForOptional(() -> Ref.ofNullable(condition.get()), supplier);
        }

        @Override
        public final <T, R extends F> BranchSelector<R> computeForOptional(Supplier<Ref<T>> condition, Supplier<R> supplier) {
            return mapOptional(condition, v -> supplier.get());
        }

        @Override
        public <T, R extends F> BranchSelector<R> mapType(Class<T> type, Function<T, R> function) {
            return mapType(IClass.typeinfo(type), function);
        }

        @Override
        public final <T, R extends F> BranchSelector<R> mapValue(T condition, Function<T, R> function) {
            return mapOptional(() -> Ref.ofNullable(condition), function);
        }

        @Override
        public <T, R extends F> BranchSelector<R> mapOptional(Ref<T> condition, Function<T, R> function) {
            return mapOptional(() -> condition, function);
        }


        @Override
        public final <T, R extends F> BranchSelector<R> mapExpression(Supplier<T> condition, Function<T, R> function) {
            return mapOptional(() -> Ref.ofNullable(condition.get()), function);
        }

        @Override
        public final F get() {
            return optional().orElseThrow(() -> NO_RESULT.apply(input));
        }

        @Override
        public final F orElse(F other) {
            return optional().orElse(other);
        }

        @SuppressWarnings("unchecked")
        protected final <R extends F> BranchSelector<R> self() {
            return BranchChain.class.cast(this);
        }

        @Override
        public Ref<F> optional() {
            return output;
        }

        @Override
        public <T, R extends F> BranchSelector<R> mapType(IClass<T> type, Function<T, R> function) {
            if (isTerminal()) {
                return self();
            }
            if(input.isPresent() && type.isInstance(input.get())) {
                return accumulate(function.apply(type.cast(input.get())));
            }
            return self();
        }



        @Override
        public <T, R extends F> BranchSelector<R> mapOptional(Supplier<Ref<T>> condition, Function<T, R> function) {
            if (isTerminal()) {
                return self();
            }
            Ref<T> optional = condition.get();
            if(optional.isPresent() && (!input.isPresent() || input.equals(optional))) {
                return accumulate(function.apply(optional.get()));
            }
            return self();
        }

        private boolean isTerminal() {
            return first && output.isPresent();
        }

        protected  <R extends F> BranchSelector<R> accumulate(R next) {
            if(next == null) {
                return self();
            }
            if(output.isPresent()) {
                throw MANY_RESULTS.apply(input, output, Ref.of(next));
            }
            return new BranchChain<>(first, input, Ref.of(next));
        }

    }

}