package se.urmo.game.map;

import lombok.extern.slf4j.Slf4j;
import se.urmo.game.entities.BaseAnimated;
import se.urmo.game.entities.ghost.Ghost;
import se.urmo.game.entities.ghost.mode.GhostMode;
import se.urmo.game.util.Direction;

import java.awt.Graphics;
import java.awt.Point;
import java.awt.image.BufferedImage;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.Arrays;
import java.util.List;
import java.util.Objects;
import java.util.stream.Stream;

@Slf4j
public class GameMap {
    public static final int MAP_TILESIZE = 16;// 16px from left
    public static final int MAP_COL_SIZE = 28;
    public static final int MAP_ROW_SIZE = 30;
    public static final int OFFSET_Y = 7 * MAP_TILESIZE; // 160px from top
    public static final int OFFSET_X = MAP_TILESIZE; // 16px from left
    private MapTile[][] mapData;
    private final int[][] csvData;
    private final long numberOfDots;


    public GameMap(String mapFilePath) {
        this.csvData = readCSV(mapFilePath, MAP_ROW_SIZE, MAP_COL_SIZE);

        this.mapData = createMap(csvData);

        this.numberOfDots = Arrays.stream(mapData)
                .flatMap(Arrays::stream)
                .filter(tile -> tile != null &&
                        (tile.getTileType() == TileType.SMALL_PELLET ||
                                tile.getTileType() == TileType.LARGE_PELLET))
                .count();

    }

    private static MapTile[][] createMap(int[][] data2) {
        return Arrays.stream(data2)
                .map(a -> Arrays.stream(a)
                        .mapToObj(i -> MapTile.byType(TileType.fromValue(i)))
                        .toArray(MapTile[]::new))
                .toArray(MapTile[][]::new);
    }

    private int[][] readCSV(String path, int mapRowSize, int mapColSize) {
        MapTile[][] data = new MapTile[mapRowSize][mapColSize];
        int[][] data2 = new int[mapRowSize][mapColSize];

        try (InputStream is = getClass().getClassLoader().getResourceAsStream(path);
             BufferedReader br = new BufferedReader(new InputStreamReader(Objects.requireNonNull(is)))) {

            String line;
            int row = 0;
            while ((line = br.readLine()) != null && row < mapRowSize) {
                String[] tokens = line.split(",");
                if (tokens.length != mapColSize) {
                    throw new IllegalStateException(
                            "Invalid map format: row " + row + " has " + tokens.length +
                                    " columns, expected " + mapColSize
                    );
                }

                data2[row++] = Arrays.stream(tokens)
                        .map(String::trim)
                        .mapToInt(Integer::parseInt)
                        .toArray();
            }

            if (row != mapRowSize) {
                throw new IllegalStateException(
                        "Invalid map format: found " + row + " rows, expected " + mapRowSize
                );
            }

        } catch (NullPointerException e) {
            log.error("Failed to open inputstream: {}", e.getMessage(), e);
        } catch (IOException e) {
            log.error("Failed to read resource: {}", e.getMessage(), e);
        }
        return data2;
    }

    public void draw(Graphics g) {
        for (int row = 0; row < mapData.length; row++) {
            for (int col = 0; col < mapData[row].length; col++) {
                BufferedImage tileImage = mapData[row][col].getImage();

                if (tileImage != null) {
                    int x = OFFSET_X + col * MAP_TILESIZE;
                    int y = OFFSET_Y + row * MAP_TILESIZE;
                    g.drawImage(tileImage, x, y, MAP_TILESIZE, MAP_TILESIZE, null);
                }
            }
        }
    }

    public boolean isPassable(int screenX, int screenY) {
        int row = screenToRow(screenY);
        int col = screenToCol(screenX);
        int tileY = (screenY - OFFSET_Y) % MAP_TILESIZE;
        int tileX = (screenX - OFFSET_X) % MAP_TILESIZE;
        //log.trace("Point[x={},y={}] is row={}, col={} with reminder x={},y={}", screenX, screenY, row, col, tileX, tileY);

        boolean[][] mask = mapData[row][col].getCollisionMask();
        boolean b = mask == null || !mask[tileY][tileX];
        //log.trace(b ? " - passable" : " - not passable");
        return b;
    }

    public boolean isSolidXY(Ghost ghost, int screenX, int screenY) {
        return isSolid(ghost, screenToRow(screenY), screenToCol(screenX));
    }


