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

import de.steamwar.bausystem.BauSystem;
import de.steamwar.bausystem.Permission;
import de.steamwar.entity.*;
import de.steamwar.linkage.Linked;
import org.bukkit.Bukkit;
import org.bukkit.Location;
import org.bukkit.Material;
import org.bukkit.block.Block;
import org.bukkit.block.BlockFace;
import org.bukkit.block.PistonMoveReaction;
import org.bukkit.block.TileState;
import org.bukkit.block.data.BlockData;
import org.bukkit.block.data.type.Piston;
import org.bukkit.entity.Display;
import org.bukkit.entity.Player;
import org.bukkit.entity.TextDisplay;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
import org.bukkit.event.block.*;
import org.bukkit.event.player.PlayerInteractEvent;
import org.bukkit.event.player.PlayerQuitEvent;

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

@Linked
public class PistonCalculator implements Listener {

    private final Map<Player, Long> debounce = new HashMap<>();
    protected final Set<Player> disablePistonCalculator = new HashSet<>();

    @EventHandler
    public void onPlayerInteract(PlayerInteractEvent event) {
        if (!Permission.BUILD.hasPermission(event.getPlayer())) return;
        if (event.getAction() != Action.RIGHT_CLICK_BLOCK) return;
        if (event.getItem() == null) {
        } else if (event.getItem() != null && event.getItem().getType() == Material.SLIME_BALL) {
        } else if (!event.getItem().getType().isBlock()) {
            debounce.put(event.getPlayer(), System.currentTimeMillis());
            return;
        } else return;
        if (event.getClickedBlock() == null) return;
        Block clickedBlock = event.getClickedBlock();
        Material blockType = clickedBlock.getType();
        if (!(blockType == Material.PISTON || blockType == Material.STICKY_PISTON)) return;
        if (disablePistonCalculator.contains(event.getPlayer())) return;
        if (System.currentTimeMillis() - debounce.getOrDefault(event.getPlayer(), 0L) <= 200) return;
        debounce.put(event.getPlayer(), System.currentTimeMillis());

        Location location = event.getClickedBlock().getLocation();
        if (pistOrders.containsKey(location)) {
            PistOrder pistOrder = pistOrders.get(location);
            if (pistOrder.server.getPlayers().contains(event.getPlayer())) {
                pistOrder.server.removePlayer(event.getPlayer());
            } else {
                pistOrder.server.addPlayer(event.getPlayer());
            }
            if (pistOrder.server.getPlayers().isEmpty()) {
                pistOrders.remove(location);
                pistOrder.server.close();
            }
            return;
        }

        PistOrder pistOrder = new PistOrder(clickedBlock);
        pistOrder.calculate();
        pistOrder.server.addPlayer(event.getPlayer());
        pistOrders.put(location, pistOrder);
    }

    @EventHandler
    public void onBlockPistonExtend(BlockPistonExtendEvent event) {
        if (pistOrders.isEmpty()) return;
        Bukkit.getScheduler().runTaskLater(BauSystem.getInstance(), () -> {
            movePistOrders(event.getDirection(), event.getBlocks());
            pistOrders.values().forEach(PistOrder::calculate);
        }, 3);
    }

    @EventHandler
    public void onBlockPistonRetract(BlockPistonRetractEvent event) {
        if (pistOrders.isEmpty()) return;
        Bukkit.getScheduler().runTaskLater(BauSystem.getInstance(), () -> {
            movePistOrders(event.getDirection(), event.getBlocks());
            pistOrders.values().forEach(PistOrder::calculate);
        }, 3);
    }

    private void movePistOrders(BlockFace direction, List<Block> blocks) {
        Set<PistOrder> orders = new HashSet<>();
        blocks.forEach(block -> {
            PistOrder pistOrder = pistOrders.get(block.getLocation());
            if (pistOrder == null) return;
            pistOrders.remove(block.getLocation());
            pistOrder.piston = pistOrder.piston.getRelative(direction);
            orders.add(pistOrder);
        });
        orders.forEach(pistOrder -> {
            pistOrders.put(pistOrder.piston.getLocation(), pistOrder);
        });
    }

    @EventHandler
    public void onBlockPlace(BlockPlaceEvent event) {
        if (pistOrders.isEmpty()) return;
        Bukkit.getScheduler().runTaskLater(BauSystem.getInstance(), () -> {
            pistOrders.values().forEach(PistOrder::calculate);
        }, 1);
    }

    @EventHandler
    public void onBlockBreak(BlockBreakEvent event) {
        if (pistOrders.isEmpty()) return;
        if (pistOrders.containsKey(event.getBlock().getLocation())) {
            PistOrder pistOrder = pistOrders.remove(event.getBlock().getLocation());
            pistOrder.server.close();
        }
        Bukkit.getScheduler().runTaskLater(BauSystem.getInstance(), () -> {
            pistOrders.values().forEach(PistOrder::calculate);
        }, 1);
    }

