'use strict';

var SceneImporter = require('./SceneImporter.js');

/**
 * @file AsepriteImporter.ts
 * @description Import Aseprite sprite editor files (.aseprite, .ase)
 * PRO-1.3: Scene Format Interoperability
 *
 * Supports:
 * - Aseprite binary format (.aseprite, .ase)
 * - Aseprite JSON export format
 *
 * Features:
 * - Layers (normal, group, reference)
 * - Frames and animation
 * - Tags (animation clips)
 * - Slices (9-slice, pivot)
 * - Palette
 * - Cel data
 * - Blend modes
 */
// ============================================================
// Aseprite Chunk Types
// ============================================================
const CHUNK_TYPES = {
    LAYER: 0x2004,
    CEL: 0x2005,
    EXTERNAL_FILES: 0x2008,
    TAGS: 0x2018,
    PALETTE: 0x2019,
    SLICE: 0x2022};
const BLEND_MODES = [
    'normal',
    'multiply',
    'screen',
    'overlay',
    'darken',
    'lighten',
    'color-dodge',
    'color-burn',
    'hard-light',
    'soft-light',
    'difference',
    'exclusion',
    'hue',
    'saturation',
    'color',
    'luminosity',
    'addition',
    'subtract',
    'divide',
];
// ============================================================
// Aseprite Importer
// ============================================================
class AsepriteImporter extends SceneImporter.SceneImporter {
    constructor() {
        super('aseprite');
        this.aseFile = null;
    }
    async import(options) {
        const startTime = Date.now();
        this.options = options;
        this.clearMessages();
        this.aseFile = null;
        const errors = await this.validate(options.sourcePath);
        if (errors.some((e) => e.fatal)) {
            return {
                ...this.createEmptyResult(),
                errors: errors,
            };
        }
        const scenes = [];
        const assets = [];
        try {
            const content = await this.readFile(options.sourcePath);
            // Detect format: binary or JSON
            if (typeof content === 'string') {
                // JSON export
                const json = JSON.parse(content);
                const spriteSheet = this.parseJsonExport(json, options.sourcePath);
                assets.push(this.createSpriteSheetAsset(spriteSheet, json.meta.image));
            }
            else {
                // Binary format
                this.aseFile = this.parseBinaryFile(content);
                if (this.aseFile) {
                    // Create sprite sheet asset
                    const spriteSheet = this.createSpriteSheetFromBinary();
                    assets.push(this.createSpriteSheetAsset(spriteSheet, options.sourcePath));
                    // Create palette asset if indexed color
                    if (this.aseFile.header.colorDepth === 8) {
                        assets.push(this.createPaletteAsset());
                    }
                }
            }
            // Create a pseudo-scene for the sprite
            if (this.aseFile || assets.length > 0) {
                const scene = this.createSpriteScene(options.sourcePath);
                scenes.push(scene);
            }
        }
        catch (error) {
            this.addError('IMPORT_ERROR', `Failed to import: ${error}`, options.sourcePath, undefined, true);
        }
        return this.createResult(scenes, assets, startTime);
    }
    async parseFile(content, filePath) {
        if (typeof content === 'string') {
            try {
                const json = JSON.parse(content);
                const spriteSheet = this.parseJsonExport(json, filePath);
                return {
                    id: this.generateId(),
                    name: spriteSheet.name,
                    type: 'animation',
                    path: json.meta.image,
                    metadata: {
                        frames: spriteSheet.frames,
                        animations: spriteSheet.animations,
                    },
                };
            }
            catch (_a) {
                return null;
            }
        }
        // Binary parsing returns scene with sprite entity
        this.aseFile = this.parseBinaryFile(content);
        if (this.aseFile) {
            return this.createSpriteScene(filePath);
        }
        return null;
    }
    async validate(sourcePath) {
        const errors = [];
        if (!this.canHandle(sourcePath)) {
            errors.push({
                code: 'INVALID_FORMAT',
                message: `File ${sourcePath} is not a valid Aseprite file`,
                file: sourcePath,
                fatal: true,
            });
        }
        return errors;
    }
    // ----------------------------------------
    // JSON Export Parsing
    // ----------------------------------------
    parseJsonExport(json, filePath) {
        const frames = [];
        const animations = {};
        // Parse frames (can be object or array)
        const frameEntries = Array.isArray(json.frames)
            ? json.frames.map((f, i) => [`frame_${i}`, f])
            : Object.entries(json.frames);
        for (const [name, frameData] of frameEntries) {
            frames.push({
                name,
                x: frameData.frame.x,
                y: frameData.frame.y,
                width: frameData.frame.w,
                height: frameData.frame.h,
                sourceWidth: frameData.sourceSize.w,
                sourceHeight: frameData.sourceSize.h,
                offsetX: frameData.spriteSourceSize.x,
                offsetY: frameData.spriteSourceSize.y,
                trimmed: frameData.trimmed,
            });
        }
        // Parse frame tags as animations
        if (json.meta.frameTags) {
            for (const tag of json.meta.frameTags) {
                const animFrames = [];
                for (let i = tag.from; i <= tag.to; i++) {
                    const frame = frameEntries[i][1];
                    animFrames.push({
                        index: i,
                        duration: frame.duration,
                        x: frameEntries[i][1].frame.x,
                        y: frameEntries[i][1].frame.y,
                        width: frameEntries[i][1].frame.w,
                        height: frameEntries[i][1].frame.h,
                    });
                }
                animations[tag.name] = {
                    name: tag.name,
                    frames: animFrames,
                    loop: true, // Determine from direction
                };
            }
        }
        // If no tags, create a default animation with all frames
        if (Object.keys(animations).length === 0 && frames.length > 0) {
            const allFrames = frameEntries.map(([_, f], index) => ({
                index,
                duration: f.duration,
                x: f.frame.x,
                y: f.frame.y,
                width: f.frame.w,
                height: f.frame.h,
            }));
            animations['default'] = {
                name: 'default',
                frames: allFrames,
                loop: true,
            };
        }
        return {
            name: filePath.substring(filePath.lastIndexOf('/') + 1).replace(/\.\w+$/, ''),
            imagePath: json.meta.image,
            frames,
            animations,
        };
    }
    // ----------------------------------------
    // Binary Format Parsing
    // ----------------------------------------
    parseBinaryFile(buffer) {
        const view = new DataView(buffer);
        let offset = 0;
        // Read header
        const header = this.readHeader(view, offset);
        offset += 128; // Header is always 128 bytes
        if (header.magicNumber !== 0xa5e0) {
            this.addError('INVALID_MAGIC', 'Invalid Aseprite file magic number');
            return null;
        }
        const file = {
            header,
            frames: [],
            palette: { size: 0, colors: [], firstColorIndex: 0, lastColorIndex: 0 },
            layers: [],
            tags: [],
            slices: [],
        };
        // Read frames
        for (let f = 0; f < header.frames; f++) {
            const frame = this.readFrame(view, offset, file);
            file.frames.push(frame);
            offset += this.getFrameSize(view, offset);
        }
        // Build layer hierarchy
        this.buildLayerHierarchy(file.layers);
        return file;
    }
    readHeader(view, offset) {
        return {
            fileSize: view.getUint32(offset, true),
            magicNumber: view.getUint16(offset + 4, true),
            frames: view.getUint16(offset + 6, true),
            width: view.getUint16(offset + 8, true),
            height: view.getUint16(offset + 10, true),
            colorDepth: view.getUint16(offset + 12, true),
            flags: view.getUint32(offset + 14, true),
            speed: view.getUint16(offset + 18, true),
            transparentIndex: view.getUint8(offset + 28),
            numColors: view.getUint16(offset + 32, true),
            pixelWidth: view.getUint8(offset + 34),
            pixelHeight: view.getUint8(offset + 35),
            gridX: view.getInt16(offset + 36, true),
            gridY: view.getInt16(offset + 38, true),
            gridWidth: view.getUint16(offset + 40, true),
            gridHeight: view.getUint16(offset + 42, true),
        };
    }
    readFrame(view, offset, file) {
        view.getUint32(offset, true);
        const frameMagic = view.getUint16(offset + 4, true);
        if (frameMagic !== 0xf1fa) {
            this.addWarning('INVALID_FRAME_MAGIC', `Invalid frame magic at offset ${offset}`);
        }
        const oldChunks = view.getUint16(offset + 6, true);
        const duration = view.getUint16(offset + 8, true);
        const newChunks = view.getUint32(offset + 12, true);
        const numChunks = newChunks === 0 ? oldChunks : newChunks;
        const frame = {
            duration,
            chunks: [],
            cels: [],
        };
        let chunkOffset = offset + 16;
        for (let c = 0; c < numChunks; c++) {
            const chunkSize = view.getUint32(chunkOffset, true);
            const chunkType = view.getUint16(chunkOffset + 4, true);
            const chunkData = new Uint8Array(view.buffer, chunkOffset + 6, chunkSize - 6);
            this.processChunk(chunkType, chunkData, frame, file);
            frame.chunks.push({
                type: chunkType,
                data: chunkData.buffer,
            });
            chunkOffset += chunkSize;
        }
        return frame;
    }
    getFrameSize(view, offset) {
        return view.getUint32(offset, true);
    }
    processChunk(type, data, frame, file) {
        const view = new DataView(data.buffer, data.byteOffset, data.byteLength);
        switch (type) {
            case CHUNK_TYPES.LAYER:
                file.layers.push(this.readLayerChunk(view));
                break;
            case CHUNK_TYPES.CEL:
                frame.cels.push(this.readCelChunk(view, data, file.header));
                break;
            case CHUNK_TYPES.TAGS:
                file.tags.push(...this.readTagsChunk(view));
                break;
            case CHUNK_TYPES.PALETTE:
                file.palette = this.readPaletteChunk(view);
                break;
            case CHUNK_TYPES.SLICE:
                file.slices.push(this.readSliceChunk(view));
                break;
            case CHUNK_TYPES.EXTERNAL_FILES:
                file.externalFiles = this.readExternalFilesChunk(view);
                break;
        }
    }
    readLayerChunk(view) {
        const flags = view.getUint16(0, true);
        const typeNum = view.getUint16(2, true);
        const childLevel = view.getUint16(4, true);
        // Skip default width/height at 6 and 8
        const blendModeNum = view.getUint16(10, true);
        const opacity = view.getUint8(12);
        // Skip 3 reserved bytes
        const nameLength = view.getUint16(16, true);
        const name = this.readString(view, 18, nameLength);
        let tilesetIndex;
        if (typeNum === 2 && view.byteLength > 18 + nameLength) {
            tilesetIndex = view.getUint32(18 + nameLength, true);
        }
        return {
            flags,
            type: typeNum === 0 ? 'normal' : typeNum === 1 ? 'group' : 'tilemap',
            childLevel,
            blendMode: BLEND_MODES[blendModeNum] || 'normal',
            opacity,
            name,
            tilesetIndex,
            visible: (flags & 0x0001) !== 0,
            editable: (flags & 0x0002) !== 0,
            lockMovement: (flags & 0x0004) !== 0,
            background: (flags & 0x0008) !== 0,
            continuous: (flags & 0x0010) !== 0,
            collapsed: (flags & 0x0020) !== 0,
            reference: (flags & 0x0040) !== 0,
            children: [],
        };
    }
    readCelChunk(view, data, header) {
        const layerIndex = view.getUint16(0, true);
        const x = view.getInt16(2, true);
        const y = view.getInt16(4, true);
        const opacity = view.getUint8(6);
        const celType = view.getUint16(7, true);
        const zIndex = view.getInt16(9, true);
        const cel = {
            layerIndex,
            x,
            y,
            opacity,
            celType: celType === 0
                ? 'raw'
                : celType === 1
                    ? 'linked'
                    : celType === 2
                        ? 'compressed'
                        : 'compressed-tilemap',
            zIndex,
        };
        // Skip 5 reserved bytes, then read type-specific data
        let offset = 16;
        if (celType === 1) {
            // Linked cel
            cel.linkedFrame = view.getUint16(offset, true);
        }
        else if (celType === 0 || celType === 2) {
            // Raw or compressed pixel data
            cel.width = view.getUint16(offset, true);
            cel.height = view.getUint16(offset + 2, true);
            if (celType === 0) {
                // Raw pixels
                const bytesPerPixel = header.colorDepth / 8;
                const pixelCount = cel.width * cel.height * bytesPerPixel;
                cel.pixels = new Uint8Array(data.slice(offset + 4, offset + 4 + pixelCount));
            }
            else {
                // Compressed (zlib)
                // Would need zlib decompression here
                // For now, store compressed data reference
                cel.pixels = new Uint8Array(data.slice(offset + 4));
            }
        }
        else if (celType === 3) {
            // Compressed tilemap
            cel.width = view.getUint16(offset, true);
            cel.height = view.getUint16(offset + 2, true);
            cel.tileWidth = view.getUint16(offset + 4, true);
            cel.tileHeight = view.getUint16(offset + 6, true);
            // Tile data follows (compressed)
        }
        return cel;
    }
    readTagsChunk(view) {
        const tags = [];
        const numTags = view.getUint16(0, true);
        // Skip 8 reserved bytes
        let offset = 10;
        for (let i = 0; i < numTags; i++) {
            const fromFrame = view.getUint16(offset, true);
            const toFrame = view.getUint16(offset + 2, true);
            const directionNum = view.getUint8(offset + 4);
            const repeat = view.getUint16(offset + 5, true);
            // Skip 6 reserved bytes
            // Skip deprecated RGB bytes at offset + 13
            // Skip 1 extra byte
            const nameLength = view.getUint16(offset + 17, true);
            const name = this.readString(view, offset + 19, nameLength);
            tags.push({
                fromFrame,
                toFrame,
                direction: directionNum === 0
                    ? 'forward'
                    : directionNum === 1
                        ? 'reverse'
                        : directionNum === 2
                            ? 'pingpong'
                            : 'pingpong-reverse',
                repeat,
                name,
                color: { red: 0, green: 0, blue: 0, alpha: 255 },
            });
            offset += 19 + nameLength;
        }
        return tags;
    }
    readPaletteChunk(view) {
        const size = view.getUint32(0, true);
        const firstColorIndex = view.getUint32(4, true);
        const lastColorIndex = view.getUint32(8, true);
        // Skip 8 reserved bytes
        const colors = [];
        let offset = 20;
        for (let i = firstColorIndex; i <= lastColorIndex; i++) {
            const hasName = view.getUint16(offset, true) & 0x0001;
            const red = view.getUint8(offset + 2);
            const green = view.getUint8(offset + 3);
            const blue = view.getUint8(offset + 4);
            const alpha = view.getUint8(offset + 5);
            let name;
            offset += 6;
            if (hasName) {
                const nameLength = view.getUint16(offset, true);
                name = this.readString(view, offset + 2, nameLength);
                offset += 2 + nameLength;
            }
            colors[i] = { red, green, blue, alpha, name };
        }
        return { size, colors, firstColorIndex, lastColorIndex };
    }
    readSliceChunk(view) {
        const numKeys = view.getUint32(0, true);
        const flags = view.getUint32(4, true);
        // Skip 4 reserved bytes
        const nameLength = view.getUint16(12, true);
        const name = this.readString(view, 14, nameLength);
        const has9Slices = (flags & 0x0001) !== 0;
        const hasPivot = (flags & 0x0002) !== 0;
        const keys = [];
        let offset = 14 + nameLength;
        for (let i = 0; i < numKeys; i++) {
            const frameNumber = view.getUint32(offset, true);
            const x = view.getInt32(offset + 4, true);
            const y = view.getInt32(offset + 8, true);
            const width = view.getUint32(offset + 12, true);
            const height = view.getUint32(offset + 16, true);
            offset += 20;
            const key = { frameNumber, x, y, width, height };
            if (has9Slices) {
                key.centerX = view.getInt32(offset, true);
                key.centerY = view.getInt32(offset + 4, true);
                key.centerWidth = view.getUint32(offset + 8, true);
                key.centerHeight = view.getUint32(offset + 12, true);
                offset += 16;
            }
            if (hasPivot) {
                key.pivotX = view.getInt32(offset, true);
                key.pivotY = view.getInt32(offset + 4, true);
                offset += 8;
            }
            keys.push(key);
        }
        return { name, flags, keys, has9Slices, hasPivot };
    }
    readExternalFilesChunk(view) {
        const files = [];
        const numEntries = view.getUint32(0, true);
        // Skip 8 reserved bytes
        let offset = 12;
        for (let i = 0; i < numEntries; i++) {
            const id = view.getUint32(offset, true);
            const typeNum = view.getUint8(offset + 4);
            // Skip 7 reserved bytes
            const pathLength = view.getUint16(offset + 12, true);
            const path = this.readString(view, offset + 14, pathLength);
            const typeMap = {
                0: 'external-palette',
                1: 'external-tileset',
                2: 'extension',
                3: 'extension-property',
            };
            files.push({
                id,
                type: typeMap[typeNum] || 'extension',
                path,
            });
            offset += 14 + pathLength;
        }
        return files;
    }
    readString(view, offset, length) {
        const bytes = new Uint8Array(view.buffer, view.byteOffset + offset, length);
        return new TextDecoder('utf-8').decode(bytes);
    }
    buildLayerHierarchy(layers) {
        const stack = [];
        for (let i = 0; i < layers.length; i++) {
            const layer = layers[i];
            // Pop stack until we find the parent level
            while (stack.length > layer.childLevel) {
                stack.pop();
            }
            // Set parent
            if (stack.length > 0) {
                layer.parent = stack[stack.length - 1];
                layers[layer.parent].children.push(i);
            }
            // Push this layer if it's a group
            if (layer.type === 'group') {
                stack.push(i);
            }
        }
    }
    // ----------------------------------------
    // Conversion
    // ----------------------------------------
    createSpriteSheetFromBinary() {
        if (!this.aseFile) {
            return { name: 'unknown', imagePath: '', frames: [] };
        }
        const frames = [];
        const animations = {};
        // Create frames
        for (let i = 0; i < this.aseFile.frames.length; i++) {
            frames.push({
                name: `frame_${i}`,
                x: 0,
                y: i * this.aseFile.header.height,
                width: this.aseFile.header.width,
                height: this.aseFile.header.height,
            });
        }
        // Create animations from tags
        for (const tag of this.aseFile.tags) {
            const animFrames = [];
            for (let i = tag.fromFrame; i <= tag.toFrame; i++) {
                animFrames.push({
                    index: i,
                    duration: this.aseFile.frames[i].duration,
                    x: 0,
                    y: i * this.aseFile.header.height,
                    width: this.aseFile.header.width,
                    height: this.aseFile.header.height,
                });
            }
            animations[tag.name] = {
                name: tag.name,
                frames: animFrames,
                loop: tag.repeat === 0, // 0 means infinite loop
            };
        }
        // Default animation if no tags
        if (Object.keys(animations).length === 0) {
            const allFrames = this.aseFile.frames.map((f, i) => ({
                index: i,
                duration: f.duration,
                x: 0,
                y: i * this.aseFile.header.height,
                width: this.aseFile.header.width,
                height: this.aseFile.header.height,
            }));
            animations['default'] = {
                name: 'default',
                frames: allFrames,
                loop: true,
            };
        }
        return {
            name: 'sprite',
            imagePath: '',
            frames,
            animations,
        };
    }
    createSpriteSheetAsset(spriteSheet, imagePath) {
        var _a, _b, _c, _d, _e;
        return {
            id: this.generateId(),
            name: spriteSheet.name,
            type: 'animation',
            path: imagePath,
            data: {
                frames: spriteSheet.frames,
                animations: spriteSheet.animations,
                width: (_a = this.aseFile) === null || _a === void 0 ? void 0 : _a.header.width,
                height: (_b = this.aseFile) === null || _b === void 0 ? void 0 : _b.header.height,
                colorDepth: (_c = this.aseFile) === null || _c === void 0 ? void 0 : _c.header.colorDepth,
                layers: (_d = this.aseFile) === null || _d === void 0 ? void 0 : _d.layers.map((l) => ({
                    name: l.name,
                    type: l.type,
                    blendMode: l.blendMode,
                    opacity: l.opacity,
                    visible: l.visible,
                })),
                slices: (_e = this.aseFile) === null || _e === void 0 ? void 0 : _e.slices.map((s) => ({
                    name: s.name,
                    has9Slices: s.has9Slices,
                    hasPivot: s.hasPivot,
                    keys: s.keys,
                })),
            },
        };
    }
    createPaletteAsset() {
        var _a;
        return {
            id: this.generateId(),
            name: 'palette',
            type: 'data',
            path: '',
            data: {
                colors: (_a = this.aseFile) === null || _a === void 0 ? void 0 : _a.palette.colors.map((c) => ({
                    r: c.red,
                    g: c.green,
                    b: c.blue,
                    a: c.alpha,
                    name: c.name,
                })),
            },
        };
    }
    createSpriteScene(filePath) {
        var _a, _b, _c, _d, _e;
        const name = filePath.substring(filePath.lastIndexOf('/') + 1).replace(/\.\w+$/, '');
        const width = ((_a = this.aseFile) === null || _a === void 0 ? void 0 : _a.header.width) || 64;
        const height = ((_b = this.aseFile) === null || _b === void 0 ? void 0 : _b.header.height) || 64;
        const entities = [];
        // Create entity for each layer
        if (this.aseFile) {
            for (let i = 0; i < this.aseFile.layers.length; i++) {
                const layer = this.aseFile.layers[i];
                if (layer.type === 'normal') {
                    entities.push({
                        id: this.generateId(),
                        name: layer.name,
                        components: [
                            {
                                type: 'Transform',
                                data: { position: { x: 0, y: 0 }, rotation: 0, scale: { x: 1, y: 1 } },
                            },
                            {
                                type: 'Sprite',
                                data: {
                                    layer: layer.name,
                                    blendMode: layer.blendMode,
                                    opacity: layer.opacity / 255,
                                },
                            },
                        ],
                        children: [],
                        tags: ['layer'],
                        active: layer.visible,
                    });
                }
            }
        }
        // Add main sprite entity
        entities.unshift({
            id: this.generateId(),
            name: name,
            components: [
                {
                    type: 'Transform',
                    data: { position: { x: width / 2, y: height / 2 }, rotation: 0, scale: { x: 1, y: 1 } },
                },
                {
                    type: 'AnimatedSprite',
                    data: {
                        spriteSheet: name,
                        currentAnimation: ((_d = (_c = this.aseFile) === null || _c === void 0 ? void 0 : _c.tags[0]) === null || _d === void 0 ? void 0 : _d.name) || 'default',
                        playing: true,
                        loop: true,
                    },
                },
            ],
            children: [],
            tags: ['sprite'],
            active: true,
        });
        return {
            id: this.generateId(),
            name,
            entities,
            settings: {
                width,
                height,
                backgroundColor: ((_e = this.aseFile) === null || _e === void 0 ? void 0 : _e.header.colorDepth) === 8
                    ? this.paletteColorToHex(this.aseFile.header.transparentIndex)
                    : '#00000000',
            },
        };
    }
    paletteColorToHex(index) {
        var _a;
        if (!((_a = this.aseFile) === null || _a === void 0 ? void 0 : _a.palette.colors[index]))
            return '#000000';
        const c = this.aseFile.palette.colors[index];
        return `#${c.red.toString(16).padStart(2, '0')}${c.green.toString(16).padStart(2, '0')}${c.blue.toString(16).padStart(2, '0')}`;
    }
    // ----------------------------------------
    // Helper Methods
    // ----------------------------------------
    async readFile(path) {
        var _a;
        if (typeof globalThis.process !== 'undefined' && ((_a = globalThis.process.versions) === null || _a === void 0 ? void 0 : _a.node)) {
            const fs = await import('fs');
            // Check if it's JSON or binary
            if (path.endsWith('.json')) {
                return fs.readFileSync(path, 'utf-8');
            }
            const buffer = fs.readFileSync(path);
            return buffer.buffer.slice(buffer.byteOffset, buffer.byteOffset + buffer.byteLength);
        }
        else if (typeof fetch !== 'undefined') {
            const response = await fetch(path);
            if (path.endsWith('.json')) {
                return response.text();
            }
            return response.arrayBuffer();
        }
        throw new Error('Unable to read file in current environment');
    }
}

exports.AsepriteImporter = AsepriteImporter;
//# sourceMappingURL=AsepriteImporter.js.map
