package dev.isxander.yacl3.impl;

import com.google.common.collect.ImmutableSet;
import dev.isxander.yacl3.api.*;
import dev.isxander.yacl3.api.controller.ControllerBuilder;
import dev.isxander.yacl3.impl.utils.YACLConstants;
import net.minecraft.ChatFormatting;
import net.minecraft.network.chat.Component;
import org.apache.commons.lang3.Validate;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

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

@ApiStatus.Internal
public class OptionImpl<T> implements Option<T> {
    private final Component name;
    private OptionDescription description;
    private final Controller<T> controller;
    private boolean available;

    private final ImmutableSet<OptionFlag> flags;

    private final StateManager<T> stateManager;
    private final List<OptionEventListener<T>> listeners;
    private int currentListenerDepth;

    public OptionImpl(
            @NotNull Component name,
            @NotNull Function<T, OptionDescription> descriptionFunction,
            @NotNull Function<Option<T>, Controller<T>> controlGetter,
            @NotNull StateManager<T> stateManager,
            boolean available,
            ImmutableSet<OptionFlag> flags,
            @NotNull Collection<OptionEventListener<T>> listeners
    ) {
        this.name = name;
        this.available = available;
        this.flags = flags;
        this.listeners = new ArrayList<>(listeners);

        this.stateManager = stateManager;
        this.controller = controlGetter.apply(this);

        this.stateManager.addListener((oldValue, newValue) ->
                triggerListener(OptionEventListener.Event.STATE_CHANGE, false));
        addEventListener((opt, event) -> description = descriptionFunction.apply(opt.pendingValue()));
        triggerListener(OptionEventListener.Event.INITIAL, false);
    }

    @Override
    public @NotNull Component name() {
        return name;
    }

    @Override
    public @NotNull OptionDescription description() {
        return this.description;
    }

    @Override
    public @NotNull Component tooltip() {
        return description.text();
    }

    @Override
    public @NotNull Controller<T> controller() {
        return controller;
    }

    @Override
    public @NotNull StateManager<T> stateManager() {
        return stateManager;
    }

    @Override
    @Deprecated
    public @NotNull Binding<T> binding() {
        if (stateManager instanceof ProvidesBindingForDeprecation) {
            return ((ProvidesBindingForDeprecation<T>) stateManager).getBinding();
        }
        throw new UnsupportedOperationException("Binding is not available for this option - using a new state manager which does not directly expose the binding as it may not have one.");
    }

    @Override
    public boolean available() {
        return available;
    }

    @Override
    public void setAvailable(boolean available) {
        boolean changed = this.available != available;

        this.available = available;

        if (changed) {
            if (!available) {
                this.stateManager.sync();
            }
            this.triggerListener(OptionEventListener.Event.AVAILABILITY_CHANGE, !available);
        }
    }

    @Override
    public @NotNull ImmutableSet<OptionFlag> flags() {
        return flags;
    }

    @Override
    public boolean changed() {
        return !this.stateManager.isSynced();
    }

    @Override
    public @NotNull T pendingValue() {
        return this.stateManager.get();
    }

    @Override
    public void requestSet(@NotNull T value) {
        Validate.notNull(value, "`value` cannot be null");

        this.stateManager.set(value);
    }

    @Override
    public boolean applyValue() {
        if (changed()) {
            this.stateManager.apply();
            return true;
        }
        return false;
    }

    @Override
    public void forgetPendingValue() {
        this.stateManager.sync();
    }

    @Override
    public void requestSetDefault() {
        this.stateManager.resetToDefault(StateManager.ResetAction.BY_OPTION);
    }

    @Override
    public boolean isPendingValueDefault() {
        return this.stateManager.isDefault();
    }

    @Override
    public void addEventListener(OptionEventListener<T> listener) {
        this.listeners.add(listener);
    }

    @Override
    @Deprecated
    public void addListener(BiConsumer<Option<T>, T> changedListener) {
        addEventListener((opt, event) -> changedListener.accept(opt, opt.pendingValue()));
    }

    private void triggerListener(OptionEventListener.Event event, boolean allowDepth) {
        if (allowDepth || currentListenerDepth == 0) {
            Validate.isTrue(
                    currentListenerDepth <= 10,
                    "Listener depth exceeded 10! Possible cyclic listener pattern: a listener triggered an event that triggered the initial event etc etc."
            );

            currentListenerDepth++;

            for (OptionEventListener<T> listener : listeners) {
                listener.onEvent(this, event);
            }

            currentListenerDepth--;
        }
    }

    @ApiStatus.Internal
    public static class BuilderImpl<T> implements Builder<T> {
        private Component name = Component.literal("Name not specified!").withStyle(ChatFormatting.RED);

        private Function<T, OptionDescription> descriptionFunction = pending -> OptionDescription.EMPTY;

        private Function<Option<T>, Controller<T>> controlGetter;


        private boolean available = true;

        private final Set<OptionFlag> flags = new HashSet<>();

        private final List<OptionEventListener<T>> listeners = new ArrayList<>();

