/*
 *   This file is a part of the SteamWar software.
 *
 *   Copyright (C) 2026 SteamWar.de-Serverteam
 *
 *   This program is free software: you can redistribute it and/or modify
 *   it under the terms of the GNU Affero General Public License as published by
 *   the Free Software Foundation, either version 3 of the License, or
 *   (at your option) any later version.
 *
 *   This program is distributed in the hope that it will be useful,
 *   but WITHOUT ANY WARRANTY; without even the implied warranty of
 *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 *   GNU Affero General Public License for more details.
 *
 *   You should have received a copy of the GNU Affero General Public License
 *   along with this program.  If not, see <https://www.gnu.org/licenses/>.
 */

package de.steamwar.velocitycore.advancements;

import com.velocitypowered.api.network.ProtocolVersion;
import com.velocitypowered.api.proxy.Player;
import com.velocitypowered.proxy.connection.MinecraftSessionHandler;
import com.velocitypowered.proxy.connection.client.ConnectedPlayer;
import com.velocitypowered.proxy.protocol.MinecraftPacket;
import com.velocitypowered.proxy.protocol.ProtocolUtils;
import com.velocitypowered.proxy.protocol.packet.chat.ComponentHolder;
import de.steamwar.messages.Chatter;
import de.steamwar.sql.SteamwarUser;
import io.netty.buffer.ByteBuf;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import lombok.ToString;
import net.kyori.adventure.text.Component;

import java.util.*;
import java.util.function.BiFunction;
import java.util.function.Function;

@RequiredArgsConstructor
public class Advancement {

    protected static final Map<SteamwarUser, Map<Advancement.Value.Key, Advancement.Value>> values = new HashMap<>();

    {
        Advancements.all.add(this);
    }

    protected final Map<SteamwarUser, Advancement.Data> data = new HashMap<>();
    protected final Map<SteamwarUser, Advancement.Value> value = new HashMap<>();

    public Advancement.Data get(SteamwarUser user) {
        return get(user, Data::new);
    }

    public Advancement.Data get(SteamwarUser user, BiFunction<Advancement, SteamwarUser, Data> function) {
        if (data.containsKey(user)) return data.get(user);
        return function.apply(this, user);
    }

    private final String identifier;
    private final Optional<Advancement> parent;
    private final Display display;

    private final HidePolicy hidePolicy;
    private final int total;

    private final Function<SteamwarUser, Integer> progressCalculator;

    @Override
    public String toString() {
        StringBuilder st = new StringBuilder();
        st.append("Advancement(");
        parent.ifPresent(advancement -> st.append(advancement.identifier).append("<-"));
        st.append(identifier);
        st.append(", total=").append(total);
        st.append(")");
        return st.toString();
    }

    @RequiredArgsConstructor
    @AllArgsConstructor
    public static class Display {
        private final Component title;
        private final Component description;
        private final String item;
        private final FrameType frameType;
        private Optional<String> background = Optional.empty();
        private final float xCoord;
        private final float yCoord;

        public enum FrameType {
            TASK,
            CHALLENGE,
            GOAL
        }
    }

    public enum HidePolicy {
        NEVER {
            @Override
            public boolean hidden(Data data) {
                return false;
            }
        },
        NO_PROGRESS {
            @Override
            public boolean hidden(Data data) {
                return data.progress == 0;
            }
        },
        PREVIOUS_UNFINISHED {
            @Override
            public boolean hidden(Data data) {
                if (data.advancement.parent.isPresent()) {
                    Advancement parent = data.advancement.parent.get();
                    Advancement.Data parentData = parent.get(data.user);
                    return parentData.progress != parentData.advancement.total;
                } else {
                    return false;
                }
            }
        },
        WITH_PREVIOUS {
            @Override
            public boolean hidden(Data data) {
                if (data.advancement.parent.isPresent()) {
                    Advancement parent = data.advancement.parent.get();
                    Advancement.Data parentData = parent.get(data.user);
                    return parentData.hidden;
                } else {
                    return false;
                }
            }
        },
        ;

        public abstract boolean hidden(Data data);
    }

    public static class Value<T extends Number> {

        @AllArgsConstructor
        public static class Key<T extends Number> {
            public static final List<Key> keys = new ArrayList<>();

            {
                keys.add(this);
            }

            private final Function<SteamwarUser, T> valueFunction;

            private Advancement.Value get(SteamwarUser user) {
                Key self = this;
                return values.computeIfAbsent(user, __ -> new HashMap<>()).computeIfAbsent(self, __ -> {
                    Value data = new Advancement.Value();
                    data.update(user, self);
                    return data;
                });
            }

            public Function<SteamwarUser, Integer> max(int neededValue) {
                return user -> {
                    double value = get(user).value.doubleValue();
                    if (value > neededValue) return Math.min(neededValue, 100);
                    return (int) (value / Math.max(neededValue / 100.0, 1));
                };
            }

            public Function<SteamwarUser, Integer> reached(int neededValue) {
                return user -> {
                    double value = get(user).value.doubleValue();
                    return value >= neededValue ? 1 : 0;
                };
            }
        }

        @Getter
        private T value;

        public void update(SteamwarUser user, Key<T> key) {
            this.value = key.valueFunction.apply(user);
        }
    }

    @ToString
    public static class Data {
        private final Advancement advancement;
        private final SteamwarUser user;

        private int progress;
        private boolean showToast = true;
        private boolean hidden = false;

