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

import de.steamwar.fightsystem.ArenaMode;
import de.steamwar.fightsystem.Config;
import de.steamwar.fightsystem.fight.Fight;
import de.steamwar.fightsystem.fight.FightPlayer;
import de.steamwar.fightsystem.fight.FightTeam;
import de.steamwar.fightsystem.states.FightState;
import de.steamwar.fightsystem.utils.BlockIdWrapper;
import de.steamwar.fightsystem.utils.CraftbukkitWrapper;
import de.steamwar.fightsystem.utils.Message;
import de.steamwar.fightsystem.utils.SWSound;
import de.steamwar.sql.NodeData;
import de.steamwar.sql.SchematicNode;
import de.steamwar.sql.SteamwarUser;
import org.bukkit.Bukkit;
import org.bukkit.Location;
import org.bukkit.block.Block;
import org.bukkit.entity.Entity;
import org.bukkit.entity.LivingEntity;
import org.bukkit.entity.Player;
import org.bukkit.inventory.ItemStack;
import org.bukkit.util.Vector;

import java.io.*;
import java.util.logging.Level;

public interface Recorder {
    DataOutputStream getStream();

    default void enable() {
        GlobalRecorder.getInstance().add(this);

        if (ArenaMode.Event.contains(Config.mode)) {
            teamIds(Config.EventTeamBlueID, Config.EventTeamRedID);
        }

        arenaInfo();
        Fight.teams().forEach(this::enableTeam);
    }

    default void enableTeam(FightTeam team) {
        if (FightState.Schem.contains(FightState.getFightState())) {
            if (team.isBlue()) {
                blueSchem(team.getSchematic(), false);
            } else {
                redSchem(team.getSchematic(), false);
            }
        }

        if (FightState.AntiSpectate.contains(FightState.getFightState())) {
            for (FightPlayer player : team.getPlayers()) {
                if (player.isLiving()) {
                    playerJoins(player.getEntity(), player.getUser());
                }
            }
        }
    }

    default void disable() {
        GlobalRecorder.getInstance().remove(this);
        try {
            getStream().close();
        } catch (IOException e) {
            Bukkit.getLogger().log(Level.SEVERE, "Could not close OutputStream", e);
        }
    }

    /*
     * PlayerJoinPacket (0x00) + int EntityId + int SWUserId
     * EntityMovePacket (0x01) + int EntityId + double x, y, z + float pitch, yaw + byte headyaw
     * EntityDespawnsPacket (0x02) + int EntityId
     * PlayerSneakPacket (0x03) + int EntityId + boolean sneaks
     * EntityAnimationPacket (0x04) + int EntityId + byte animation
     * TNTSpawnPacket (0x05) + int EntityId
     * EntitySpeedPacket (0x06) + int EntityId + double dx, dy, dz
     * PlayerItemPacket (0x07) + int EntityId + String item + boolean enchanted + String slot
     * ArrowSpawnPacket (0x08) + int EntityId
     * FireballSpawnPacket (0x09) + int EntityId
     * BowSpanPacket (0x0a) + int EntityId + boolean start + hand
     * PlayerDamagePacket (0x0b) + int EntityId
     * SetOnFire (0x0c) + int EntityId + boolean perma
     *
     * DEPRECATED ArenaInfo (0x20) + bool blueNegZ + byte arenaY + int arenaMinX + int arenaMinZ
     * ArenaInfo (0x21) + bool blueNegZ + int arenaMinX + int arenaMinY + int arenaMinZ
     *
     * DEPRECATED BlockPacket (0x30) + pos int, byte, int + int BlockState
     * ParticlePacket (0x31) + double x, y, z + string particleType
     * SoundPacket (0x32) + int x, y, z + string soundType + string soundCategory + float volume, pitch
     * DEPRECATED ShortBlockPacket (0x33) + pos relative to ArenaMinX,ArenaMinZ byte, byte, byte + short BlockState
     * SoundAtPlayerPacket (0x34) + string (soundType, soundCategory) + float volume, pitch
     * ShortBlockPacket (0x35) + pos relative to ArenaMinX,BluePasteY,ArenaMinZ byte, byte, byte + short BlockState
     * BlockPacket (0x36) + pos int, short, int + int BlockState
     *
     *
     * DEPRECATED ChatPacket (0xa0) + String message
     * DEPRECATED ActionBarPacket (0xa1) + String message
     * DEPRECATED SystemPacket (0xa2) + String message
     * CountdownPacket (0xa3) + String message, int displaytime, Message appendix
     * ChatPacket (0xa4) + Message
     * SystemPacket (0xa5) + Message
     *
     * BlueSchemPacket (0xb0) + int blueSchemId
     * RedSchemPacket (0xb1) + int redSchemId
     * TeamIDPacket (0xb2) + int blueTeamId, redTeamId
     * BlueEmbeddedSchemPacket (0xb3) + int blueSchemId + gzipt NBT blob
     * RedEmbeddedSchemPacket (0xb4) + int redSchemId + gzipt NBT blob
     * BlueSchemRotatePacket (0xb5) + boolean changeRotate
     * RedSchemRotatePacket (0xb6) + boolean changeRotate
     *
     * DEPRECATED ScoreboardTitlePacket (0xc0) + String scoreboardTitle
     * DEPRECATED ScoreboardDataPacket (0xc1) + String key + int value
     * DEPRECATED BossBarPacket (0xc2) + double leftBlueProgress, leftRedProgress + String leftBlueText, leftRedText
     * DEPRECATED SubtitlePacket (0xc3) + String subtitle
     * DEPRECATED PrintWinPacket (0xc4) + String title, subtitle
     * SubtitlePacket (0xc5) + Message
     * WinPacket (0xc6) + byte team + Message subtitle
     * BossBarPacket (0xc7) + double leftBlueProgress, leftRedProgress + Message leftBlueText, leftRedText
     *
     * CommentPacket (0xfe) + String comment
     * TickPacket (0xff)
     *
     * Message-Format
     * String message + byte prefixed Object-Params + byte 0x00
     * 0x00: End of message
     * 0x01: boolean following
     * 0x02: byte following
     * 0x03: short following
     * 0x04: int following
     * 0x05: float following
     * 0x06: double following
     * 0x07: String following
     * 0x08: Message following
     * */

