import {
    Node, Sprite, SpriteFrame, UITransform, resources, Color, Graphics,
    ImageAsset, Texture2D, Vec3,
} from 'cc';
import { Direction } from '../core/Define';
import { cellToWorld } from '../core/GridCoords';
import { LevelConfig, SpawnConfig, SpawnKind } from '../level/LevelTypes';
import { resolvePropPlacement } from '../level/EntitySpawnPlacement';
import { PropController } from '../controller/PropController';
import { PlayerController } from '../controller/PlayerController';
import { VehicleController } from '../controller/VehicleController';
import { PlayerActionAnimator } from './PlayerActionAnimator';
import { PlayerAction, resolvePlayerAnimPaths } from './PlayerAnimPaths';
import {
    resolveEntityScaleMul,
} from '../level/EntitySpawnDefaults';
import { getTilePivot } from './TilePivots';
import { getTileDrawSize, resolveTilePixelSize } from './TileSizes';
import { alignTileNode, findLevelChildByName, forEachLevelEntityNode } from '../level/TileLayout';
import { ensureResourcesBundle } from '../core/ResourcesBundle';
import {
    canonicalThemeKey,
    entityTextureCandidates,
    resolveThemeFolder,
} from './ThemeEntityTextures';
import { getThemeTilePath } from '../theme/ThemeRegistry';
import {
    EntityVisualOptions,
    collectLevelEntityTexturePaths,
    entityFlipX,
    normalizeTexturePath,
    resolveEntityTexturePaths,
} from './EntityTextureResolver';
import {
    EntityDisplayKind,
    ensureSpriteFrameSize,
    fitEntityDisplaySize,
} from './EntityDisplayRefs';

type SpriteKey = 'player_F' | 'player_B' | 'ship_F' | 'ship_B' | 'coin' | 'tile' | 'jump' | 'wall';

/** 与 Unity / GameController UIStyleNames 一致（仅用于 UI 主题别名） */
const UI_STYLE_TO_FOLDER: Record<string, string> = {
    default: 'silu',
    chinese: 'chinese',
    redArmy: 'redArmy',
    redarmy: 'redArmy',
    numMan: 'numMan',
    snow: 'snow',
    sanxing: 'sanxing',
    silu: 'silu',
};

export function normalizeTheme(uiStyle: string | undefined): string {
    if (!uiStyle) return 'silu';
    return UI_STYLE_TO_FOLDER[uiStyle] ?? canonicalThemeKey(uiStyle);
}

const FILE_BY_KEY: Record<SpriteKey, string> = {
    player_F: 'player_F',
    player_B: 'player_B',
    ship_F: 'ship_F',
    ship_B: 'ship_B',
    coin: 'coin',
    tile: 'Baseblock',
    jump: 'JumpBlock',
    wall: 'WallBlock',
};

export class VisualAssets {
    private static frames = new Map<string, SpriteFrame>();
    /** 载具异步预载代次，避免 spawn 与 refresh 并发时旧任务覆盖贴图 */
    private static vehicleVisualGeneration = new WeakMap<Node, number>();
    /** 按 resources 路径缓存的贴图 */
    private static pathFrames = new Map<string, SpriteFrame>();
    /** 同路径并发加载去重，避免刷新时部分贴图加载失败 */
    private static pathLoading = new Map<string, Promise<SpriteFrame | null>>();
    /** 瓦片贴图：按「主题:瓦片名」缓存，多关卡多主题并存 */
    private static namedFrames = new Map<string, SpriteFrame>();

    /** 切关 / 贴图 meta 更新后清缓存，避免仍用旧 sprite-frame 尺寸 */
    static clearNamedTileCache() {
        this.namedFrames.clear();
    }

    /** 切关时清实体贴图路径缓存，避免 ImageAsset 直载帧尺寸陈旧 */
    static clearPathFrameCache() {
        this.pathFrames.clear();
        this.pathLoading.clear();
    }
    private static loading: Promise<void> | null = null;
    /** 角色 / UI 用主题（与关卡砖块主题独立） */
    private static uiTheme = 'silu';

    /** 设置角色/UI 主题；不清空已加载的关卡砖块贴图 */
    static setTheme(uiStyle: string, force = false) {
        const next = normalizeTheme(uiStyle);
        if (!force && this.uiTheme === next) return;
        this.uiTheme = next;
        this.loading = null;
    }

