/*
 *   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.BauSystem;
import de.steamwar.bausystem.features.tpslimit.TPSUtils;
import de.steamwar.bausystem.region.Region;
import de.steamwar.linkage.Linked;
import de.steamwar.linkage.LinkedInstance;
import org.bukkit.Bukkit;
import org.bukkit.block.Block;
import org.bukkit.entity.TNTPrimed;
import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority;
import org.bukkit.event.Listener;
import org.bukkit.event.entity.EntityExplodeEvent;
import org.bukkit.event.entity.EntitySpawnEvent;

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

@Linked
public class TraceRecorder implements Listener {

    @LinkedInstance
    public static TraceRecorder instance;

    /**
     * Map for all traces being actively recorded
     */
    private final Map<Region, TraceRecordingWrapper> activeTraces = new HashMap<>();

    /**
     * Map for all TNTs being traced, by region
     */
    private final Map<Region, List<TNTPrimed>> trackedTNT = new HashMap<>();

    /**
     * Map from TNT to Region the TNT spawned in
     */
    private final Map<TNTPrimed, Region> tntSpawnRegion = new HashMap<>();

    /**
     * Maps a tracked tnt entity to its entire recording history
     */
    private final Map<TNTPrimed, List<TNTPoint>> historyMap = new HashMap<>();

    /**
     * Regions where auto-trace is enabled
     */
    private final Set<Region> autoTraceRegions = new HashSet<>();

    public void init() {
        BauSystem.runTaskTimer(BauSystem.getInstance(), () -> {
            record();
            checkForAutoTraceFinish();
        }, 0, 1);
    }

    public void addAutoTraceRegion(Region region) {
        autoTraceRegions.add(region);
    }

    public void removeAutoTraceRegion(Region region) {
        autoTraceRegions.remove(region);
    }

    /**
     * Makes checks for whether auto traces finished
     */
    public void checkForAutoTraceFinish() {
        for (Region region : autoTraceRegions) {
            if (autoTraceRegions.contains(region) && trackedTNT.getOrDefault(region, Collections.emptyList()).size() == 0) {
                stopRecording(region);
            }
        }
    }

    /**
     * Starts a recording at the given region
     *
     * @param region region to be recorded
     */
    public Trace startRecording(Region region) {
        if (activeTraces.containsKey(region)) {
            return activeTraces.get(region).getTrace();
        }

        TraceRecordingWrapper wrappedTrace = new TraceRecordingWrapper(region);
        activeTraces.put(region, wrappedTrace);
        return wrappedTrace.getTrace();
    }

    /**
     * Stops the recording at the given region
     *
     * @param region region to stop recording
     */
    public void stopRecording(Region region) {
        TraceRecordingWrapper wrappedTrace = activeTraces.getOrDefault(region, null);
        if (wrappedTrace == null) return;

        wrappedTrace.finalizeRecording();
        activeTraces.remove(region);
        for (TNTPrimed tnt : trackedTNT.getOrDefault(region, Collections.emptyList())) {
            historyMap.remove(tnt);
        }
        trackedTNT.put(region, new ArrayList<>());
    }

    /**
     * Internal methode to record all tracked TNT Entities
     */
    private void record() {
        for (Region region : activeTraces.keySet()) {
            TraceRecordingWrapper wrappedTrace = activeTraces.get(region);
            Iterator<TNTPrimed> iter = trackedTNT.getOrDefault(region, Collections.emptyList()).iterator();
            while (iter.hasNext()) {
                TNTPrimed tnt = iter.next();
                if (tnt.getFuseTicks() == 80) continue;
                TNTPoint record = record(tnt, wrappedTrace, Collections.emptyList());
                if (record == null) {
                    iter.remove();
                    tntSpawnRegion.remove(tnt);
                    historyMap.remove(tnt);
                    tntSpawnRegion.remove(tnt);
                } else {
                    wrappedTrace.addRecord(record);
                }
            }
            wrappedTrace.commitRecorded();
        }
    }

    /**
     * Internal methode to record exploded tnt
     *
     * @param tntPrimed       tnt exploding
     * @param wrappedTrace    the trace to record the tnt for wrapped with metadata
     * @param destroyedBlocks the blocks destoryed by the passed tnt
     * @return the record creaded of the passed tnt
     */
    private TNTPoint record(TNTPrimed tntPrimed, TraceRecordingWrapper wrappedTrace, List<Block> destroyedBlocks) {
        List<TNTPoint> history = historyMap.getOrDefault(tntPrimed, new ArrayList<>());

        // Failsave for tnt entering unloaded chunks
        if (tntPrimed == null || tntPrimed.isDead() || history.size() > 0 && history.get(history.size() - 1).getFuse() == tntPrimed.getFuseTicks()) {
            return null;
        }

        int tntID;

        if (history.size() == 0) {
            try {
                historyMap.put(tntPrimed, history);
            } catch (NullPointerException e) {
                Logger logger = Bukkit.getLogger();
                //TODO remove when no longer neccecary
                logger.log(Level.WARNING, "Nullpointer thrown by historyMap");
                logger.log(Level.WARNING, "TNT History: " + history);
                logger.log(Level.WARNING, "History Map: " + historyMap);
                throw e;
            }
            tntID = wrappedTrace.getNextOpenRecordIdAndIncrement();
        } else {
            tntID = history.get(0).getTntId();
        }

        boolean isExplosion = tntPrimed.getFuseTicks() == 0;
        if (isExplosion) {
            wrappedTrace.activateExplosionRecorded();
        }
        boolean afterFirstExplosion = wrappedTrace.isExplosionRecorded();

        TNTPoint record = new TNTPoint(tntID, tntPrimed, isExplosion, afterFirstExplosion, TPSUtils.currentRealTick.get() - wrappedTrace.getStartTick(), history, destroyedBlocks);
        history.add(record);

        return record;
    }

    public boolean isAutoTraceEnabledInRegion(Region region) {
        return autoTraceRegions.contains(region);
    }

    public boolean isTraceActiveInRegion(Region region) {
        return activeTraces.containsKey(region);
    }

    /**
     * Get the trace that is currently recorded in the given region
     *
     * @param region the region to get the trace for
     * @return the trace recorded in region or empty if no trace is recorded in region
     */
    public Optional<Trace> getActiveTraceForRegion(Region region) {
        TraceRecordingWrapper traceWrapper = activeTraces.get(region);

        if (traceWrapper == null) {
            return Optional.empty();
        } else {
            return Optional.of(traceWrapper.getTrace());
        }
    }

    public long getStartTimeOfTraceInRegion(Region region) {
        TraceRecordingWrapper wrapper = activeTraces.get(region);
        if (wrapper == null) return 0;
        return wrapper.getStartTick();
    }

    /**
     * Event for TNTs beeing spawn.
     * Registers newly spawned TNT to be traced if reqired
     *
     * @param event
     */
    @EventHandler(priority = EventPriority.LOWEST, ignoreCancelled = true)
    public void onTNTSpawn(EntitySpawnEvent event) {
        if (!(event.getEntity() instanceof TNTPrimed)) return;

        Region region = Region.getRegion(event.getLocation());

        if (autoTraceRegions.contains(region) && !activeTraces.containsKey(region)) {
            startRecording(region);
        }

        if (activeTraces.containsKey(region)) {
            // Check whether set for tracking already exists. Creating it if necessary
            if (!trackedTNT.containsKey(region)) {
                trackedTNT.put(region, new ArrayList<>());
            }

            trackedTNT.get(region).add((TNTPrimed) event.getEntity());
            tntSpawnRegion.put((TNTPrimed) event.getEntity(), region);
            activeTraces.get(region).addRecord(record((TNTPrimed) event.getEntity(), activeTraces.get(region), Collections.emptyList()));
        }
    }

    /**
     * Event for TNTs exploding
     * Unregisters TNTs from beeing traced on explode
     *
     * @param event
     */
    @EventHandler(priority = EventPriority.LOWEST, ignoreCancelled = true)
    public void onTNTExplode(EntityExplodeEvent event) {
        if (!(event.getEntity() instanceof TNTPrimed)) return;
        Region region = tntSpawnRegion.getOrDefault((TNTPrimed) event.getEntity(), null);
        if (region == null) return;
        trackedTNT.get(region).remove((TNTPrimed) event.getEntity());
        tntSpawnRegion.remove((TNTPrimed) event.getEntity());

        activeTraces.get(region).addRecord(record((TNTPrimed) event.getEntity(), activeTraces.get(region), event.blockList()));
        tntSpawnRegion.remove((TNTPrimed) event.getEntity());
        historyMap.remove((TNTPrimed) event.getEntity());
    }
}
