/*
 *   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.observer;

import de.steamwar.bausystem.region.Point;
import de.steamwar.core.SWPlayer;
import org.bukkit.Location;
import org.bukkit.Material;
import org.bukkit.Particle;
import org.bukkit.block.Block;
import org.bukkit.block.BlockFace;
import org.bukkit.block.data.Bisected;
import org.bukkit.block.data.BlockData;
import org.bukkit.block.data.type.*;
import org.bukkit.block.data.type.Observer;
import org.bukkit.craftbukkit.block.impl.CraftPoweredRail;
import org.bukkit.entity.Player;

import java.util.*;

public class ObserverTracer implements SWPlayer.Component {

    private static final Set<BlockFace> ALLOWED = EnumSet.of(BlockFace.NORTH, BlockFace.SOUTH, BlockFace.EAST, BlockFace.WEST, BlockFace.UP, BlockFace.DOWN);

    private Player player;
    private Block block;
    Set<Point> seen = new HashSet<>();
    private List<Block> blockList = new ArrayList<>();

    public ObserverTracer(Player player, Block block) {
        this.player = player;
        this.block = block;
    }

    public void show() {
        if (!ObserverTracerListener.enabled.contains(player)) {
            return;
        }
        for (Point p : seen) {
            Location location = p.toLocation(player);
            spawnParticle(player, location, location.getBlock());
        }
    }

    public boolean trace() {
        int size = seen.size();

        seen.clear();
        blockList.clear();
        if (block.getType() != Material.OBSERVER) {
            return seen.size() != size;
        }

        blockList.add(block);
        while (!blockList.isEmpty()) {
            Block b = blockList.remove(0);
            Point point = Point.fromLocation(b.getLocation());
            if (seen.contains(point)) {
                continue;
            }
            seen.add(point);
            spawnParticle(player, b.getLocation(), b);

            if (b.getType() == Material.OBSERVER) {
                calculateObserver(b);
                continue;
            }

            BlockData blockData = b.getBlockData();
            if (blockData instanceof Fence) {
                calculateSimple(b);
                continue;
            }

            if (blockData instanceof Door) {
                if (((Door) blockData).getHalf() == Bisected.Half.BOTTOM) {
                    blockList.add(b.getLocation().add(0, 1, 0).getBlock());
                } else {
                    blockList.add(b.getLocation().add(0, -1, 0).getBlock());
                }
            }
            if (checkAllowed(b, blockData)) {
                calculateSpecial(b);
                continue;
            }

            if (b.getType().isBlock() && b.getType().isSolid() && b.getType().isOccluding()) {
                calculateSolidBlock(b);
            }
        }
        return seen.size() != size;
    }

    private void spawnParticle(Player player, Location location, Block block) {
        player.spawnParticle(Particle.FLAME, location.clone().add(0.5, 0.5, 0.5), 1, 0, 0, 0, 0);
        if (block.getType() == Material.OBSERVER) {
            BlockFace blockFace = ((Observer) block.getBlockData()).getFacing();
            blockFace = blockFace.getOppositeFace();
            player.spawnParticle(Particle.FLAME, location.clone().add(0.5 + blockFace.getModX() / 5.0, 0.5 + blockFace.getModY() / 5.0, 0.5 + blockFace.getModZ() / 5.0), 1, 0, 0, 0, 0);
        }
    }

    private void calculateObserver(Block block) {
        // TODO: Implement single Repeater, single Redstone, DetectorRail, PoweredRail, Diagonal Piston

        Observer observer = (Observer) block.getBlockData();
        for (BlockFace blockFace : ALLOWED) {
            Location location = block.getLocation().add(blockFace.getModX(), blockFace.getModY(), blockFace.getModZ());
            Block b = location.getBlock();
            if (blockFace == observer.getFacing().getOppositeFace() && !b.getType().isAir() && b.getType() != Material.OBSERVER) {
                blockList.add(b);
                if (checkMaterial(b)) {
                    calculateSolidBlock(b);
                }
                continue;
            }
            if (b.getType() != Material.OBSERVER) {
                continue;
            }
            if (((Observer) b.getBlockData()).getFacing() == blockFace.getOppositeFace()) {
                blockList.add(b);
            }
        }
        if (observer.getFacing() == BlockFace.UP) {
            Location location = block.getLocation();
            location.add(0, -2, 0);
            if (location.getY() >= 0) {
                Block b = location.getBlock();
                if (b.getType() == Material.DROPPER || b.getType() == Material.DISPENSER) {
                    blockList.add(b);
                }
            }
        }
        if (observer.getFacing() != BlockFace.UP && observer.getFacing() != BlockFace.DOWN) {
            Location location = block.getLocation();
            BlockFace blockFace = observer.getFacing().getOppositeFace();
            location.add(blockFace.getModX(), -1, blockFace.getModZ());
            if (location.getY() >= 0) {
                Block b = location.getBlock();
                if (b.getType() == Material.DROPPER || b.getType() == Material.DISPENSER) {
                    blockList.add(b);
                }
            }
        }
    }

    private void calculateSolidBlock(Block block) {
        for (BlockFace blockFace : ALLOWED) {
            Location location = block.getLocation().add(blockFace.getModX(), blockFace.getModY(), blockFace.getModZ());
            Block b = location.getBlock();
            if (checkAllowed(b, b.getBlockData())) {
                blockList.add(b);
            }
        }
    }

    private boolean checkAllowed(Block block, BlockData blockData) {
        if (checkMaterial(block)) return true;
        if (block.getType() == Material.BELL) {
            return true;
        }

        return blockData instanceof Door
                || blockData instanceof Gate
                || blockData instanceof CraftPoweredRail
                || blockData instanceof TrapDoor
                || blockData instanceof GlassPane;
    }

    private boolean checkMaterial(Block block) {
        if (block.getType() == Material.DROPPER) {
            return true;
        }
        if (block.getType() == Material.HOPPER) {
            return true;
        }
        if (block.getType() == Material.DISPENSER) {
            return true;
        }
        if (block.getType() == Material.REDSTONE_LAMP) {
            return true;
        }
        if (block.getType() == Material.NOTE_BLOCK) {
            return true;
        }
        return false;
    }

    private void calculateSpecial(Block block) {
        BlockData blockData = block.getBlockData();
        Location blockLocation = block.getLocation();
        if (blockData instanceof TrapDoor) {
            TrapDoor trapDoor = (TrapDoor) blockData;
            fence(trapDoor.getFacing().getOppositeFace(), blockLocation);
        }
        if (blockData instanceof Door) {
            Door door = (Door) blockData;
            fence(door.getFacing().getOppositeFace(), blockLocation);
            fence(doorHinge(door.getFacing(), door.getHinge()), blockLocation);
        }
        calculateSimple(block);
    }

    private void fence(BlockFace current, Location location) {
        Block b = location.clone().add(current.getModX(), current.getModY(), current.getModZ()).getBlock();
        if (b.getBlockData() instanceof Fence || b.getBlockData() instanceof GlassPane) {
            blockList.add(b);
        }
    }

    private BlockFace doorHinge(BlockFace blockFace, Door.Hinge hinge) {
        switch (blockFace) {
            case WEST:
                if (hinge == Door.Hinge.RIGHT) {
                    return BlockFace.NORTH;
                } else {
                    return BlockFace.SOUTH;
                }
            case SOUTH:
                if (hinge == Door.Hinge.RIGHT) {
                    return BlockFace.WEST;
                } else {
                    return BlockFace.EAST;
                }
            case EAST:
                if (hinge == Door.Hinge.RIGHT) {
                    return BlockFace.SOUTH;
                } else {
                    return BlockFace.NORTH;
                }
            case NORTH:
                if (hinge == Door.Hinge.RIGHT) {
                    return BlockFace.EAST;
                } else {
                    return BlockFace.WEST;
                }
            default:
                throw new SecurityException();
        }
    }

    private void calculateSimple(Block block) {
        Location blockLocation = block.getLocation();
        for (BlockFace blockFace : ALLOWED) {
            Location location = blockLocation.clone().add(blockFace.getModX(), blockFace.getModY(), blockFace.getModZ());
            Block b = location.getBlock();
            if (b.getType() == Material.OBSERVER) {
                Observer current = (Observer) b.getBlockData();
                if (current.getFacing() == blockFace.getOppositeFace()) {
                    blockList.add(b);
                }
            }
        }
    }
}
