import { Container, Graphics, Texture, Sprite } from 'pixi.js';
import { Grid } from './pathfinding';
import { StaticGrid } from './collision';
import { IsoLayout, DEFAULT_ISO, isoToScreen, screenToIso } from '../math/iso';
import { hashTile } from '../math/rng';

/**
 * Высокий объект на тайле: рисуется спрайтом (якорь в центре ромба) или,
 * если текстуры нет, колонной высотой height мировых юнитов (·tileW px).
 * ground — id тайла, каким рисовать землю под объектом (по умолчанию — сам объект).
 * shadow — спрайт шириной в тайл экранно накрывает тайл «за» объектом
 * (tx-1, ty-1): он блокируется для движения (герой не заходит «внутрь» дома).
 */
export interface TallSpec {
    height: number;
    ground?: number;
    shadow?: boolean;
}

/**
 * Крупный объект на несколько тайлов (дом, большое дерево): footprint w×h
 * от северо-западного угла (x, y). Коллизия — весь footprint; земля под ним —
 * ground (если задан); спрайт якорится низом в центр footprint'а.
 * Спрайт живёт в map.propViews — сцена добавляет его в свой IsoDepthLayer
 * через addRect, чтобы герои корректно перекрывались объектом.
 */
export interface PropData {
    /** id текстуры спрайта. */
    id: number;
    /** Северо-западный тайл footprint. */
    x: number;
    y: number;
    /** Размер footprint в тайлах (по умолчанию 1×1). */
    w?: number;
    h?: number;
    /** Чем рисовать землю под footprint (по умолчанию — не трогать). */
    ground?: number;
    /** Высота в мировых юнитах — для плейсхолдера без текстуры. */
    height?: number;
}

/** Проп после нормализации (w/h заполнены). */
export interface PropPlaced extends PropData {
    w: number;
    h: number;
}

/** Спрайт пропа + его footprint — сцена вставляет view в свой depth-слой. */
export interface PropView {
    view: Container;
    prop: PropPlaced;
}

/**
 * Мелкий плоский декор на тайле (кустик, камень, проплешина) — рисуется
 * плашмя между землёй и высокими объектами, глубина не сортируется.
 */
export interface DecorData {
    /** id текстуры декора. */
    id: number;
    /** Тайл (ромб), на котором лежит декор. */
    x: number;
    y: number;
    /** Смещение от центра ромба в px виртуального разрешения. */
    dx?: number;
    dy?: number;
}

/**
 * Изометрическая тайл-карта.
 * Данные карты — числа (id тайлов); отрисовка — по таблице id -> Texture.
 * Проходимость определяется набором блокирующих id; тайлы «высоких» объектов
 * (стены, деревья) рисуются поверх земли по строкам глубины.
 */
export interface TileMapData {
    width: number;
    height: number;
    /** Индексы тайлов, length = width * height. */
    tiles: number[];
    /** Тайлы с этими id непроходимы. */
    blocked: number[];
    /** Тайлы с этими id рисуются как вертикальные объекты. */
    tall?: Record<number, number | TallSpec>;
    /** Крупные объекты с footprint'ом в несколько тайлов. */
    props?: PropData[];
    /**
     * Варианты текстур: id -> список id-шников вариантов (включая сам id).
     * Клетка рисуется одним из вариантов — детерминированно по (x, y, id),
     * поэтому вид карты стабилен между запусками и при setTile.
     */
    variants?: Record<number, number[]>;
    /** Мелкий плоский декор (микро-кусты, камни) — движок только рисует список. */
    decor?: DecorData[];
}

/** Спрайты одной ячейки — для перерисовки тайла без перестроения карты. */
interface CellSprites {
    ground?: Sprite;
    object?: Sprite;
    /** Плейсхолдеры без текстуры (чистятся вместе с ячейкой при перерисовке). */
    groundPlaceholder?: Container;
    objectPlaceholder?: Container;
    /** id тайла, по которому создан спрайт (для регистрации в анимациях). */
    groundId?: number;
    objectId?: number;
}

/** Анимация тайлов одного id: кадры, fps и общий таймлайн на все клетки. */
interface TileAnim {
    frames: Texture[];
    fps: number;
    t: number;
    sprites: Set<Sprite>;
}

