import {
    IsometricTileMap,
    worldToScreen,
    screenToWorld,
    worldNorm,
    worldToTile,
    tileToWorld,
    moveTowardsW,
    moveCircle,
    circleFits,
    separateCircles,
    SpriteAnimator,
    Container,
    Sprite,
    Texture,
    type SceneRegistry,
    type Vec2
} from '@rpg/engine';
import { PLAYER_COMBAT } from './combat/stats';

/** Максимальный радиус тел, из которых выталкивается герой (NPC — 0.35, запас). */
const BODY_PROBE = 0.5;

/** Порог прибытия в waypoint (юниты): меньше шага любого кадра — на глаз незаметно. */
const ARRIVE_EPS = 1e-4;

/** Кадры героя по направлениям. side смотрит вправо; влево — флип. */
export interface HeroTextures {
    down: [Texture, Texture];
    up: [Texture, Texture];
    side: [Texture, Texture];
}

type Facing = 'down' | 'up' | 'left' | 'right';

/**
 * Управление героем: путь по клику (A*), плавное движение по маршруту,
 * покадровая анимация ходьбы по направлению движения.
 * Позиция — в мировых юнитах (1 юнит = 1 тайл, ноги героя).
 */
export class PlayerController {
    readonly view: Container;
    /** Именованные клипы: walk_* на ходу, idle_* — стоп-кадр направления. */
    readonly animator: SpriteAnimator;

    /** Спрайт тела (публичный: хит-флэши урона через FxLayer.flash). */
    readonly sprite: Sprite;
    private facing: Facing = 'down';
    /** Кэш имени клипа ходьбы (меняется только с facing — setFacing). */
    private facingClip = 'walk_down';

    /** Позиция в мировых юнитах (источник истины, ноги героя). */
    private pos: Vec2;
    private path: { x: number; y: number }[] = [];
    private waypoint: Vec2 | null = null;
    /** Скорость в мировых юнитах/сек. */
    private speed = 1.75;
    /** Множитель скорости (зоны наката без маски — 0.5). Меняется сценой каждый кадр. */
    speedMul = 1;
    /** Реестр объектов сцены (тела NPC) — задаётся сценой после создания. */
    private bodies: SceneRegistry | null = null;

    constructor(
        private map: IsometricTileMap,
        textures: HeroTextures,
        startTile: { x: number; y: number },
        private onStep?: () => void
    ) {
        this.view = new Container();
        this.sprite = new Sprite(textures.down[0]); // публичный: флэши урона (FxLayer.flash)
        this.sprite.anchor.set(0.5, 1); // ноги в точке позиции
        this.view.addChild(this.sprite);
        this.animator = new SpriteAnimator(this.sprite, {
            walk_down: { frames: textures.down, fps: 6 },
            walk_up: { frames: textures.up, fps: 6 },
            walk_side: { frames: textures.side, fps: 6 },
            idle_down: { frames: [textures.down[0]], loop: 'once' },
            idle_up: { frames: [textures.up[0]], loop: 'once' },
            idle_side: { frames: [textures.side[0]], loop: 'once' }
        }, 'idle_down');

        this.pos = tileToWorld(startTile.x, startTile.y);
        this.syncView();
    }

    /** Текущее направление взгляда (для боя: куда бьёт конус). */
    get dir(): 'down' | 'up' | 'left' | 'right' {
        return this.facing;
    }

    /** Подключить реестр тел — герой не проходит сквозь NPC. */
    setBodies(registry: SceneRegistry | null): void {
        this.bodies = registry;
    }

    /** Вытолкнуть себя из тел (каждый актор двигает только себя). */
    private separateBodies(): void {
        if (!this.bodies) return;
        for (const hit of this.bodies.near(this.pos, PLAYER_COMBAT.radius + BODY_PROBE, 'npc')) {
            separateCircles(this.pos, PLAYER_COMBAT.radius, hit.pos, hit.radius);
        }
    }

    /** Вектор направления взгляда в мировых юнитах (экранные оси — мировые диагонали). */
    get dirVector(): Vec2 {
        switch (this.facing) {
            case 'up':
                return { x: -1, y: -1 };
            case 'left':
                return { x: -1, y: 1 };
            case 'right':
                return { x: 1, y: -1 };
            default:
                return { x: 1, y: 1 };
        }
    }

    /** Пойти по готовому пути тайлов (мост: scene:walk/approach, кат-сцены). */
    followPath(path: { x: number; y: number }[]): void {
        if (path.length === 0) return;
        this.path = path;
        this.advanceWaypoint();
    }

    update(dt: number): void {
        // Тела: даже стоячий герой выталкивается (враг мог втолкнуть его в NPC).
        this.separateBodies();
        if (!this.waypoint) {
            this.playIdle();
            return;
        }
        this.pos = moveTowardsW(this.pos, this.waypoint, this.speed * this.speedMul * dt);
        if (this.arrivedAt(this.waypoint)) {
            this.pos = { x: this.waypoint.x, y: this.waypoint.y }; // снап: вью и тайлы без дрейфа
            this.onStep?.(); // ступили на новый тайл
            this.advanceWaypoint();
            this.syncView(); // вью на точном прибытии (иначе кадр отстаёт)
            return;
        }
        this.faceMovement();
        this.animator.resume();
        this.animator.play(this.walkClip());
        this.animator.update(dt);
        this.syncView();
    }