    static getUiTheme(): string {
        return this.uiTheme;
    }

    private static frameKey(key: SpriteKey, theme?: string): string {
        const t = theme ? resolveThemeFolder(theme) : this.uiTheme;
        return `${t}:${key}`;
    }

    private static resourcePath(key: SpriteKey, theme?: string): string {
        const folder = resolveThemeFolder(theme ?? this.uiTheme);
        if (key === 'coin') return `textures/${folder}/Prop_kuai1`;
        const file = FILE_BY_KEY[key];
        return `textures/${folder}/${file}`;
    }

    private static pathCandidates(key: SpriteKey, theme?: string): string[] {
        const t = theme ?? this.uiTheme;
        if (key === 'player_F') return entityTextureCandidates(t, 'playerFront');
        if (key === 'player_B') return entityTextureCandidates(t, 'playerBack');
        if (key === 'ship_F') return entityTextureCandidates(t, 'shipFront');
        if (key === 'ship_B') return entityTextureCandidates(t, 'shipBack');
        const folder = resolveThemeFolder(t);
        if (key === 'coin') {
            return [
                `textures/${folder}/Prop_kuai1`,
                `textures/${folder}/Prop_kuai2`,
                `textures/${folder}/Prop_kuai`,
                `textures/${folder}/Prop`,
                `textures/${folder}/coin`,
                'textures/ui/coin',
                'textures/silu/Prop_kuai1',
            ];
        }
        const base = `textures/${folder}/${FILE_BY_KEY[key]}`;
        const paths = [base];
        if (folder !== 'silu') paths.push(`textures/silu/${FILE_BY_KEY[key]}`);
        return paths;
    }

    static async preload(uiStyle?: string): Promise<void> {
        if (uiStyle) this.setTheme(uiStyle);
        if (this.loading) return this.loading;
        this.loading = (async () => {
            const keys = Object.keys(FILE_BY_KEY) as SpriteKey[];
            const results = await Promise.all(keys.map((k) => this.loadOne(k)));
            await Promise.all([
                this.loadOne('player_F'),
                this.loadOne('player_B'),
                this.loadOne('ship_F'),
                this.loadOne('ship_B'),
            ]);
            const ok = results.filter(Boolean).length;
            console.log(`[VisualAssets] UI 主题=${this.uiTheme} 贴图 ${ok}/${keys.length}`);
            if (ok === 0) {
                console.warn('[VisualAssets] 未加载到贴图，请运行 tools/import_unity_textures.py 并刷新资源');
            }
        })().catch((e) => {
            console.error('[VisualAssets] preload failed', e);
            this.loading = null;
        });
        return this.loading;
    }

    private static loadOne(key: SpriteKey, theme?: string): Promise<boolean> {
        const fk = this.frameKey(key, theme);
        if (this.frames.has(fk)) return Promise.resolve(true);
        const tryPaths = this.pathCandidates(key, theme);

        const attempt = (path: string): Promise<SpriteFrame | null> => new Promise((resolve) => {
            resources.load(`${path}/spriteFrame`, SpriteFrame, (err, sf) => {
                if (!err && sf) {
                    resolve(sf);
                    return;
                }
                resources.load(path, SpriteFrame, (err2, sf2) => {
                    if (!err2 && sf2) {
                        resolve(sf2);
                        return;
                    }
                    resources.load(path, ImageAsset, (err3, img) => {
                        if (!err3 && img) {
                            const tex = new Texture2D();
                            tex.image = img;
                            const frame = new SpriteFrame();
                            frame.texture = tex;
                            ensureSpriteFrameSize(frame, img.width, img.height);
                            resolve(frame);
                        } else {
                            resolve(null);
                        }
                    });
                });
            });
        });

        return (async () => {
            await ensureResourcesBundle();
            for (const p of tryPaths) {
                const sf = await attempt(p);
                if (sf) {
                    this.frames.set(fk, sf);
                    return true;
                }
            }
            console.warn(`[VisualAssets] 加载失败: ${tryPaths[0]}`);
            return false;
        })();
    }

