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

import com.destroystokyo.paper.event.server.ServerTickEndEvent;
import de.steamwar.bausystem.BauSystem;
import de.steamwar.bausystem.Permission;
import de.steamwar.bausystem.SWUtils;
import de.steamwar.bausystem.features.simulator.data.Simulator;
import de.steamwar.bausystem.features.simulator.data.SimulatorElement;
import de.steamwar.bausystem.features.simulator.data.SimulatorGroup;
import de.steamwar.bausystem.features.simulator.data.observer.ObserverElement;
import de.steamwar.bausystem.features.simulator.data.observer.ObserverPhase;
import de.steamwar.bausystem.features.simulator.data.redstone.RedstoneElement;
import de.steamwar.bausystem.features.simulator.data.redstone.RedstonePhase;
import de.steamwar.bausystem.features.simulator.data.tnt.TNTElement;
import de.steamwar.bausystem.features.simulator.data.tnt.TNTPhase;
import de.steamwar.bausystem.features.simulator.execute.SimulatorExecutor;
import de.steamwar.bausystem.features.simulator.gui.SimulatorGroupGui;
import de.steamwar.bausystem.features.simulator.gui.SimulatorGui;
import de.steamwar.bausystem.features.simulator.gui.base.SimulatorBaseGui;
import de.steamwar.bausystem.utils.BauMemberUpdateEvent;
import de.steamwar.bausystem.utils.ItemUtils;
import de.steamwar.core.SWPlayer;
import de.steamwar.cursor.Cursor;
import de.steamwar.entity.REntity;
import de.steamwar.entity.REntityServer;
import de.steamwar.inventory.SWAnvilInv;
import de.steamwar.linkage.Linked;
import lombok.AllArgsConstructor;
import lombok.Getter;
import org.bukkit.Bukkit;
import org.bukkit.Location;
import org.bukkit.Material;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority;
import org.bukkit.event.Listener;
import org.bukkit.event.block.Action;
import org.bukkit.event.inventory.InventoryClickEvent;
import org.bukkit.event.inventory.InventoryDragEvent;
import org.bukkit.event.player.*;
import org.bukkit.inventory.ItemStack;
import org.bukkit.util.Vector;

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

@Linked
public class SimulatorCursor implements Listener {

    private static final Map<Player, CursorType> cursorType = Collections.synchronizedMap(new HashMap<>());
    private static final Map<Player, REntityServer> emptyTargetServers = Collections.synchronizedMap(new HashMap<>());

    public static boolean isSimulatorItem(ItemStack itemStack) {
        return ItemUtils.isItem(itemStack, "simulator");
    }

    private static boolean hasSimulatorItem(Player player) {
        return isSimulatorItem(player.getInventory().getItemInMainHand()) || isSimulatorItem(player.getInventory().getItemInOffHand());
    }

    private static Set<Player> scheduledUpdates = new HashSet<>();

    private static void scheduleCursorUpdate(Player player) {
        scheduledUpdates.add(player);
    }

    @EventHandler
    public void onServerTickEnd(ServerTickEndEvent event) {
        scheduledUpdates.forEach(SimulatorCursor::calcCursor);
        scheduledUpdates.clear();
    }

    @EventHandler
    public void onPlayerJoin(PlayerJoinEvent event) {
        if (!Permission.BUILD.hasPermission(event.getPlayer())) return;
        scheduleCursorUpdate(event.getPlayer());
    }

    @EventHandler
    public void onPlayerDropItem(PlayerDropItemEvent event) {
        if (!Permission.BUILD.hasPermission(event.getPlayer())) return;
        scheduleCursorUpdate(event.getPlayer());
    }

    @EventHandler
    public void onPlayerItemHeld(PlayerItemHeldEvent event) {
        if (!Permission.BUILD.hasPermission(event.getPlayer())) return;
        scheduleCursorUpdate(event.getPlayer());
    }

    @EventHandler
    public void onInventoryClick(InventoryClickEvent event) {
        if (!(event.getWhoClicked() instanceof Player player)) return;
        if (!Permission.BUILD.hasPermission(player)) return;
        scheduleCursorUpdate(player);
    }

    @EventHandler
    public void onInventoryDrag(InventoryDragEvent event) {
        if (!(event.getWhoClicked() instanceof Player player)) return;
        if (!Permission.BUILD.hasPermission(player)) return;
        scheduleCursorUpdate(player);
    }

    @EventHandler
    public void onBauMemberUpdate(BauMemberUpdateEvent event) {
        event.getChanged().forEach(SimulatorCursor::calcCursor);
    }

    @EventHandler
    public void onPlayerQuit(PlayerQuitEvent event) {
        cursorType.remove(event.getPlayer());
        removeCursor(event.getPlayer());
        scheduledUpdates.remove(event.getPlayer());
    }

