/*
 *   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.TraceEntity;
import de.steamwar.bausystem.features.tracer.rendering.ViewFlag;
import de.steamwar.bausystem.region.Region;
import de.steamwar.entity.REntity;
import de.steamwar.entity.REntityServer;
import lombok.Getter;
import lombok.Setter;
import lombok.SneakyThrows;
import org.bukkit.entity.Player;

import java.io.File;
import java.lang.ref.SoftReference;
import java.util.*;
import java.util.stream.Collectors;
import java.util.stream.Stream;

public class Trace {
    /**
     * UUID of the trace used for
     */
    @Getter
    private final UUID uuid;

    /**
     * File the records are saved in
     */
    @Getter
    private final File recordsSaveFile;

    /**
     * Region the trace was recorded in
     */
    @Getter
    private final Region region;

    /**
     * Date the trace was recorded at
     */
    @Getter
    private final Date date;

    /**
     * Records of TNTs, making up the trace
     */
    private SoftReference<List<TNTPoint>> records;

    @Setter
    @Getter
    private int tntIdCount;

    /**
     * A map of all REntityServers rendering this trace
     */
    private final Map<Player, REntityServer> entityServerMap = new HashMap<>();

    /**
     * Constructor for the creation of a new trace
     *
     * @param region     the region the trace is created at
     * @param recordList the list for the records making up this trace
     */
    @SneakyThrows
    public Trace(Region region, List<TNTPoint> recordList) {
        this.uuid = UUID.randomUUID();
        this.region = region;
        this.date = new Date();
        records = new SoftReference<>(recordList);
        recordsSaveFile = new File(TraceRepository.tracesFolder, uuid + ".records");
    }

    /**
     * Constructor for deserialising a trace from the file system
     */
    @SneakyThrows
    protected Trace(UUID uuid, Region region, Date date, File recordsFile, int tntIdCount) {
        recordsSaveFile = recordsFile;
        this.uuid = uuid;
        this.region = region;
        this.date = date;
        this.records = new SoftReference<>(null);
        this.tntIdCount = tntIdCount;
    }

    /**
     * Gets the histories of all tnts in this trace
     *
     * @return the histories of this trace
     */
    public Set<List<TNTPoint>> getHistories() {
        Set<List<TNTPoint>> histories = new HashSet<>();

        for (TNTPoint record : getRecords()) {
            histories.add(record.getHistory());
        }

        return histories;
    }

    /**
     * List of all used ids
     */
    public List<String> getUsedIds() {
        return getRecords()
                .stream()
                .map(TNTPoint::getTntId)
                .map(i -> i + "")
                .collect(Collectors.toList());
    }

    /**
     * Renders this traces
     *
     * @param player              The player the trace is rendered to
     * @param playerTraceShowData The showData for modifying the rendering
     */
    public void render(Player player, PlayerTraceShowData playerTraceShowData) {
        REntityServer entityServer = entityServerMap.get(player);
        if (entityServer != null) {
            entityServer.getEntities().forEach(REntity::die);
        } else {
            entityServer = new REntityServer();
            entityServer.addPlayer(player);
            entityServer.setCallback((p, rEntity, entityAction) -> {
                if (entityAction != REntityServer.EntityAction.INTERACT) {
                    return;
                }
                if (rEntity instanceof TraceEntity) {
                    ((TraceEntity) rEntity).printIntoChat(p);
                }
            });
            entityServerMap.put(player, entityServer);
        }
        render(getRecords(), entityServer, playerTraceShowData);
    }

    /**
     * Renders specific records to this trace rendering
     *
     * @param records             The records to be rendered
     * @param player              The player the records are rendered to
     * @param playerTraceShowData The showData for modifying the rendering
     */
    protected void render(List<TNTPoint> records, Player player, PlayerTraceShowData playerTraceShowData) {
        REntityServer entityServer = entityServerMap.computeIfAbsent(player, k -> {
            REntityServer newEntityServer = new REntityServer();
            newEntityServer.addPlayer(k);
            newEntityServer.setCallback((p, rEntity, entityAction) -> {
                if (entityAction != REntityServer.EntityAction.INTERACT) {
                    return;
                }
                if (rEntity instanceof TraceEntity) {
                    ((TraceEntity) rEntity).printIntoChat(p);
                }
            });
            return newEntityServer;
        });

        render(records, entityServer, playerTraceShowData);
    }

    /**
     * Internal methode to render records to a REntityServer
     *
     * @param records             Records to render
     * @param entityServer        The show server the trace will be renderd to
     * @param playerTraceShowData The showData for modifying the rendering
     */
    private void render(List<TNTPoint> records, REntityServer entityServer, PlayerTraceShowData playerTraceShowData) {
        if (records.isEmpty()) {
            return;
        }

        Set<ViewFlag> flagList = playerTraceShowData.getEffectiveViewFlags();

        // Apply filters
        Stream<TNTPoint> workingRecordsStream = records.stream();
        for (ViewFlag flag : flagList) {
            workingRecordsStream = flag.filter(workingRecordsStream);
        }
        List<TNTPoint> workingRecords = workingRecordsStream.collect(Collectors.toList());

        // Bundle records at unique positions
        List<List<TNTPoint>> bundles = bundleRecords(workingRecords, playerTraceShowData.getBundleFilter());

        // Render bundled records
        List<TraceEntity> entities = new LinkedList<>();

        for (List<TNTPoint> bundle : bundles) {
            entities.add(new TraceEntity(entityServer, bundle.get(0).getLocation(), bundle.get(0).isExplosion(), bundle,
                    this));
        }

        // Apply modifiers
        for (ViewFlag flag : flagList) {
            flag.modify(entityServer, entities);
        }
    }

    /**
     * Bundles the passed TNTRecords based on whether they are at the same location
     *
     * @param records The TNTRecords that are supposed to be bundled
     * @param filter  A filter specefieng whether records can be bundled
     * @return A list of bundles
     */
    private List<List<TNTPoint>> bundleRecords(List<TNTPoint> records, BundleFilter filter) {
        if (filter == BundleFilter.NONE) {
            return records.stream().map(List::of).collect(Collectors.toList());
        }

        List<List<TNTPoint>> bundles = new ArrayList<>();

        recordsLoop:
        for (TNTPoint record : records) {
            for (int i = bundles.size() - 1; i >= 0; i--) {
                List<TNTPoint> bundle = bundles.get(i);

                Boolean filterResult = filter.function.apply(record, bundle.get(0));
                if (filterResult == null) {
                    ArrayList<TNTPoint> newBundle = new ArrayList<>();
                    newBundle.add(record);
                    bundles.add(newBundle);
                    continue recordsLoop;
                } else if (filterResult) {
                    bundle.add(record);
                    continue recordsLoop;
                }
            }

            ArrayList<TNTPoint> newBundle = new ArrayList<>();
            newBundle.add(record);
            bundles.add(newBundle);
        }

        return bundles;
    }

    /**
     * Hides this trail for the given player
     *
     * @param player
     */
    public void hide(Player player) {
        REntityServer entityServer = entityServerMap.remove(player);
        if (entityServer == null) {
            return;
        }
        entityServer.removePlayer(player);
        if (entityServer.getPlayers().isEmpty()) {
            entityServer.close();
        }
    }

    /**
     * Hides this trace for all players
     */
    public void hide() {
        entityServerMap.forEach((player, entityServer) -> {
            entityServer.close();
        });
        entityServerMap.clear();
    }

    /**
     * Loads the records of this trace from storage to memory
     */
    private void loadRecords() {
        records = new SoftReference<>(TraceRepository.readTraceRecords(this));
    }

    public synchronized List<TNTPoint> getRecords() {
        if (records.get() == null) {
            loadRecords();
        }
        return records.get();
    }

    @Override
    public String toString() {
        return "Trace{" +
                "uuid=" + uuid +
                ", region=" + region +
                ", creationTime=" + date +
                ", recordsSaveFile=" + recordsSaveFile.getName() +
                ", tntCount=" + tntIdCount +
                ", records=" + getRecords() +
                '}';
    }
}
