import { Container } from 'pixi.js';
import { Shake } from './shake';
import { DEFAULT_ISO, type IsoLayout, screenToWorld, worldRectToScreen, worldToScreen } from '../math/iso';
import type { Vec2 } from '../math/Vec2';

/**
 * Камера виртуального разрешения: позиция — центр взгляда в мировых юнитах
 * (1 юнит = 1 тайл). Округляет проекцию до целого пикселя (apply — единственная
 * точка перевода мир→экран), чтобы пиксель-арт не «дрожал».
 * Границы прокрутки — мировой прямоугольник в юнитах (у карты: {0, 0, w, h});
 * клэмп выполняется по его экранному bbox (проекция).
 * Поддерживает тряску (addShake) — движок вызывает update(dt) каждый тик.
 */
export interface CameraBounds {
    x: number;
    y: number;
    width: number;
    height: number;
}

export class Camera {
    /** Центр взгляда, мировые юниты. */
    x = 0;
    y = 0;
    /** Мировой прямоугольник прокрутки, юниты. */
    bounds: CameraBounds | null = null;
    /**
     * «Мёртвая зона» (виртуальные границы экрана), экранные пиксели: пока цель
     * внутри окна, камера стоит; у края окна цель толкает камеру на величину выхода.
     * null — камера всегда центрируется на цели.
     */
    deadZonePx: { width: number; height: number } | null = null;
    private readonly shake = new Shake();

    constructor(
        private readonly viewWidth: number,
        private readonly viewHeight: number,
        private readonly iso: IsoLayout = DEFAULT_ISO
    ) {}

    follow(targetX: number, targetY: number): void {
        const dz = this.deadZonePx;
        if (dz) {
            // Окно живёт на экране: сравниваем проекции, двигаем каждую экранную
            // ось только на величину выхода цели за окно.
            const c = worldToScreen(this.x, this.y, this.iso);
            const t = worldToScreen(targetX, targetY, this.iso);
            const ex = t.x < c.x - dz.width / 2 ? t.x - (c.x - dz.width / 2)
                : t.x > c.x + dz.width / 2 ? t.x - (c.x + dz.width / 2)
                : 0;
            const ey = t.y < c.y - dz.height / 2 ? t.y - (c.y - dz.height / 2)
                : t.y > c.y + dz.height / 2 ? t.y - (c.y + dz.height / 2)
                : 0;
            if (ex !== 0 || ey !== 0) {
                const w = screenToWorld(ex, ey, this.iso);
                this.x += w.x;
                this.y += w.y;
            }
        } else {
            this.x = targetX;
            this.y = targetY;
        }
        this.clamp();
    }

    /** Мгновенно поставить камеру на точку (спавн, телепорт, смена локации). */
    snap(targetX: number, targetY: number): void {
        const dz = this.deadZonePx;
        this.deadZonePx = null;
        this.follow(targetX, targetY);
        this.deadZonePx = dz;
    }

    /** Запустить толчок: амплитуда в экранных пикселях, длительность в секундах. */
    addShake(strength: number, duration: number): void {
        this.shake.add(strength, duration);
    }

    clearShake(): void {
        this.shake.clear();
    }

    get shaking(): boolean {
        return this.shake.active;
    }

    /** Обновление тряски — движок вызывает каждый фиксированный шаг. */
    update(dt: number): void {
        this.shake.update(dt);
    }

    apply(container: Container): void {
        const o = this.shake.offset;
        const c = worldToScreen(this.x, this.y, this.iso);
        container.position.set(this.originX(c.x) + o.x, this.originY(c.y) + o.y);
    }

    /**
     * Экранная позиция мировой точки с учётом камеры и тряски — то, куда точка
     * реально спроецируется следующим кадром (без округления). Для слоёв вне
     * worldRoot (свет, эффекты), которым нужна проекция без лага на кадр.
     */
    toScreen(wx: number, wy: number): Vec2 {
        const o = this.shake.offset;
        const c = worldToScreen(this.x, this.y, this.iso);
        const p = worldToScreen(wx, wy, this.iso);
        return { x: this.originX(c.x) + o.x + p.x, y: this.originY(c.y) + o.y + p.y };
    }

    private originX(cProjX: number): number {
        return Math.floor(this.viewWidth / 2) - Math.round(cProjX);
    }

    private originY(cProjY: number): number {
        return Math.floor(this.viewHeight / 2) - Math.round(cProjY);
    }

    private clamp(): void {
        if (!this.bounds) return;
        const halfW = this.viewWidth / 2;
        const halfH = this.viewHeight / 2;
        // Мировой прямоугольник -> экранный bbox; клэмпим проекцию центра.
        const b = worldRectToScreen(
            this.bounds.x,
            this.bounds.y,
            this.bounds.width,
            this.bounds.height,
            this.iso
        );
        const c = worldToScreen(this.x, this.y, this.iso);
        const px = b.width > this.viewWidth
            ? Math.min(Math.max(c.x, b.x + halfW), b.x + b.width - halfW)
            : b.x + b.width / 2;
        const py = b.height > this.viewHeight
            ? Math.min(Math.max(c.y, b.y + halfH), b.y + b.height - halfH)
            : b.y + b.height / 2;
        const w = screenToWorld(px, py, this.iso);
        this.x = w.x;
        this.y = w.y;
    }
}