/*
 *   This file is a part of the SteamWar software.
 *
 *   Copyright (C) 2023  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.fightsystem.ai;

import com.sk89q.worldedit.extent.clipboard.Clipboard;
import com.sk89q.worldedit.math.BlockVector3;
import com.sk89q.worldedit.regions.Region;
import com.sk89q.worldedit.world.block.BlockType;
import de.steamwar.fightsystem.Config;
import de.steamwar.sql.SchematicData;
import de.steamwar.sql.SchematicNode;
import org.bukkit.util.Vector;

import java.io.IOException;
import java.util.*;

public class LixfelPathplanner {

    private static BlockType getBlockType(Clipboard clipboard, BlockVector3 vector) {
        return clipboard.getBlock(vector).getBlockType();
    }

    private static boolean nonsolid(Clipboard clipboard, BlockVector3 vector) {
        return !getBlockType(clipboard, vector).getMaterial().isSolid();
    }

    private static Vector toBukkit(BlockVector3 vector) {
        return new Vector(vector.getX() + 0.5, vector.getY(), vector.getZ() + 0.5);
    }

    private final List<Vector> walkable = new ArrayList<>();
    private final Map<Vector, Vector[]> neighbours = new HashMap<>();

    public LixfelPathplanner(SchematicNode schem) {
        try {
            fillWalkable(new SchematicData(schem).load());
        } catch (IOException e) {
            throw new IllegalStateException(e);
        }
    }

    public List<Vector> getWalkable() {
        return walkable;
    }

    private void fillWalkable(Clipboard clipboard) {
        BlockVector3 min = clipboard.getRegion().getMinimumPoint().subtract(Config.PreperationArea, 0, Config.PreperationArea); //TODO assumes nonextended Schematic with maximal size
        Region region = clipboard.getRegion();
        clipboard.getRegion().forEach(vector -> {
            BlockVector3 below = vector.subtract(0, 1, 0);
            if(!region.contains(below))
                return;

            BlockType belowMaterial = getBlockType(clipboard, below);
            BlockVector3 above = vector.add(0, 1, 0);
            if(nonsolid(clipboard, vector)) {
                if(
                        (belowMaterial.getMaterial().isSolid() || belowMaterial.getId().equals("minecraft:ladder")) &&
                                (!region.contains(above) || nonsolid(clipboard, above))
                )
                    walkable.add(toBukkit(vector.subtract(min)));
            } else {
                if(!region.contains(above))
                    walkable.add(toBukkit(above.subtract(min)));
            }
        });

        for(Vector vector : walkable) {
            neighbours.put(vector, walkable.stream().filter(neighbour -> neighbouring(neighbour, vector)).filter(neighbour -> neighbour != vector).toArray(Vector[]::new));
        }
    }

    public List<Vector> planToAnywhere(Vector start, Vector destination) {
        Vector intermediate = walkable.stream().filter(vector -> neighbouring(vector, destination)).findAny().orElse(null);

        if(intermediate == null)
            return Collections.emptyList();

        List<Vector> plan = plan(start, intermediate);
        plan.add(destination);

        return plan;
    }

    public List<Vector> plan(Vector start, Vector destination) {
        if(neighbouring(start, destination))
            return Collections.singletonList(destination);

        Map<Vector, Vector> approach = new HashMap<>();
        Set<Vector> checking = Collections.singleton(destination);

        while(!checking.isEmpty()) {
            Set<Vector> toCheck = new HashSet<>();
            for(Vector current : checking) {
                Vector firstStep = Arrays.stream(neighbours.get(current))
                        .filter(vector -> !approach.containsKey(vector))
                        .filter(next -> {
                            approach.put(next, current);
                            toCheck.add(next);
                            return neighbouring(next, start);
                        })
                        .findAny().orElse(null);

                if(firstStep != null) {
                    List<Vector> path = new ArrayList<>();
                    path.add(firstStep);

                    while(path.get(path.size()-1) != destination) {
                        path.add(approach.get(path.get(path.size()-1)));
                    }

                    return path;
                }
            }
            checking = toCheck;
        }

        return Collections.emptyList();
    }

    private boolean neighbouring(Vector a, Vector b) {
        return Math.abs(a.getX() - b.getX()) <= 1 && Math.abs(a.getY() - b.getY()) <= 1 && Math.abs(a.getZ() - b.getZ()) <= 1;
    }
}