    static getFrame(key: SpriteKey, theme?: string): SpriteFrame | null {
        return this.frames.get(this.frameKey(key, theme)) ?? null;
    }

    /** 按关卡配置预加载实体贴图（自定义路径 + theme 回退） */
    static async preloadLevelEntities(config: LevelConfig | null | undefined): Promise<void> {
        if (!config) return;
        const paths = collectLevelEntityTexturePaths(
            config.theme,
            config.entityTextures,
            config.spawns,
            config,
        );
        await Promise.all(paths.map((p) => this.loadTexturePath(p)));
    }

    /** 切关 / 软重置前预加载本关瓦片 + 实体贴图，避免生成占位块 */
    static async ensureLevelAssetsReady(
        config: LevelConfig | null | undefined,
        tileNames: string[],
    ): Promise<void> {
        if (!config) return;
        const theme = normalizeTheme(config.theme || 'silu');
        await Promise.all([
            this.preloadLevelTiles(theme, tileNames),
            this.preloadLevelEntities(config),
        ]);
    }

    /** 按路径顺序解析贴图（先缓存后异步加载） */
    private static async resolveFirstTexture(paths: string[]): Promise<SpriteFrame | null> {
        const validPaths = paths
            .map((p) => normalizeTexturePath(p))
            .filter((p): p is string => !!p);
        for (const p of validPaths) {
            const cached = this.getPathFrame(p);
            if (cached) return cached;
        }
        for (const p of validPaths) {
            const loaded = await this.loadTexturePath(p);
            if (loaded) return loaded;
        }
        return null;
    }

    /** @deprecated 使用 preloadLevelEntities */
    static async preloadEntityTheme(mapTheme: string): Promise<void> {
        const keys: SpriteKey[] = ['player_F', 'player_B', 'ship_F', 'ship_B', 'coin'];
        await Promise.all(keys.map((k) => this.loadOne(k, mapTheme)));
    }

    static getPathFrame(texPath: string): SpriteFrame | null {
        const key = normalizeTexturePath(texPath);
        if (!key) return null;
        return this.pathFrames.get(key) ?? null;
    }

    private static pathCacheKey(texPath: unknown): string {
        return normalizeTexturePath(texPath) ?? '';
    }

    static loadTexturePath(texPath: string): Promise<SpriteFrame | null> {
        if (typeof texPath !== 'string') {
            return Promise.resolve(null);
        }
        const key = normalizeTexturePath(texPath);
        if (!key || key === '[object Set]' || key === '[object Object]') {
            console.warn('[VisualAssets] 贴图路径无效:', texPath);
            return Promise.resolve(null);
        }
        const cached = this.pathFrames.get(key);
        if (cached) return Promise.resolve(cached);

        const inflight = this.pathLoading.get(key);
        if (inflight) return inflight;

        const attempt = (path: string): Promise<SpriteFrame | null> => new Promise((resolve) => {
            resources.load(`${path}/spriteFrame`, SpriteFrame, (err, sf) => {
                if (!err && sf) {
                    resolve(sf);
                    return;
                }
                resources.load(path, SpriteFrame, (err2, sf2) => {
                    if (!err2 && sf2) {
                        resolve(sf2);
                        return;
                    }
                    resources.load(path, ImageAsset, (err3, img) => {
                        if (!err3 && img) {
                            const tex = new Texture2D();
                            tex.image = img;
                            const frame = new SpriteFrame();
                            frame.texture = tex;
                            ensureSpriteFrameSize(frame, img.width, img.height);
                            resolve(frame);
                        } else {
                            resolve(null);
                        }
                    });
                });
            });
        });

        const job = (async () => {
            await ensureResourcesBundle();
            const sf = await attempt(key);
            if (sf) {
                this.pathFrames.set(key, sf);
                return sf;
            }
            console.warn(`[VisualAssets] 贴图路径加载失败: ${key}`);
            return null;
        })().finally(() => {
            this.pathLoading.delete(key);
        });
        this.pathLoading.set(key, job);
        return job;
    }

    private static namedFrameKey(theme: string, tileName: string): string {
        return `${resolveThemeFolder(theme)}:${tileName}`;
    }

