/*
 *   This file is a part of the SteamWar software.
 *
 *   Copyright (C) 2025 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.bausystem.features.simulator.execute;

import de.steamwar.bausystem.features.simulator.data.tnt.TNTPhase;
import de.steamwar.bausystem.region.Region;
import de.steamwar.bausystem.region.flags.Flag;
import de.steamwar.bausystem.utils.PasteBuilder;
import de.steamwar.bausystem.utils.bossbar.BauSystemBossbar;
import org.bukkit.Bukkit;
import org.bukkit.Location;
import org.bukkit.entity.TNTPrimed;
import org.bukkit.event.EventHandler;
import org.bukkit.event.HandlerList;
import org.bukkit.event.Listener;
import org.bukkit.event.entity.EntityExplodeEvent;

import java.util.*;
import java.util.stream.Collectors;

import static de.steamwar.bausystem.features.simulator.execute.Direction.Y;

public class StabGenerator extends StabStep implements Listener {

    private int recordings = 0;
    private List<Integer> currentDepths = new ArrayList<>();
    private int lastDepth = 0;

    private int retries = 0;

    private final Map<Integer, Set<Location>> destroyedBlocksPerSlice = new HashMap<>();

    private Set<Integer> gabStart = new HashSet<>();
    private Set<TNTPhase> failedAtLeastOnce = new HashSet<>();

    @EventHandler
    public void onEntityExplode(EntityExplodeEvent event) {
        if (!(event.getEntity() instanceof TNTPrimed)) return;
        if (Region.getRegion(event.getEntity().getLocation()) == data.region) {
            event.blockList().forEach(block -> {
                if (!data.region.getTestblockArea().inRegion(block.getLocation(), true)) {
                    return;
                }
                int component = data.direction.component.apply(block.getLocation().toVector());
                destroyedBlocksPerSlice.computeIfAbsent(component, __ -> new HashSet<>())
                        .add(block.getLocation());
            });
        }
    }

    public StabGenerator(StabData data) {
        super(data);
    }

    @Override
    protected void start() {
        try {
            PasteBuilder.ClipboardProvider clipboardProvider = new PasteBuilder.ClipboardProviderImpl(data.clipboard);
            PasteBuilder pasteBuilder = new PasteBuilder(clipboardProvider);
            if (data.region.getRegionData().has(Flag.COLOR).isReadable()) {
                pasteBuilder.color(data.region.getRegionData().get(Flag.COLOR).getWithDefault());
            }
            data.region.getTestblockArea().reset(pasteBuilder, true);
        } catch (SecurityException e) {
            stop();
            throw e;
        }

        if (data.cancel) {
            HandlerList.unregisterAll(this);
            return;
        }

        runSimulator(this::calculateStab);
    }

    private void calculateStab() {
        TNTPhase lastPhase = data.phases.getLast();

        List<Map.Entry<Integer, Set<Location>>> locations = destroyedBlocksPerSlice.entrySet()
                .stream()
                .sorted(Comparator.comparingInt(Map.Entry::getKey))
                .collect(Collectors.toList());
        int depth = 0;
        for (int i = 0; i < locations.size(); i++) {
            if (data.direction != Y && i > 0 && Math.abs(locations.get(i - 1).getKey() - locations.get(i).getKey()) > 3) {
                if (gabStart.add(locations.get(i).getKey())) {
                    Bukkit.getLogger().log(StabData.LEVEL, "Increasing tnt count by {0} because of gap", StabData.TNT_INCREASE);
                    lastPhase.setCount(lastPhase.getCount() + StabData.TNT_INCREASE);
                    recordings = 0;
                    currentDepths.clear();

                    run();
                    return;
                }
            }
            if (i == 0 || i == locations.size() - 1) {
                if (locations.get(i).getValue().size() > StabData.MIN_BLOCK_TO_COUNT_AS_DEPTH) {
                    depth++;
                }
            } else {
                depth++;
            }
        }

        if (depth > 0) {
            Bukkit.getLogger().log(StabData.LEVEL, "{0} {1} {2}", new Object[]{depth, destroyedBlocksPerSlice.size(), destroyedBlocksPerSlice.values().stream().map(Set::size).collect(Collectors.toList())});
            destroyedBlocksPerSlice.clear();
            currentDepths.add(depth);
        } else {
            destroyedBlocksPerSlice.clear();
            lastPhase.setTickOffset(lastPhase.getTickOffset() + 1);
            Bukkit.getLogger().log(StabData.LEVEL, "No dimension - Increasing tickOffset to: {0}", lastPhase.getTickOffset());
            lastPhase.setOrder(0);
            if (lastPhase.getTickOffset() > 80) {
                stop();
            } else {
                run();
            }
            return;
        }

        int minDepth = currentDepths.stream().min(Integer::compareTo).orElse(0);
        int maxDepth = currentDepths.stream().max(Integer::compareTo).orElse(0);
        data.currentDepth = maxDepth;

        int countWithoutLast = 0;
        for (int i = 0; i < data.phases.size() - 1; i++) {
            countWithoutLast += data.phases.get(i).getCount();
        }
        countWithoutLast -= gabStart.size();

        boolean moreTNTNeeded = maxDepth - countWithoutLast >= lastPhase.getCount() - 5;
        if (moreTNTNeeded) {
            Bukkit.getLogger().log(StabData.LEVEL, "Increasing tnt count by {0}", StabData.TNT_INCREASE);
            lastPhase.setCount(lastPhase.getCount() + StabData.TNT_INCREASE);
            recordings = 0;
            currentDepths.clear();

            run();
            return;
        }

        if (recordings++ < StabData.MAX_RECORDINGS) {
            run();
            return;
        }

        recordings = 0;
        currentDepths.clear();

        if (maxDepth - minDepth > lastPhase.getCount()) {
            Bukkit.getLogger().log(StabData.LEVEL, "Stab failed at least once. Adding one tnt to {0}", minDepth - 3);
            int current = 0;
            TNTPhase last = null;
            for (TNTPhase phase : data.phases) {
                if (current < minDepth - 3) {
                    current += phase.getCount();
                    last = phase;
                    continue;
                }

                if (failedAtLeastOnce.add(last)) {
                    last = null;
                    break;
                }

                if (last != null) {
                    last.setCount(last.getCount() + 1);
                    Bukkit.getLogger().log(StabData.LEVEL, "Added to phase {0} now has {1} tnt", new Object[]{data.phases.indexOf(last), last.getCount()});
                }
                break;
            }

            if (last != null) {
                run();
                return;
            }
        }

        Bukkit.getLogger().log(StabData.LEVEL, "No more TNT needed on phase adjusting - {0} new count; {1} current count", new Object[]{maxDepth - countWithoutLast, lastPhase.getCount()});
        lastPhase.setCount(maxDepth - countWithoutLast);
        if (lastPhase.getCount() <= 0) {
            Bukkit.getLogger().log(StabData.LEVEL, "Count was 0 or negative - removing last phase");
            data.phases.removeLast();
        }

        if (maxDepth > data.depthLimit) {
            Bukkit.getLogger().log(StabData.LEVEL, "Depth is greater than {0} - finished", data.depthLimit);
            new StabFinalizer(data);
            return;
        }
        if (maxDepth <= lastDepth) {
            if (lastPhase.getCount() <= 0) {
                retries++;
            }
            if (lastPhase.getCount() > 0 || retries > StabData.MAX_TICK_DIFFERENCE) {
                Bukkit.getLogger().log(StabData.LEVEL, "Depth is equal to last depth recorded {0} - finished", maxDepth);
                new StabFinalizer(data);
                return;
            }
        }
        lastDepth = maxDepth;

        newPhase(data, lastPhase);
        run();
    }

    public static void newPhase(StabData data, TNTPhase lastPhase) {
        Bukkit.getLogger().log(StabData.LEVEL, "Adding new phase in next tick");
        TNTPhase nextPhase = new TNTPhase();
        nextPhase.setCount(StabData.TNT_INCREASE);
        nextPhase.setTickOffset(lastPhase.getTickOffset() + 1);
        nextPhase.setXJump(lastPhase.isXJump());
        nextPhase.setYJump(lastPhase.isYJump());
        nextPhase.setZJump(lastPhase.isZJump());
        data.phases.add(nextPhase);
    }

    @Override
    protected void bossbar(BauSystemBossbar bossbar, boolean finished) {
        bossbar.setProgress(Math.min(data.currentDepth / (double) data.depthLimit, 1.0));
        if (finished) {
            bossbar.setTitle("§e" + data.currentDepth + "§8/§7" + data.depthLimit);
            return;
        }

        StringBuilder st = new StringBuilder();
        st.append("§7Direction §e").append(data.direction);
        st.append("   §e").append(data.currentDepth).append("§8/§7").append(data.depthLimit);
        if (recordings > 0) {
            st.append("   §7Retries§8:§e ").append(recordings).append("§8/§7").append(StabData.MAX_RECORDINGS);
        }
        bossbar.setTitle(st.toString());
    }
}