export class IsometricTileMap implements Grid {
    readonly data: TileMapData;
    readonly iso: IsoLayout;
    readonly view: Container;

    private textures: Map<number, Texture>;
    /** Проходимость (blocked/пропы/тени) — единая с collision.gridOf логика. */
    private grid: StaticGrid;
    private tallSpecs: Map<number, TallSpec>;
    /** Пропы после нормализации; их footprint блокирует клетки карты. */
    readonly props: PropPlaced[];
    /** Спрайты пропов: сцена вставляет их в свой IsoDepthLayer (addRect). */
    readonly propViews: PropView[] = [];
    private groundLayer: Container;
    private decorLayer: Container;
    private objectsLayer: Container;
    /** Декор по тайлам (key = ty*width+tx) — один спрайт на тайл. */
    private decorSprites = new Map<number, Sprite>();
    private cells = new Map<number, CellSprites>();
    private anims = new Map<number, TileAnim>();
    private spriteAnims = new Map<Sprite, number>();
    /** Варианты текстур по id (из данных или setTileVariants). */
    private variants = new Map<number, number[]>();
    /** Окно куллинга (тайлы); null — без куллинга (всё видно). */
    private cullRange: { x0: number; y0: number; x1: number; y1: number } | null = null;

    get width(): number {
        return this.data.width;
    }

    get height(): number {
        return this.data.height;
    }

    constructor(data: TileMapData, textures: Map<number, Texture>, iso: IsoLayout = DEFAULT_ISO) {
        this.data = data;
        this.iso = iso;
        this.textures = textures;
        this.grid = new StaticGrid(data);
        for (const [id, ids] of Object.entries(data.variants ?? {})) {
            this.variants.set(Number(id), ids);
        }
        this.tallSpecs = new Map(
            Object.entries(data.tall ?? {}).map(([id, spec]) => [
                Number(id),
                typeof spec === 'number' ? { height: spec } : spec
            ])
        );
        this.view = new Container();

        const ground = new Container();
        const decor = new Container();
        const objects = new Container();
        this.groundLayer = ground;
        this.decorLayer = decor;
        this.objectsLayer = objects;
        // Порядок детей = порядок отрисовки: земля -> декор (плашмя) -> объекты.
        this.view.addChild(ground, decor, objects);

        // Сортировка глубины по диагонали (tx + ty) — классика изометрии.
        const cells: { tx: number; ty: number }[] = [];
        for (let ty = 0; ty < data.height; ty++) {
            for (let tx = 0; tx < data.width; tx++) {
                cells.push({ tx, ty });
            }
        }
        cells.sort((a, b) => a.tx + a.ty - (b.tx + b.ty));

        for (const cell of cells) {
            this.drawCell(cell.tx, cell.ty);
        }

        // Пропы: footprint блокирует клетки (в grid), под ним рисуем ground,
        // спрайт — в propViews (сцена вставит его в свой depth-слой с addRect).
        this.props = (data.props ?? []).map((p) => ({ ...p, w: p.w ?? 1, h: p.h ?? 1 }));
        for (const prop of this.props) {
            this.drawPropGround(prop);
            this.propViews.push({ view: this.makePropView(prop), prop });
        }

        // Декор из данных: движок только рисует список (генерация — на стороне игры).
        for (const d of data.decor ?? []) {
            const tex = this.textures.get(d.id);
            if (tex) this.addDecor(d.x, d.y, tex, { dx: d.dx, dy: d.dy });
        }
    }

    /**
     * Положить декор на тайл (микро-куст, камень): плашмя, якорь в центре ромба,
     * опциональное смещение dx/dy для живости. Один спрайт на тайл — повторный
     * addDecor заменяет предыдущий. setTile декор не трогает.
     * Возвращает спрайт (игра может его анимировать); тайл вне карты — undefined.
     */
    addDecor(tx: number, ty: number, tex: Texture, opts: { dx?: number; dy?: number } = {}): Sprite | undefined {
        if (tx < 0 || ty < 0 || tx >= this.data.width || ty >= this.data.height) return undefined;
        this.clearDecor(tx, ty);
        const p = isoToScreen(tx, ty, this.iso);
        const s = new Sprite(tex);
        s.anchor.set(0.5, 0.5);
        s.position.set(p.x + (opts.dx ?? 0), p.y + this.iso.tileH / 2 + (opts.dy ?? 0));
        this.decorLayer.addChild(s);
        this.decorSprites.set(ty * this.data.width + tx, s);
        return s;
    }

