/*
 *   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.FightSystem;
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.sql.SteamwarUser;
import de.steamwar.techhider.TechHider;
import lombok.Getter;
import org.bukkit.GameMode;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
import org.bukkit.event.player.PlayerQuitEvent;

import javax.crypto.*;
import javax.crypto.spec.SecretKeySpec;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.util.concurrent.ConcurrentHashMap;

public class TechHiderWrapper extends StateDependent implements TechHider.LocationEvaluator, Listener {

    public static final boolean ENABLED = !Config.OnlyPublicSchematics && !Config.test() && Config.TechhiderActive;

    @Getter
    private final ConcurrentHashMap<Player, Region> hiddenRegion = new ConcurrentHashMap<>();
    private final ConcurrentHashMap<Player, Long> patterns = new ConcurrentHashMap<>();
    private final TechHider techHider;
    private final SecretKey key;

    public TechHiderWrapper() {
        super(ENABLED, FightState.Schem);
        techHider = new TechHider(this, Config.ObfuscateWith, Config.HiddenBlocks, Config.HiddenBlockEntities);

        try {
            key = new SecretKeySpec(Files.readAllBytes(new File(System.getProperty("user.home"), "hullhider.key").toPath()), "AES");
        } catch (IOException e) {
            throw new SecurityException(e);
        }

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

    @Override
    public void enable() {
        techHider.enable();
    }

    @Override
    public void disable() {
        techHider.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.impl.sendChunk(player, chunkX, chunkZ);
        });
    }

    @Override
    public boolean suppressInteractions(Player player) {
        return player.getGameMode() == GameMode.SPECTATOR;
    }

    @Override
    public boolean skipChunk(Player player, int chunkX, int chunkZ) {
        return getHiddenRegion(player).chunkOutside(chunkX, chunkZ);
    }

    @Override
    public boolean skipChunkSection(Player player, int chunkX, int chunkY, int chunkZ) {
        return getHiddenRegion(player).chunkSectionOutside(chunkX, chunkY, chunkZ);
    }

    @Override
    public TechHider.State check(Player player, int x, int y, int z) {
        if(hiddenRegion.computeIfAbsent(player, this::getHiddenRegion).inRegion(x, y, z)) {
            if(FightSystem.getHullHider().isBlockHidden(player, x, y, z)) {
                int id = ((y&3) << 4) + ((z&3) << 2) + (x&3);
                return (patterns.computeIfAbsent(player, this::getPattern) & (1L << id)) == 0 ? TechHider.State.HIDE_AIR : TechHider.State.HIDE;
            } else {
                return TechHider.State.CHECK;
            }
        } else {
            return TechHider.State.SKIP;
        }
    }

    public long getPattern(Player player) {
        long pattern = SteamwarUser.get(player.getUniqueId()).getId();

        byte[] bytes = new byte[]{
                (byte) (pattern & 0xFF),
                (byte) ((pattern >>> 8) & 0xFF),
                (byte) ((pattern >>> 16) & 0xFF),
                (byte) ((pattern >>> 24) & 0xFF)
        };

        try {
            Cipher cipher = Cipher.getInstance("AES_256/CFB/NoPadding");
            cipher.init(Cipher.ENCRYPT_MODE, key);
            bytes = cipher.doFinal(bytes);
        } catch (IllegalBlockSizeException | BadPaddingException | NoSuchAlgorithmException | NoSuchPaddingException | InvalidKeyException e) {
            throw new SecurityException(e);
        }

        pattern = bytes[0] & 0xFFL |
                ((bytes[1] & 0xFFL) << 8) |
                ((bytes[2] & 0xFFL) << 16) |
                ((bytes[3] & 0xFFL) << 24);

        /* XXXO   OOOX
        *  XXXO   OOOX
        *  XXOX   OOXO
        *  OOXX   XXOO */
        pattern |= 0b1110_1110_1101_0011___0001_0001_0010_1100___0000_0000_0000_0000___0000_0000_0000_0000L;
        return pattern;
    }

    @Override
    public boolean blockPrecise(Player player, int chunkX, int chunkY, int chunkZ) {
        return FightSystem.getHullHider().blockPrecise(player, chunkX, chunkY, 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();
    }
}