    @EventHandler
    public void onPlayerQuit(PlayerQuitEvent event) {
        debounce.remove(event.getPlayer());
        disablePistonCalculator.remove(event.getPlayer());
    }

    public void clear(Player player) {
        Set<Location> toRemove = new HashSet<>();
        pistOrders.forEach((location, pistOrder) -> {
            pistOrder.server.removePlayer(player);
            if (pistOrder.server.getPlayers().isEmpty()) {
                toRemove.add(location);
                pistOrder.server.close();
            }
        });
        toRemove.forEach(pistOrders::remove);
    }

    private final BlockFace[] FACES = new BlockFace[]{BlockFace.UP, BlockFace.DOWN, BlockFace.NORTH, BlockFace.SOUTH, BlockFace.EAST, BlockFace.WEST};

    private final Map<Location, PistOrder> pistOrders = new HashMap<>();

    private final class PistOrder {
        private Block piston;
        private REntityServer server = new REntityServer();

        private boolean pulling = false;
        private List<Location> movedBlocks = new ArrayList<>();
        private List<Location> brokenBlocks = new ArrayList<>();
        private Set<Location> immovableBlocks = new HashSet<>();

        public PistOrder(Block piston) {
            this.piston = piston;
        }

        public void calculate() {
            movedBlocks.clear();
            brokenBlocks.clear();
            immovableBlocks.clear();

            if (piston.isEmpty()) {
                Bukkit.getScheduler().runTaskLater(BauSystem.getInstance(), () -> {
                    server.close();
                    pistOrders.remove(piston.getLocation());
                }, 0);
                return;
            }

            BlockData blockData = piston.getBlockData();
            if (!(blockData instanceof Piston)) return;
            Piston pistonData = (Piston) blockData;
            if (piston.getType() == Material.PISTON && pistonData.isExtended()) {
                visualize();
                return;
            }

            pulling = piston.getType() == Material.STICKY_PISTON && (piston.getRelative(pistonData.getFacing()).getType() == Material.AIR || pistonData.isExtended());

            calculate(piston, pistonData.getFacing(), (pulling ? pistonData.getFacing().getOppositeFace() : pistonData.getFacing()));

            Collections.reverse(movedBlocks);
            Collections.reverse(brokenBlocks);

            visualize();
        }

        private void calculate(Block origin, BlockFace facing, BlockFace direction) {
            Block calcOrigin = origin;
            if (facing != direction) calcOrigin = origin.getRelative(facing, 3);

            List<Block> toCalc = new LinkedList<>();
            calcDirection(origin, null, calcOrigin, facing != direction ? origin.getRelative(facing) : null, facing, direction, toCalc);

            while (!toCalc.isEmpty()) {
                Block current = toCalc.remove(0);
                if (!movedBlocks.contains(current.getLocation())) {
                    movedBlocks.add(current.getLocation());
                }

                Material type = current.getType();
                if (type != Material.SLIME_BLOCK && type != Material.HONEY_BLOCK) continue;
                Material oppositeType = type == Material.SLIME_BLOCK ? Material.HONEY_BLOCK : Material.SLIME_BLOCK;

                for (BlockFace face : FACES) {
                    Block block = current.getRelative(face);
                    if (block.getType().isAir()) continue;
                    if (isImmovable(block) || block.getPistonMoveReaction() == PistonMoveReaction.BREAK) continue;
                    if (face != direction || face == direction.getOppositeFace()) {
                        switch (block.getType()) {
                            case BLACK_GLAZED_TERRACOTTA:
                            case GRAY_GLAZED_TERRACOTTA:
                            case BLUE_GLAZED_TERRACOTTA:
                            case BROWN_GLAZED_TERRACOTTA:
                            case CYAN_GLAZED_TERRACOTTA:
                            case GREEN_GLAZED_TERRACOTTA:
                            case LIGHT_BLUE_GLAZED_TERRACOTTA:
                            case LIGHT_GRAY_GLAZED_TERRACOTTA:
                            case LIME_GLAZED_TERRACOTTA:
                            case MAGENTA_GLAZED_TERRACOTTA:
                            case ORANGE_GLAZED_TERRACOTTA:
                            case PINK_GLAZED_TERRACOTTA:
                            case PURPLE_GLAZED_TERRACOTTA:
                            case RED_GLAZED_TERRACOTTA:
                            case WHITE_GLAZED_TERRACOTTA:
                            case YELLOW_GLAZED_TERRACOTTA:
                                continue;
                            default:
                                break;
                        }
                    }
                    if (block.getType() != oppositeType) {
                        if (!movedBlocks.contains(block.getLocation())) toCalc.add(block);
                        calcDirection(null, origin, block, null, facing, direction, toCalc);
                    }
                }
            }

            movedBlocks.remove(origin.getLocation());
            if (facing != direction) movedBlocks.remove(origin.getRelative(facing, 1).getLocation());
            if (pulling) immovableBlocks.remove(origin.getRelative(facing).getLocation());
        }