    /** Убрать декор с тайла (no-op, если его нет). */
    clearDecor(tx: number, ty: number): void {
        const s = this.decorSprites.get(ty * this.data.width + tx);
        if (!s) return;
        s.destroy();
        this.decorSprites.delete(ty * this.data.width + tx);
    }

    /** Земля под footprint'ом пропа (если задан ground и текстура есть). */
    private drawPropGround(prop: PropPlaced): void {
        const groundTex = prop.ground !== undefined ? this.textures.get(prop.ground) : undefined;
        if (!groundTex) return;
        for (let ty = prop.y; ty < prop.y + prop.h; ty++) {
            for (let tx = prop.x; tx < prop.x + prop.w; tx++) {
                const p = isoToScreen(tx, ty, this.iso);
                const s = new Sprite(groundTex);
                s.anchor.set(0.5, 0);
                s.position.set(p.x, p.y);
                this.groundLayer.addChild(s);
            }
        }
    }

    /** Спрайт пропа (якорь низ-центр footprint'а) или колонна-плейсхолдер. */
    private makePropView(prop: PropPlaced): Container {
        const c = isoToScreen(prop.x + prop.w / 2, prop.y + prop.h / 2, this.iso);
        const tex = this.textures.get(prop.id);
        if (tex) {
            const s = new Sprite(tex);
            s.anchor.set(0.5, 1);
            s.position.set(c.x, c.y);
            return s;
        }
        return this.drawPlaceholder(c.x, c.y, prop.w, prop.h, prop.height ?? 2);
    }

    /**
     * Колонна-плейсхолдер: основание — параллелограмм footprint (w×h юнитов),
     * выдавленный вверх на height юнитов. Три видимые грани: верх, лево, право.
     */
    private drawPlaceholder(cx: number, cy: number, w: number, h: number, height: number): Container {
        const hw = this.iso.tileW / 2;
        const hh = this.iso.tileH / 2;
        const top = height * this.iso.tileW;
        // Углы основания относительно центра (проекция мирового смещения).
        const nw = { x: ((h - w) / 2) * hw, y: -((w + h) / 2) * hh };
        const ne = { x: ((w + h) / 2) * hw, y: ((h - w) / 2) * hh };
        const se = { x: ((w - h) / 2) * hw, y: ((w + h) / 2) * hh };
        const sw = { x: -((w + h) / 2) * hw, y: ((w - h) / 2) * hh };
        const g = new Graphics();
        const face = (a: typeof nw, b: typeof nw, fill: number) =>
            g.poly([cx + a.x, cy + a.y - top, cx + b.x, cy + b.y - top, cx + b.x, cy + b.y, cx + a.x, cy + a.y]).fill(fill);
        // верхняя грань
        g.poly([cx + nw.x, cy + nw.y - top, cx + ne.x, cy + ne.y - top, cx + se.x, cy + se.y - top, cx + sw.x, cy + sw.y - top]).fill(0x6b5a44);
        // левые и правые грани (к юго-западному и юго-восточному ребру)
        face(sw, se, 0x4a3b2a);
        face(se, ne, 0x352a1e);
        return g;
    }

    /**
     * Изменить тайл: обновляет данные и перерисовывает одну ячейку
     * (сбор предметов, посадка цветов, разрушаемые стены и т.п.).
     */
    setTile(x: number, y: number, id: number): void {
        if (x < 0 || y < 0 || x >= this.data.width || y >= this.data.height) return;
        this.data.tiles[y * this.data.width + x] = id;
        this.drawCell(x, y);
    }

    /**
     * Анимированный тайл: клетки с этим id переключают кадры по одному общему
     * таймлайну (дёшево при сотнях клеток воды). Регистрирует и уже
     * нарисованные клетки. frames — текстуры кадров (вода_1, вода_2, ...).
     * Анимация сильнее вариантов: варианты этого id снимаются.
     */
    setTileAnimation(id: number, frames: Texture[], fps: number): void {
        if (frames.length === 0 || fps <= 0) return;
        this.variants.delete(id);
        this.anims.set(id, { frames, fps, t: 0, sprites: new Set() });
        for (const cell of this.cells.values()) {
            if (cell.ground && cell.groundId === id) this.registerAnim(cell.ground, id);
            if (cell.object && cell.objectId === id) this.registerAnim(cell.object, id);
        }
    }