    private static final Map<Player, Long> LAST_SNEAKS = new HashMap<>();

    static {
        Bukkit.getScheduler().runTaskTimer(BauSystem.getInstance(), () -> {
            long millis = System.currentTimeMillis();
            LAST_SNEAKS.entrySet().removeIf(entry -> millis - entry.getValue() > 200);
        }, 1, 1);
    }

    @EventHandler(priority = EventPriority.HIGH)
    public void onPlayerToggleSneak(PlayerToggleSneakEvent event) {
        if (!event.isSneaking()) return;
        Player player = event.getPlayer();
        if (!hasSimulatorItem(player)) {
            return;
        }
        if (LAST_SNEAKS.containsKey(player)) {
            CursorType currentType = cursorType.getOrDefault(player, CursorType.TNT);
            if (currentType == CursorType.TNT) {
                cursorType.put(player, CursorType.REDSTONE_BLOCK);
            } else {
                cursorType.put(player, CursorType.TNT);
            }
            calcCursor(player);
        } else {
            LAST_SNEAKS.put(player, System.currentTimeMillis());
        }
    }

    public static CursorType getCursorType(Player player) {
        return cursorType.getOrDefault(player, CursorType.TNT);
    }

    public static void setCursorType(Player player, CursorType cursorType) {
        SimulatorCursor.cursorType.put(player, cursorType);
        calcCursor(player);
    }

    public static void calcCursor(Player player) {
        if (!Permission.BUILD.hasPermission(player) || !hasSimulatorItem(player)) {
            if (removeCursor(player) | SimulatorWatcher.show(null, player)) {
                SWUtils.sendToActionbar(player, "");
            }
            return;
        }

        Simulator simulator = SimulatorStorage.getSimulator(player);
        if (simulator != null && simulator.getStabGenerator() != null) {
            removeCursor(player);
            SimulatorWatcher.show(null, player);
            SWUtils.sendToActionbar(player, "§cGenerating Stab");
            return;
        }

        SimulatorWatcher.show(simulator, player);
        Cursor cursor = getOrCreateCursor(player, simulator, cursorType.getOrDefault(player, CursorType.TNT));
        cursor.renderDeduplicated();
    }

    private static Cursor getOrCreateCursor(Player player, Simulator simulator, CursorType type) {
        REntityServer targetServer = simulator == null ? emptyTargetServers.computeIfAbsent(player, __ -> new REntityServer()) : SimulatorWatcher.getEntityServerOfSimulator(simulator);
        SWPlayer swPlayer = SWPlayer.of(player);
        Optional<Cursor> activeCursor = swPlayer.getComponent(Cursor.class);

        Cursor cursor = activeCursor.orElse(null);
        if (cursor == null || cursor.getTargetServer() != targetServer) {
            swPlayer.removeComponent(Cursor.class);
            cursor = new Cursor(
                    targetServer,
                    player,
                    Material.GLASS,
                    type.material,
                    type.cursorModes,
                    (location, hitEntity, action) -> handlePlayerClick(player, location, hitEntity, action),
                    (location, hitEntity) -> sendCursorActionbar(player, SimulatorStorage.getSimulator(player), location != null, hitEntity.isPresent())
            );
        } else {
            cursor.setCursorMaterial(type.material);
            cursor.setAllowedCursorModes(type.cursorModes);
        }

        return cursor;
    }

    private static synchronized boolean removeCursor(Player player) {
        Optional<Cursor> cursor = SWPlayer.of(player).getComponent(Cursor.class);
        cursor.ifPresent(__ -> SWPlayer.of(player).removeComponent(Cursor.class));
        REntityServer emptyTargetServer = emptyTargetServers.remove(player);
        if (emptyTargetServer != null) {
            emptyTargetServer.close();
        }
        return cursor.isPresent();
    }

    private static void sendCursorActionbar(Player player, Simulator simulator, boolean hasCursorLocation, boolean hasHitEntity) {
        if (!hasCursorLocation) {
            SWUtils.sendToActionbar(player, simulator == null ? "§eSelect Simulator" : "§eOpen Simulator");
        } else if (simulator == null) {
            SWUtils.sendToActionbar(player, "§eCreate new Simulator");
        } else if (hasHitEntity) {
            SWUtils.sendToActionbar(player, "§eEdit Position");
        } else {
            SWUtils.sendToActionbar(player, "§eAdd new " + cursorType.getOrDefault(player, CursorType.TNT).name);
        }
    }

    @Getter
    @AllArgsConstructor
    public enum CursorType {
        TNT(Material.TNT, Material.GUNPOWDER, List.of(Cursor.CursorMode.FREE, Cursor.CursorMode.SURFACE_ALIGNED), "TNT", vector -> new TNTElement(vector).add(new TNTPhase())),
        REDSTONE_BLOCK(Material.REDSTONE_BLOCK, Material.REDSTONE, List.of(Cursor.CursorMode.BLOCK_ALIGNED), "Redstone Block", vector -> new RedstoneElement(vector).add(new RedstonePhase())),
        OBSERVER(Material.OBSERVER, Material.QUARTZ, List.of(Cursor.CursorMode.BLOCK_ALIGNED), "Observer", vector -> new ObserverElement(vector).add(new ObserverPhase())),
        ;

