package dev.isxander.yacl3.api;

import com.google.common.collect.ImmutableSet;
import dev.isxander.yacl3.api.controller.ControllerBuilder;
import dev.isxander.yacl3.impl.OptionImpl;
import net.minecraft.network.chat.Component;
import org.jetbrains.annotations.NotNull;

import java.util.*;
import java.util.function.BiConsumer;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Supplier;

public interface Option<T> {
    /**
     * Name of the option
     */
    @NotNull Component name();

    @NotNull OptionDescription description();

    /**
     * Tooltip (or description) of the option.
     * Rendered on hover.
     */
    @Deprecated
    @NotNull Component tooltip();

    /**
     * Widget provider for a type of option.
     *
     * @see dev.isxander.yacl3.gui.controllers
     */
    @NotNull Controller<T> controller();

    /**
     * Binding for the option.
     * Controls setting, getting and default value.
     *
     * @see Binding
     */
    @NotNull Binding<T> binding();

    /**
     * If the option can be configured
     */
    boolean available();

    /**
     * Sets if the option can be configured after being built
     *
     * @see Option#available()
     */
    void setAvailable(boolean available);

    /**
     * Tasks that needs to be executed upon applying changes.
     */
    @NotNull ImmutableSet<OptionFlag> flags();

    /**
     * Checks if the pending value is not equal to the current set value
     */
    boolean changed();

    /**
     * Value in the GUI, ready to set the actual bound value or be undone.
     */
    @NotNull T pendingValue();

    /**
     * Sets the pending value
     */
    void requestSet(@NotNull T value);

    /**
     * Applies the pending value to the bound value.
     * Cannot be undone.
     *
     * @return if there were changes to apply {@link Option#changed()}
     */
    boolean applyValue();

    /**
     * Sets the pending value to the bound value.
     */
    void forgetPendingValue();

    /**
     * Sets the pending value to the default bound value.
     */
    void requestSetDefault();

    /**
     * Checks if the current pending value is equal to its default value
     */
    boolean isPendingValueDefault();

    default boolean canResetToDefault() {
        return true;
    }

    /**
     * Adds a listener for when the pending value changes
     */
    void addListener(BiConsumer<Option<T>, T> changedListener);

    static <T> Builder<T> createBuilder() {
        return new OptionImpl.BuilderImpl<>();
    }

    /**
     * Creates a builder to construct an {@link Option}
     *
     * @param <T> type of the option's value
     * @param typeClass used to capture the type
     */
    @Deprecated
    static <T> Builder<T> createBuilder(Class<T> typeClass) {
        return createBuilder();
    }

    interface Builder<T> {
        /**
         * Sets the name to be used by the option.
         *
         * @see Option#name()
         */
        Builder<T> name(@NotNull Component name);

        /**
         * Sets the description to be used by the option.
         * @see OptionDescription
         * @param description the static description.
         * @return this builder
         */
        Builder<T> description(@NotNull OptionDescription description);

        /**
         * Sets the function to get the description by the option's current value.
         *
         * @see OptionDescription
         * @param descriptionFunction the function to get the description by the option's current value.
         * @return this builder
         */
        Builder<T> description(@NotNull Function<T, OptionDescription> descriptionFunction);

        Builder<T> controller(@NotNull Function<Option<T>, ControllerBuilder<T>> controllerBuilder);

        /**
         * Sets the controller for the option.
         * This is how you interact and change the options.
         *
         * @see dev.isxander.yacl3.gui.controllers
         */
        Builder<T> customController(@NotNull Function<Option<T>, Controller<T>> control);

        /**
         * Sets the binding for the option.
         * Used for default, getter and setter.
         *
         * @see Binding
         */
        Builder<T> binding(@NotNull Binding<T> binding);

        /**
         * Sets the binding for the option.
         * Shorthand of {@link Binding#generic(Object, Supplier, Consumer)}
         *
         * @param def default value of the option, used to reset
         * @param getter should return the current value of the option
         * @param setter should set the option to the supplied value
         * @see Binding
         */
        Builder<T> binding(@NotNull T def, @NotNull Supplier<@NotNull T> getter, @NotNull Consumer<@NotNull T> setter);

        /**
         * Sets if the option can be configured
         *
         * @see Option#available()
         */
        Builder<T> available(boolean available);

        /**
         * Adds a flag to the option.
         * Upon applying changes, all flags are executed.
         * {@link Option#flags()}
         */
        Builder<T> flag(@NotNull OptionFlag... flag);

        /**
         * Adds a flag to the option.
         * Upon applying changes, all flags are executed.
         * {@link Option#flags()}
         */
        Builder<T> flags(@NotNull Collection<? extends OptionFlag> flags);

        /**
         * Instantly invokes the binder's setter when modified in the GUI.
         * Prevents the user from undoing the change
         * <p>
         * Does not support {@link Option#flags()}!
         */
        Builder<T> instant(boolean instant);

        /**
         * Adds a listener to the option. Invoked upon changing the pending value.
         *
         * @see Option#addListener(BiConsumer)
         */
        Builder<T> listener(@NotNull BiConsumer<Option<T>, T> listener);

        /**
         * Adds multiple listeners to the option. Invoked upon changing the pending value.
         *
         * @see Option#addListener(BiConsumer)
         */
        Builder<T> listeners(@NotNull Collection<BiConsumer<Option<T>, T>> listeners);

        Option<T> build();
    }
}