    /** Тик анимаций тайлов; из update сцены (no-op, если анимаций нет). */
    update(dt: number): void {
        for (const anim of this.anims.values()) {
            anim.t += dt;
            const tex = anim.frames[Math.floor(anim.t * anim.fps) % anim.frames.length];
            for (const s of anim.sprites) s.texture = tex;
        }
    }

    private registerAnim(sprite: Sprite, id: number): void {
        this.anims.get(id)?.sprites.add(sprite);
        this.spriteAnims.set(sprite, id);
    }

    /**
     * Рантайм-варианты текстур (движок не знает, что это — трава или пепел):
     * клетки id рисуются детерминированным выбором из ids (включая сам id).
     * Анимированный id вариантов не получает — анимация сильнее.
     */
    setTileVariants(id: number, ids: number[]): void {
        if (ids.length === 0 || this.anims.has(id)) return;
        this.variants.set(id, ids);
        for (const [key, cell] of this.cells) {
            if (cell.groundId === id || cell.objectId === id) {
                this.drawCell(key % this.data.width, Math.floor(key / this.data.width));
            }
        }
    }

    private unregisterAnim(sprite: Sprite): void {
        const id = this.spriteAnims.get(sprite);
        if (id === undefined) return;
        this.anims.get(id)?.sprites.delete(sprite);
        this.spriteAnims.delete(sprite);
    }

    /** Нарисовать (или перерисовать после setTile) одну ячейку карты. */
    private drawCell(tx: number, ty: number): void {
        const iso = this.iso;
        const id = this.data.tiles[ty * this.data.width + tx];
        const key = ty * this.data.width + tx;
        const old = this.cells.get(key);
        if (old?.ground) { this.unregisterAnim(old.ground); old.ground.destroy(); }
        if (old?.object) { this.unregisterAnim(old.object); old.object.destroy(); }
        old?.groundPlaceholder?.destroy();
        old?.objectPlaceholder?.destroy();
        const fresh: CellSprites = {};
        this.cells.set(key, fresh);

        const tall = this.tallSpecs.get(id);
        const p = isoToScreen(tx, ty, iso);

        // Земля: под высоким объектом рисуем его ground-тайл (или плейсхолдер).
        const groundBase = tall?.ground ?? id;
        const groundId = this.variantOf(groundBase, tx, ty);
        fresh.groundId = groundBase; // для матчинга анимаций — базовый id
        const groundTex = this.textures.get(groundId);
        if (groundTex) {
            const s = new Sprite(groundTex);
            s.anchor.set(0.5, 0);
            s.position.set(p.x, p.y);
            this.groundLayer.addChild(s);
            fresh.ground = s;
            this.registerAnim(s, groundBase);
        } else {
            // Нет текстуры — плейсхолдер-ромб: зелёный (проходимо) или коричневый (блок).
            const g = new Graphics();
            const hw = iso.tileW / 2;
            const hh = iso.tileH / 2;
            g.poly([p.x, p.y, p.x + hw, p.y + hh, p.x, p.y + iso.tileH, p.x - hw, p.y + hh]);
            g.fill(this.grid.isBlockedId(id) ? 0x4a3b2a : 0x2d5a27);
            this.groundLayer.addChild(g);
            fresh.groundPlaceholder = g;
        }

        // Высокий объект: спрайт с якорем в центре ромба, иначе колонна.
        if (tall) {
            fresh.objectId = id;
            const objTex = this.textures.get(this.variantOf(id, tx, ty));
            if (objTex) {
                const s = new Sprite(objTex);
                s.anchor.set(0.5, 1);
                s.position.set(p.x, p.y + iso.tileH / 2);
                this.objectsLayer.addChild(s);
                fresh.object = s;
                this.registerAnim(s, id);
            } else {
                const ph = this.drawPlaceholder(p.x, p.y + iso.tileH / 2, 1, 1, tall.height);
                this.objectsLayer.addChild(ph);
                fresh.objectPlaceholder = ph;
            }
        }
    }

