import {
    _decorator, Asset, Component, ImageAsset, Node, resources, Sprite, SpriteFrame, Texture2D, UITransform,
} from 'cc';
import { Direction } from '../core/Define';
import { EntityVisualOptions, entityFlipX } from './EntityTextureResolver';
import {
    computeEntityUniformScale, ensureSpriteFrameSize, playerSeqFrameAnchorY,
    sizeFromUniformScale, spriteOriginalSize,
} from './EntityDisplayRefs';
import { VisualAssets } from './VisualAssets';
import { GameManager } from '../manager/GameManager';
import { Movement } from '../gameplay/Movement';
import { resolveLevelDrawRoot, sortIsoTiles } from '../level/TileLayout';
import { resolveThemeId } from '../theme/ThemeDatabase';
import {
    PlayerAction, PlayerAnimPaths, actionFolder, resolvePlayerAnimPaths,
} from './PlayerAnimPaths';

const { ccclass } = _decorator;

const FRAME_INTERVAL: Record<PlayerAction, number> = {
    [PlayerAction.Idle]: 0.22,
    [PlayerAction.Move]: 0.12,
    [PlayerAction.Jump]: 0.1,
    [PlayerAction.Win]: 0.14,
    [PlayerAction.Fail]: 0,
};

const seqCache = new Map<string, SpriteFrame[]>();

async function loadSpriteFrame(path: string): Promise<SpriteFrame | null> {
    const loadSf = (p: string) => new Promise<SpriteFrame | null>((resolve) => {
        resources.load(p, SpriteFrame, (err, sf) => resolve(!err && sf ? sf : null));
    });
    const loadImg = (p: string) => new Promise<SpriteFrame | null>((resolve) => {
        resources.load(p, ImageAsset, (err, img) => {
            if (err || !img) {
                resolve(null);
                return;
            }
            const tex = new Texture2D();
            tex.image = img;
            const frame = new SpriteFrame();
            frame.texture = tex;
            ensureSpriteFrameSize(frame, img.width, img.height);
            resolve(frame);
        });
    });
    return (await loadSf(`${path}/spriteFrame`))
        ?? (await loadSf(path))
        ?? (await loadImg(path));
}

/** 从文件名推断序列帧顺序（兼容 1.png / 走1.png / 小红军走1.png / 机器人-1.png） */
function frameOrderFromName(assetPath: string): number {
    const stem = assetPath.replace(/\\/g, '/').split('/').pop()?.replace(/\.[^.]+$/, '') ?? assetPath;
    if (stem === '机器人') return 0;
    const dash = stem.match(/-(\d+)$/);
    if (dash) return parseInt(dash[1], 10);
    const trail = stem.match(/(\d+)$/);
    if (trail) return parseInt(trail[1], 10) - 1;
    return 0;
}

function imageToSpriteFrame(img: ImageAsset): SpriteFrame {
    const tex = new Texture2D();
    tex.image = img;
    const frame = new SpriteFrame();
    frame.texture = tex;
    ensureSpriteFrameSize(frame, img.width, img.height);
    return frame;
}

function loadDirAssets<T extends Asset>(
    folder: string,
    type: new (...args: never[]) => T,
): Promise<T[]> {
    return new Promise((resolve) => {
        resources.loadDir(folder, type, (err, assets) => {
            if (err || !assets?.length) {
                resolve([]);
                return;
            }
            resolve(assets);
        });
    });
}

async function loadFrameSequence(folder: string): Promise<SpriteFrame[]> {
    const hit = seqCache.get(folder);
    if (hit) return hit;

    const spriteAssets = await loadDirAssets(folder, SpriteFrame);
    let entries: { order: number; frame: SpriteFrame }[] = spriteAssets.map((sf) => ({
        order: frameOrderFromName(sf.name),
        frame: sf,
    }));

    if (entries.length === 0) {
        const images = await loadDirAssets(folder, ImageAsset);
        entries = images.map((img) => ({
            order: frameOrderFromName(img.name),
            frame: imageToSpriteFrame(img),
        }));
    }

    if (entries.length === 0) {
        const frames: SpriteFrame[] = [];
        for (let i = 1; i <= 16; i++) {
            const leaf = folder.split('/').pop() ?? '';
            const candidates = [
                `${folder}/${i}`,
                leaf ? `${folder}/${leaf}${i}` : '',
                i === 1 ? `${folder}/机器人` : `${folder}/机器人-${i - 1}`,
                `${folder}/小红军${leaf}${i}`,
            ].filter(Boolean);
            let sf: SpriteFrame | null = null;
            for (const path of candidates) {
                sf = await loadSpriteFrame(path);
                if (sf) break;
            }
            if (!sf) break;
            frames.push(sf);
        }
        if (frames.length > 0) seqCache.set(folder, frames);
        return frames;
    }

    entries.sort((a, b) => a.order - b.order || a.frame.name.localeCompare(b.frame.name));
    const frames = entries.map((e) => e.frame);
    seqCache.set(folder, frames);
    return frames;
}