        private @Nullable Binding<T> binding;
        private boolean instantDeprecated = false;

        private @Nullable StateManager<T> stateManager;

        @Override
        public Builder<T> name(@NotNull Component name) {
            Validate.notNull(name, "`name` cannot be null");

            this.name = name;
            return this;
        }

        @Override
        public Builder<T> description(@NotNull OptionDescription description) {
            return description(opt -> description);
        }

        @Override
        public Builder<T> description(@NotNull Function<T, OptionDescription> descriptionFunction) {
            this.descriptionFunction = descriptionFunction;
            return this;
        }

        @Override
        public Builder<T> controller(@NotNull Function<Option<T>, ControllerBuilder<T>> controllerBuilder) {
            Validate.notNull(controllerBuilder, "`controllerBuilder` cannot be null");

            return customController(opt -> controllerBuilder.apply(opt).build());
        }

        @Override
        public Builder<T> customController(@NotNull Function<Option<T>, Controller<T>> control) {
            Validate.notNull(control, "`control` cannot be null");

            this.controlGetter = control;
            return this;
        }

        @Override
        public Builder<T> stateManager(@NotNull StateManager<T> stateManager) {
            Validate.notNull(stateManager, "`stateManager` cannot be null");

            Validate.isTrue(binding == null, "Cannot set state manager when binding is set");
            Validate.isTrue(!instantDeprecated, "Cannot set state manager when instant is set");

            this.stateManager = stateManager;
            return this;
        }

        @Override
        public Builder<T> binding(@NotNull Binding<T> binding) {
            Validate.notNull(binding, "`binding` cannot be null");
            Validate.isTrue(stateManager == null, "Cannot set binding when state manager is set");

            this.binding = binding;
            return this;
        }

        @Override
        public Builder<T> binding(@NotNull T def, @NotNull Supplier<@NotNull T> getter, @NotNull Consumer<@NotNull T> setter) {
            Validate.notNull(def, "`def` must not be null");
            Validate.notNull(getter, "`getter` must not be null");
            Validate.notNull(setter, "`setter` must not be null");

            return binding(Binding.generic(def, getter, setter));
        }

        @Override
        public Builder<T> available(boolean available) {
            this.available = available;
            return this;
        }

        @Override
        public Builder<T> flag(@NotNull OptionFlag... flag) {
            Validate.notNull(flag, "`flag` must not be null");

            this.flags.addAll(Arrays.asList(flag));
            return this;
        }

        @Override
        public Builder<T> flags(@NotNull Collection<? extends OptionFlag> flags) {
            Validate.notNull(flags, "`flags` must not be null");

            this.flags.addAll(flags);
            return this;
        }

        @Override
        @Deprecated
        public Builder<T> instant(boolean instant) {
            Validate.isTrue(stateManager == null, "Cannot set instant when state manager is set");
            YACLConstants.LOGGER.error("Option.Builder#instant is deprecated behaviour. Please use a custom state manager instead: `.state(StateManager.createInstant(Binding))`");

            this.instantDeprecated = instant;
            return this;
        }

        @Override
        public Builder<T> addListener(@NotNull OptionEventListener<T> listener) {
            Validate.notNull(listener, "`listener` must not be null");

            this.listeners.add(listener);
            return this;
        }

        @Override
        public Builder<T> addListeners(@NotNull Collection<@NotNull OptionEventListener<T>> optionEventListeners) {
            Validate.notNull(optionEventListeners, "`optionEventListeners` must not be null");

            this.listeners.addAll(optionEventListeners);
            return this;
        }

        @Override
        public Builder<T> listener(@NotNull BiConsumer<Option<T>, T> listener) {
            Validate.notNull(listener, "`listener` must not be null");

            return this.addListener((opt, event) -> listener.accept(opt, opt.pendingValue()));
        }

        @Override
        public Builder<T> listeners(@NotNull Collection<BiConsumer<Option<T>, T>> listeners) {
            Validate.notNull(listeners, "`listeners` must not be null");

            this.addListeners(listeners.stream()
                    .map(listener ->
                            (OptionEventListener<T>) (opt, event) ->
                                    listener.accept(opt, opt.pendingValue())
                    ).toList()
            );
            return this;
        }

        @Override
        public Option<T> build() {
            Validate.notNull(controlGetter, "`control` must not be null when building `Option`");

            if (instantDeprecated) {
                if (binding == null) {
                    throw new IllegalStateException("Cannot build option with instant when binding is not set");
                }
                Validate.isTrue(flags.isEmpty(), "instant application does not support option flags");

                this.stateManager = StateManager.createInstant(binding);
            } else if (binding != null) {
                stateManager = StateManager.createSimple(binding);
            }
            Validate.notNull(stateManager, "State manager must be set, either by using .binding() to create a simple manager or .state() to create an advanced one");

            Validate.isTrue(!stateManager.isAlwaysSynced() || flags.isEmpty(), "Always synced state managers do not support option flags.");

            return new OptionImpl<>(name, descriptionFunction, controlGetter, stateManager, available, ImmutableSet.copyOf(flags), listeners);
        }
    }
}