package cz.jzitnik.tui;

import cz.jzitnik.game.entities.Block;
import cz.jzitnik.game.entities.SteveData;
import cz.jzitnik.game.Game;
import cz.jzitnik.game.sprites.Air;
import cz.jzitnik.game.sprites.SimpleSprite;
import cz.jzitnik.game.blocks.Chest;
import cz.jzitnik.game.blocks.Furnace;
import cz.jzitnik.game.ui.Healthbar;
import cz.jzitnik.tui.utils.SpriteCombiner;
import lombok.Getter;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;

import org.jline.terminal.Terminal;

import java.awt.*;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;

/**
 * Responsible for rendering the current game state to the terminal.
 * Handles multiple types of windows (e.g., world, inventory, furnace) and draws block sprites accordingly.
 */
@Slf4j
public class ScreenRenderer {
    private final SpriteList spriteList;
    private final Terminal terminal;
    private boolean rendering = false;

    /**
     * Constructs a {@code ScreenRenderer} with the given sprite list and terminal.
     *
     * @param spriteList List of available sprites for rendering.
     * @param terminal   Terminal instance used for output.
     */
    public ScreenRenderer(SpriteList spriteList, Terminal terminal) {
        this.spriteList = spriteList;
        this.terminal = terminal;
    }

    /**
     * Optional coordinates of the currently selected block. Used for visual highlighting.
     */
    @Getter
    @Setter
    private Optional<Point> selectedBlock = Optional.empty();

    /**
     * Finds the coordinates of the player's lower or upper body in the world array.
     *
     * @param world The 2D world array of blocks.
     * @return Integer array of coordinates [x, y], or null if player not found.
     */
    private Point getPlayerCords(List<Block>[][] world) {
        for (int i = 0; i < world.length; i++) {
            for (int j = 0; j < world[i].length; j++) {
                var steve = world[i][j].stream().filter(x -> x.getBlockId().equals("steve")).findFirst();
                if (steve.isPresent()) {
                    var steveData = (SteveData) steve.get().getData();

                    if (steveData.isTop()) {
                        return new Point(j, i + 1);
                    } else {
                        return new Point(j, i);
                    }
                }
            }
        }
        return null;
    }

    /**
     * Resolves the visual texture for a given block based on its type and state.
     *
     * @param block      The block to render.
     * @param spriteList List of sprites.
     * @param game       Game instance (needed for state checks).
     * @return String representing the block's rendered sprite.
     */
    public String getTexture(Block block, SpriteList spriteList, Game game) {
        if (Air.class.isAssignableFrom(spriteList.getSprite(block.getSprite()).getClass())) {
            var air = (Air) spriteList.getSprite(block.getSprite());
            return air.getSprite(game.getDaytime());
        }

        if (block.getSpriteState().isEmpty()) {
            return spriteList.getSprite(block.getSprite()).getSprite();
        }

        return spriteList.getSprite(block.getSprite()).getSprite(block.getSpriteState().get());
    }

