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

import com.comphenix.tinyprotocol.TinyProtocol;
import de.steamwar.core.Core;
import net.minecraft.network.protocol.game.ServerboundInteractPacket;
import org.bukkit.Bukkit;
import org.bukkit.Location;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority;
import org.bukkit.event.HandlerList;
import org.bukkit.event.Listener;
import org.bukkit.event.player.PlayerMoveEvent;
import org.bukkit.event.player.PlayerQuitEvent;

import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.BiConsumer;
import java.util.function.BiFunction;
import java.util.function.Consumer;
import java.util.stream.Collectors;


public class REntityServer implements Listener {

    private static final HashSet<REntity> emptyEntities = new HashSet<>(0);
    private static final HashSet<Player> emptyPlayers = new HashSet<>(0);

    private final ConcurrentHashMap<Integer, REntity> entityMap = new ConcurrentHashMap<>();
    private final HashMap<Long, HashSet<REntity>> entities = new HashMap<>();
    private final HashMap<Long, Set<Player>> players = new HashMap<>();
    private final HashMap<Player, Location> lastLocation = new HashMap<>();
    private final HashMap<Player, Integer> viewDistance = new HashMap<>();

    private final Set<Player> playersThatClicked = Collections.synchronizedSet(new HashSet<>());

    private final BiFunction<Player, ServerboundInteractPacket, Object> filter = (player, packet) -> {
        REntity entity = entityMap.get(packet.getEntityId());
        if (entity == null) return packet;

        if (playersThatClicked.contains(player)) return null;

        playersThatClicked.add(player);
        REntityAction action = packet.isAttack() ? REntityAction.ATTACK : REntityAction.INTERACT;
        Bukkit.getScheduler().runTask(Core.getInstance(), () -> {
            playersThatClicked.remove(player);
            if (entity instanceof RInteractableEntity interactable && interactable.getCallback() != null) {
                interactable.getCallback().onAction(player, entity, action);
            }
        });
        return null;
    };

    public REntityServer() {
        Core.getInstance().getServer().getPluginManager().registerEvents(this, Core.getInstance());
        TinyProtocol.instance.addFilter(ServerboundInteractPacket.class, filter);
    }

    public void addPlayer(Player player) {
        Location location = player.getLocation();
        lastLocation.put(player, location);
        viewDistance.put(player, viewRadius(player));
        forChunkInView(player, location, (x, z) -> addPlayerToChunk(player, x, z));
    }

    public void removePlayer(Player player) {
        if (!viewDistance.containsKey(player)) {
            return;
        }
        forChunkInView(player, lastLocation.remove(player), (x, z) -> removePlayerFromChunk(player, x, z));
        viewDistance.remove(player);
    }

    public List<Player> getPlayers() {
        return new ArrayList<>(viewDistance.keySet());
    }

    public void close() {
        TinyProtocol.instance.removeFilter(ServerboundInteractPacket.class, filter);
        for (Player player : lastLocation.keySet().toArray(new Player[0])) {
            removePlayer(player);
        }
        HandlerList.unregisterAll(this);
    }

    void addEntity(REntity entity) {
        entityMap.put(entity.entityId, entity);
        addEntityToChunk(entity);
        entity.list(packet -> updateEntity(entity, packet));
        entity.spawn(packet -> updateEntity(entity, packet));
    }

    void preEntityMove(REntity entity, double toX, double toZ) {
        long fromId = entityToId(entity);
        long toId = posToId(toX, toZ);
        if (fromId == toId) return;

        if (!entity.isHidden()) {
            onMissing(players.get(fromId), players.get(toId), entity::despawn);
        }
        onMissing(players.get(fromId), players.get(toId), entity::delist);
        removeEntityFromChunk(entity);
    }

    void postEntityMove(REntity entity, double fromX, double fromZ) {
        long fromId = posToId(fromX, fromZ);
        long toId = entityToId(entity);
        if (fromId == toId) return;

        addEntityToChunk(entity);
        onMissing(players.get(toId), players.get(fromId), entity::list);
        if (!entity.isHidden()) {
            onMissing(players.get(toId), players.get(fromId), entity::spawn);
        }
    }

    void updateEntity(REntity entity, Object packet) {
        if (entity.isHidden()) return;

        for (Player player : players.getOrDefault(entityToId(entity), emptyPlayers)) {
            TinyProtocol.instance.sendPacket(player, packet);
        }
    }

    void removeEntity(REntity entity) {
        entity.despawn(packet -> updateEntity(entity, packet));
        removeEntityFromChunk(entity);
        entity.delist(packet -> updateEntity(entity, packet));
        entityMap.remove(entity.entityId);
    }

    public List<REntity> getEntities() {
        return new ArrayList<>(entityMap.values());
    }

