package se.urmo.game.entities.pacman;

import lombok.Getter;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
import se.urmo.game.collision.CollisionChecker;
import se.urmo.game.entities.BaseAnimated;
import se.urmo.game.graphics.SpriteLocation;
import se.urmo.game.graphics.SpriteSheetManager;
import se.urmo.game.main.LevelManager;
import se.urmo.game.map.GameMap;
import se.urmo.game.util.Direction;
import se.urmo.game.util.LoadSave;
import se.urmo.game.util.MyPoint;

import java.awt.Graphics;
import java.awt.Image;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.image.BufferedImage;
import java.util.Arrays;
import java.util.stream.Stream;


@Slf4j
public class PacMan extends BaseAnimated {
    public static final int PACMAN_SIZE = 32;
    public static final int PACMAN_OFFSET = PACMAN_SIZE / 2;
    private static final int COLLISION_BOX_SIZE = 16;
    private static final int COLLISION_BOX_OFFSET = (PACMAN_SIZE - COLLISION_BOX_SIZE) / 2;
    private static final int ANIMATION_UPDATE_FREQUENCY = 10;
    private static final double BASE_SPEED = 0.40;
    private static final long FRAME_NS = 80_000_000L; // 80 ms
    public static final int PACMAN_SPRITE_FRAMES = 4;
    private final MyPoint startPosition;
    private final CollisionChecker collisionChecker;
    private final LevelManager levelManager;
    private final Sprites sprites;
    private boolean moving = false;
    private MyPoint position; //
    @Setter
    @Getter
    private Direction direction = Direction.NONE;
    private BufferedImage[] deathFrames; // working copy
    private long lastChangeNs;
    // animation state
    private PacmanState state = PacmanState.ALIVE;
    private int deathFrameIdx = 0;
    private double speed;

    public PacMan(CollisionChecker collisionChecker, LevelManager levelManager) {
        super(ANIMATION_UPDATE_FREQUENCY, PACMAN_SPRITE_FRAMES);
        this.collisionChecker = collisionChecker;
        this.levelManager = levelManager;
        this.position = new MyPoint(
                26 * GameMap.MAP_TILESIZE + GameMap.OFFSET_X,
                13 * GameMap.MAP_TILESIZE + GameMap.OFFSET_Y + ((double) GameMap.MAP_TILESIZE / 2));
        this.startPosition = this.position;
        this.sprites = loadAnimation();
        this.speed = BASE_SPEED * levelManager.getPacmanLevelSpeed();
    }

    private Sprites loadAnimation() {
        BufferedImage[][] spriteMap = new BufferedImage[6][PACMAN_SPRITE_FRAMES];
        BufferedImage[] deathFrames;

        BufferedImage[] animation = SpriteSheetManager.get(SpriteLocation.PACMAN).getAnimation(0);
        spriteMap[Direction.RIGHT.ordinal()] = animation;
        spriteMap[Direction.LEFT.ordinal()] = Arrays.stream(animation)
                .map(i -> LoadSave.rotate(i, Direction.LEFT.angel))
                .toArray(BufferedImage[]::new);
        spriteMap[Direction.DOWN.ordinal()] = Arrays.stream(animation)
                .map(i -> LoadSave.rotate(i, Direction.DOWN.angel))
                .toArray(BufferedImage[]::new);
        spriteMap[Direction.UP.ordinal()] = Arrays.stream(animation)
                .map(i -> LoadSave.rotate(i, Direction.UP.angel))
                .toArray(BufferedImage[]::new);
        deathFrames = Stream.concat(
                        Arrays.stream(SpriteSheetManager.get(SpriteLocation.PACMAN).getAnimation(1)),
                        Arrays.stream(SpriteSheetManager.get(SpriteLocation.PACMAN).getAnimation(2)))
                .toArray(BufferedImage[]::new);
        return new Sprites(spriteMap, deathFrames);
    }

    public void draw(Graphics g) {
        switch (state) {
            case ALIVE -> drawAlive(g);
            case DYING -> drawDead(g);
        }
    }

    private void drawAlive(Graphics g) {
        if (state != PacmanState.ALIVE) return; // ignore if not dying/dead
        g.drawImage(
                sprites.spriteSheets[direction == Direction.NONE ? 0 : direction.ordinal()][aniIndex],
                (int) position.x - PACMAN_OFFSET,
                (int) position.y - PACMAN_OFFSET,
                PACMAN_SIZE,
                PACMAN_SIZE, null);
    }

    private void drawDead(Graphics g) {
        if (state == PacmanState.ALIVE) return; // ignore if not dying/dead

        g.drawImage(
                deathFrames[deathFrameIdx],
                (int) position.x - PACMAN_OFFSET,
                (int) position.y - PACMAN_OFFSET,
                PACMAN_SIZE,
                PACMAN_SIZE,
                null
        );
    }

    public void update() {
        switch (state) {
            case ALIVE -> updateAlive();
            case DYING -> updateDead();
        }
    }

    private void updateDead() {
        if (state != PacmanState.DYING) return;

        long now = System.nanoTime();
        while (now - lastChangeNs >= FRAME_NS && deathFrameIdx < sprites.deathFrames.length - 1) { // FRAME_NS has passed and not all frames has been drawn
            deathFrameIdx++;
            lastChangeNs += FRAME_NS; // carry over exact cadence
        }
    }

    private void updateAlive() {
        if (state != PacmanState.ALIVE) return;

        if (moving) {
            MyPoint destination = collisionChecker.getValidDestination(direction, getNewPosition(), COLLISION_BOX_SIZE, COLLISION_BOX_SIZE);

            if (destination != null) {
                position = destination;
            }
        }
    }

    private MyPoint getNewPosition() {
        return new MyPoint(position.x + direction.dx * getSpeed(), position.y + direction.dy * getSpeed());
    }

    private double getSpeed() {
        return this.speed;
    }

    public double distanceTo(Point point) {
        return new Point((int) position.x, (int) position.y).distance(point);
    }

    public void setState(PacmanState state) {
        this.state = state;
        switch (state) {
            case ALIVE -> {
                position = startPosition;
                aniIndex = 0; // reset animation to start
                deathFrameIdx = 0;
                speed = BASE_SPEED * levelManager.getPacmanLevelSpeed(); // Recalculate
            }
            case DYING -> {
                deathFrameIdx = 0;
                lastChangeNs = System.nanoTime(); // reset stopwatch right now
                deathFrames = Arrays.stream(sprites.deathFrames)
                        .map(img -> LoadSave.rotate(img, direction.angel))
                        .toArray(BufferedImage[]::new);
            }
        }
    }

    public Image getLifeIcon() {
        return sprites.spriteSheets[0][1];
    }

    public Rectangle getBounds() {
        return new Rectangle(
                (int) (position.x - COLLISION_BOX_OFFSET),
                (int) (position.y - COLLISION_BOX_OFFSET),
                COLLISION_BOX_SIZE,
                COLLISION_BOX_SIZE);
    }

    public Point getPosition() {
        return new Point((int) position.x, (int) position.y);
    }

    public void setMoving(boolean b) {
        moving = b;
        paused = !b;
    }

    public enum PacmanState {
        ALIVE, DYING, DEAD
    }

    record Sprites(BufferedImage[][] spriteSheets, BufferedImage[] deathFrames) {
    }
}