    /**
     * Renders the entire screen based on the current game window.
     * Uses ANSI escape codes to clear and redraw the terminal content.
     *
     * @param game Current game state to render.
     */
    public void render(Game game) {
        if (rendering) {
            return;
        }
        rendering = true;
        log.debug("Rendering frame");
        var world = game.getWorld();
        StringBuilder main = new StringBuilder();
        main.append("\033[H\033[2J");

        switch (game.getWindow()) {
            // Different screens: Probably will need a rewrite
            case INVENTORY -> game.getInventory().renderFull(main, terminal, spriteList, true, Optional.empty(), game);
            case CRAFTING_TABLE -> game.getGameStates().craftingTable.render(main, terminal, spriteList);
            case CHEST -> ((Chest) game.getWorld()[game.getGameStates().clickY][game.getGameStates().clickX].stream()
                    .filter(i -> i.getBlockId().equals("chest")).toList().getFirst().getData()).render(game, main,
                            terminal, spriteList);
            case FURNACE -> ((Furnace) game.getWorld()[game.getGameStates().clickY][game.getGameStates().clickX]
                    .stream().filter(i -> i.getBlockId().equals("furnace")).toList().getFirst().getData()).render(game,
                            main, terminal, spriteList);
            case ESC -> game.getGameStates().dependencies.escape.render(main, terminal);
            case OPTIONS -> game.getGameStates().dependencies.options.render(main, terminal);
            case SAVE_EXIT -> game.getGameStates().dependencies.escape.renderSave(main, terminal);
            case SAVED -> game.getGameStates().dependencies.escape.renderSaved(main, terminal);
            case DEATH_SCREEN -> game.getGameStates().dependencies.deathScreen.render(main, terminal, game);
            case WORLD -> {
                // World

                Point cords = getPlayerCords(world);
                if (cords == null)
                    return;

                int playerX = cords.x;
                int playerY = cords.y;

                int terminalWidth = terminal.getWidth();
                int terminalHeight = terminal.getHeight();

                int[] data = ScreenMovingCalculationProvider.calculate(playerX, playerY, terminalHeight, terminalWidth,
                        world[0].length, world.length);

                // Calculate visible area boundaries
                int startX = data[0];
                int endX = data[1];
                int startY = data[2];
                int endY = data[3];

                int visibleWidth = endX - startX;
                int visibleHeight = endY - startY;

                if (visibleWidth % 2 != 0) {
                    endX = Math.max(startX, endX - 1);
                }
                if (visibleHeight % 2 != 0) {
                    endY = Math.max(startY, endY - 1);
                }

                StringBuilder[] lines = new StringBuilder[(endY - startY) * 25 + 1];
                for (int i = 0; i < lines.length; i++) {
                    lines[i] = new StringBuilder();
                }

                int counter = 0;
                for (int y = startY; y < endY; y++) {
                    for (int x = startX; x < endX; x++) {
                        List<Block> blocks = world[y][x];
                        List<String> sprites = new ArrayList<>(blocks.stream()
                                .map(block -> getTexture(block, spriteList, game))
                                .toList());

                        if (selectedBlock.isPresent() && selectedBlock.get().x == x
                                && selectedBlock.get().y == y) {
                            StringBuilder stringBuilder = getStringBuilder();

                            sprites.add(stringBuilder.toString());
                        }

                        var steve = blocks.stream().filter(block -> block.getBlockId().equals("steve")).findFirst();
                        if (steve.isPresent() && game.getPlayer().isBurning()) {
                            var steveData = (SteveData) steve.get().getData();

                            if (!steveData.isTop()) {
                                SimpleSprite fire = new SimpleSprite("fire.ans");
                                sprites.add(fire.getSprite());
                            }
                        }

                        var burningBlocks = blocks.stream().filter(Block::isOnFire).toList();
                        if (!burningBlocks.isEmpty()) {
                            SimpleSprite fire = new SimpleSprite("fire.ans");
                            sprites.add(blocks.indexOf(burningBlocks.getLast()) + 1, fire.getSprite());
                        }

                        String sprite = SpriteCombiner.combineSprites(sprites.toArray(String[]::new));

                        String[] spriteLines = sprite.split("\n");

                        for (int i = 0; i < spriteLines.length; i++) {
                            lines[counter * 25 + i].append(spriteLines[i]).append("\033[0m");
                        }
                    }
                    counter++;
                }

                // World
                for (int i = 0; i < lines.length; i++) {
                    main.append(lines[i]);
                    if (i < lines.length - 1) {
                        main.append("\n");
                    }
                }

                // Empty space between world and hotbar
                main.append("\n\n\n");

                Healthbar.render(main, spriteList, terminal, game);

                game.getInventory().renderHotbar(main, spriteList, terminal, false, game);
            }
        }

        log.debug("Frame rendered");
        System.out.println(main);
        rendering = false;
    }

    /**
     * Builds a bordered visual block (used for highlighting selected blocks).
     *
     * @return A StringBuilder containing a sprite-like bordered area.
     */
    private static StringBuilder getStringBuilder() {
        StringBuilder stringBuilder = new StringBuilder();

        stringBuilder.append("\033[38;5;231;48;5;231m▓".repeat(50));
        stringBuilder.append("\n");

        for (int i = 0; i < 23; i++) {
            stringBuilder.append("\033[38;5;231;48;5;231m▓");
            stringBuilder.append("\033[38;5;231;48;5;231m▓");
            stringBuilder.append("\033[0m ".repeat(46));
            stringBuilder.append("\033[38;5;231;48;5;231m▓");
            stringBuilder.append("\033[38;5;231;48;5;231m▓");
            stringBuilder.append("\n");
        }

        stringBuilder.append("\033[38;5;231;48;5;231m▓".repeat(50));
        stringBuilder.append("\n");

        return stringBuilder;
    }
}