/**
 * 序列帧角色动画（对齐 Unity Animator Action 0–4）。
 * 无 skin 动画目录时回退 VisualAssets 静态贴图。
 */
@ccclass('PlayerActionAnimator')
export class PlayerActionAnimator extends Component {
    private action = PlayerAction.Idle;
    private direction = Direction.South;
    private theme = 'silu';
    private scaleMul = 1;
    private animPaths: PlayerAnimPaths | null = null;
    private frames: SpriteFrame[] = [];
    private frameIdx = 0;
    private frameTimer = 0;
    private loadingGen = 0;
    private useSequence = false;
    /** 主题级统一缩放（按最高序列帧锁定，避免待机/走路切换时重新算 scale） */
    private lockedUniformScale: number | null = null;
    /** 脚点对齐参考帧（与 lockedUniformScale 同源，取最高帧） */
    private refIdleFrame: SpriteFrame | null = null;
    private displayLockPromise: Promise<void> | null = null;

    private get spriteNode(): Node {
        return this.node;
    }

    configure(theme: string | undefined, direction: Direction, scaleMul = 1) {
        const nextTheme = theme ?? 'silu';
        const themeChanged = resolveThemeId(this.theme) !== resolveThemeId(nextTheme)
            || this.scaleMul !== scaleMul;
        this.theme = nextTheme;
        this.direction = direction;
        this.scaleMul = scaleMul;
        if (themeChanged) {
            this.lockedUniformScale = null;
            this.refIdleFrame = null;
            this.displayLockPromise = null;
        }
        this.animPaths = resolvePlayerAnimPaths(this.theme);
        this.useSequence = this.animPaths !== null;
        if (!this.useSequence) {
            VisualAssets.applyPlayerSprite(this.spriteNode, direction, { theme: this.theme }, scaleMul);
            return;
        }
        void this.applyActionFrames(this.action);
    }

    setDirection(direction: Direction, options?: EntityVisualOptions, scaleMul = 1) {
        const nextTheme = options?.theme ?? this.theme;
        if (
            this.direction === direction
            && this.theme === nextTheme
            && this.scaleMul === scaleMul
            && this.useSequence
            && this.frames.length > 0
        ) {
            const flipX = entityFlipX(direction);
            this.spriteNode.setScale(flipX ? -1 : 1, 1, 1);
            return;
        }
        this.direction = direction;
        if (options?.theme) this.theme = options.theme;
        this.scaleMul = scaleMul;
        this.animPaths = resolvePlayerAnimPaths(this.theme);
        this.useSequence = this.animPaths !== null;
        if (!this.useSequence) {
            VisualAssets.applyPlayerSprite(this.spriteNode, direction, options ?? { theme: this.theme }, scaleMul);
            return;
        }
        void this.applyActionFrames(this.action);
    }

    setAction(action: PlayerAction, force = false) {
        if (!force && this.action === action && this.useSequence && this.frames.length > 0) return;
        const actionChanged = this.action !== action;
        this.action = action;
        this.frameIdx = 0;
        this.frameTimer = 0;
        if (!this.useSequence) {
            VisualAssets.applyPlayerSprite(
                this.spriteNode, this.direction, { theme: this.theme }, this.scaleMul,
            );
            if (actionChanged && (action === PlayerAction.Win || action === PlayerAction.Fail)) {
                this.refreshResultDrawOrder();
            }
            return;
        }
        void this.applyActionFrames(action, actionChanged);
    }

    getAction(): PlayerAction {
        return this.action;
    }

