package zenith.zov.client.hud.elements.component;

import com.darkmagician6.eventapi.EventManager;
import com.darkmagician6.eventapi.EventTarget;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.function.BooleanSupplier;
import java.util.function.Supplier;
import net.minecraft.client.gui.screen.ChatScreen;
import net.minecraft.component.DataComponentTypes;
import net.minecraft.component.type.PotionContentsComponent;
import net.minecraft.item.Item;
import net.minecraft.item.ItemStack;
import net.minecraft.item.Items;
import net.minecraft.network.packet.s2c.play.CooldownUpdateS2CPacket;
import net.minecraft.network.packet.s2c.play.PlayerRespawnS2CPacket;
import net.minecraft.potion.Potions;
import net.minecraft.registry.Registries;
import zenith.zov.Zenith;
import zenith.zov.base.animations.base.Animation;
import zenith.zov.base.animations.base.Easing;
import zenith.zov.base.events.impl.server.EventPacket;
import zenith.zov.base.font.Font;
import zenith.zov.base.font.Fonts;
import zenith.zov.base.theme.Theme;
import zenith.zov.client.hud.elements.draggable.DraggableHudElement;
import zenith.zov.utility.math.MathUtil;
import zenith.zov.utility.render.display.base.BorderRadius;
import zenith.zov.utility.render.display.base.CustomDrawContext;
import zenith.zov.utility.render.display.base.color.ColorRGBA;
import zenith.zov.utility.render.display.shader.DrawUtil;

public class CooldownComponent extends DraggableHudElement {
    private final Map<String, CooldownComponent.CooldownModule> keyModules = new LinkedHashMap<>();
    private final Animation animationWidth = new Animation(200L, 100.0F, Easing.QUAD_IN_OUT);
    private final Animation animationScale = new Animation(200L, 0.0F, Easing.QUAD_IN_OUT);
    private final Animation animationVisible = new Animation(200L, 0.0F, Easing.QUAD_IN_OUT);
    private final Map<String, CooldownComponent.TimedCooldown> customCooldowns = new LinkedHashMap<>();
    private final Map<String, CooldownComponent.TimedCooldown> vanillaCooldowns = new LinkedHashMap<>();

    public CooldownComponent(
        String name, float initialX, float initialY, float windowWidth, float windowHeight, float offsetX, float offsetY, DraggableHudElement.Align align
    ) {
        super(name, initialX, initialY, windowWidth, windowHeight, offsetX, offsetY, align);
        EventManager.register(this);
    }

    @EventTarget
    public void onPacket(EventPacket e) {
        if (mc != null && mc.world != null) {
            if (e.getPacket() instanceof CooldownUpdateS2CPacket pkt) {
                Item item = (Item)Registries.ITEM.get(pkt.cooldownGroup());
                int ticks = pkt.cooldown();
                String id = "vanilla:" + item.getTranslationKey();
                if (ticks == 0) {
                    this.vanillaCooldowns.remove(id);
                    this.keyModules.remove(id);
                    return;
                }

                long nowTicks = mc.world.getTime();
                this.vanillaCooldowns
                    .put(id, CooldownComponent.TimedCooldown.fromTicks(id, item.getName().getString(), item.getDefaultStack(), nowTicks, (long)ticks));
            } else if (e.getPacket() instanceof PlayerRespawnS2CPacket) {
                this.vanillaCooldowns.clear();
                this.customCooldowns.clear();
                this.keyModules.clear();
            }
        }
    }

    @EventTarget
    public void onCustomCooldown(EventPacket e) {
        if (mc != null && mc.player != null) {
            ItemStack itemStack = mc.player.getActiveItem();
            if (itemStack != null) {
                if (mc.player.getItemUseTime() >= itemStack.getMaxUseTime(mc.player)) {
                    PotionContentsComponent data = (PotionContentsComponent)itemStack.get(DataComponentTypes.POTION_CONTENTS);
                    if (data != null && (data.getColor() == 33461 || data.getColor() == -515037)) {
                        ItemStack medikStak = Items.POTION.getDefaultStack();
                        medikStak.set(
                            DataComponentTypes.POTION_CONTENTS,
                            new PotionContentsComponent(Optional.of(Potions.SWIFTNESS), Optional.of(data.getColor()), List.of(), Optional.empty())
                        );
                        if (data.getColor() == -515037) {
                            this.addCustomCooldown(medikStak, "Heal", 10000L);
                        } else {
                            this.addCustomCooldown(medikStak, "Heal", 10000L);
                        }
                    }
                }
            }
        }
    }

