package cz.jzitnik.game.logic.services.flowing;

import cz.jzitnik.game.Game;
import cz.jzitnik.game.annotations.BreakableByWater;
import cz.jzitnik.game.annotations.CustomLogic;
import cz.jzitnik.game.entities.Block;
import cz.jzitnik.game.entities.items.ItemBlockSupplier;
import cz.jzitnik.game.logic.CustomLogicInterface;
import cz.jzitnik.game.sprites.Water;
import cz.jzitnik.tui.ScreenRenderer;
import lombok.AllArgsConstructor;
import lombok.Getter;

import java.awt.*;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

@CustomLogic
public class FlowingLogic implements CustomLogicInterface {
    private static final int RADIUS = 20;

    @AllArgsConstructor
    @Getter
    private static class LiquidBlock {
        private Block block;
        private int x;
        private int y;
    }

    @Override
    public void nextIteration(Game game, ScreenRenderer ignored) {
        processFlow(game, "water");
        processFlow(game, "lava");
    }

    private void processFlow(Game game, String liquidId) {
        int[] data = game.getPlayerCords();
        var world = game.getWorld();
        int playerX = data[0];
        int playerY = data[1];

        int startX = Math.max(0, playerX - RADIUS);
        int startY = Math.max(0, playerY - RADIUS);
        int endX = Math.min(world[0].length - 1, playerX + RADIUS);
        int endY = Math.min(world.length - 1, playerY + RADIUS);

        List<LiquidBlock> sourceBlocks = new ArrayList<>();

        for (int y = startY; y <= endY; y++) {
            for (int x = startX; x <= endX; x++) {
                var blocks = world[y][x];
                var sourceLiquidBlocks = blocks.stream()
                        .filter(block -> block.getBlockId().equals(liquidId) && ((FlowingData) block.getData()).isSource())
                        .toList();
                if (!sourceLiquidBlocks.isEmpty()) {
                    sourceBlocks.add(new LiquidBlock(sourceLiquidBlocks.getFirst(), x, y));
                }
                world[y][x].removeAll(blocks.stream()
                        .filter(i -> i.getBlockId().equals(liquidId) && !((FlowingData) i.getData()).isSource()).toList());
            }
        }

        for (LiquidBlock sourceBlock : sourceBlocks) {
            flow(world, sourceBlock.getX(), sourceBlock.getY(), 5, new HashSet<>(), liquidId);
        }
    }

    private void flow(List<Block>[][] world, int x, int y, int strength, Set<Point> visited, String liquidId) {
        if (y + 1 < world.length && canFlowInto(world[y + 1][x])) {
            var blocks = world[y + 1][x];
            List<Block> remove = new ArrayList<>();
            for (Block block : blocks) {
                if (block.getClass().isAnnotationPresent(BreakableByWater.class)) {
                    remove.add(block);
                }
            }
            blocks.removeAll(remove);
            Block newLiquid = ItemBlockSupplier.getBlock(liquidId);
            newLiquid.setSpriteState(Water.WaterState.get(5));
            ((FlowingData) newLiquid.getData()).setSource(false);
            world[y + 1][x].add(newLiquid);
            flow(world, x, y + 1, 5, visited, liquidId);
            return;
        }

        if (strength == 1 || visited.contains(new Point(x, y))) {
            return;
        }
        visited.add(new Point(x, y));

        if (x - 1 >= 0 && canFlowInto(world[y][x - 1])) {
            try {
                transformOrFlow(world, x - 1, y, strength, visited, liquidId);
            } catch (Exception _) {}
        }

        if (x + 1 < world[y].length && canFlowInto(world[y][x + 1])) {
            try {
                transformOrFlow(world, x + 1, y, strength, visited, liquidId);
            } catch (Exception _) {}
        }
    }

    private void transformOrFlow(List<Block>[][] world, int newX, int newY, int strength, Set<Point> visited, String liquidId) {
        var blocks = world[newY][newX];
        List<Block> remove = new ArrayList<>();
        for (Block block : blocks) {
            if (block.getClass().isAnnotationPresent(BreakableByWater.class)) {
                remove.add(block);
            }
        }
        blocks.removeAll(remove);

        Block newLiquid = ItemBlockSupplier.getBlock(liquidId);
        newLiquid.setSpriteState(Water.WaterState.get(strength - 1));
        ((FlowingData) newLiquid.getData()).setSource(false);
        world[newY][newX].add(newLiquid);
        flow(world, newX, newY, strength - 1, visited, liquidId);
    }

    private boolean canFlowInto(List<Block> blocks) {
        return blocks.stream().allMatch(
                block -> block.getBlockId().equals("steve") || block.getBlockId().equals("air") || block.isMob() || block.isFlowing() || block.getClass().isAnnotationPresent(BreakableByWater.class));
    }
}
