package net.andreinc.mockneat.unit.seq;

import net.andreinc.mockneat.MockNeat;
import net.andreinc.mockneat.abstraction.MockUnit;
import net.andreinc.mockneat.types.enums.DictType;
import net.andreinc.mockneat.utils.file.FileManager;

import java.util.Arrays;
import java.util.Iterator;
import java.util.List;
import java.util.function.Supplier;

import static net.andreinc.mockneat.utils.ValidationUtils.*;

public final class Seq<T> implements MockUnit<T> {

    private final Iterable<T> iterable;
    private Iterator<T> iterator;

    private boolean cycle;
    private Supplier<T> after;

    /**
     * <p>Returns a new {@code Seq<String>} object that can be used to generate values by iterating through all the lines associated with a {@code DictType}</p>
     *
     * @param dictType The {@code DictType} to iterate over
     * @return A re-usable {@code Seq<String>}. The class implements {@code MockUnitString}
     */
    public static Seq<?> seq(DictType dictType) {
        return MockNeat.threadLocal().seq(dictType);
    }

    /**
     * <p>Returns a new {@code Seq<T>} object that can be used to generate values by iterating trough the given {@code Iterable<T>} in order.</p>
     *
     * @param iterable The {@code Iterable<T>} values.
     * @param <T> The type of both the resulting {@code Seq<T>} and the {@code Iterable<T>}
     *
     * @return A re-usable {@code Seq<T>} object. The class implements {@code MockUnit<T>}
     */
    public static <T> Seq<T> seq(Iterable<T> iterable) {
        return MockNeat.threadLocal().seq(iterable);
    }


    /**
     * <p>Returns a {@code Seq<T>} object that can be used to generate values by iterating through all the elements of an array in order.<p>
     *
     * @param array The array to iterate over
     * @param <T> The type of the array
     * @return A re-usable {@code Seq<T>}. The class implement {@code MockUnit<T>}
     */
    public static <T> Seq<T> seq(T[] array) {
        return MockNeat.threadLocal().seq(array);
    }

    /**
     * <p>Returns a new {@code Seq<String>} object that can be used to generate values by iterating through all the lines associated with a {@code DictType}</p>
     *
     * @param dictType The {@code DictType} to iterate over
     * @return A re-usable {@code Seq<String>}. The class implements {@code MockUnitString}
     */
    public static Seq<String> fromDict(DictType dictType) {
        notNull(dictType, "dictType");
        List<String> lines = FileManager.getInstance().getLines(dictType);
        return new Seq<>(lines);
    }

    /**
     * <p>Returns a new {@code Seq<T>} object that can be used to generate values by iterating trough the given {@code Iterable<T>} in order.</p>
     *
     * @param iterable The {@code Iterable<T>} values.
     * @param <T> The type of both the resulting {@code Seq<T>} and the {@code Iterable<T>}
     *
     * @return A re-usable {@code Seq<T>} object. The class implements {@code MockUnit<T>}
     */
    public static <T> Seq<T> fromIterable(Iterable<T> iterable) {
        notNull(iterable, "iterable");
        return new Seq<>(iterable);
    }

    /**
     * <p>Returns a {@code Seq<T>} object that can be used to generate values by iterating through all the elements of an array in order.<p>
     *
     * @param array The array to iterate over
     * @param <T> The type of the array
     * @return A re-usable {@code Seq<T>}. The class implement {@code MockUnit<T>}
     */
    public static <T> Seq<T> fromArray(T[] array) {
        notNull(array, "array");
        return new Seq<>(Arrays.asList(array));
    }

    private Seq(Iterable<T> iterable) {
        notNull(iterable, "iterable");
        this.iterable = iterable;
        this.iterator = iterable.iterator();
        isTrue(iterator.hasNext(), IMPOSSIBLE_TO_SEQ_OVER_EMPTY_COLLECTION);
    }

    /**
     * <p>Enables cycling through the elements if the finite source of elements is exhausted.</p>
     *
     * <p>For example if an array has 3 elements, and we create the sequence from it, after calling the {@code get()} 3 times, a {@code null} will be retrieved.</p>
     *
     * @param value By default it's false
     * @return The same {@code Seq<?>} object.
     */
    public Seq<T> cycle(boolean value) {
        this.cycle = value;
        return this;
    }

    /**
     * <p>If {@link #cycle(boolean)} is not enabled, the @param after is the default value generated by the sequence.</p>
     *
     * @param after The default value after Sequence it's generated.
     * @return The same {@code Seq<T>} object.
     */
    public Seq<T> after(T after) {
        this.after = () -> after;
        return this;
    }

    /**
     * <p>If {@link #cycle(boolean)} is not enabled, this method is used to generate values after the sequence is exhausted.</p>
     * @param after The {@code MockUnit<T>} to be used after the sequence is exhausted.
     * @return The same {@code Seq<T>} object.
     */
    public Seq<T> afterDoMock(MockUnit<T> after) {
        this.after = after.supplier();
        return this;
    }

    @Override
    public Supplier<T> supplier() {
        return () -> {
            if (iterator.hasNext())
                return (T) iterator.next();
            else
                if (cycle) {
                    this.iterator = iterable.iterator();
                    return (T) iterator.next();
                }
                else {
                    if (after == null) {
                        return null;
                    }
                    return (T) after.get();
                }
        };
    }

}