    /** 按关卡主题目录加载瓦片（theme 来自 levels-database.json，与 UI 主题无关） */
    static loadNamedFrame(tileName: string, theme: string): Promise<SpriteFrame | null> {
        if (typeof tileName !== 'string') {
            return Promise.resolve(null);
        }
        const tile = tileName.trim();
        if (!tile) return Promise.resolve(null);
        const folder = resolveThemeFolder(theme);
        const cacheKey = this.namedFrameKey(folder, tile);
        const cached = this.namedFrames.get(cacheKey);
        if (cached) return Promise.resolve(cached);

        const base = `textures/${folder}/${tile}`;
        const tryPaths: string[] = [];
        const themed = getThemeTilePath(theme, tile);
        if (themed) tryPaths.push(themed);
        tryPaths.push(base);
        if (folder !== 'silu') tryPaths.push(`textures/silu/${tile}`);

        const attempt = (path: string): Promise<SpriteFrame | null> => new Promise((resolve) => {
            resources.load(`${path}/spriteFrame`, SpriteFrame, (err, sf) => {
                if (!err && sf) {
                    resolve(sf);
                    return;
                }
                resources.load(path, SpriteFrame, (err2, sf2) => {
                    if (!err2 && sf2) {
                        resolve(sf2);
                        return;
                    }
                    resources.load(path, ImageAsset, (err3, img) => {
                        if (!err3 && img) {
                            const tex = new Texture2D();
                            tex.image = img;
                            const frame = new SpriteFrame();
                            frame.texture = tex;
                            ensureSpriteFrameSize(frame, img.width, img.height);
                            resolve(frame);
                        } else {
                            resolve(null);
                        }
                    });
                });
            });
        });

        return (async () => {
            for (const p of tryPaths) {
                const sf = await attempt(p);
                if (sf) {
                    this.namedFrames.set(cacheKey, sf);
                    return sf;
                }
            }
            console.warn(`[VisualAssets] 瓦片贴图缺失: ${base}`);
            return null;
        })();
    }

    static applyNamedTile(
        node: Node,
        tileName: string,
        alpha = 255,
        cellX?: number,
        cellY?: number,
        theme?: string,
    ) {
        const folder = resolveThemeFolder(theme);
        const cacheKey = this.namedFrameKey(folder, tileName);
        const sf = this.namedFrames.get(cacheKey);
        if (sf) {
            this.applyTileFrame(node, sf, tileName, alpha, cellX, cellY, theme ?? folder);
            return Promise.resolve();
        }
        return this.loadNamedFrame(tileName, theme ?? folder).then((loaded) => {
            if (loaded && node?.isValid) {
                this.applyTileFrame(node, loaded, tileName, alpha, cellX, cellY, theme ?? folder);
            }
        });
    }

    /** 预加载某关卡所需的全部瓦片贴图（可与其他主题缓存共存） */
    static preloadLevelTiles(theme: string, tileNames: string[]): Promise<void> {
        return Promise.all(tileNames.map((n) => this.loadNamedFrame(n, theme))).then(() => undefined);
    }

    /** 等距砖块：格子锚点 + 各瓦片 Unity pivot（与 Tilemap 一致，不做 keep-position 补偿） */
    private static applyTileFrame(
        node: Node,
        sf: SpriteFrame,
        tileName: string,
        alpha = 255,
        cellX?: number,
        cellY?: number,
        theme?: string,
    ) {
        let ui = node.getComponent(UITransform);
        if (!ui) ui = node.addComponent(UITransform);
        const spr = node.getComponent(Sprite) || node.addComponent(Sprite);
        spr.enabled = true;
        spr.spriteFrame = sf;
        spr.sizeMode = Sprite.SizeMode.CUSTOM;
        const source = resolveTilePixelSize(tileName, sf, theme);
        const draw = getTileDrawSize(tileName, source.width, source.height, theme);
        ui.setContentSize(draw.width, draw.height);
        const pivot = getTilePivot(tileName, theme);
        ui.setAnchorPoint(pivot.x, pivot.y);
        if (cellX !== undefined && cellY !== undefined && !Number.isNaN(cellX) && !Number.isNaN(cellY)) {
            alignTileNode(node, cellX, cellY, tileName, theme);
        }
        spr.color = new Color(255, 255, 255, alpha);
        node.setScale(1, 1, 1);
    }

