/*
 *   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.slaves.laufbau.states;

import com.sk89q.worldedit.EditSession;
import de.steamwar.bausystem.BauSystem;
import de.steamwar.bausystem.features.slaves.laufbau.BlockBoundingBox;
import de.steamwar.bausystem.features.slaves.laufbau.Cuboid;
import de.steamwar.bausystem.features.tracer.TNTPoint;
import de.steamwar.bausystem.features.tracer.Trace;
import de.steamwar.bausystem.features.tracer.TraceManager;
import de.steamwar.bausystem.region.Point;
import de.steamwar.bausystem.utils.FlatteningWrapper;
import org.bukkit.Location;
import org.bukkit.World;
import org.bukkit.entity.Player;
import org.bukkit.util.Vector;

import java.util.*;
import java.util.function.BiPredicate;

public class ProcessingTracesState implements LaufbauState {

    private final long start = System.currentTimeMillis();
    private final World world;
    private final BiPredicate<Vector, Integer> inRegionCheck;
    private final EditSession editSession;
    private final List<BlockBoundingBox> elements;
    private final int factor;

    private final List<Trace> Traces;
    private final List<TNTPoint> TNTPoints = new ArrayList<>();
    private final int totalTraces;

    private final Set<Point> affectedBlocks = new HashSet<>();
    private final Map<Point, Set<Cuboid>> cuboidsPerChunk = new HashMap<>();

    public ProcessingTracesState(World world, BiPredicate<Vector, Integer> inRegionCheck, EditSession editSession, List<BlockBoundingBox> elements, int factor) {
        this.world = world;
        this.inRegionCheck = inRegionCheck;
        this.editSession = editSession;
        this.elements = elements;
        this.factor = factor;

        Traces = new ArrayList<>(TraceManager.instance.getAll());
        totalTraces = Traces.size();
    }

    @Override
    public String actionBarMessage(Player p) {
        return BauSystem.MESSAGE.parse("LAUFBAU_SIMPLE_PROGRESS", p, BauSystem.MESSAGE.parse("LAUFBAU_STATE_PROCESSING_TRACES", p), totalTraces - Traces.size(), totalTraces, eta(p, start, totalTraces - Traces.size(), totalTraces));
    }

    private boolean inRegion(Vector location, int expansion) {
        return inRegionCheck.test(location, expansion);
    }

    @Override
    public boolean hasNext() {
        return !Traces.isEmpty() || !TNTPoints.isEmpty();
    }

    @Override
    public void next() {
        if (TNTPoints.isEmpty()) {
            Trace trace = Traces.remove(0);
            trace.getHistories().stream().flatMap(Collection::stream).forEach(TNTPoints::add);
            return;
        }

        TNTPoint current = TNTPoints.remove(0);
        if (FlatteningWrapper.impl.inWater(world, current.getLocation().toVector())) return;
        if (!(inRegion(current.getLocation().toVector(), 1) || (current.getPrevious().isPresent() && inRegion(current.getPrevious().get().getLocation().toVector(), 1)))) {
            return;
        }

        Location location = current.getLocation();
        if (current.getPrevious().isPresent()) {
            Location previousLocation = current.getPrevious().get().getLocation();
            Location movement = location.clone().subtract(previousLocation);
            calculateCuboid(new Cuboid(previousLocation.getX() - 0.49, Math.min(previousLocation.getY(), location.getY()), previousLocation.getZ() - 0.49, 0.98, Math.abs(movement.getY()) + 0.98, 0.98));
            if (movement.getX() >= movement.getZ()) {
                calculateCuboid(new Cuboid(Math.min(previousLocation.getX(), location.getX()) - 0.49, location.getY(), previousLocation.getZ() - 0.49, Math.abs(movement.getX()) + 0.98, 0.98, 0.98));
                calculateCuboid(new Cuboid(location.getX() - 0.49, location.getY(), Math.min(previousLocation.getZ(), location.getZ()) - 0.49, 0.98, 0.98, Math.abs(movement.getZ()) + 0.98));
            } else {
                calculateCuboid(new Cuboid(previousLocation.getX() - 0.49, location.getY(), Math.min(previousLocation.getZ(), location.getZ()) - 0.49, 0.98, 0.98, Math.abs(movement.getZ()) + 0.98));
                calculateCuboid(new Cuboid(Math.min(previousLocation.getX(), location.getX()) - 0.49, location.getY(), location.getZ() - 0.49, Math.abs(movement.getX()) + 0.98, 0.98, 0.98));
            }
        } else {
            calculateCuboid(new Cuboid(location.getX() - 0.49, location.getY(), location.getZ() - 0.49, 0.98, 0.98, 0.98));
        }
    }

    private void calculateCuboid(Cuboid cuboid) {
        for (double x = cuboid.getX() - 2; x < cuboid.getX() + cuboid.getDx() + 2; x++) {
            for (double y = cuboid.getY() - 2; y < cuboid.getY() + cuboid.getDy() + 2; y++) {
                for (double z = cuboid.getZ() - 2; z < cuboid.getZ() + cuboid.getDz() + 2; z++) {
                    Vector location = new Vector(x, y, z);
                    if (inRegion(location, 0)) {
                        affectedBlocks.add(new Point(location.getBlockX(), location.getBlockY(), location.getBlockZ()));
                        cuboidsPerChunk.computeIfAbsent(new Point(location.getBlockX() / factor, location.getBlockY() / factor, location.getBlockZ() / factor), __ -> new HashSet<>())
                                .add(cuboid);
                    }
                }
            }
        }
    }

    @Override
    public LaufbauState getNextState() {
        return new CreatingLaufState(affectedBlocks, cuboidsPerChunk, world, editSession, elements, factor);
    }
}
