/*
 *   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.killchecker;

import de.steamwar.bausystem.BauSystem;
import de.steamwar.bausystem.region.Point;
import de.steamwar.bausystem.region.Region;
import de.steamwar.bausystem.utils.bossbar.BauSystemBossbar;
import de.steamwar.bausystem.utils.bossbar.BossBarService;
import de.steamwar.entity.REntity;
import de.steamwar.entity.REntityServer;
import de.steamwar.entity.RFallingBlockEntity;
import org.bukkit.Bukkit;
import org.bukkit.Material;
import org.bukkit.World;
import org.bukkit.block.Block;
import org.bukkit.boss.BarColor;
import org.bukkit.entity.Player;

import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;

public class KillcheckerVisualizer {

    private static final Material[] MATERIALS = new Material[]{
            Material.LIME_STAINED_GLASS,
            Material.LIME_CONCRETE,
            Material.GREEN_STAINED_GLASS,
            Material.GREEN_CONCRETE,
            Material.YELLOW_STAINED_GLASS,
            Material.YELLOW_CONCRETE,
            Material.ORANGE_STAINED_GLASS,
            Material.ORANGE_CONCRETE,
            Material.RED_STAINED_GLASS,
            Material.RED_CONCRETE,
            Material.PURPLE_STAINED_GLASS,
            Material.PURPLE_CONCRETE,
            Material.BLACK_STAINED_GLASS,
            Material.BLACK_CONCRETE,
    };
    private static final World WORLD = Bukkit.getWorlds().get(0);
    
    private static final double SURROUND = 4.5;
    
    private final Point minPoint;
    private final Point maxPoint;

    private final int yArea;
    private final int zArea;
    private final int xArea;

    private final Region region;
    private final BossBarService bossBarService;

    public KillcheckerVisualizer(Region region, BossBarService bossBarService) {
        this.region = region;
        this.minPoint = region.getBuildArea().getMinPoint(false);
        this.maxPoint = region.getBuildArea().getMaxPoint(false);

        yArea = (maxPoint.getX() - minPoint.getX()) * (maxPoint.getZ() - minPoint.getZ());
        zArea = (maxPoint.getX() - minPoint.getX()) * (maxPoint.getY() - minPoint.getY());
        xArea = (maxPoint.getY() - minPoint.getY()) * (maxPoint.getZ() - minPoint.getZ());

        this.bossBarService = bossBarService;
    }

    private final REntityServer outline = new REntityServer();
    private final REntityServer inner = new REntityServer();

    private final Map<Point, Integer> killCount = new HashMap<>();
    private final Set<Point> outlinePointsCache = new HashSet<>();
    private final Map<Point, REntity> rEntities = new HashMap<>();

    private double percent = 0;
    private int kills = 0;
    private int cannonCount = 0;

    public void recalc() {
        Set<Cuboid> cuboids = new HashSet<>();
        Set<Point> points = new HashSet<>();
        for (int x = minPoint.getX() + 1; x < maxPoint.getX(); x++) {
            for (int y = minPoint.getY(); y < maxPoint.getY(); y++) {
                for (int z = minPoint.getZ() + 1; z < maxPoint.getZ(); z++) {
                    if (points.contains(new Point(x, y, z))) continue;
                    Block block = WORLD.getBlockAt(x, y, z);
                    if (block.getType().isAir()) continue;
                    String name = block.getType().name();
                    if (!name.endsWith("_WOOL") && !name.endsWith("_STAINED_GLASS") && !name.endsWith("_CONCRETE") && !name.endsWith("_TERRACOTTA")) continue;
                    if (name.equals("_GLAZED_TERRACOTTA")) continue;
                    Cuboid cuboid = create(block.getType(), x, y, z);
                    cuboids.add(cuboid);
                    for (int dx = (int) cuboid.getX(); dx <= cuboid.getDx(); dx++) {
                        for (int dy = (int) cuboid.getY(); dy <= cuboid.getDy(); dy++) {
                            for (int dz = (int) cuboid.getZ(); dz <= cuboid.getDz(); dz++) {
                                points.add(new Point(dx, dy, dz));
                            }
                        }
                    }
                }
            }
        }
        cannonCount = cuboids.size();

        Map<Point, Integer> kill = new HashMap<>();
        int yKills = 0;
        int yCount = 0;
        Set<Point> yPoints = new HashSet<>();
        for (int x = minPoint.getX(); x <= maxPoint.getX(); x++) {
            for (int z = minPoint.getZ(); z <= maxPoint.getZ(); z++) {
                Set<Cuboid> cuboidSet = new HashSet<>();
                for (Cuboid cuboid : cuboids) {
                    if (x >= cuboid.getX() - SURROUND && x < cuboid.getDx() + SURROUND && z >= cuboid.getZ() - SURROUND && z < cuboid.getDz() + SURROUND) {
                        cuboidSet.add(cuboid);
                    }
                }
                if (cuboidSet.size() > 1) {
                    yCount++;
                    yKills += splitIntoDoubleKills(cuboidSet.size());
                    Point p2 = new Point(x, maxPoint.getY() + 1, z);
                    yPoints.add(p2);
                    kill.put(p2, Math.max(kill.getOrDefault(p2, 0), cuboidSet.size()));
                }
            }
        }

        int xKills = 0;
        int xCount = 0;
        Set<Point> xPoints = new HashSet<>();
        for (int y = minPoint.getY(); y <= maxPoint.getY(); y++) {
            for (int z = minPoint.getZ(); z <= maxPoint.getZ(); z++) {
                Set<Cuboid> cuboidSet = new HashSet<>();
                for (Cuboid cuboid : cuboids) {
                    if (y >= cuboid.getY() - SURROUND && y < cuboid.getDy() + SURROUND && z >= cuboid.getZ() - SURROUND && z < cuboid.getDz() + SURROUND) {
                        cuboidSet.add(cuboid);
                    }
                }
                if (cuboidSet.size() > 1) {
                    xCount++;
                    xKills += splitIntoDoubleKills(cuboidSet.size());
                    Point p1 = new Point(minPoint.getX() - 1, y, z);
                    xPoints.add(p1);
                    kill.put(p1, Math.max(kill.getOrDefault(p1, 0), cuboidSet.size()));
                    Point p2 = new Point(maxPoint.getX() + 1, y, z);
                    xPoints.add(p2);
                    kill.put(p2, Math.max(kill.getOrDefault(p2, 0), cuboidSet.size()));
                }
            }
        }

        int zKills = 0;
        int zCount = 0;
        Set<Point> zPoints = new HashSet<>();
        for (int x = minPoint.getX(); x <= maxPoint.getX(); x++) {
            for (int y = minPoint.getY(); y <= maxPoint.getY(); y++) {
                Set<Cuboid> cuboidSet = new HashSet<>();
                for (Cuboid cuboid : cuboids) {
                    if (x >= cuboid.getX() - SURROUND && x < cuboid.getDx() + SURROUND && y >= cuboid.getY() - SURROUND && y < cuboid.getDy() + SURROUND) {
                        cuboidSet.add(cuboid);
                    }
                }
                if (cuboidSet.size() > 1) {
                    zCount++;
                    zKills += splitIntoDoubleKills(cuboidSet.size());
                    Point p1 = new Point(x, y, minPoint.getZ() - 1);
                    zPoints.add(p1);
                    kill.put(p1, Math.max(kill.getOrDefault(p1, 0), cuboidSet.size()));
                    Point p2 = new Point(x, y, maxPoint.getZ() + 1);
                    zPoints.add(p2);
                    kill.put(p2, Math.max(kill.getOrDefault(p2, 0), cuboidSet.size()));
                }
            }
        }

        Set<Point> outlinePoints = new HashSet<>();
        yPoints.forEach(point -> {
            Point p1 = new Point(point.getX() - 1, point.getY(), point.getZ());
            Point p2 = new Point(point.getX() + 1, point.getY(), point.getZ());
            Point p3 = new Point(point.getX(), point.getY(), point.getZ() - 1);
            Point p4 = new Point(point.getX(), point.getY(), point.getZ() + 1);

            Point p5 = new Point(point.getX() - 1, point.getY(), point.getZ() - 1);
            Point p6 = new Point(point.getX() - 1, point.getY(), point.getZ() + 1);
            Point p7 = new Point(point.getX() + 1, point.getY(), point.getZ() - 1);
            Point p8 = new Point(point.getX() + 1, point.getY(), point.getZ() + 1);

            int count = kill.get(point);

            int surrounded = 0;
            if (kill.getOrDefault(p1, 0) == count) surrounded++;
            if (kill.getOrDefault(p2, 0) == count) surrounded++;
            if (kill.getOrDefault(p3, 0) == count) surrounded++;
            if (kill.getOrDefault(p4, 0) == count) surrounded++;
            if (surrounded != 4) outlinePoints.add(point);
            if (kill.getOrDefault(p5, 0) != count) outlinePoints.add(point);
            if (kill.getOrDefault(p6, 0) != count) outlinePoints.add(point);
            if (kill.getOrDefault(p7, 0) != count) outlinePoints.add(point);
            if (kill.getOrDefault(p8, 0) != count) outlinePoints.add(point);
        });
        xPoints.forEach(point -> {
            Point p1 = new Point(point.getX(), point.getY() - 1, point.getZ());
            Point p2 = new Point(point.getX(), point.getY() + 1, point.getZ());
            Point p3 = new Point(point.getX(), point.getY(), point.getZ() - 1);
            Point p4 = new Point(point.getX(), point.getY(), point.getZ() + 1);

            Point p5 = new Point(point.getX(), point.getY() - 1, point.getZ() - 1);
            Point p6 = new Point(point.getX(), point.getY() - 1, point.getZ() + 1);
            Point p7 = new Point(point.getX(), point.getY() + 1, point.getZ() - 1);
            Point p8 = new Point(point.getX(), point.getY() + 1, point.getZ() + 1);

            int count = kill.get(point);

            int surrounded = 0;
            if (kill.getOrDefault(p1, 0) == count) surrounded++;
            if (kill.getOrDefault(p2, 0) == count) surrounded++;
            if (kill.getOrDefault(p3, 0) == count) surrounded++;
            if (kill.getOrDefault(p4, 0) == count) surrounded++;
            if (surrounded != 4) outlinePoints.add(point);
            if (kill.getOrDefault(p5, 0) != count) outlinePoints.add(point);
            if (kill.getOrDefault(p6, 0) != count) outlinePoints.add(point);
            if (kill.getOrDefault(p7, 0) != count) outlinePoints.add(point);
            if (kill.getOrDefault(p8, 0) != count) outlinePoints.add(point);
        });
        zPoints.forEach(point -> {
            Point p1 = new Point(point.getX() - 1, point.getY(), point.getZ());
            Point p2 = new Point(point.getX() + 1, point.getY(), point.getZ());
            Point p3 = new Point(point.getX(), point.getY() - 1, point.getZ());
            Point p4 = new Point(point.getX(), point.getY() + 1, point.getZ());

            Point p5 = new Point(point.getX() - 1, point.getY() - 1, point.getZ());
            Point p6 = new Point(point.getX() - 1, point.getY() + 1, point.getZ());
            Point p7 = new Point(point.getX() + 1, point.getY() - 1, point.getZ());
            Point p8 = new Point(point.getX() + 1, point.getY() + 1, point.getZ());

            int count = kill.get(point);

            int surrounded = 0;
            if (kill.getOrDefault(p1, 0) == count) surrounded++;
            if (kill.getOrDefault(p2, 0) == count) surrounded++;
            if (kill.getOrDefault(p3, 0) == count) surrounded++;
            if (kill.getOrDefault(p4, 0) == count) surrounded++;
            if (surrounded != 4) outlinePoints.add(point);
            if (kill.getOrDefault(p5, 0) != count) outlinePoints.add(point);
            if (kill.getOrDefault(p6, 0) != count) outlinePoints.add(point);
            if (kill.getOrDefault(p7, 0) != count) outlinePoints.add(point);
            if (kill.getOrDefault(p8, 0) != count) outlinePoints.add(point);
        });

        double xPercent = zCount / (double) xArea;
        double yPercent = yCount / (double) yArea;
        double zPercent = xCount / (double) zArea;
        percent = (xPercent + yPercent + zPercent) / 3;
        kills = zKills + yKills + xKills;
        outline.getPlayers().forEach(this::updateBossBar);

        Set<Point> pointSet = new HashSet<>(killCount.keySet());
        Set<Point> outlinePointsCacheLast = new HashSet<>(outlinePointsCache);
        outlinePointsCache.clear();
        for (Point point : pointSet) {
            if (!kill.containsKey(point)) {
                rEntities.get(point).die();
                rEntities.remove(point);
                killCount.remove(point);
            }
        }
        kill.forEach((point, count) -> {
            if (rEntities.containsKey(point)) {
                if (killCount.get(point) == count && outlinePoints.contains(point) == outlinePointsCacheLast.contains(point)) return;
                rEntities.get(point).die();
            }
            RFallingBlockEntity entity = new RFallingBlockEntity(outlinePoints.contains(point) ? outline : inner, point.toLocation(WORLD, 0.5, 0, 0.5), MATERIALS[Math.min(count - 1, MATERIALS.length) - 1]);
            entity.setNoGravity(true);
            rEntities.put(point, entity);
            if (outlinePoints.contains(point)) outlinePointsCache.add(point);
            killCount.put(point, count);
        });
    }

    private int splitIntoDoubleKills(int kills) {
        return kills * (kills - 1) / 2;
    }

    private void updateBossBar(Player player) {
        BauSystemBossbar bossbar = bossBarService.get(player, region, "killchecker");
        bossbar.setTitle(BauSystem.MESSAGE.parse("KILLCHECKER_BOSSBAR", player, kills, ((int) (percent * 1000) / 10.0), cannonCount));
        bossbar.setProgress(Math.min(Math.max(percent, 0), 1));

        if (percent >= 0.35) {
            bossbar.setColor(BarColor.RED);
        } else if (percent >= 0.25) {
            bossbar.setColor(BarColor.PURPLE);
        } else if (percent >= 0.15) {
            bossbar.setColor(BarColor.YELLOW);
        } else {
            bossbar.setColor(BarColor.GREEN);
        }
    }

    private Cuboid create(Material type, int x, int y, int z) {
        Set<Point> checked = new HashSet<>();
        Set<Point> points = new HashSet<>();
        points.add(new Point(x, y, z));
        while (!points.isEmpty()) {
            Point point = points.iterator().next();
            points.remove(point);
            if (!checked.add(point)) continue;
            if (point.getX() < minPoint.getX() || point.getX() > maxPoint.getX()) continue;
            if (point.getY() < minPoint.getY() || point.getY() > maxPoint.getY()) continue;
            if (point.getZ() < minPoint.getZ() || point.getZ() > maxPoint.getZ()) continue;
            
            if (WORLD.getBlockAt(point.getX() + 1, point.getY(), point.getZ()).getType() == type) {
                points.add(new Point(point.getX() + 1, point.getY(), point.getZ()));
            }
            if (WORLD.getBlockAt(point.getX(), point.getY() + 1, point.getZ()).getType() == type) {
                points.add(new Point(point.getX(), point.getY() + 1, point.getZ()));
            }
            if (WORLD.getBlockAt(point.getX(), point.getY(), point.getZ() + 1).getType() == type) {
                points.add(new Point(point.getX(), point.getY(), point.getZ() + 1));
            }
            if (WORLD.getBlockAt(point.getX() - 1, point.getY(), point.getZ()).getType() == type) {
                points.add(new Point(point.getX() - 1, point.getY(), point.getZ()));
            }
            if (WORLD.getBlockAt(point.getX(), point.getY() - 1, point.getZ()).getType() == type) {
                points.add(new Point(point.getX(), point.getY() - 1, point.getZ()));
            }
            if (WORLD.getBlockAt(point.getX(), point.getY(), point.getZ() - 1).getType() == type) {
                points.add(new Point(point.getX(), point.getY(), point.getZ() - 1));
            }
        }

        int minX = Integer.MAX_VALUE;
        int maxX = Integer.MIN_VALUE;
        int minY = Integer.MAX_VALUE;
        int maxY = Integer.MIN_VALUE;
        int minZ = Integer.MAX_VALUE;
        int maxZ = Integer.MIN_VALUE;
        for (Point point : checked) {
            if (point.getX() < minX) minX = point.getX();
            if (point.getX() > maxX) maxX = point.getX();
            if (point.getY() < minY) minY = point.getY();
            if (point.getY() > maxY) maxY = point.getY();
            if (point.getZ() < minZ) minZ = point.getZ();
            if (point.getZ() > maxZ) maxZ = point.getZ();
        }

        return new Cuboid(minX, minY, minZ, maxX, maxY, maxZ);
    }

    public void show(Player player, boolean onlyOutline) {
        outline.addPlayer(player);
        if (!onlyOutline) {
            inner.addPlayer(player);
        } else {
            inner.removePlayer(player);
        }
        updateBossBar(player);
    }

    public boolean hide(Player player) {
        outline.removePlayer(player);
        inner.removePlayer(player);
        bossBarService.remove(player, region, "killchecker");
        if (outline.getPlayers().isEmpty() && inner.getPlayers().isEmpty()) {
            outline.close();
            inner.close();
            return true;
        }
        return false;
    }
}