    private static applySpriteFrame(node: Node, sf: SpriteFrame, tileName: string, alpha = 255, theme?: string) {
        this.applyTileFrame(node, sf, tileName, alpha, undefined, undefined, theme);
    }

    static applyPlayerSprite(
        node: Node,
        direction: Direction,
        options: EntityVisualOptions = {},
        scaleMul = 1,
    ) {
        const flipX = entityFlipX(direction);
        const paths = resolveEntityTexturePaths('player', direction, options);
        this.applyTexturePaths(node, paths, {
            flipX,
            displayKind: 'player',
            displayMul: scaleMul,
            alpha: 255,
            theme: options.theme,
        });
    }

    /** 载具四向贴图：按 direction 直接选图，不做 flipX */
    static applyVehicleIconForDirection(
        node: Node,
        direction: Direction,
        options: EntityVisualOptions = {},
        scaleMul = 1,
    ) {
        const paths = resolveEntityTexturePaths('vehicle', direction, options);
        this.applyTexturePaths(node, paths, {
            flipX: false,
            displayKind: 'vehicle',
            displayMul: scaleMul,
            alpha: 255,
            theme: options.theme,
        });
    }

    /** @deprecated 使用 applyVehicleIconForDirection */
    static applyVehicleIcon(
        node: Node,
        isFront: boolean,
        flipX: boolean,
        options: EntityVisualOptions = {},
        scaleMul = 1,
    ) {
        const direction = isFront ? Direction.South : Direction.North;
        this.applyVehicleIconForDirection(node, direction, options, scaleMul);
    }

    static applyVehicleSprite(
        node: Node,
        direction: Direction,
        options: EntityVisualOptions = {},
        scaleMul = 1,
    ) {
        this.applyVehicleIconForDirection(node, direction, options, scaleMul);
    }

    /** 预载载具四向贴图，显示由 VehicleController.setIcon 负责 */
    private static async preloadVehicleTextures(
        node: Node,
        _direction: Direction,
        options: EntityVisualOptions,
    ): Promise<void> {
        const gen = (this.vehicleVisualGeneration.get(node) ?? 0) + 1;
        this.vehicleVisualGeneration.set(node, gen);
        const paths = new Set<string>();
        for (let d = Direction.North; d <= Direction.West; d++) {
            for (const p of resolveEntityTexturePaths('vehicle', d, options)) {
                paths.add(p);
            }
        }
        await Promise.all([...paths].map((p) => this.loadTexturePath(p)));
        if (gen !== this.vehicleVisualGeneration.get(node)) return;
    }

    static setupEntityVisual(
        node: Node,
        kind: SpawnKind,
        direction: Direction | undefined,
        options: EntityVisualOptions,
        scaleMul = 1,
    ) {
        void this.setupEntityVisualAsync(node, kind, direction, options, scaleMul);
    }