    public void addCustomCooldown(ItemStack stack, long durationMs) {
        String id = "custom:" + stack.getItem().getTranslationKey();
        this.addCustomCooldownReal(id, stack.getItem().getName().getString(), stack, durationMs);
    }

    public void addCustomCooldown(ItemStack stack, String displayName, long durationMs) {
        this.addCustomCooldownReal("custom:" + displayName, displayName, stack, durationMs);
    }

    public void addCustomCooldownReal(String id, String displayName, ItemStack iconStack, long durationMs) {
        if (mc != null) {
            if (!this.keyModules.containsKey(id)) {
                long nowNs = System.nanoTime();
                this.customCooldowns.put(id, CooldownComponent.TimedCooldown.fromReal(id, displayName, iconStack, nowNs, Math.max(1L, durationMs) * 1000000L));
            }
        }
    }

    public void addCustomCooldownTicks(String id, String displayName, ItemStack iconStack, long durationTicks) {
        if (mc != null && mc.world != null) {
            if (!this.keyModules.containsKey(id)) {
                long nowTicks = mc.world.getTime();
                this.customCooldowns.put(id, CooldownComponent.TimedCooldown.fromTicks(id, displayName, iconStack, nowTicks, Math.max(1L, durationTicks)));
            }
        }
    }

    @Override
    public void render(CustomDrawContext ctx) {
        if (mc != null) {
            Font iconFont = Fonts.ICONS.getFont(6.0F);
            long nowTicks = mc != null && mc.world != null ? mc.world.getTime() : 0L;

            for (CooldownComponent.TimedCooldown tc : this.vanillaCooldowns.values()) {
                if (!tc.isFinished(nowTicks)) {
                    this.keyModules
                        .computeIfAbsent(
                            tc.id,
                            k -> new CooldownComponent.CooldownModule(
                                    tc.icon,
                                    tc.displayName,
                                    tc.getProgress(nowTicks),
                                    () -> tc.getProgress(mc.world != null ? mc.world.getTime() : nowTicks),
                                    () -> tc.isFinished(mc.world != null ? mc.world.getTime() : nowTicks),
                                    () -> tc.getRemainingSeconds(mc.world != null ? mc.world.getTime() : nowTicks)
                                )
                        );
                }
            }

            for (CooldownComponent.TimedCooldown tcx : this.customCooldowns.values()) {
                if (tcx.base == CooldownComponent.TimeBase.TICKS) {
                    if (!tcx.isFinished(nowTicks)) {
                        this.keyModules
                            .computeIfAbsent(
                                tcx.id,
                                k -> new CooldownComponent.CooldownModule(
                                        tc.icon,
                                        tc.displayName,
                                        tc.getProgress(nowTicks),
                                        () -> tc.getProgress(mc.world != null ? mc.world.getTime() : nowTicks),
                                        () -> tc.isFinished(mc.world != null ? mc.world.getTime() : nowTicks),
                                        () -> tc.getRemainingSeconds(mc.world != null ? mc.world.getTime() : nowTicks)
                                    )
                            );
                    }
                } else if (!tcx.isFinished(0L)) {
                    this.keyModules
                        .computeIfAbsent(
                            tcx.id,
                            k -> new CooldownComponent.CooldownModule(
                                    tc.icon,
                                    tc.displayName,
                                    tc.getProgress(0L),
                                    () -> tc.getProgress(0L),
                                    () -> tc.isFinished(0L),
                                    () -> tc.getRemainingSeconds(0L)
                                )
                        );
                }
            }

            if (this.keyModules.isEmpty()) {
                this.animationScale.update(0.0F);
            } else {
                boolean singleAndHidden = this.keyModules.size() == 1 && this.keyModules.values().iterator().next().animation.getTargetValue() == 0.0F;
                this.animationScale.update(singleAndHidden ? 0.0F : 1.0F);
            }

            float x = this.x;
            float y = this.y;
            float height = (float)(18.0 + this.keyModules.values().stream().mapToDouble(CooldownComponent.CooldownModule::getHeight).sum());
            float width = (float)this.keyModules.values().stream().mapToDouble(CooldownComponent.CooldownModule::updateWidth).max().orElse(100.0);
            width = this.animationWidth.update(width);
            this.width = width;
            this.height = height;
            this.animationVisible.update(mc.currentScreen instanceof ChatScreen || !this.keyModules.isEmpty());
            Theme theme = Zenith.getInstance().getThemeManager().getCurrentTheme();
            ctx.pushMatrix();
            ctx.getMatrices().translate(x + width / 2.0F, y + height / 2.0F, 0.0F);
            ctx.getMatrices().scale(this.animationVisible.getValue(), this.animationVisible.getValue(), 1.0F);
            ctx.getMatrices().translate(-(x + width / 2.0F), -(y + height / 2.0F), 0.0F);
            BorderRadius radius6 = BorderRadius.all(6.0F);
            DrawUtil.drawBlurHud(ctx.getMatrices(), x, y, width, height, 21.0F, BorderRadius.all(4.0F), ColorRGBA.WHITE);
            ctx.drawRoundedRect(x, y, width, height, radius6, theme.getForegroundLight());
            ctx.drawText(iconFont, "N", x + 8.0F, y + (18.0F - iconFont.height()) / 2.0F, theme.getColor());
            ctx.drawText(iconFont, "M", x + width - 8.0F - iconFont.width("M"), y + (18.0F - iconFont.height()) / 2.0F, theme.getWhiteGray());
            Font font = Fonts.MEDIUM.getFont(6.0F);
            ctx.drawText(font, "Cooldown", x + 8.0F + 8.0F + 2.0F, y + (18.0F - font.height()) / 2.0F, theme.getWhite());
            if (this.animationVisible.getValue() == 1.0F) {
                float kmY = y + 18.0F;
                int i = 0;

                for (CooldownComponent.CooldownModule km : this.keyModules.values()) {
                    km.render(ctx, x, kmY, width, i);
                    kmY += km.getHeight();
                    i++;
                }
            }

            this.keyModules.entrySet().removeIf(entry -> entry.getValue().isDelete());
            if (mc != null && mc.world != null) {
                long t = mc.world.getTime();
                this.vanillaCooldowns.entrySet().removeIf(e -> e.getValue().isFinished(t));
                this.customCooldowns
                    .entrySet()
                    .removeIf(e -> e.getValue().base == CooldownComponent.TimeBase.TICKS ? e.getValue().isFinished(t) : e.getValue().isFinished(0L));
            }

            ctx.drawRoundedBorder(x, y, width, height, 0.1F, BorderRadius.all(4.0F), theme.getForegroundStroke());
            DrawUtil.drawRoundedCorner(
                ctx.getMatrices(), x, y, width, height, 0.1F, Math.min(20.0F, Math.max(12.0F, height / 2.5F)), theme.getColor(), BorderRadius.all(4.0F)
            );
            ctx.popMatrix();
        }
    }

