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

import com.comphenix.tinyprotocol.TinyProtocol;
import de.steamwar.entity.REntity;
import de.steamwar.fightsystem.Config;
import de.steamwar.fightsystem.FightSystem;
import de.steamwar.fightsystem.fight.FightTeam;
import lombok.AllArgsConstructor;
import lombok.Getter;
import org.bukkit.Location;
import org.bukkit.Material;
import org.bukkit.block.Block;
import org.bukkit.entity.Entity;
import org.bukkit.entity.Player;

import java.util.*;
import java.util.function.BiConsumer;
import java.util.logging.Level;
import java.util.stream.IntStream;

public class Hull {

    private static final IntVector[] NO_BRANCHES = new IntVector[0];

    private static boolean isOccluding(Material material) {
        return material.isOccluding() || Config.GameModeConfig.Techhider.HiddenBlocks.contains(material);
    }

    @Getter
    private final Region region;
    private final boolean groundVisible;
    private final IntVector[] directions;

    private final BitSet occluding;
    private final BitSet visibility;
    private final HashMap<IntVector, BitSet> visibilityDirections = new HashMap<>(); // Contains the visible directions of each occluding visible block
    private final HashSet<IntVector> uncoveredSurface = new HashSet<>();

    private final HashSet<Player> players = new HashSet<>();
    private final HashSet<Entity> entities = new HashSet<>();
    private final HashSet<REntity> rentities = new HashSet<>();

    public Hull(FightTeam team) {
        this.region = team.getSchemRegion();
        this.groundVisible = region.getMinY() != Config.PlayerRegion.getMinY();
        this.occluding = new BitSet(region.volume());
        this.visibility = new BitSet(region.volume());

        IntVector primaryDirection = new IntVector(0, 0, team.isBlue() == (Config.BlueToRedZ > 0) ? -1 : 1);
        directions = IntStream.range(0, 27)
                .mapToObj(v -> new IntVector(v % 3 - 1, (v / 3) % 3 - 1, v / 9 - 1))
                .filter(v -> v.sqLength() == 1 || v.x * primaryDirection.x + v.y * primaryDirection.y + v.z * primaryDirection.z == 1) // Not pointing away from primary direction
                .toArray(IntVector[]::new);

        for (IntVector direction : directions) {
            visibilityDirections.put(direction, new BitSet(region.volume()));
        }
    }

    public boolean blockPrecise(Player player, int chunkX, int chunkY, int chunkZ) {
        return players.contains(player) && !region.chunkSectionOutside(chunkX, chunkY, chunkZ);
    }

    public boolean isBlockHidden(Player player, int x, int y, int z) {
        return region.inRegion(x, y, z) && players.contains(player) && !visibility.get(((y - region.getMinY()) * region.getSizeZ() + (z - region.getMinZ())) * region.getSizeX() + (x - region.getMinX()));
    }

    public boolean isLocationHidden(Player player, Location location) {
        return players.contains(player) && region.inRegion(location) && !visibility.get(new IntVector(location).toId(region));
    }

    public void addPlayer(Player player) {
        if (players.add(player)) {
            for (Entity entity : entities) {
                BlockIdWrapper.impl.untrackEntity(player, entity);
            }
        }
    }

    public void removePlayer(Player player, boolean activeRemoval) {
        if (players.remove(player) && activeRemoval) {
            for (Entity entity : entities) {
                BlockIdWrapper.impl.trackEntity(player, entity);
            }
            // techhider triggers block change sending
        }
    }

    public void checkEntity(Entity entity) {
        Location location = entity.getLocation();
        if (region.inRegion(location) && !visibility.get(new IntVector(location).toId(region))) {
            if (entities.add(entity)) {
                for (Player player : players) {
                    BlockIdWrapper.impl.untrackEntity(player, entity);
                }
            }
        } else {
            if (entities.remove(entity)) {
                for (Player player : players) {
                    BlockIdWrapper.impl.trackEntity(player, entity);
                }
            }
        }
    }

    public void removeEntity(Entity entity) {
        entities.remove(entity);
    }

    public void checkREntity(REntity entity) {
        Location location = new Location(Config.world, entity.getX(), entity.getY(), entity.getZ());
        if (region.inRegion(location) && !visibility.get(new IntVector(location).toId(region))) {
            if (rentities.add(entity)) entity.hide(true);
        } else {
            if (rentities.remove(entity)) entity.hide(false);
        }
    }

    public void removeREntity(REntity entity) {
        rentities.remove(entity);
    }

    public void initialize() {
        visibility.clear();
        occluding.clear();
        uncoveredSurface.clear();
        for (BitSet directionalVisibility : visibilityDirections.values()) {
            directionalVisibility.clear();
        }

        long start = System.currentTimeMillis();
        region.forEach((x, y, z) -> {
            IntVector block = new IntVector(x, y, z);
            if (isOccluding(Config.world.getBlockAt(x, y, z).getType())) {
                occluding.set(block.toId(region));
            }
        });
        forEachBorder((root, direction) -> uncoverBlocks(new NullList<>(), root, direction));
        FightSystem.getPlugin().getLogger().log(Level.INFO, () -> "[HullHider] initialisation finished: " + (System.currentTimeMillis() - start) + " ms, visible blocks: " + visibility.cardinality());
    }