    /** 等待贴图就绪后再挂载精灵，避免可拾取物显示为色块占位 */
    static async setupEntityVisualAsync(
        node: Node,
        kind: SpawnKind,
        direction: Direction | undefined,
        options: EntityVisualOptions,
        scaleMul = 1,
    ): Promise<void> {
        const mul = resolveEntityScaleMul(kind, scaleMul);
        const visualOpts: EntityVisualOptions = {
            theme: options.theme,
            entityTextures: options.entityTextures,
            spawnTexture: options.spawnTexture,
            propPlacement: options.propPlacement,
        };
        const mapTheme = options.theme;
        if (kind === 'player') {
            const animPaths = resolvePlayerAnimPaths(mapTheme);
            const animator = node.getComponent(PlayerActionAnimator);
            if (animPaths && animator) {
                animator.configure(mapTheme, direction ?? Direction.South, mul);
                animator.setAction(PlayerAction.Idle, true);
                return;
            }
            const paths = resolveEntityTexturePaths('player', direction, visualOpts);
            const flipX = entityFlipX(direction ?? Direction.South);
            const ok = await this.applyTexturePathsAsync(node, paths, {
                flipX,
                displayKind: 'player',
                displayMul: mul,
                alpha: 255,
                theme: mapTheme,
            });
            if (!ok) this.applyPlayerSprite(node, direction ?? Direction.South, visualOpts, mul);
            return;
        }
        if (kind === 'vehicle') {
            const dir = direction ?? Direction.North;
            await this.preloadVehicleTextures(node, dir, visualOpts);
            if (!node?.isValid) return;
            node.getComponent(VehicleController)?.refreshIcon()
                ?? this.applyVehicleIconForDirection(node, dir, visualOpts, mul);
            return;
        }
        if (kind === 'prop') {
            const paths = resolveEntityTexturePaths('prop', direction, visualOpts);
            const displayKind: EntityDisplayKind = visualOpts.propPlacement === 'ground' ? 'propGround' : 'prop';
            const ok = await this.applyTexturePathsAsync(node, paths, {
                flipX: false,
                displayKind,
                displayMul: mul,
                alpha: 255,
                theme: mapTheme,
            });
            if (!ok) {
                await Promise.all(paths.map((p) => this.loadTexturePath(p)));
                await this.applyTexturePathsAsync(node, paths, {
                    flipX: false,
                    displayKind,
                    displayMul: mul,
                    alpha: 255,
                    theme: mapTheme,
                });
            }
            return;
        }
        if (kind === 'prop_decor') {
            const paths = resolveEntityTexturePaths('prop_decor', direction, visualOpts);
            await this.applyTexturePathsAsync(node, paths, {
                flipX: false,
                displayKind: 'prop_decor',
                displayMul: mul,
                alpha: 120,
                theme: mapTheme,
            });
        }
    }

    /** 软重置 / 同关刷新：优先用实体当前逻辑朝向，避免贴图回退到 spawn 方向 */
    static async refreshLevelEntityVisuals(levelRoot: Node, config: LevelConfig): Promise<void> {
        if (!levelRoot?.isValid || !config) return;
        const theme = config.theme || 'silu';
        const playerNode = findLevelChildByName(levelRoot, 'Player')
            ?? (() => {
                let found: Node | null = null;
                forEachLevelEntityNode(levelRoot, (c) => {
                    if (!found && c.getComponent(PlayerController)) found = c;
                });
                return found;
            })();
        const playerCtrl = playerNode?.getComponent(PlayerController) ?? null;
        const jobs: Promise<void>[] = [];
        for (const s of config.spawns ?? []) {
            if (s.kind === 'player' && resolvePlayerAnimPaths(theme)) {
                continue;
            }
            const node = this.findSpawnNode(levelRoot, s);
            if (!node?.isValid) continue;
            const dir = this.resolveRuntimeEntityDirection(s, node, playerCtrl);
            let propPlacement = s.propPlacement;
            if (s.kind === 'prop' && !propPlacement) {
                propPlacement = resolvePropPlacement(s, config);
            }
            jobs.push(this.setupEntityVisualAsync(node, s.kind, dir, {
                theme,
                entityTextures: config.entityTextures,
                spawnTexture: s.texture,
                propPlacement,
            }, s.scale));
        }
        await Promise.all(jobs);
        for (const s of config.spawns ?? []) {
            if (s.kind !== 'vehicle') continue;
            const node = this.findSpawnNode(levelRoot, s);
            node?.getComponent(VehicleController)?.refreshIcon();
        }
    }

    private static resolveRuntimeEntityDirection(
        s: SpawnConfig,
        node: Node,
        playerCtrl: PlayerController | null,
    ): Direction | undefined {
        if (s.kind === 'player') {
            return node.getComponent(PlayerController)?.direction
                ?? this.readSpawnDirection(s.playerDirection);
        }
        if (s.kind === 'vehicle') {
            const vc = node.getComponent(VehicleController);
            if (vc && playerCtrl) {
                if (playerCtrl.getRideVehicle() === vc) {
                    return playerCtrl.direction;
                }
                const pCell = playerCtrl.getCommittedCell() ?? playerCtrl.getSpawnCell();
                const vCell = vc.getCommittedCell() ?? vc.getSpawnCell()
                    ?? new Vec3(s.x, s.y, 0);
                if (pCell && pCell.x === vCell.x && pCell.y === vCell.y) {
                    return playerCtrl.direction;
                }
            }
            return vc?.direction ?? this.readSpawnDirection(s.vehicleDirection);
        }
        return undefined;
    }

