/*
 *   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.features.tracer.rendering.BundleFilter;
import de.steamwar.bausystem.features.tracer.rendering.PlayerTraceShowData;
import de.steamwar.bausystem.features.tracer.rendering.dynamicflags.AtFlag;
import de.steamwar.bausystem.features.tracer.rendering.dynamicflags.IsolateFlag;
import de.steamwar.bausystem.region.Region;
import de.steamwar.linkage.Linked;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
import org.bukkit.event.player.PlayerQuitEvent;

import java.io.File;
import java.util.*;
import java.util.stream.Collectors;

import static de.steamwar.bausystem.features.tracer.TraceRepository.tracesFolder;

@Linked
public class TraceManager implements Listener {

    public static TraceManager instance;

    {
        instance = this;
    }

    public void init() {
        if (!tracesFolder.exists()) tracesFolder.mkdir();

        File[] traceFiles = tracesFolder.listFiles();
        if (traceFiles == null) return;

        boolean hasMetaFiles = false;
        for (File traceFile : traceFiles) {
            if (traceFile.getName().contains(".meta")) {
                hasMetaFiles = true;
            }
        }
        if (hasMetaFiles) {
            for (File traceFile : traceFiles) {
                traceFile.delete();
            }
            traceFiles = new File[0];
        }

        for (File traceFile : traceFiles) {
            Trace trace = TraceRepository.readTrace(traceFile);
            if (trace == null) {
                traceFile.delete();
                continue;
            }
            add(trace);
        }
    }

    /**
     * List of all current traces
     */
    private final Map<Region, Map<Integer, Trace>> tracesByRegion = new HashMap<>();

    private final Map<Region, Map<Player, PlayerTraceShowData>> showDataPerRegionPerPlayer = new HashMap<>();

    private final Map<Player, Set<Player>> followerMap = new HashMap<>();

    /**
     * Utility variable to keep track of the next open trace id;
     */
    private int nextOpenId = 0;

    /**
     * Adds a new trace to the global record
     *
     * @param trace Trace to be added
     * @return id of the created trace
     */
    protected int add(Trace trace) {
        showDataPerRegionPerPlayer.getOrDefault(trace.getRegion(), Collections.emptyMap()).forEach((player, playerTraceShowData) -> {
            trace.render(player, playerTraceShowData);
            followerMap.getOrDefault(player, Collections.emptySet()).forEach(follower -> {
                trace.render(follower, playerTraceShowData);
            });
        });

        tracesByRegion.computeIfAbsent(trace.getRegion(), region -> new HashMap<>()).put(nextOpenId, trace);
        nextOpenId++;
        return nextOpenId;
    }

    /**
     * Get the id of the given trace
     *
     * @param trace
     */
    public int getId(Trace trace) {
        for (Map.Entry<Integer, Trace> entry : tracesByRegion.getOrDefault(trace.getRegion(), Collections.emptyMap()).entrySet()) {
            if (entry.getValue() == trace) return entry.getKey();
        }
        return -1;
    }

    /**
     * Renders only the given records to the specified trace
     *
     * @param trace
     * @param recordsToAdd
     */
    protected void showPartial(Trace trace, List<TNTPoint> recordsToAdd) {
        showDataPerRegionPerPlayer.getOrDefault(trace.getRegion(), Collections.emptyMap()).forEach((player, playerTraceShowData) -> {
            trace.render(recordsToAdd, player, playerTraceShowData);
            followerMap.getOrDefault(player, Collections.emptySet()).forEach(follower -> {
                trace.render(recordsToAdd, follower, playerTraceShowData);
            });
        });
    }

    protected Map<Player, PlayerTraceShowData> getTraceShowDataPlayerMapping(Region region) {
        return showDataPerRegionPerPlayer.getOrDefault(region, new HashMap<>());
    }

    /**
     * Removes the trace with the given id
     *
     * @param trace the trace to be removed
     */
    public void remove(Trace trace) {
        Map<Integer, Trace> traces = tracesByRegion.getOrDefault(trace.getRegion(), Collections.emptyMap());
        Integer traceId = traces.entrySet().stream()
                .filter(entry -> entry.getValue() == trace)
                .map(Map.Entry::getKey)
                .findFirst()
                .orElse(null);
        if (traceId == null) {
            throw new RuntimeException("Trace not found while trying to remove see (c978eb98-b0b2-4009-91d8-acfa34e2831a)");
        }
        traces.remove(traceId);
        trace.hide();
        trace.getRecordsSaveFile().delete();
    }

    /**
     * Clears all traces
     */
    public void clear(Region region) {
        showDataPerRegionPerPlayer.getOrDefault(region, new HashMap<>())
                .keySet()
                .forEach(player -> {
                    Set<Player> players = followerMap.getOrDefault(player, Collections.emptySet());
                    tracesByRegion.getOrDefault(region, new HashMap<>()).values().forEach(trace -> {
                        trace.hide(player);
                        players.forEach(trace::hide);
                    });
                });
        tracesByRegion.getOrDefault(region, new HashMap<>())
                .forEach((i, trace) -> {
                    if (trace.getRegion() != region) return;
                    trace.getRecordsSaveFile().delete();
                });
        tracesByRegion.getOrDefault(region, new HashMap<>()).clear();
    }

    /**
     * Methode to get all traces in a certain region
     *
     * @param region Region to look for traces in
     * @return All traces recorded in the given Region
     */
    public Collection<Trace> get(Region region) {
        return tracesByRegion.getOrDefault(region, Collections.emptyMap()).values();
    }

    /**
     * Methode to get the trace with specific id
     *
     * @param index index of the trace
     * @return the trace with given id
     */
    public Optional<Trace> get(int index) {
        for (Map.Entry<Region, Map<Integer, Trace>> intermediate : tracesByRegion.entrySet()) {
            if (intermediate.getValue().containsKey(index)) {
                return Optional.ofNullable(intermediate.getValue().get(index));
            }
        }
        return Optional.empty();
    }

    /**
     * Methode to get all traces
     *
     * @return internal list of all current traces
     */
    public Collection<Trace> getAll() {
        return tracesByRegion.values().stream().map(Map::values).flatMap(Collection::stream).collect(Collectors.toList());
    }

    /**
     * @return all ids of active traces
     */
    public Set<Integer> getAllIds() {
        return tracesByRegion.values().stream().map(Map::keySet).flatMap(Collection::stream).collect(Collectors.toSet());
    }

    /**
     * Shows traces for the player of the current Region
     *
     * @param player
     * @param playerTraceShowData
     */
    public void show(Player player, PlayerTraceShowData playerTraceShowData) {
        unfollow(player);

        Region region = Region.getRegion(player.getLocation());
        showDataPerRegionPerPlayer.computeIfAbsent(region, ignored -> new HashMap<>()).put(player, playerTraceShowData);
        tracesByRegion.getOrDefault(region, Collections.emptyMap()).forEach((integer, trace) -> {
            trace.render(player, playerTraceShowData);
            followerMap.getOrDefault(player, Collections.emptySet()).forEach(follower -> {
                trace.render(follower, playerTraceShowData);
            });
        });
    }

    /**
     * Hides traces for the player of the current Region
     *
     * @param player
     */
    public void hide(Player player) {
        unfollow(player);

        Region region = Region.getRegion(player.getLocation());
        PlayerTraceShowData previous = showDataPerRegionPerPlayer.getOrDefault(region, Collections.emptyMap()).remove(player);
        if (previous == null) return;
        tracesByRegion.getOrDefault(region, Collections.emptyMap()).forEach((integer, trace) -> {
            trace.hide(player);
            followerMap.getOrDefault(player, Collections.emptySet()).forEach(trace::hide);
        });
    }

    /**
     * Makes the given player
     *
     * @param follower
     * @param following
     * @return
     */
    public boolean follow(Player follower, Player following) {
        if (followerMap.containsKey(follower)) return false;
        if (followerMap.entrySet().stream().anyMatch(playerSetEntry -> playerSetEntry.getValue().contains(follower))) {
            unfollow(follower);
        }

        followerMap.computeIfAbsent(following, ignored -> new HashSet<>()).add(follower);

        showDataPerRegionPerPlayer.forEach((region, playerPlayerTraceShowDataMap) -> {
            if (playerPlayerTraceShowDataMap.containsKey(follower)) {
                tracesByRegion.getOrDefault(region, Collections.emptyMap()).forEach((integer, trace) -> trace.hide(follower));
            }

            PlayerTraceShowData playerTraceShowData = playerPlayerTraceShowDataMap.get(following);
            if (playerTraceShowData == null) return;
            tracesByRegion.getOrDefault(region, Collections.emptyMap()).forEach((integer, trace) -> {
                trace.render(follower, playerTraceShowData);
            });
        });
        return true;
    }

    public void unfollow(Player follower) {
        if (followerMap.containsKey(follower)) return;
        List<Player> toRemove = new ArrayList<>();
        Set<Player> toHide = new HashSet<>();
        followerMap.forEach((player, players) -> {
            if (players.remove(follower)) toHide.add(follower);
            if (players.isEmpty()) toRemove.add(player);
        });
        toRemove.forEach(followerMap::remove);

        showDataPerRegionPerPlayer.forEach((region, playerPlayerTraceShowDataMap) -> {
            toHide.forEach(player -> {
                if (!playerPlayerTraceShowDataMap.containsKey(player)) return;
                tracesByRegion.getOrDefault(region, Collections.emptyMap()).forEach((integer, trace) -> {
                    trace.hide(follower);
                });
            });

            PlayerTraceShowData playerTraceShowData = playerPlayerTraceShowDataMap.get(follower);
            if (playerTraceShowData == null) return;
            tracesByRegion.getOrDefault(region, Collections.emptyMap()).forEach((integer, trace) -> {
                trace.render(follower, playerTraceShowData);
            });
        });
    }

    /**
     * Modifies the render for the given player, to only show tnts at the given time
     * interval
     *
     * @param player
     * @param from   start of time interval
     * @param to     end of time interval
     */
    public void renderAt(Player player, int from, int to) {
        unfollow(player);

        Region region = Region.getRegion(player.getLocation());
        PlayerTraceShowData playerTraceShowData = showDataPerRegionPerPlayer
                .computeIfAbsent(region, ignored -> new HashMap<>())
                .computeIfAbsent(player, ignored -> new PlayerTraceShowData(BundleFilter.DEFAULT));

        AtFlag atFlag = playerTraceShowData.getViewFlag(AtFlag.class);
        if (atFlag == null) {
            atFlag = new AtFlag(from, to);
            playerTraceShowData.addViewFlag(atFlag);
        } else {
            atFlag.update(from, to);
        }

        tracesByRegion.getOrDefault(region, Collections.emptyMap()).forEach((integer, trace) -> {
            trace.render(player, playerTraceShowData);
            followerMap.getOrDefault(player, Collections.emptySet()).forEach(follower -> {
                trace.render(follower, playerTraceShowData);
            });
        });
    }

    /**
     * Toggles the isolated render for the given records and player
     *
     * @param player  the player the trace is shown to
     * @param ptrace  the trace for whitch isolation is toggled
     * @param records the records of the trace for which isolation is toggled
     */
    public void isolate(Player player, Trace ptrace, TNTPoint... records) {
        unfollow(player);

        Region region = Region.getRegion(player.getLocation());
        PlayerTraceShowData playerTraceShowData = showDataPerRegionPerPlayer
                .computeIfAbsent(region, ignored -> new HashMap<>())
                .computeIfAbsent(player, ignored -> new PlayerTraceShowData(BundleFilter.DEFAULT));

        IsolateFlag isolateFlag;
        if (playerTraceShowData.hasViewFlagOnly(IsolateFlag.class)) {
            isolateFlag = playerTraceShowData.getViewFlag(IsolateFlag.class);
        } else if (playerTraceShowData.hasNoViewFlags()) {
            isolateFlag = new IsolateFlag();
            playerTraceShowData.addViewFlag(isolateFlag);
        } else {
            playerTraceShowData = new PlayerTraceShowData(BundleFilter.DEFAULT);
            isolateFlag = new IsolateFlag();
            playerTraceShowData.addViewFlag(isolateFlag);
            showDataPerRegionPerPlayer.get(region).put(player, playerTraceShowData);
        }

        for (TNTPoint record : records) {
            isolateFlag.toggleId(record.getTntId());
        }

        if (isolateFlag.isEmpty() && playerTraceShowData.hasViewFlagOnly(IsolateFlag.class) && records.length != 0) {
            playerTraceShowData.removeViewFlag(IsolateFlag.class);
        }

        PlayerTraceShowData finalPlayerTraceShowData = playerTraceShowData;
        tracesByRegion.getOrDefault(region, Collections.emptyMap()).forEach((integer, trace) -> {
            if (trace.getUuid() == ptrace.getUuid() || finalPlayerTraceShowData.hasNoViewFlags()) {
                trace.render(player, finalPlayerTraceShowData);
                followerMap.getOrDefault(player, Collections.emptySet()).forEach(follower -> {
                    trace.render(follower, finalPlayerTraceShowData);
                });
            } else {
                trace.hide(player);
            }
        });
    }

    @EventHandler
    public void onPlayerQuit(PlayerQuitEvent event) {
        unfollow(event.getPlayer());
        new ArrayList<>(followerMap.getOrDefault(event.getPlayer(), Collections.emptySet())).forEach(this::unfollow);

        showDataPerRegionPerPlayer.forEach((region, playerPlayerTraceShowDataMap) -> {
            playerPlayerTraceShowDataMap.remove(event.getPlayer());
        });

        tracesByRegion.forEach((region, integerTraceMap) -> {
            integerTraceMap.forEach((integer, trace) -> {
                trace.hide(event.getPlayer());
            });
        });
    }
}
