/*
 *   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 de.steamwar.core.CraftbukkitWrapper;
import de.steamwar.fightsystem.Config;
import de.steamwar.fightsystem.events.BoardingEvent;
import de.steamwar.fightsystem.events.TeamLeaveEvent;
import de.steamwar.fightsystem.events.TeamSpawnEvent;
import de.steamwar.fightsystem.fight.Fight;
import de.steamwar.fightsystem.fight.FightTeam;
import de.steamwar.fightsystem.states.FightState;
import de.steamwar.fightsystem.states.StateDependent;
import de.steamwar.fightsystem.states.StateDependentListener;
import de.steamwar.techhider.AccessPrivilegeProvider;
import de.steamwar.techhider.TechHider;
import lombok.Getter;
import net.minecraft.core.Holder;
import net.minecraft.core.registries.BuiltInRegistries;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.world.level.block.Block;
import net.minecraft.world.level.block.entity.BlockEntityType;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.material.FluidState;
import net.minecraft.world.level.material.Fluids;
import org.bukkit.GameMode;
import org.bukkit.Material;
import org.bukkit.craftbukkit.CraftWorld;
import org.bukkit.craftbukkit.util.CraftMagicNumbers;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
import org.bukkit.event.player.PlayerQuitEvent;

import java.util.Collection;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.stream.Collectors;
import java.util.stream.Stream;

public class TechHiderWrapper extends StateDependent implements Listener {

    public static final boolean ENABLED = !Config.OnlyPublicSchematics && !Config.test() && Config.GameModeConfig.Techhider.Active;

    @Getter
    private final ConcurrentHashMap<Player, Region> hiddenRegion = new ConcurrentHashMap<>();
    private final HullHider hullHider;

    public TechHiderWrapper(HullHider hullHider) {
        super(ENABLED, FightState.All);

        this.hullHider = hullHider;

        new StateDependentListener(ENABLED, FightState.All, this);
        register();
    }

    @Override
    public void enable() {
        Set<BlockState> blockStatesToObfuscate = getBlockStatesToHideFromMaterials(Config.GameModeConfig.Techhider.HiddenBlocks);

        Set<Block> blocksToObfuscate = Config.GameModeConfig.Techhider.HiddenBlocks.stream()
                .map(CraftMagicNumbers::getBlock)
                .collect(Collectors.toUnmodifiableSet());

        Set<BlockEntityType<?>> blockEntityTypeToObfuscate = Config.GameModeConfig.Techhider.HiddenBlockEntities.stream()
                .map(id -> {
                    ResourceLocation loc = ResourceLocation.parse(id);
                    return BuiltInRegistries.BLOCK_ENTITY_TYPE.get(loc).orElse(null);
                })
                .filter(Objects::nonNull)
                .map(Holder.Reference::value)
                .collect(Collectors.toUnmodifiableSet());

        new TechHider(CraftMagicNumbers.getBlock(Config.GameModeConfig.Techhider.ObfuscateWith), new AccessPrivilegeProvider() {
            @Override
            public boolean isEveryonePrivilegedToAccessAllDataWithinChunk(int chunkX, int chunkZ) {
                return Fight.teams().stream().map(FightTeam::getExtendRegion).allMatch(region -> region.chunkOutside(chunkX, chunkZ));
            }

            @Override
            public boolean isPlayerPrivilegedToAccessPosition(Player p, int blockX, int blockY, int blockZ) {
                return !hullHider.isBlockHidden(p, blockX, blockY, blockZ);
            }

            @Override
            public boolean isPlayerPrivilegedToAccessBlock(Player p, int blockX, int blockY, int blockZ, Block block) {
                return !getHiddenRegion(p).inRegion(blockX, blockY, blockZ) || !blocksToObfuscate.contains(block);
            }

            @Override
            public boolean isPlayerPrivilegedToAccessBlockState(Player p, int blockX, int blockY, int blockZ, BlockState blockState) {
                return !getHiddenRegion(p).inRegion(blockX, blockY, blockZ) || !blockStatesToObfuscate.contains(blockState);
            }

            // TODO will require entity tracking on the netty thread to prevent future race conditions
            @Override
            public boolean isPlayerPrivilegedToAccessEntity(Player p, int entityId) {
                net.minecraft.world.entity.Entity nmsEntity = ((CraftWorld) p.getWorld()).getHandle().moonrise$getEntityLookup().get(entityId);

                if (nmsEntity != null) {
                    return !hullHider.isBlockHidden(p, nmsEntity.getBlockX(), nmsEntity.getBlockY(), nmsEntity.getBlockZ());
                } else {
                    return true;
                }
            }

            @Override
            public boolean isPlayerPrivilegedToAccessBlockEntity(Player p, int blockX, int blockY, int blockZ, BlockEntityType<?> type) {
                return !getHiddenRegion(p).inRegion(blockX, blockY, blockZ) || !blockEntityTypeToObfuscate.contains(type);
            }

            @Override
            public boolean isPlayerPrivilegedToPerformAction(Player p) {
                return p.getGameMode() != GameMode.SPECTATOR;
            }
        });
    }

    @Override
    public void disable() {
        hiddenRegion.clear();
    }

    @EventHandler
    public void teamJoin(TeamSpawnEvent e) {
        e.getFightPlayer().ifPlayer(player -> hiddenRegion.put(player, getHiddenRegion(player)));
    }

    @EventHandler
    public void boarding(BoardingEvent e) {
        e.getFightPlayer().ifPlayer(player -> hiddenRegion.put(player, getHiddenRegion(player)));
    }

    @EventHandler
    public void teamLeave(TeamLeaveEvent e) {
        e.getFightPlayer().ifPlayer(player -> {
            if (player.isOnline()) {
                hiddenRegion.put(player, getHiddenRegion(player));
            }
        });
    }

    @EventHandler
    public void playerQuit(PlayerQuitEvent e) {
        Player player = e.getPlayer();
        hiddenRegion.remove(player);
    }

    public void reloadChunks(Player player, Region region, Region exclusion) {
        if (!ENABLED || !FightState.Schem.contains(FightState.getFightState()) || !player.isOnline()) return;

        region.forEachChunk((chunkX, chunkZ) -> {
            if (exclusion.chunkOutside(chunkX, chunkZ)) {
                CraftbukkitWrapper.sendChunk(player, chunkX, chunkZ);
            }
        });
    }

    private Region getHiddenRegion(Player player) {
        if (Config.isReferee(player)) return Region.EMPTY;

        FightTeam team = Fight.getPlayerTeam(player);
        if (team == null) return Config.ArenaRegion;

        if (team.canPlayerEntern(player)) return Region.EMPTY;

        return Fight.getOpposite(team).getExtendRegion();
    }

    private Stream<BlockState> getWaterloggedBlockStates() {
        FluidState waterFluidState = Fluids.WATER.getSource(false);

        return BuiltInRegistries.BLOCK.stream()
                .map((block) -> block.getStateDefinition().getPossibleStates())
                .flatMap(Collection::stream)
                .filter((blockState -> blockState.getFluidState() == waterFluidState));
    }

    private Set<BlockState> getBlockStatesToHideFromMaterials(Set<Material> materials) {
        Stream<BlockState> allStatesFromMaterials = materials.stream()
                .map(CraftMagicNumbers::getBlock)
                .map((block) -> block.getStateDefinition().getPossibleStates())
                .flatMap(Collection::stream);

        if(materials.contains(Material.WATER)) {
            return Stream.concat(allStatesFromMaterials, getWaterloggedBlockStates()).collect(Collectors.toUnmodifiableSet());
        }
        else {
            return allStatesFromMaterials.collect(Collectors.toUnmodifiableSet());
        }
    }
}
