/*
 *   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.bausystem.features.tracer;

import de.steamwar.bausystem.region.Region;
import de.steamwar.bausystem.region.RegionSystem;
import lombok.Cleanup;
import lombok.SneakyThrows;
import org.bukkit.Bukkit;
import org.bukkit.Location;
import org.bukkit.util.Vector;

import java.io.*;
import java.util.*;
import java.util.zip.GZIPInputStream;
import java.util.zip.GZIPOutputStream;

public class TraceRepository {

    /**
     * Increment this when changing serialisation format
     */
    public static final int SERIALISATION_VERSION = 3;
    public static final int WRITE_TICK_DATA = 0b00000001;
    public static final int EXPLOSION = 0b00000010;
    public static final int IN_WATER = 0b00000100;
    public static final int AFTER_FIRST_EXPLOSION = 0b00001000;
    public static final int DESTROYED_BUILD_AREA = 0b00010000;
    public static final int DESTROYED_TEST_BLOCK = 0b00100000;
    public static File tracesFolder = new File(Bukkit.getWorlds().get(0).getWorldFolder(), "traces");

    @SneakyThrows
    public static Trace readTrace(File recordsFile) {
        @Cleanup
        ObjectInputStream reader = new ObjectInputStream(new GZIPInputStream(new FileInputStream(recordsFile)));
        UUID uuid;
        try {
            uuid = UUID.fromString(reader.readUTF());
        } catch (IllegalArgumentException e) {
            return null;
        }
        Region region;
        try {
            region = RegionSystem.INSTANCE.getRegion(UUID.fromString(reader.readUTF())).orElse(null);
            if (region == null) return null;
        } catch (IllegalArgumentException e) {
            return null;
        }
        Date date = (Date) reader.readObject();
        int serialisationVersion = reader.readInt();
        if (serialisationVersion != SERIALISATION_VERSION) {
            return null;
        }
        int tntIdCount = reader.readInt();

        return new Trace(uuid, region, date, recordsFile, tntIdCount);
    }

    @SneakyThrows
    protected static void writeTrace(Trace trace, List<TNTPoint> records) {
        ObjectOutputStream outputStream = new ObjectOutputStream(new GZIPOutputStream(new FileOutputStream(trace.getRecordsSaveFile())));
        outputStream.writeUTF(trace.getUuid().toString());
        outputStream.writeUTF(trace.getRegion().getID().toString());
        outputStream.writeObject(trace.getDate());
        outputStream.writeInt(SERIALISATION_VERSION);

        Map<Integer, List<TNTPoint>> pointsByTNTId = new HashMap<>();
        records.forEach(tntPoint -> {
            pointsByTNTId.computeIfAbsent(tntPoint.getTntId(), integer -> new ArrayList<>()).add(tntPoint);
        });

        outputStream.writeInt(pointsByTNTId.size());
        for (Map.Entry<Integer, List<TNTPoint>> entry : pointsByTNTId.entrySet()) {
            outputStream.writeInt(entry.getKey());
            outputStream.writeInt(entry.getValue().size());

            for (int i = 0; i < entry.getValue().size(); i++) {
                TNTPoint current = entry.getValue().get(i);
                if (i == 0) {
                    writeTNTPoint(outputStream, current, true);
                    continue;
                }

                TNTPoint last = entry.getValue().get(i - 1);

                boolean writeTickData = true;
                if (last.getTicksSinceStart() + 1 == current.getTicksSinceStart() && last.getFuse() - 1 == current.getFuse()) {
                    writeTickData = false;
                }

                writeTNTPoint(outputStream, current, writeTickData);
            }
        }

        outputStream.flush();
        outputStream.close();
    }

    @SneakyThrows
    private static void writeTNTPoint(ObjectOutputStream outputStream, TNTPoint tntPoint, boolean writeTickData) {
        byte data = 0;
        if (writeTickData) data |= WRITE_TICK_DATA;
        if (tntPoint.isExplosion()) data |= EXPLOSION;
        if (tntPoint.isInWater()) data |= IN_WATER;
        if (tntPoint.isAfterFirstExplosion()) data |= AFTER_FIRST_EXPLOSION;
        if (tntPoint.isDestroyedBuildArea()) data |= DESTROYED_BUILD_AREA;
        if (tntPoint.isDestroyedTestBlock()) data |= DESTROYED_TEST_BLOCK;
        outputStream.write(data);

        if (writeTickData) {
            outputStream.writeLong(tntPoint.getTicksSinceStart());
            outputStream.writeInt(tntPoint.getFuse());
        }

        Location location = tntPoint.getLocation();
        outputStream.writeDouble(location.getX());
        outputStream.writeDouble(location.getY());
        outputStream.writeDouble(location.getZ());

        Vector velocity = tntPoint.getVelocity();
        outputStream.writeDouble(velocity.getX());
        outputStream.writeDouble(velocity.getY());
        outputStream.writeDouble(velocity.getZ());
    }

    @SneakyThrows
    protected static TNTPoint readTraceRecord(int tntId, TNTPoint last, ObjectInputStream objectInput) {

        int data = objectInput.read();
        boolean explosion = (data & EXPLOSION) > 0;
        boolean inWater = (data & IN_WATER) > 0;
        boolean afterFirstExplosion = (data & AFTER_FIRST_EXPLOSION) > 0;
        boolean destroyedBuildArea = (data & DESTROYED_BUILD_AREA) > 0;
        boolean destroyedTestBlock = (data & DESTROYED_TEST_BLOCK) > 0;

        long ticksSinceStart;
        int fuse;
        if ((data & WRITE_TICK_DATA) > 0) {
            ticksSinceStart = objectInput.readLong();
            fuse = objectInput.readInt();
        } else {
            ticksSinceStart = last.getTicksSinceStart() + 1;
            fuse = last.getFuse() - 1;
        }

        double locX = objectInput.readDouble();
        double locY = objectInput.readDouble();
        double locZ = objectInput.readDouble();
        Location location = new Location(Bukkit.getWorlds().get(0), locX, locY, locZ);

        double velX = objectInput.readDouble();
        double velY = objectInput.readDouble();
        double velZ = objectInput.readDouble();
        Vector velocity = new Vector(velX, velY, velZ);

        return new TNTPoint(tntId, explosion, inWater, afterFirstExplosion, destroyedBuildArea, destroyedTestBlock, ticksSinceStart, fuse, location, velocity, Collections.emptyList());
    }

    @SneakyThrows
    protected static List<TNTPoint> readTraceRecords(Trace trace) {
        File recordsFile = trace.getRecordsSaveFile();
        @Cleanup
        ObjectInputStream inputStream = new ObjectInputStream(new GZIPInputStream(new FileInputStream(recordsFile)));
        inputStream.readUTF();
        inputStream.readUTF();
        inputStream.readObject();
        inputStream.readInt();
        inputStream.readInt();

        List<TNTPoint> records = new ArrayList<>();
        Map<Integer, List<TNTPoint>> histories = new HashMap<>();
        for (int i = 0; i < trace.getTntIdCount(); i++) {
            int tntId = inputStream.readInt();
            int size = inputStream.readInt();
            List<TNTPoint> points = histories.computeIfAbsent(tntId, id -> new ArrayList<>());

            TNTPoint last = null;
            for (int j = 0; j < size; j++) {
                TNTPoint point = readTraceRecord(tntId, last, inputStream);
                point.setHistory(points);
                points.add(point);
                last = point;
                records.add(point);
            }
        }
        return records;
    }
}