    private static readSpawnDirection(raw?: Direction | string): Direction | undefined {
        if (raw === undefined || raw === null) return undefined;
        if (typeof raw === 'number') return raw as Direction;
        const name = String(raw).replace(/^Direction\./, '');
        const key = name as keyof typeof Direction;
        if (Object.prototype.hasOwnProperty.call(Direction, key)) {
            const v = Direction[key];
            if (typeof v === 'number') return v as Direction;
        }
        return undefined;
    }

    private static findSpawnNode(levelRoot: Node, s: SpawnConfig): Node | null {
        if (s.kind === 'player') {
            let found = findLevelChildByName(levelRoot, 'Player');
            if (found) return found;
            forEachLevelEntityNode(levelRoot, (c) => {
                if (!found && c.getComponent(PlayerController)) found = c;
            });
            return found;
        }
        if (s.kind === 'vehicle') {
            let found: Node | null = null;
            forEachLevelEntityNode(levelRoot, (c) => {
                if (!found && c.getComponent(VehicleController)) found = c;
            });
            return found;
        }
        if (s.kind === 'prop') {
            let found = findLevelChildByName(levelRoot, `Prop_${s.x}_${s.y}`);
            if (found) return found;
            forEachLevelEntityNode(levelRoot, (c) => {
                if (found) return;
                const prop = c.getComponent(PropController);
                const cell = prop?.getSpawnCell();
                if (cell && cell.x === s.x && cell.y === s.y) found = c;
            });
            return found;
        }
        if (s.kind === 'prop_decor') {
            return findLevelChildByName(levelRoot, 'PropDecor')
                ?? (() => {
                    let found: Node | null = null;
                    forEachLevelEntityNode(levelRoot, (c) => {
                        if (!found && c.name === 'PropDecor') found = c;
                    });
                    return found;
                })();
        }
        return null;
    }

    private static async applyTexturePathsAsync(
        node: Node,
        paths: string[],
        opts: {
            flipX: boolean;
            displayKind: EntityDisplayKind;
            displayMul: number;
            alpha: number;
            theme?: string;
        },
    ): Promise<boolean> {
        const sf = await this.resolveFirstTexture(paths);
        if (!sf || !node?.isValid) return false;
        this.commitTexturePaths(node, sf, opts);
        return true;
    }

    private static applyTexturePaths(
        node: Node,
        paths: string[],
        opts: {
            flipX: boolean;
            displayKind: EntityDisplayKind;
            displayMul: number;
            alpha: number;
            theme?: string;
        },
    ) {
        const validPaths = paths
            .map((p) => normalizeTexturePath(p))
            .filter((p): p is string => !!p);
        let sf: SpriteFrame | null = null;
        for (const p of validPaths) {
            sf = this.getPathFrame(p);
            if (sf) break;
        }
        if (sf) this.commitTexturePaths(node, sf, opts);
    }

    /** 贴图在根节点；四向贴图已含朝向，根节点 scale 恒为 1 */
    private static prepareVehicleSpriteRoot(root: Node) {
        const legacy = root.getChildByName('VehicleVisual');
        if (legacy?.isValid) legacy.destroy();
        root.setScale(1, 1, 1);
    }

    private static commitTexturePaths(
        node: Node,
        sf: SpriteFrame,
        opts: {
            flipX: boolean;
            displayKind: EntityDisplayKind;
            displayMul: number;
            alpha: number;
            theme?: string;
        },
    ) {
        const { flipX, displayKind, displayMul, alpha, theme } = opts;
        const isVehicle = displayKind === 'vehicle';
        if (isVehicle) this.prepareVehicleSpriteRoot(node);
        const target = node;

        let ui = target.getComponent(UITransform);
        if (!ui) {
            ui = target.addComponent(UITransform);
            ui.setContentSize(48, 48);
        }
        const g = target.getComponent(Graphics);
        if (g) g.destroy();
        const sprite = target.getComponent(Sprite) || target.addComponent(Sprite);
        sprite.spriteFrame = sf;
        sprite.sizeMode = Sprite.SizeMode.CUSTOM;
        const fit = fitEntityDisplaySize(displayKind, sf, displayMul, theme);
        ui.setAnchorPoint(fit.anchorX, fit.anchorY);
        ui.setContentSize(fit.width, fit.height);
        sprite.color = new Color(255, 255, 255, alpha);
        if (isVehicle) {
            target.setScale(1, 1, 1);
        } else {
            target.setScale(flipX ? -1 : 1, 1, 1);
        }
    }