        private void calcDirection(Block origin, Block blockOrigin, Block calcOrigin, Block ignore, BlockFace facing, BlockFace direction, List<Block> toCalc) {
            for (int i = 1; i < 24; i++) {
                Block block = calcOrigin.getRelative(direction, i);
                if (block.equals(ignore)) return;
                if (block.getPistonMoveReaction() == PistonMoveReaction.BREAK) {
                    if (!brokenBlocks.contains(block.getLocation())) {
                        brokenBlocks.add(block.getLocation());
                    }
                    return;
                }
                if (isImmovable(block)) {
                    immovableBlocks.add(block.getLocation());
                    return;
                }
                if (block.getType().isAir()) return;
                if (facing == direction && block.equals(blockOrigin)) {
                    immovableBlocks.add(block.getLocation());
                    return;
                }
                if (facing != direction && (block.equals(origin) || block.getRelative(facing.getOppositeFace()).equals(origin))) {
                    return;
                }
                if (!movedBlocks.contains(block.getLocation())) toCalc.add(block);
            }
        }

        private void visualize() {
            server.getEntities().forEach(REntity::die);

            for (int i = 0; i < movedBlocks.size(); i++) {
                Location location = movedBlocks.get(i);
                int order = i + 1;

                CCubedTextDisplay display = new CCubedTextDisplay(server, location);
                display.setText("§e" + order);
                display.setBackgroundColor(0);
                display.setShadowed(false);

                Set<BlockFace> toHide = Arrays.stream(FACES).filter(blockFace -> {
                    return movedBlocks.contains(location.clone().add(blockFace.getModX(), blockFace.getModY(), blockFace.getModZ()));
                }).collect(Collectors.toSet());
                display.hide(toHide);
            }

            for (int i = 0; i < brokenBlocks.size(); i++) {
                Location location = brokenBlocks.get(i);
                int order = i + 1;

                RTextDisplay textDisplay = new RTextDisplay(server, location.clone().add(0.5, 0.4, 0.5));
                textDisplay.setText("§c" + order);
                textDisplay.setBillboard(Display.Billboard.CENTER);
                textDisplay.setAlignment(TextDisplay.TextAlignment.CENTER);
                textDisplay.setSeeThrough(true);
                textDisplay.setBackgroundColor(0);
                textDisplay.setShadowed(false);
                textDisplay.setBrightness(new Display.Brightness(15, 15));
            }

            for (Location location : immovableBlocks) {
                CWireframe wireframe = new CWireframe(server);
                wireframe.setPos1(location);
                wireframe.setPos2(location);
                wireframe.setWidth(1 / 32f);
                wireframe.setBlock(Material.RED_CONCRETE.createBlockData());
            }

            CWireframe wireframe = new CWireframe(server);
            wireframe.setPos1(piston.getLocation());
            wireframe.setPos2(piston.getLocation());
            if (!immovableBlocks.isEmpty()) {
                wireframe.setBlock(Material.RED_CONCRETE.createBlockData());
            } else if (movedBlocks.size() > 12) {
                wireframe.setBlock(Material.YELLOW_CONCRETE.createBlockData());
            } else {
                wireframe.setBlock(Material.LIME_CONCRETE.createBlockData());
            }
            wireframe.setWidth(1 / 32f);

            RTextDisplay textDisplay = new RTextDisplay(server, piston.getLocation().clone().add(0.5, 0.3, 0.5));
            StringBuilder text = new StringBuilder();
            if (pulling) {
                text.append("§ePull\n");
            } else {
                text.append("§ePush\n");
            }
            text.append("§f").append(movedBlocks.size()).append(" §eBlocks");
            textDisplay.setText(text.toString());
            textDisplay.setBillboard(Display.Billboard.CENTER);
            textDisplay.setAlignment(TextDisplay.TextAlignment.CENTER);
            textDisplay.setSeeThrough(true);
            textDisplay.setBackgroundColor(0);
            textDisplay.setShadowed(false);
            textDisplay.setBrightness(new Display.Brightness(15, 15));
        }
    }

    private boolean isImmovable(Block block) {
        if (block.isEmpty() || block.isLiquid()) {
            return false;
        }
        BlockData blockData = block.getBlockData();
        if (blockData instanceof Piston) {
            return ((Piston) blockData).isExtended();
        }
        if (block.getState() instanceof TileState) {
            return true;
        }
        PistonMoveReaction reaction = block.getPistonMoveReaction();
        if (reaction == PistonMoveReaction.IGNORE) return true;
        if (reaction == PistonMoveReaction.BLOCK) return true;
        switch (block.getType()) {
            case OBSIDIAN:
                return true;
            default:
                return false;
        }
    }
}