    private class CooldownModule {
        private final Animation animation = new Animation(150L, 0.01F, Easing.QUAD_IN_OUT);
        private final Animation animationColor = new Animation(200L, Easing.QUAD_IN_OUT);
        private final Animation animationProgress;
        private final ItemStack iconStack;
        private final String displayName;
        private final Supplier<Float> progressSupplier;
        private final BooleanSupplier finishedSupplier;
        private final Supplier<Integer> remainingSecondsSupplier;

        public CooldownModule(
            ItemStack iconStack,
            String displayName,
            float initialProgress,
            Supplier<Float> progressSupplier,
            BooleanSupplier finishedSupplier,
            Supplier<Integer> remainingSecondsSupplier
        ) {
            this.iconStack = iconStack;
            this.displayName = displayName;
            this.progressSupplier = progressSupplier;
            this.finishedSupplier = finishedSupplier;
            this.remainingSecondsSupplier = remainingSecondsSupplier;
            this.animationProgress = new Animation(200L, initialProgress, Easing.LINEAR);
        }

        public float updateWidth() {
            float width = 120.0F;
            String timeStr = this.formatTime(this.remainingSecondsSupplier.get());
            float moduleTextWidth = Fonts.MEDIUM.getWidth(this.displayName, 6.0F);
            float timeTextWidth = Fonts.MEDIUM.getWidth(timeStr, 6.0F);
            float rightPad = 8.0F + timeTextWidth;
            float widthText = width - (rightPad + 10.0F + 5.0F + 10.0F);
            if (widthText < 8.0F + moduleTextWidth + 8.0F) {
                float deltaWidth = moduleTextWidth + 8.0F + 8.0F - widthText;
                width += deltaWidth;
            }

            return width;
        }

        public float getHeight() {
            return 18.0F * this.animation.getValue();
        }