    /**
     * Вариант id для клетки: детерминированный выбор по хэшу (без анимации).
     * Публичный — игре полезно знать, какой id фактически нарисован.
     */
    variantOf(id: number, tx: number, ty: number): number {
        const ids = this.variants.get(id);
        return ids ? ids[hashTile(tx, ty, id) % ids.length]! : id;
    }

    /**
     * Показать только тайлы, чьи спрайты пересекают прямоугольник в px слоя
     * view (мировые px: экранный канвас минус position worldRoot); null —
     * вернуть всё видимым. Спрайты переключаются только при смене тайлового
     * окна; запас по краям — на tall-объекты (растут вверх от якоря).
     */
    cullToRect(rect: { x: number; y: number; width: number; height: number } | null): void {
        if (!rect) {
            if (this.cullRange !== null) this.applyCull(null);
            return;
        }
        const { x, y } = screenToIso(rect.x, rect.y, this.iso);
        const { x: x1, y: y1 } = screenToIso(rect.x + rect.width, rect.y, this.iso);
        const { x: x2, y: y2 } = screenToIso(rect.x, rect.y + rect.height, this.iso);
        const { x: x3, y: y3 } = screenToIso(rect.x + rect.width, rect.y + rect.height, this.iso);
        const margin = this.tallMarginRows();
        const range = {
            x0: Math.min(x, x1, x2, x3) - 1, // спрайты шире точки якоря
            x1: Math.max(x, x1, x2, x3) + 1,
            y0: Math.min(y, y1, y2, y3) - margin, // tall растёт вверх по строкам
            y1: Math.max(y, y1, y2, y3) + 1
        };
        const last = this.cullRange;
        if (last && last.x0 === range.x0 && last.x1 === range.x1 && last.y0 === range.y0 && last.y1 === range.y1) {
            return; // окно в тайлах не сдвинулось — спрайты не трогаем
        }
        this.applyCull(range);
    }

    /** Видим ли тайл при текущем окне куллинга (для тестов и отладки). */
    isTileVisible(tx: number, ty: number): boolean {
        const r = this.cullRange;
        if (!r) return true;
        return tx >= r.x0 && tx <= r.x1 && ty >= r.y0 && ty <= r.y1;
    }

    /** Запас строк сверху: высота самого высокого tall-спрайта, переведённая в тайлы. */
    private tallMarginRows(): number {
        let maxPx = 0;
        for (const spec of this.tallSpecs.values()) {
            maxPx = Math.max(maxPx, spec.height * this.iso.tileW);
        }
        return Math.ceil(maxPx / this.iso.tileH) + 1;
    }

    /** Переключить видимость всех спрайтов ячеек/декора под окном. */
    private applyCull(range: { x0: number; y0: number; x1: number; y1: number } | null): void {
        this.cullRange = range;
        const w = this.data.width;
        for (const [key, cell] of this.cells) {
            const visible = range === null || this.tileInRange(key % w, Math.floor(key / w), range);
            if (cell.ground) cell.ground.visible = visible;
            if (cell.object) cell.object.visible = visible;
            if (cell.groundPlaceholder) cell.groundPlaceholder.visible = visible;
            if (cell.objectPlaceholder) cell.objectPlaceholder.visible = visible;
        }
        for (const [key, s] of this.decorSprites) {
            s.visible = range === null || this.tileInRange(key % w, Math.floor(key / w), range);
        }
    }

    private tileInRange(tx: number, ty: number, r: { x0: number; y0: number; x1: number; y1: number }): boolean {
        return tx >= r.x0 && tx <= r.x1 && ty >= r.y0 && ty <= r.y1;
    }

    isWalkable(x: number, y: number): boolean {
        return this.grid.isWalkable(x, y);
    }

    /** Тайл накрыт footprint'ом пропа (для LOS/снарядов: дом — препятствие). */
    isPropBlocked(x: number, y: number): boolean {
        return this.grid.isPropBlocked(x, y);
    }

    /** Границы карты в мировых юнитах (для ограничения камеры). */
    get worldBounds(): { x: number; y: number; width: number; height: number } {
        return { x: 0, y: 0, width: this.data.width, height: this.data.height };
    }
}