    /** 旧接口：按 SpriteKey + theme 缓存（UI 等仍可用） */
    static applyPlayerSpriteLegacy(node: Node, direction: Direction, mapTheme?: string, scaleMul = 1) {
        const isFront = direction === Direction.South || direction === Direction.East;
        const flipX = direction === Direction.West || direction === Direction.East;
        const sk: SpriteKey = isFront ? 'player_F' : 'player_B';
        this.applySprite(node, sk, flipX, 1, 255, mapTheme, scaleMul, 'player');
    }

    static applyVehicleSpriteLegacy(node: Node, direction: Direction, mapTheme?: string, scaleMul = 1) {
        this.applyVehicleSprite(node, direction, { theme: mapTheme }, scaleMul);
    }

    static setupEntityVisualLegacy(
        node: Node,
        kind: SpawnKind,
        direction?: Direction,
        mapTheme?: string,
        scaleMul = 1,
    ) {
        const mul = resolveEntityScaleMul(kind, scaleMul);
        if (kind === 'player') {
            this.applyPlayerSpriteLegacy(node, direction ?? Direction.South, mapTheme, mul);
            return;
        }
        if (kind === 'vehicle') {
            this.applyVehicleSpriteLegacy(node, direction ?? Direction.North, mapTheme, mul);
            return;
        }
        if (kind === 'prop') {
            this.applySprite(node, 'coin', false, mul, 255, mapTheme);
            return;
        }
        if (kind === 'prop_decor') {
            this.applySprite(node, 'coin', false, mul, 120, mapTheme);
        }
    }

    static applySprite(
        node: Node,
        key: SpriteKey,
        flipX: boolean,
        scale = 1,
        alpha = 255,
        mapTheme?: string,
        heightMul = 1,
        heightKind?: 'player' | 'vehicle',
    ) {
        let ui = node.getComponent(UITransform);
        if (!ui) {
            ui = node.addComponent(UITransform);
            ui.setContentSize(48, 48);
        }

        const sf = this.getFrame(key, mapTheme);
        if (!sf && mapTheme) {
            void this.loadOne(key, mapTheme).then((ok) => {
                if (ok && node?.isValid) {
                    this.applySprite(node, key, flipX, scale, alpha, mapTheme, heightMul, heightKind);
                }
            });
        }
        const spr = node.getComponent(Sprite);
        const g = node.getComponent(Graphics);

        if (sf) {
            if (g) g.destroy();
            const sprite = spr || node.addComponent(Sprite);
            sprite.spriteFrame = sf;
            sprite.sizeMode = Sprite.SizeMode.CUSTOM;
            const isPlayer = key === 'player_F' || key === 'player_B';
            const isShip = key === 'ship_F' || key === 'ship_B';
            if (isPlayer || isShip) {
                const kind: EntityDisplayKind = isPlayer ? 'player' : 'vehicle';
                const fit = fitEntityDisplaySize(kind, sf, heightMul, mapTheme);
                ui.setAnchorPoint(fit.anchorX, fit.anchorY);
                ui.setContentSize(fit.width, fit.height);
            } else {
                const fit = fitEntityDisplaySize('prop', sf, scale, mapTheme);
                ui.setAnchorPoint(fit.anchorX, fit.anchorY);
                ui.setContentSize(fit.width, fit.height);
            }
            sprite.color = new Color(255, 255, 255, alpha);
        } else if (!spr) {
            const graphics = g || node.addComponent(Graphics);
            graphics.fillColor = key === 'coin'
                ? new Color(255, 220, 0, alpha)
                : new Color(80, 160, 255, alpha);
            const w = ui.contentSize.width * 0.45 * scale;
            graphics.clear();
            graphics.rect(-w, -w, w * 2, w * 2);
            graphics.fill();
        } else if (g) {
            g.destroy();
        }

        const sx = flipX ? -1 : 1;
        node.setScale(sx, 1, 1);
    }
}