    public void updateBlockVisibility(Block b, Material changedType) {
        IntVector root = new IntVector(b.getX(), b.getY(), b.getZ());
        if (root.notInRegion(region)) return;

        int id = root.toId(region);
        if (!occluding.get(id) || isOccluding(changedType)) return;

        occluding.clear(id);
        if (!visibility.get(id)) return;

        List<IntVector> uncovered = new ArrayList<>();
        for (Map.Entry<IntVector, BitSet> directionalVisibility : visibilityDirections.entrySet()) {
            if (directionalVisibility.getValue().get(id)) {
                directionalVisibility.getValue().clear(id);
                uncoverBlocks(uncovered, root, directionalVisibility.getKey());
            }
        }

        uncoveredSurface.addAll(new HashSet<>(uncovered));
        uncoveredSurface.remove(root);
    }

    public void sendUncoveredBlocks() {
        Map<IntVector, List<IntVector>> sectionWise = new HashMap<>();

        for (IntVector uncovered : uncoveredSurface) {
            sectionWise.computeIfAbsent(new IntVector(uncovered.x >> 4, uncovered.y >> 4, uncovered.z >> 4), section -> new ArrayList<>()).add(uncovered);
        }
        uncoveredSurface.clear();

        for (Map.Entry<IntVector, List<IntVector>> entry : sectionWise.entrySet()) {
            Object packet = HullHiderWrapper.impl.generateBlockChangePacket(entry.getValue());
            players.forEach(player -> TinyProtocol.instance.sendPacket(player, packet));
        }
    }

    private void forEachBorder(BiConsumer<IntVector, IntVector> f) {
        for (int x = region.getMinX(); x < region.getMaxX(); x++) {
            for (int z = region.getMinZ(); z < region.getMaxZ(); z++) {
                if (groundVisible) {
                    f.accept(new IntVector(x, region.getMinY(), z), new IntVector(0, 1, 0));
                }
                f.accept(new IntVector(x, region.getMaxY() - 1, z), new IntVector(0, -1, 0));
            }
        }

        for (int x = region.getMinX(); x < region.getMaxX(); x++) {
            for (int y = region.getMinY(); y < region.getMaxY(); y++) {
                f.accept(new IntVector(x, y, region.getMinZ()), new IntVector(0, 0, 1));
                f.accept(new IntVector(x, y, region.getMaxZ() - 1), new IntVector(0, 0, -1));
            }
        }

        for (int z = region.getMinZ(); z < region.getMaxZ(); z++) {
            for (int y = region.getMinY(); y < region.getMaxY(); y++) {
                f.accept(new IntVector(region.getMinX(), y, z), new IntVector(1, 0, 0));
                f.accept(new IntVector(region.getMaxX() - 1, y, z), new IntVector(-1, 0, 0));
            }
        }
    }

    private void uncoverBlocks(List<IntVector> uncovered, IntVector block, IntVector direction) {
        uncoverBlocks(
                uncovered, block, direction, visibilityDirections.get(direction),
                direction.sqLength() == 1
                        ? Arrays.stream(directions)
                          .filter(v -> v.sqLength() >= 2 && v.x * direction.x + v.y * direction.y + v.z * direction.z == 1)
                          .toArray(IntVector[]::new)
                        : NO_BRANCHES
        );
    }

    private void uncoverBlocks(List<IntVector> uncovered, IntVector block, IntVector direction, BitSet directionalVisibility, IntVector[] branchDirections) {
        if (block.notInRegion(region)) return;

        int id = block.toId(region);
        if (directionalVisibility.get(id)) return;
        directionalVisibility.set(id);

        if (!visibility.get(id)) {
            visibility.set(id);
            uncovered.add(block);
        }

        if (occluding.get(id)) return;

        uncoverBlocks(uncovered, block.add(direction), direction, directionalVisibility, branchDirections);
        for (IntVector branchDirection : branchDirections) {
            uncoverBlocks(uncovered, block.add(branchDirection), branchDirection, visibilityDirections.get(branchDirection), NO_BRANCHES);
        }
    }


    @Getter
    @AllArgsConstructor
    public static class IntVector {
        private final int x;
        private final int y;
        private final int z;

        public IntVector(Location location) {
            this.x = location.getBlockX();
            this.y = location.getBlockY();
            this.z = location.getBlockZ();
        }

        public boolean notInRegion(Region region) {
            return !region.inRegion(x, y, z);
        }

        public int toId(Region region) {
            return ((y - region.getMinY()) * region.getSizeZ() + (z - region.getMinZ())) * region.getSizeX() + (x - region.getMinX());
        }

        public IntVector add(int x, int y, int z) {
            return new IntVector(this.x + x, this.y + y, this.z + z);
        }

        public IntVector add(IntVector v) {
            return add(v.x, v.y, v.z);
        }

        public int sqLength() {
            return x * x + y * y + z * z;
        }

        @Override
        public int hashCode() {
            return y << 24 ^ x << 12 ^ z;
        }

        @Override
        public boolean equals(Object o) {
            if (o == null || this.getClass() != o.getClass()) {
                return false;
            }

            IntVector v = (IntVector) o;
            return x == v.x && y == v.y && z == v.z;
        }

        @Override
        public String toString() {
            return x + "," + y + "," + z;
        }
    }

    private static class NullList<E> extends AbstractList<E> {
        @Override
        public void add(int index, E element) {
            // Straight to /dev/null!
        }

        @Override
        public E get(int index) {
            return null;
        }

        @Override
        public int size() {
            return 0;
        }
    }
}