    default void playerJoins(LivingEntity p, SteamwarUser user) {
        write(0x00, p.getEntityId(), user.getId());
        entityMoves(p);
    }

    default void entityMoves(Entity e) {
        Location location = e.getLocation();
        write(0x01, e.getEntityId(), location.getX(), location.getY(), location.getZ(), location.getPitch(), location.getYaw(), (byte) (CraftbukkitWrapper.impl.headRotation(e) * 256 / 360));
    }

    default void entityDespawns(Entity e) {
        write(0x02, e.getEntityId());
    }

    default void playerSneak(Player p, boolean sneaks) {
        write(0x03, p.getEntityId(), sneaks);
    }

    default void entityAnimation(Entity e, int animation) {
        write(0x04, e.getEntityId(), (byte) animation);
    }

    default void tntSpawn(Entity e) {
        write(0x05, e.getEntityId());
        entityMoves(e);
        entitySpeed(e);
    }

    default void entitySpeed(Entity e) {
        Vector velocity = e.getVelocity();
        write(0x06, e.getEntityId(), velocity.getX(), velocity.getY(), velocity.getZ());
    }

    default void item(Player p, ItemStack item, String slot) {
        write(0x07, p.getEntityId(), "minecraft:" + item.getType().name().toLowerCase(), !item.getEnchantments().isEmpty(), slot);
    }

    default void arrowSpawn(Entity e) {
        write(0x08, e.getEntityId());
        entityMoves(e);
        entitySpeed(e);
        if (e.getFireTicks() > 0) setOnFire(e, true);
    }

    default void fireballSpawn(Entity e) {
        write(0x09, e.getEntityId());
        entityMoves(e);
        entitySpeed(e);
    }

    default void bowSpan(Entity e, boolean start, boolean offHand) {
        write(0x0a, e.getEntityId(), start, offHand);
    }

    default void damageAnimation(LivingEntity p) {
        write(0x0b, p.getEntityId());
    }

    default void setOnFire(Entity e, boolean perma) {
        write(0x0c, e.getEntityId(), perma);
    }

    default void arenaInfo() {
        write(0x21, Config.blueNegZ(), Config.ArenaRegion.getMinX(), Config.BluePasteRegion.getMinY(), Config.ArenaRegion.getMinZ());
    }

    default void blockChange(Block block) {
        int blockState = BlockIdWrapper.impl.blockToId(block);

        int shortX = block.getX() - Config.ArenaRegion.getMinX();
        int shortY = block.getY() - Config.BluePasteRegion.getMinY();
        int shortZ = block.getZ() - Config.ArenaRegion.getMinZ();
        if ((short) blockState == blockState && shortX >= 0 && shortX < 256 && shortY >= 0 && shortZ >= 0 && shortZ < 256) {
            //Short block packet
            write(0x35, (byte) shortX, (byte) shortY, (byte) shortZ, (short) blockState);
        } else {
            //Block packet
            write(0x36, block.getX(), (short) block.getY(), block.getZ(), blockState);
        }
    }

    default void particle(double x, double y, double z, String particleType) {
        write(0x31, x, y, z, particleType);
    }

    default void sound(int x, int y, int z, SWSound soundType, String soundCategory, float volume, float pitch) {
        write(0x32, x, y, z, soundType.getSound().name(), soundCategory, volume, pitch);
    }