    update(dt: number) {
        if (!this.useSequence || this.frames.length <= 1) return;
        const interval = FRAME_INTERVAL[this.action];
        if (interval <= 0) return;

        const speedMul = GameManager.instance?.getGameSpeed() ?? Movement.getSpeedMultiplier();
        this.frameTimer += dt * speedMul;
        if (this.frameTimer < interval) return;
        this.frameTimer = 0;

        const loop = this.action === PlayerAction.Move
            || this.action === PlayerAction.Jump
            || this.action === PlayerAction.Win
            || this.action === PlayerAction.Idle;
        if (loop) {
            this.frameIdx = (this.frameIdx + 1) % this.frames.length;
        } else if (this.frameIdx < this.frames.length - 1) {
            this.frameIdx++;
        }
        this.showFrame(this.frames[this.frameIdx]!);
    }

    private lockDisplayFromFrame(sf: SpriteFrame) {
        this.lockedUniformScale = computeEntityUniformScale(
            'player', sf, this.scaleMul, this.theme,
        );
        this.refIdleFrame = sf;
    }

    /** 用正/背面全部动作序列里的最高帧锁定显示盒，避免走路帧更高导致缩放闪动 */
    private ensureThemeDisplayLock(): Promise<void> {
        if (this.lockedUniformScale != null) return Promise.resolve();
        if (this.displayLockPromise) return this.displayLockPromise;
        if (!this.animPaths) return Promise.resolve();

        this.displayLockPromise = (async () => {
            let tallest: SpriteFrame | null = null;
            let maxH = 0;
            const sides = [this.animPaths!.front, this.animPaths!.back];
            for (const side of sides) {
                for (const folder of [side.idle, side.move, side.jump]) {
                    const frames = await loadFrameSequence(folder);
                    for (const sf of frames) {
                        const h = spriteOriginalSize(sf).height;
                        if (h > maxH) {
                            maxH = h;
                            tallest = sf;
                        }
                    }
                }
            }
            if (tallest && this.lockedUniformScale == null) {
                this.lockDisplayFromFrame(tallest);
            }
        })();
        return this.displayLockPromise;
    }

    private refreshResultDrawOrder() {
        const root = resolveLevelDrawRoot(this.node);
        if (root) sortIsoTiles(root);
    }

    private async applyActionFrames(action: PlayerAction, sortOnLoad = false) {
        if (!this.animPaths) return;
        const gen = ++this.loadingGen;
        const isFront = this.direction === Direction.South || this.direction === Direction.East;
        const side = isFront ? this.animPaths.front : this.animPaths.back;
        await this.ensureThemeDisplayLock();
        let folder = actionFolder(side, action);

        let frames = await loadFrameSequence(folder);
        if (frames.length === 0 && action === PlayerAction.Win) {
            folder = side.idle;
            frames = await loadFrameSequence(folder);
            if (frames.length > 1) frames = frames.slice(0, 2);
        }
        if (frames.length === 0) {
            VisualAssets.applyPlayerSprite(
                this.spriteNode, this.direction, { theme: this.theme }, this.scaleMul,
            );
            return;
        }
        if (gen !== this.loadingGen || !this.node.isValid) return;

        this.frames = frames;
        this.frameIdx = 0;
        this.frameTimer = 0;
        this.showFrame(frames[0]!);
        if (sortOnLoad && (action === PlayerAction.Win || action === PlayerAction.Fail)) {
            this.refreshResultDrawOrder();
        }
    }

    private showFrame(sf: SpriteFrame) {
        const flipX = entityFlipX(this.direction);
        let ui = this.spriteNode.getComponent(UITransform);
        if (!ui) {
            ui = this.spriteNode.addComponent(UITransform);
            ui.setContentSize(48, 48);
        }
        const sprite = this.spriteNode.getComponent(Sprite) ?? this.spriteNode.addComponent(Sprite);
        sprite.spriteFrame = sf;
        sprite.sizeMode = Sprite.SizeMode.CUSTOM;
        if (this.lockedUniformScale == null) {
            this.lockDisplayFromFrame(sf);
        }
        const uniform = this.lockedUniformScale ?? computeEntityUniformScale(
            'player', sf, this.scaleMul, this.theme,
        );
        const fit = sizeFromUniformScale(sf, uniform, 'player', this.theme);
        const ref = this.refIdleFrame ?? sf;
        const anchorY = ref === sf
            ? fit.anchorY
            : playerSeqFrameAnchorY(sf, ref, fit.anchorY);
        ui.setAnchorPoint(0.5, anchorY);
        ui.setContentSize(fit.width, fit.height);
        this.spriteNode.setScale(flipX ? -1 : 1, 1, 1);
    }
}