    public <T extends REntity> List<T> getEntitiesByType(Class<T> clazz) {
        return entityMap.values().stream().filter(clazz::isInstance).map(clazz::cast).collect(Collectors.toList());
    }

    private void addEntityToChunk(REntity entity) {
        entities.computeIfAbsent(entityToId(entity), i -> new HashSet<>()).add(entity);
    }

    private void removeEntityFromChunk(REntity entity) {
        long id = entityToId(entity);
        HashSet<REntity> entitiesInChunk = entities.get(id);
        if (entitiesInChunk == null) return;
        entitiesInChunk.remove(entity);
        if (entitiesInChunk.isEmpty()) entities.remove(id);
    }

    @EventHandler(ignoreCancelled = true, priority = EventPriority.MONITOR)
    public void onMove(PlayerMoveEvent e) {
        Player player = e.getPlayer();
        Location from = lastLocation.get(player);
        Location to = e.getTo();
        if (from == null || to == null) return;

        int fromX = posToChunk(from.getX());
        int fromZ = posToChunk(from.getZ());
        int toX = posToChunk(to.getX());
        int toZ = posToChunk(to.getZ());
        if (fromX == toX && fromZ == toZ) return;

        lastLocation.put(player, to);

        int toViewDistance = viewRadius(player);
        forChunkInView(player, from, (x, z) -> {
            if (Math.abs(x - toX) > toViewDistance || Math.abs(z - toZ) > toViewDistance) {
                removePlayerFromChunk(player, x, z);
            }
        });

        int fromViewDistance = this.viewDistance.put(player, toViewDistance);
        forChunkInView(player, to, (x, z) -> {
            if (Math.abs(x - fromX) > fromViewDistance || Math.abs(z - fromZ) > fromViewDistance) {
                addPlayerToChunk(player, x, z);
            }
        });
    }

    @EventHandler
    public void onQuit(PlayerQuitEvent e) {
        Player player = e.getPlayer();
        Location location = lastLocation.remove(player);
        if (location == null) return;

        forChunkInView(player, location, (x, z) -> {
            long id = chunkToId(x, z);
            Set<Player> playersInChunk = players.get(id);
            playersInChunk.remove(player);
            if (playersInChunk.isEmpty()) players.remove(id);
        });
        viewDistance.remove(player);
    }

    private void onMissing(Set<Player> of, Set<Player> in, Consumer<Consumer<Object>> packetProvider) {
        if (of == null) return;

        for (Player player : of) {
            if (in == null || !in.contains(player)) {
                packetProvider.accept(packet -> TinyProtocol.instance.sendPacket(player, packet));
            }
        }
    }

    private void forChunkInView(Player player, Location location, BiConsumer<Integer, Integer> func) {
        int chunkX = posToChunk(location.getX());
        int chunkZ = posToChunk(location.getZ());
        int viewDistance = this.viewDistance.get(player);

        for (int x = chunkX - viewDistance; x <= chunkX + viewDistance; x++) {
            for (int z = chunkZ - viewDistance; z <= chunkZ + viewDistance; z++) {
                func.accept(x, z);
            }
        }
    }

    private void addPlayerToChunk(Player player, int x, int z) {
        long id = chunkToId(x, z);
        players.computeIfAbsent(id, i -> new HashSet<>()).add(player);
        for (REntity entity : entities.getOrDefault(id, emptyEntities)) {
            entity.list(packet -> TinyProtocol.instance.sendPacket(player, packet));
            if (!entity.isHidden()) {
                entity.spawn(packet -> TinyProtocol.instance.sendPacket(player, packet));
            }
        }
    }

    private void removePlayerFromChunk(Player player, int x, int z) {
        long id = chunkToId(x, z);

        Set<Player> playersInChunk = players.get(id);
        playersInChunk.remove(player);
        if (playersInChunk.isEmpty()) players.remove(id);

        for (REntity entity : entities.getOrDefault(id, emptyEntities)) {
            if (!entity.isHidden()) {
                entity.despawn(packet -> TinyProtocol.instance.sendPacket(player, packet));
            }
            entity.delist(packet -> TinyProtocol.instance.sendPacket(player, packet));
        }
    }

    public void tick() {
        for (REntity entity : entityMap.values()) {
            entity.tick();
        }
    }

    private int posToChunk(double coord) {
        return (int) (coord / 16) - (coord < 0 ? 1 : 0);
    }

    private int viewRadius(Player player) {
        return player.getClientViewDistance() / 2;
    }

    private long entityToId(REntity entity) {
        return posToId(entity.x(), entity.z());
    }

    private long posToId(double x, double z) {
        return chunkToId(posToChunk(x), posToChunk(z));
    }

    private long chunkToId(int x, int z) {
        return ((long) x << 32) + z;
    }
}