    default void soundAtPlayer(String soundType, float volume, float pitch) {
        write(0x34, soundType, volume, pitch);
    }

    default void countdown(String message, int displaytime, Message appendix) {
        write(0xa3, message, displaytime, appendix);
    }

    default void chat(String msg, Object... params) {
        write(0xa4, new Message(msg, params));
    }

    default void system(String msg, Object... params) {
        write(0xa5, new Message(msg, params));
    }

    default void teamIds(int blueTeamId, int redTeamId) {
        write(0xb2, blueTeamId, redTeamId);
    }

    default void blueSchem(int schemId, boolean changeRotate) {
        rotate(0xb5, changeRotate);
        schem(0xb3, 0xb0, schemId);
    }

    default void redSchem(int schemId, boolean changeRotate) {
        rotate(0xb6, changeRotate);
        schem(0xb4, 0xb1, schemId);
    }

    default void rotate(int packetId, boolean changeRotate) {
        write(packetId, changeRotate);
    }

    default void schem(int embedId, int noEmbedId, int schemId) {
        if (schemId == 0) {
            write(noEmbedId, schemId);
            return;
        }

        ByteArrayOutputStream buffer = new ByteArrayOutputStream();
        try {
            copy(NodeData.getLatest(SchematicNode.getSchematicNode(schemId)).schemData(true), buffer);
        } catch (EOFException e) {
            Bukkit.getLogger().log(Level.INFO, "EOFException ignored");
        } catch (IOException e) {
            Bukkit.getLogger().log(Level.SEVERE, "Could not embed schematic", e);
            disable();
            return;
        }

        write(embedId, schemId, buffer.toByteArray());
    }

    static void copy(InputStream input, OutputStream output) throws IOException {
        int bytes;
        for (byte[] buffer = new byte[8192]; (bytes = input.read(buffer)) > 0; ) {
            output.write(buffer, 0, bytes);
        }
    }

    default void bossBar(double leftBlueProgress, double leftRedProgress, Message leftBlueText, Message leftRedText) {
        write(0xc7, leftBlueProgress, leftRedProgress, leftBlueText, leftRedText);
    }

    default void subtitle(Message subtitle) {
        write(0xc5, subtitle);
    }

    default void winMessage(FightTeam team, String subtitle, Object... params) {
        byte bTeam = 0x00;
        if (team != null) {
            bTeam = (byte) (team.isBlue() ? 0x01 : 0x02);
        }
        write(0xc6, bTeam, new Message(subtitle, params));
    }

    default void tick() {
        write(0xff);
    }

    default void write(int id, Object... objects) {
        DataOutputStream stream = getStream();
        synchronized (stream) {
            try {
                stream.writeByte(id);
                for (Object o : objects) {
                    writeObject(stream, o);
                }
                stream.flush();
            } catch (IOException e) {
                Bukkit.getLogger().log(Level.SEVERE, "Could not write", e);
                disable();
            }
        }
    }

    default void writeObject(DataOutputStream stream, Object o) throws IOException {
        if (o instanceof Boolean) {
            stream.writeBoolean((Boolean) o);
        } else if (o instanceof Byte) {
            stream.writeByte((Byte) o);
        } else if (o instanceof Short) {
            stream.writeShort((Short) o);
        } else if (o instanceof Integer) {
            stream.writeInt((Integer) o);
        } else if (o instanceof Long) {
            stream.writeLong((Long) o);
        } else if (o instanceof Float) {
            stream.writeFloat((Float) o);
        } else if (o instanceof Double) {
            stream.writeDouble((Double) o);
        } else if (o instanceof String) {
            stream.writeUTF((String) o);
        } else if (o instanceof byte[]) {
            stream.write((byte[]) o);
        } else if (o instanceof Message) {
            writeMessage(stream, (Message) o);
        } else {
            throw new SecurityException("Undefined write for: " + o.getClass().getName());
        }
    }

    default void writeMessage(DataOutputStream stream, Message message) throws IOException {
        stream.writeUTF(message.getMsg());
        for (Object o : message.getParams()) {
            if (o instanceof Boolean) {
                stream.writeByte(0x01);
            } else if (o instanceof Byte) {
                stream.writeByte(0x02);
            } else if (o instanceof Short) {
                stream.writeByte(0x03);
            } else if (o instanceof Integer) {
                stream.writeByte(0x04);
            } else if (o instanceof Float) {
                stream.writeByte(0x05);
            } else if (o instanceof Double) {
                stream.writeByte(0x06);
            } else if (o instanceof String) {
                stream.writeByte(0x07);
            } else if (o instanceof Message) {
                stream.writeByte(0x08);
            } else {
                throw new SecurityException("Undefined message serialization for: " + o.getClass().getName());
            }
            writeObject(stream, o);
        }
        stream.writeByte(0x00);
    }

}