    public boolean isSolid(BaseAnimated entity, int row, int col) {
        // Check for out of bounds
        if (col >= columns() || col < 0) return true;
        if (row >= rows() || row < 0) return true;

        // Get the tile information
        MapTile mapTile = mapData[row][col];
        boolean solid = mapTile.isSolid();

        // Special case: If the entity is a Ghost, it can pass through certain solid tiles
        // This allows ghosts to move through areas that would be solid for other entities
        if (entity instanceof Ghost) {
            // For Ghost entities, check if this is a ghost-passable tile
            // You might want to refine this logic based on specific tile types or ghost states

            // Example: Allow ghosts to pass through ghost house door IF in house mode
            if (mapTile.getTileType() == TileType.DOOR && ((Ghost) entity).currentMode() == GhostMode.HOUSE) {
                return false; // Not solid for ghosts
            }
        }

        //log.debug("[{}][{}] {}", row, col, mapTile.getTileType());
        return solid;

    }

    public boolean removeTileImage(Point screen) {
        if (screen == null || mapData == null) {
            return false;
        }

        int row = screenToRow(screen);
        int col = screenToCol(screen);

        if (row < 0 || row >= mapData.length || col < 0 || col >= mapData[0].length) {
            return false;
        }

        MapTile tile = mapData[row][col];
        TileType type = tile.getTileType();

        if (type.isRemovable() && tile.getImage() != null) {
            // log.debug("Removing tile {}", tile);
            tile.setImage(null);
            return true;
        }
        return false;

    }

    private static int screenToCol(Point point) {
        return screenToCol(point.x);
    }

    private static int screenToRow(Point point) {
        return screenToRow(point.y);
    }

    public int columns() {
        return MAP_COL_SIZE;
    }

    public int rows() {
        return MAP_ROW_SIZE;
    }

    public int colToWorldX(int col) {
        return col * GameMap.MAP_TILESIZE + GameMap.OFFSET_X;
    }

    public int rowToWorldY(int row) {
        return row * GameMap.MAP_TILESIZE + GameMap.OFFSET_Y;
    }

    public int worldXtoCol(int x) {
        return x / GameMap.MAP_TILESIZE;
    }

    public int worldYtoRow(int y) {
        return y / GameMap.MAP_TILESIZE;
    }

    public Point coordToWorld(int row, int col) {
        return new Point(colToWorldX(col), rowToWorldY(row));
    }

    public Point worldToScreen(Point world) {
        return new Point(world.x + OFFSET_X, world.y + OFFSET_Y);
    }

    public Point screenToWorld(Point screen) {
        return new Point(screen.x - OFFSET_X, screen.y - OFFSET_Y);
    }

    public static int screenToCol(int screenX) {
        return (screenX - OFFSET_X) / MAP_TILESIZE;
    }

    public static int screenToRow(int screenY) {
        return (screenY - OFFSET_Y) / MAP_TILESIZE;
    }

    public static int colToScreen(int col) {
        return col * MAP_TILESIZE + OFFSET_X;
    }

    public static int rowToScreen(int row) {
        return row * MAP_TILESIZE + OFFSET_Y;
    }

    public MapTile getTile(Point screenPos) {
        int r = screenToRow(screenPos);
        int c = screenToCol(screenPos);

        return mapData[r][c];

    }

    public List<Direction> directionAlternatives(BaseAnimated entity, int screenX, int screenY) {
        int row = (screenY - GameMap.OFFSET_Y) / GameMap.MAP_TILESIZE;
        int col = (screenX - GameMap.OFFSET_X) / GameMap.MAP_TILESIZE;

        record DirectionCheck(int rowOffset, int colOffset, Direction direction) {
        }
        log.debug("At ({},{}), [{}][{}]", screenX, screenY, row, col);
        return Stream.of(
                        new DirectionCheck(0, 1, Direction.RIGHT),
                        new DirectionCheck(0, -1, Direction.LEFT),
                        new DirectionCheck(1, 0, Direction.DOWN),
                        new DirectionCheck(-1, 0, Direction.UP)
                )
                .filter(dc -> {
                    int r = row + dc.rowOffset;
                    int c = col + dc.colOffset;
                    boolean solid = isSolid(entity, r, c);
                    log.debug("[{}][{}] {} is {}", r, c, dc.direction, solid ? "solid" : " not solid");
                    return !solid;
                })
                .map(DirectionCheck::direction)
                .toList();
    }

    public boolean isAligned(Point pos) {
        int row = pos.x % GameMap.MAP_TILESIZE;
        int col = pos.y % GameMap.MAP_TILESIZE;
        return row == GameMap.MAP_TILESIZE / 2 && col == GameMap.MAP_TILESIZE / 2;
    }

    public long numberOfDots() {
        return this.numberOfDots;
        //return 50;
    }

    public void reset() {
    mapData = createMap(csvData);
    }
}