    get moving(): boolean {
        return this.waypoint !== null;
    }

    /** Текущий тайл героя. */
    currentTile(): { x: number; y: number } {
        return (
            worldToTile(this.pos.x, this.pos.y, this.map.data.width, this.map.data.height) ?? { x: 0, y: 0 }
        );
    }

    /** Прямое движение (тач-джойстик/стик): экранный вектор -1..1, движение с проверкой стен. */
    moveFree(dir: Vec2, dt: number): void {
        const len = Math.hypot(dir.x, dir.y);
        if (len < 0.01) {
            this.stop();
            return;
        }
        // Экранная ось джойстика -> мировое направление (экранные оси — мировые диагонали).
        const w = screenToWorld(dir.x, dir.y);
        const n = worldNorm(w.x, w.y);
        const step = this.speed * this.speedMul * dt;
        // Круг тела скользит вдоль стен (оси раздельно).
        const moved = moveCircle(this.map, this.pos, { x: n.x * step, y: n.y * step }, PLAYER_COMBAT.radius);
        this.separateBodies();
        if (moved) {
            // Поворот — по экранным компонентам смещения (кадры down/up/side).
            const proj = worldToScreen(n.x, n.y);
            this.setFacing(
                Math.abs(proj.x) >= Math.abs(proj.y) ? (proj.x >= 0 ? 'right' : 'left') : proj.y >= 0 ? 'down' : 'up'
            );
            this.animator.resume();
            this.animator.play(this.walkClip());
            this.animator.update(dt);
            this.syncView();
        } else {
            this.stop();
        }
    }

    /** Позиция героя (ноги) в мировых юнитах. */
    get position(): Vec2 {
        return { x: this.pos.x, y: this.pos.y };
    }

    /** Текущий кадр спрайта (для дебага анимации). */
    get currentTexture(): Texture {
        return this.sprite.texture;
    }

    /** Прервать текущий путь (например, при уроне). */
    stop(): void {
        this.path = [];
        this.waypoint = null;
    }

    /** Имя клипа ходьбы по текущему направлению — кэш из setFacing. */
    private walkClip(): string {
        return this.facingClip;
    }

    /** Стоп-кадр текущего направления (без «застыла на произвольном кадре ходьбы»). */
    private playIdle(): void {
        this.animator.play(`idle_${this.facing === 'left' || this.facing === 'right' ? 'side' : this.facing}`);
    }

    /** Мгновенно переместить героя в тайл (респаун, переходы между локациями). */
    teleportTo(tile: { x: number; y: number }): void {
        this.stop();
        this.pos = tileToWorld(tile.x, tile.y);
        this.syncView();
    }

    /** Отброс: сдвиг позиции, только если тело помещается (для боя), юниты. */
    applyKnockback(dir: Vec2, dist: number): void {
        const nx = this.pos.x + dir.x * dist;
        const ny = this.pos.y + dir.y * dist;
        if (circleFits(this.map, { x: nx, y: ny }, PLAYER_COMBAT.radius)) {
            this.pos.x = nx;
            this.pos.y = ny;
            this.syncView();
        }
        this.stop();
    }

    /** Направление — по доминирующей экранной оси движения к следующей точке пути. */
    private faceMovement(): void {
        if (!this.waypoint) return;
        const dx = this.waypoint.x - this.pos.x;
        const dy = this.waypoint.y - this.pos.y;
        const proj = worldToScreen(dx, dy);
        this.setFacing(
            Math.abs(proj.x) >= Math.abs(proj.y) ? (proj.x >= 0 ? 'right' : 'left') : proj.y >= 0 ? 'down' : 'up'
        );
    }

    /** Прибытие с eps: moveTowardsW обещает точность, но расталкивание тел сдвигает. */
    private arrivedAt(t: Vec2): boolean {
        return Math.abs(this.pos.x - t.x) < ARRIVE_EPS && Math.abs(this.pos.y - t.y) < ARRIVE_EPS;
    }

    /** Сменить направление взгляда (кадры + флип спрайта + кэш имени клипа). */
    private setFacing(facing: Facing): void {
        if (facing === this.facing) return;
        this.facing = facing;
        this.sprite.scale.x = facing === 'left' ? -1 : 1;
        // Имя клипа ходьбы зависит только от facing: кэш вместо строки каждый тик.
        this.facingClip = `walk_${facing === 'left' || facing === 'right' ? 'side' : facing}`;
    }

    private advanceWaypoint(): void {
        const next = this.path.shift();
        this.waypoint = next ? tileToWorld(next.x, next.y) : null;
    }

    /** Вью-позиция: единственная точка перевода мир→экран (округление до px). */
    private syncView(): void {
        const s = worldToScreen(this.pos.x, this.pos.y);
        this.view.position.set(Math.round(s.x), Math.round(s.y));
    }
}