        public void render(CustomDrawContext ctx, float x, float y, float width, int i) {
            float progress = this.updateProgressAndGet();
            Theme theme = Zenith.getInstance().getThemeManager().getCurrentTheme();
            Font font = Fonts.MEDIUM.getFont(6.0F);
            this.animation.update(progress > 0.0F ? 1.0F : 0.0F);
            ctx.pushMatrix();
            ctx.getMatrices().translate(x + width / 2.0F, y + 9.0F, 0.0F);
            float deltaAnim = this.animation.getValue();
            ctx.getMatrices().scale(deltaAnim, deltaAnim, 1.0F);
            ctx.getMatrices().translate(-(x + width / 2.0F), -(y + 9.0F), 0.0F);
            this.animationColor.update(i % 2 == 0 ? 1.0F : 0.0F);
            ColorRGBA backgroundColor = theme.getForegroundLight().mix(theme.getForegroundColor(), this.animationColor.getValue());
            ctx.drawRoundedRect(
                x, y, width, 18.0F, i == CooldownComponent.this.keyModules.size() - 1 ? BorderRadius.bottom(4.0F, 4.0F) : BorderRadius.ZERO, backgroundColor
            );
            ctx.pushMatrix();
            ctx.getMatrices().translate(x + 6.0F, y + 5.0F, 0.0F);
            ctx.getMatrices().scale(0.5F, 0.5F, 1.0F);
            ctx.drawItem(this.iconStack, 0, 0);
            ctx.popMatrix();
            ctx.drawText(font, this.displayName, x + 8.0F + 8.0F + 8.0F, y + (18.0F - font.height()) / 2.0F, theme.getWhite());
            String timeStr = this.formatTime(this.remainingSecondsSupplier.get());
            float timeWidth = Fonts.MEDIUM.getWidth(timeStr, 6.0F);
            ctx.drawText(font, timeStr, x + width - timeWidth - 8.0F, y + (18.0F - font.height()) / 2.0F, theme.getColor());
            ctx.popMatrix();
        }

        public boolean isDelete() {
            return this.animation.getValue() == 0.0F && this.finishedSupplier.getAsBoolean();
        }

        private float updateProgressAndGet() {
            float p = this.progressSupplier.get();
            this.animationProgress.update(p);
            return MathUtil.round(this.animationProgress.getValue());
        }

        private String formatTime(int totalSeconds) {
            if (totalSeconds < 0) {
                totalSeconds = 0;
            }

            int minutes = totalSeconds / 60;
            int seconds = totalSeconds % 60;
            return String.format("%d:%02d", minutes, seconds);
        }
    }

    private static enum TimeBase {
        TICKS,
        REAL;
    }

    private static class TimedCooldown {
        final String id;
        final String displayName;
        final ItemStack icon;
        final CooldownComponent.TimeBase base;
        final long start;
        final long duration;

        private TimedCooldown(String id, String displayName, ItemStack icon, CooldownComponent.TimeBase base, long start, long duration) {
            this.id = id;
            this.displayName = displayName;
            this.icon = icon;
            this.base = base;
            this.start = start;
            this.duration = duration;
        }

        static CooldownComponent.TimedCooldown fromTicks(String id, String name, ItemStack icon, long startTicks, long durationTicks) {
            return new CooldownComponent.TimedCooldown(id, name, icon, CooldownComponent.TimeBase.TICKS, startTicks, durationTicks);
        }

        static CooldownComponent.TimedCooldown fromReal(String id, String name, ItemStack icon, long startNs, long durationNs) {
            return new CooldownComponent.TimedCooldown(id, name, icon, CooldownComponent.TimeBase.REAL, startNs, durationNs);
        }

        float getProgress(long nowTick) {
            long remain = switch (this.base) {
                case TICKS -> Math.max(0L, this.start + this.duration - nowTick);
                case REAL -> Math.max(0L, this.start + this.duration - System.nanoTime());
            };
            return this.duration <= 0L ? 0.0F : (float)remain / (float)this.duration;
        }

        boolean isFinished(long nowTick) {
            return switch (this.base) {
                case TICKS -> nowTick >= this.start + this.duration;
                case REAL -> System.nanoTime() >= this.start + this.duration;
            };
        }

        int getRemainingSeconds(long nowTick) {
            long remain = switch (this.base) {
                case TICKS -> Math.max(0L, this.start + this.duration - nowTick);
                case REAL -> Math.max(0L, this.start + this.duration - System.nanoTime());
            };
            return this.base == CooldownComponent.TimeBase.TICKS ? (int)(remain / 20L) : (int)(remain / 1000000000L);
        }
    }
}