        public final Material material;
        public final Material nonSelectedMaterial;
        public final List<Cursor.CursorMode> cursorModes;
        public final String name;
        public final Function<Vector, SimulatorElement<?>> elementFunction;
    }

    private static void handlePlayerClick(Player player, Location cursorLocation, Optional<REntity> hitEntity, Action action) {
        if (!Permission.BUILD.hasPermission(player)) return;
        if (!hasSimulatorItem(player)) {
            return;
        }

        Simulator simulator = SimulatorStorage.getSimulator(player);

        if (action == Action.LEFT_CLICK_BLOCK || action == Action.LEFT_CLICK_AIR) {
            if (simulator == null) {
                return;
            }
            SimulatorExecutor.run(player, simulator, null);
            return;
        }

        if (action != Action.RIGHT_CLICK_BLOCK && action != Action.RIGHT_CLICK_AIR) {
            return;
        }

        if (simulator == null) {
            if (cursorLocation == null) {
                SimulatorStorage.openSimulatorSelector(player);
            } else {
                SWAnvilInv anvilInv = new SWAnvilInv(player, "Name");
                anvilInv.setCallback(s -> {
                    Simulator sim = SimulatorStorage.getSimulator(s);
                    if (sim != null) {
                        BauSystem.MESSAGE.send("SIMULATOR_NAME_ALREADY_EXISTS", player);
                        return;
                    }
                    if (!s.matches("[a-zA-Z_0-9-]+")) {
                        BauSystem.MESSAGE.send("SIMULATOR_NAME_INVALID", player);
                        return;
                    }
                    sim = new Simulator(s);
                    SimulatorStorage.addSimulator(s, sim);
                    createElement(player, cursorLocation, sim);
                    SimulatorStorage.setSimulator(player, sim);
                });
                anvilInv.open();
            }
            return;
        }

        if (cursorLocation == null) {
            new SimulatorGui(player, simulator).open();
            return;
        }

        if (hitEntity.isPresent()) {
            openElement(player, simulator, hitEntity.get());
            return;
        }

        createElement(player, cursorLocation, simulator);
    }

    private static void openElement(Player player, Simulator simulator, REntity hitEntity) {
        Vector vector = new Vector(hitEntity.getX(), hitEntity.getY(), hitEntity.getZ());
        List<SimulatorElement<?>> elements = simulator.getGroups().stream().map(SimulatorGroup::getElements).flatMap(List::stream).filter(element -> {
            return element.getWorldPos().distanceSquared(vector) < (1 / 16.0) * (1 / 16.0);
        }).collect(Collectors.toList());

        switch (elements.size()) {
            case 0:
                return;
            case 1:
                SimulatorElement<?> element = elements.get(0);
                SimulatorGroup group1 = element.getGroup(simulator);
                SimulatorBaseGui back = new SimulatorGui(player, simulator);
                if (group1.getElements().size() > 1) {
                    back = new SimulatorGroupGui(player, simulator, group1, back);
                }
                element.open(player, simulator, group1, back);
                break;
            default:
                List<SimulatorGroup> parents = elements.stream().map(e -> e.getGroup(simulator)).distinct().collect(Collectors.toList());
                if (parents.size() == 1) {
                    SimulatorGui simulatorGui = new SimulatorGui(player, simulator);
                    new SimulatorGroupGui(player, simulator, parents.get(0), simulatorGui).open();
                } else {
                    SimulatorGroup group2 = new SimulatorGroup();
                    group2.setMaterial(null);
                    group2.getElements().addAll(elements);
                    SimulatorGui simulatorGui = new SimulatorGui(player, simulator);
                    new SimulatorGroupGui(player, simulator, group2, simulatorGui).open();
                }
                break;
        }
    }

    private static void createElement(Player player, Location cursorLocation, Simulator simulator) {
        CursorType type = cursorType.getOrDefault(player, CursorType.TNT);
        Vector vector = cursorLocation.toVector();
        if (type == CursorType.REDSTONE_BLOCK) {
            vector.subtract(new Vector(0.5, 0, 0.5));
        }
        SimulatorElement<?> element = type.elementFunction.apply(vector);
        SimulatorGroup group = new SimulatorGroup().add(element);
        simulator.getGroups().add(group);
        SimulatorGui simulatorGui = new SimulatorGui(player, simulator);
        element.open(player, simulator, group, simulatorGui);
        SimulatorWatcher.update(simulator);
        calcCursor(player);
    }
}