        public Data(Advancement advancement, SteamwarUser user) {
            this.advancement = advancement;
            advancement.data.put(user, this);
            this.user = user;
            this.progress = advancement.progressCalculator.apply(user);
            checkHidden();
            checkFinished();
            new Packet(this, showToast).send();
        }

        public Data(Advancement advancement, SteamwarUser user, int progress) {
            this.advancement = advancement;
            advancement.data.put(user, this);
            this.user = user;
            this.progress = progress;
            checkHidden();
            checkFinished();
            new Packet(this, showToast).send();
        }

        public void update() {
            this.progress = advancement.progressCalculator.apply(user);
            checkHidden();

            new Packet(this, showToast).send();
            // Update Advancements that have this as parent
            Advancements.getAll()
                    .stream()
                    .filter(advancement -> advancement.parent.filter(value -> value == this.advancement).isPresent())
                    .map(advancement -> advancement.get(user))
                    .forEach(Advancement.Data::update);

            checkFinished();
        }

        private void checkHidden() {
            hidden = advancement.hidePolicy.hidden(this);
        }

        private void checkFinished() {
            if (progress == advancement.total) {
                showToast = false;
            }
        }

        private void encodeAdvancement(ByteBuf byteBuf, ProtocolVersion protocolVersion, boolean showToast) {
            ProtocolUtils.writeString(byteBuf, advancement.identifier);
            if (advancement.parent.isPresent()) {
                byteBuf.writeBoolean(true);
                ProtocolUtils.writeString(byteBuf, advancement.parent.get().identifier);
            } else {
                byteBuf.writeBoolean(false);
            }

            { // Display
                byteBuf.writeBoolean(true);
                new ComponentHolder(protocolVersion, advancement.display.title).write(byteBuf);
                new ComponentHolder(protocolVersion, advancement.display.description).write(byteBuf);
                { // Slot
                    ProtocolUtils.writeVarInt(byteBuf, 1);
                    int itemId = Items.values
                            .get(protocolVersion.name().replace("MINECRAFT_", "V_"))
                            .getAsJsonObject()
                            .get(advancement.display.item)
                            .getAsInt();
                    ProtocolUtils.writeVarInt(byteBuf, itemId);
                    ProtocolUtils.writeVarInt(byteBuf, 0);
                    ProtocolUtils.writeVarInt(byteBuf, 0);
                }
                ProtocolUtils.writeVarInt(byteBuf, advancement.display.frameType.ordinal());
                if (advancement.display.background.isPresent()) {
                    byteBuf.writeInt(0x01 | (showToast ? 0x02 : 0x00) | (hidden ? 0x04 : 0x00));
                    ProtocolUtils.writeString(byteBuf, advancement.display.background.get());
                } else {
                    byteBuf.writeInt((showToast ? 0x02 : 0x00) | (hidden ? 0x04 : 0x00));
                }
                byteBuf.writeFloat(advancement.display.xCoord);
                byteBuf.writeFloat(advancement.display.yCoord);
            }

            ProtocolUtils.writeVarInt(byteBuf, advancement.total);
            for (int i = 0; i < advancement.total; i++) {
                ProtocolUtils.writeVarInt(byteBuf, 1);
                ProtocolUtils.writeString(byteBuf, advancement.identifier + "_" + i);
            }

            byteBuf.writeBoolean(false); // No Telemetry
        }

        private void encodeProgress(ByteBuf byteBuf) {
            ProtocolUtils.writeString(byteBuf, this.advancement.identifier);
            ProtocolUtils.writeVarInt(byteBuf, advancement.total);
            for (int i = 0; i < advancement.total; i++) {
                ProtocolUtils.writeString(byteBuf, advancement.identifier + "_" + i);
                if (i == advancement.total - 1 && advancement.total == progress) {
                    byteBuf.writeBoolean(true);
                    byteBuf.writeLong(new Date().getTime());
                } else if (i < progress) {
                    byteBuf.writeBoolean(true);
                    byteBuf.writeLong(0);
                } else {
                    byteBuf.writeBoolean(false);
                }
            }
        }
    }

    protected record Packet(Data data, boolean showToast) implements MinecraftPacket {
        public void send() {
            Player player = Chatter.of(data.user).getPlayer();
            ((ConnectedPlayer) player).getConnection().write(this);
        }

        @Override
        public void decode(ByteBuf byteBuf, ProtocolUtils.Direction direction, ProtocolVersion protocolVersion) {
            throw new UnsupportedOperationException();
        }

        @Override
        public void encode(ByteBuf byteBuf, ProtocolUtils.Direction direction, ProtocolVersion protocolVersion) {
            byteBuf.writeBoolean(false); // Clear

            if (!data.hidden) {
                ProtocolUtils.writeVarInt(byteBuf, 1);
                data.encodeAdvancement(byteBuf, protocolVersion, showToast);
                ProtocolUtils.writeVarInt(byteBuf, 0); // No Advancements to remove
                ProtocolUtils.writeVarInt(byteBuf, 1);
                data.encodeProgress(byteBuf);
            } else {
                ProtocolUtils.writeVarInt(byteBuf, 0); // No Advancements to update
                ProtocolUtils.writeVarInt(byteBuf, 1);
                ProtocolUtils.writeString(byteBuf, data.advancement.identifier);
                ProtocolUtils.writeVarInt(byteBuf, 0); // No Advancements Progress to update
            }

            byteBuf.writeBoolean(true); // Show Advancements
        }

        @Override
        public boolean handle(MinecraftSessionHandler minecraftSessionHandler) {
            return false;
        }
    }
}
