"use strict";
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
Object.defineProperty(exports, "__esModule", { value: true });
const ste_events_1 = require("ste-events");
const StorageUtilities_1 = require("../storage/StorageUtilities");
const ModelGeometryDefinition_1 = require("../minecraft/ModelGeometryDefinition");
const IProjectItemData_1 = require("../app/IProjectItemData");
const EntityTypeResourceDefinition_1 = require("../minecraft/EntityTypeResourceDefinition");
const Utilities_1 = require("../core/Utilities");
const exifr_1 = require("exifr");
const Log_1 = require("../core/Log");
class BlockbenchModel {
    constructor() {
        this._isLoaded = false;
        this._onLoaded = new ste_events_1.EventDispatcher();
    }
    get data() {
        return this._data;
    }
    set data(content) {
        this._data = content;
    }
    get isLoaded() {
        return this._isLoaded;
    }
    get file() {
        return this._file;
    }
    set file(newFile) {
        this._file = newFile;
    }
    get onLoaded() {
        return this._onLoaded.asEvent();
    }
    get name() {
        if (this._data) {
            return this._data.name;
        }
        return undefined;
    }
    get id() {
        if (this._id) {
            return this._id;
        }
        if (this._data && this._data.model_identifier) {
            return this._data.model_identifier;
        }
        return undefined;
    }
    set id(newId) {
        this._id = newId;
        if (this._data && newId !== undefined) {
            this._data.model_identifier = newId;
        }
    }
    static ensureFromContent(content) {
        const bd = new BlockbenchModel();
        const obj = JSON.parse(content);
        bd.data = obj;
        return bd;
    }
    getMinecraftUVFace(face) {
        if (face.uv.length >= 4) {
            return {
                uv: [face.uv[0], face.uv[1]],
                uv_size: [Math.abs(face.uv[2] - face.uv[0]), Math.abs(face.uv[3] - face.uv[1])],
            };
        }
        else if (face.uv.length >= 2) {
            return {
                uv: [face.uv[0], face.uv[1]],
                uv_size: [1, 1],
            };
        }
        return { uv: [0, 0], uv_size: [1, 1] };
    }
    async updateGeometryFromModel(geo) {
        if (this.data?.resolution) {
            geo.textureheight = this.data.resolution.height;
            geo.texturewidth = this.data.resolution.width;
        }
        if (this.data?.visible_box && this.data.visible_box.length === 3) {
            geo.visible_bounds_width = this.data.visible_box[0];
            geo.visible_bounds_height = this.data.visible_box[1];
            geo.visible_bounds_offset = [0, this.data.visible_box[2], 0];
        }
        const bonesByName = {};
        const cubesById = {};
        const locatorsById = {};
        if (this.data?.elements) {
            geo.bones = [];
            for (const elt of this.data.elements) {
                if (elt.type === "cube") {
                    if (elt.from && elt.from.length === 3 && elt.to && elt.to.length === 3) {
                        let uvTarg = undefined;
                        if (elt.box_uv) {
                            uvTarg = elt.uv_offset;
                        }
                        else if (elt.faces) {
                            uvTarg = {
                                north: this.getMinecraftUVFace(elt.faces.north),
                                east: this.getMinecraftUVFace(elt.faces.east),
                                south: this.getMinecraftUVFace(elt.faces.south),
                                west: this.getMinecraftUVFace(elt.faces.west),
                                up: this.getMinecraftUVFace(elt.faces.up),
                                down: this.getMinecraftUVFace(elt.faces.down),
                            };
                        }
                        let cube = {
                            origin: elt.from,
                            size: [
                                Math.abs(elt.to[0] - elt.from[0]),
                                Math.abs(elt.to[1] - elt.from[1]),
                                Math.abs(elt.to[2] - elt.from[2]),
                            ],
                            uv: uvTarg,
                        };
                        if (elt.rotation && elt.rotation.length === 3) {
                            cube.rotation = [-elt.rotation[0], -elt.rotation[1], -elt.rotation[2]];
                        }
                        if (elt.origin) {
                            cube.pivot = elt.origin;
                        }
                        cubesById[elt.uuid] = cube;
                    }
                }
                else if (elt.type === "locator") {
                    locatorsById[elt.uuid] = elt;
                }
            }
        }
        if (this.data?.outliner) {
            this.processOutlineItems(this.data.outliner, bonesByName, cubesById, locatorsById);
        }
        for (const boneName in bonesByName) {
            const bone = bonesByName[boneName];
            geo.bones.push(bone);
        }
    }
    processOutlineItems(outlineItems, bonesByName, cubesById, locatorsById, parent) {
        for (const outlineItem of outlineItems) {
            if (outlineItem && typeof outlineItem === "string" && parent) {
                const elt = cubesById[outlineItem];
                if (elt) {
                    if (!parent.cubes) {
                        parent.cubes = [];
                    }
                    parent.cubes.push(elt);
                }
                else {
                    const lead = locatorsById[outlineItem];
                    if (lead && lead.name && lead.position) {
                        if (!parent.locators) {
                            parent.locators = {};
                        }
                        parent.locators[lead.name] = lead.position;
                    }
                }
            }
            else if (outlineItem && typeof outlineItem !== "string" && outlineItem.name) {
                let bone = bonesByName[outlineItem.name];
                if (!bone) {
                    bone = {
                        name: outlineItem.name,
                        pivot: [],
                        cubes: undefined,
                        locators: undefined,
                    };
                    bonesByName[outlineItem.name] = bone;
                }
                bone.pivot = outlineItem.origin;
                if (parent) {
                    bone.parent = parent.name;
                }
                if (outlineItem.children) {
                    this.processOutlineItems(outlineItem.children, bonesByName, cubesById, locatorsById, bone);
                }
            }
        }
    }
    async integrateIntoProject(project) {
        const modelId = this.id;
        if (modelId) {
            let geoToUpdate = undefined;
            for (const item of project.items) {
                if (item.itemType === IProjectItemData_1.ProjectItemType.modelGeometryJson && geoToUpdate === undefined) {
                    await item.ensureFileStorage();
                    if (item.file) {
                        const modelDefOuter = await ModelGeometryDefinition_1.default.ensureOnFile(item.file);
                        if (modelDefOuter && modelDefOuter.definitions) {
                            geoToUpdate = modelDefOuter.getById(modelId);
                        }
                    }
                }
                else if (item.itemType === IProjectItemData_1.ProjectItemType.entityTypeResource) {
                    // ensure references to textures if an entiy exists
                    await item.ensureFileStorage();
                    if (item.file) {
                        const etrd = await EntityTypeResourceDefinition_1.default.ensureOnFile(item.file);
                        if (etrd && etrd.id === modelId) {
                            if (this.data && this.data.textures && etrd.textures) {
                                for (const texture of this.data.textures) {
                                    let path = texture.path ? texture.path : texture.name;
                                    path = StorageUtilities_1.default.canonicalizePath(path);
                                    let hasPath = false;
                                    let hasDefault = false;
                                    const texturesIndex = path.indexOf("textures/");
                                    if (texturesIndex >= 0) {
                                        path = path.substring(texturesIndex);
                                        for (const textureKey in etrd.textures) {
                                            const targetPath = etrd.textures[textureKey];
                                            if (textureKey === "default") {
                                                hasDefault = true;
                                            }
                                            if (targetPath && StorageUtilities_1.default.isPathEqual(targetPath, path)) {
                                                hasPath = true;
                                            }
                                        }
                                        if (!hasPath) {
                                            if (!hasDefault) {
                                                etrd.textures["default"] = path;
                                            }
                                            else {
                                                etrd.textures[texture.name] = path;
                                            }
                                        }
                                    }
                                }
                            }
                        }
                    }
                }
            }
            // a model file doesn't exist, so let's keep one.
            if (!geoToUpdate && project.projectFolder) {
                let modelName = this.data?.name;
                if (modelName === undefined) {
                    modelName = modelId;
                    const colonNamespaceSep = modelName.lastIndexOf(":");
                    if (colonNamespaceSep >= 0) {
                        modelName = modelName.substring(colonNamespaceSep + 1);
                    }
                }
                const defaultRp = await project.getDefaultResourcePackFolder();
                if (defaultRp) {
                    const modelsFolder = defaultRp.ensureFolder("models");
                    await modelsFolder.ensureExists();
                    const newFileName = await StorageUtilities_1.default.getUniqueFileName(modelName, "json", modelsFolder);
                    const newFile = modelsFolder.ensureFile(newFileName);
                    const modelGen = await ModelGeometryDefinition_1.default.ensureOnFile(newFile);
                    if (modelGen) {
                        modelGen.ensureDefault(modelId);
                        if (modelGen.definitions.length >= 0) {
                            geoToUpdate = modelGen.definitions[0];
                        }
                    }
                }
            }
            if (geoToUpdate) {
                this.updateGeometryFromModel(geoToUpdate);
            }
        }
        if (this.data && this.data.textures) {
            for (const texture of this.data.textures) {
                let setItem = false;
                if (texture.name) {
                    let path = texture.path ? texture.path : texture.name;
                    const bytes = Utilities_1.default.base64ToUint8Array(texture.source);
                    if (bytes && project.projectFolder) {
                        path = StorageUtilities_1.default.canonicalizePath(path);
                        const texturesIndex = path.indexOf("textures/");
                        if (texturesIndex >= 0) {
                            path = path.substring(texturesIndex);
                        }
                        // first, try to match an item by its path leaf
                        for (const item of project.items) {
                            if (item.itemType === IProjectItemData_1.ProjectItemType.texture && !setItem) {
                                await item.ensureFileStorage();
                                if (item.file) {
                                    const projectPath = item.file.getFolderRelativePath(project.projectFolder);
                                    if (projectPath && projectPath.endsWith(path)) {
                                        item.file.setContent(bytes);
                                        setItem = true;
                                    }
                                }
                            }
                        }
                        // we didn't match by path, but try to match by file name?
                        if (!setItem) {
                            for (const item of project.items) {
                                if (item.itemType === IProjectItemData_1.ProjectItemType.texture && !setItem) {
                                    if (item.file && item.file.name === texture.name) {
                                        item.file.setContent(bytes);
                                        setItem = true;
                                    }
                                }
                            }
                        }
                        // we didn't find a match, so create a new texture
                        if (!setItem) {
                            const defaultRp = await project.getDefaultResourcePackFolder();
                            if (defaultRp && project.projectFolder) {
                                // the path is not standard Minecraft, let's just create a new texture path in RP
                                if (!path.startsWith("textures/")) {
                                    path = "textures/" + texture.name;
                                }
                                const file = defaultRp.ensureFile(path);
                                file.setContent(bytes);
                                const projectPath = file.getFolderRelativePath(project.projectFolder);
                                if (projectPath) {
                                    project.ensureItemByProjectPath(projectPath, IProjectItemData_1.ProjectItemStorageType.singleFile, file.name, IProjectItemData_1.ProjectItemType.texture, undefined, IProjectItemData_1.ProjectItemCreationType.normal, file);
                                }
                            }
                        }
                    }
                }
            }
        }
    }
    static async ensureOnFile(file, loadHandler) {
        let bd;
        if (file.manager === undefined) {
            bd = new BlockbenchModel();
            bd.file = file;
            file.manager = bd;
        }
        if (file.manager !== undefined && file.manager instanceof BlockbenchModel) {
            bd = file.manager;
            if (!bd.isLoaded && loadHandler) {
                bd.onLoaded.subscribe(loadHandler);
            }
            await bd.load();
            return bd;
        }
        return bd;
    }
    async persist() {
        if (this._file === undefined) {
            return;
        }
        const pjString = JSON.stringify(this._data, null, 2);
        this._file.setContent(pjString);
    }
    async save() {
        if (this._file === undefined) {
            return;
        }
        this.persist();
        await this._file.saveContent(false);
    }
    async load() {
        if (this._file === undefined || this._isLoaded) {
            return;
        }
        await this._file.loadContent();
        if (this._file.content === null || this._file.content instanceof Uint8Array) {
            return;
        }
        this.id = this._file.name;
        this._data = StorageUtilities_1.default.getJsonObject(this._file);
        this._isLoaded = true;
    }
    static createEmptyModel(name, identifier) {
        return {
            meta: {
                format_version: "4.10",
                model_format: "bedrock",
                box_uv: true,
            },
            name: name,
            model_identifier: identifier,
            variable_placeholder_buttons: [],
            variable_placeholders: "",
            visible_box: [1, 1, 1],
            bedrock_animation_mode: "entity",
            timeline_setups: [],
            unhandled_root_fields: {},
            resolution: { width: 64, height: 32 },
            elements: [],
            outliner: [],
        };
    }
    static async exportModel(modelProjectItem, modelIndex) {
        if (modelIndex === undefined) {
            modelIndex = 0;
        }
        await modelProjectItem.ensureFileStorage();
        let clientEntityItem = undefined;
        let clientEntity = undefined;
        let model = undefined;
        if (modelProjectItem.file) {
            model = await ModelGeometryDefinition_1.default.ensureOnFile(modelProjectItem.file);
        }
        if (modelProjectItem.parentItems) {
            for (const parentItemOuter of modelProjectItem.parentItems) {
                if (parentItemOuter.parentItem.itemType === IProjectItemData_1.ProjectItemType.entityTypeResource) {
                    clientEntityItem = parentItemOuter.parentItem;
                    if (clientEntityItem && clientEntityItem.file) {
                        clientEntity = await EntityTypeResourceDefinition_1.default.ensureOnFile(clientEntityItem.file);
                    }
                }
            }
        }
        if (!model || model.identifiers.length === 0 || !model.file || !model.wrapper || model.definitions.length === 0) {
            return undefined;
        }
        const bbmodel = this.createEmptyModel(StorageUtilities_1.default.getBaseFromName(model.file.name), model.identifiers[modelIndex]);
        const textureWidth = model.getTextureWidth(modelIndex);
        const textureHeight = model.getTextureHeight(modelIndex);
        if (textureWidth !== undefined && textureHeight !== undefined) {
            bbmodel.resolution = {
                width: textureWidth,
                height: textureHeight,
            };
        }
        const visibleBoundsWidth = model.getVisibleBoundsWidth(modelIndex);
        const visibleBoundsHeight = model.getVisibleBoundsHeight(modelIndex);
        const visibleBoundsOffset = model.getVisibleBoundsOffset(modelIndex);
        if (visibleBoundsWidth && visibleBoundsHeight && visibleBoundsOffset && visibleBoundsOffset.length > 1) {
            bbmodel.visible_box = [visibleBoundsWidth, visibleBoundsHeight, visibleBoundsOffset[1]];
        }
        const def = model.definitions[modelIndex];
        const outlinerEltsByName = {};
        let colorIndex = 0;
        let rootBone = undefined;
        let hasMultipleRoots = false;
        for (const bone of def.bones) {
            let rot = bone.rotation;
            if (rot) {
                if (rot.length === 3) {
                    rot[0] = -rot[0];
                    rot[1] = -rot[1];
                    rot[2] = -rot[2];
                }
            }
            const outLinerElt = {
                name: bone.name,
                origin: bone.pivot,
                rotation: rot,
                bedrock_binding: "",
                color: colorIndex,
                uuid: Utilities_1.default.createUuid(),
                export: true,
                mirror_uv: false,
                isOpen: false,
                locked: false,
                visibility: true,
                autouv: 0,
                children: [],
            };
            outlinerEltsByName[bone.name] = outLinerElt;
            colorIndex++;
            if (colorIndex > 7) {
                colorIndex = 0;
            }
            if (bone.parent === undefined) {
                bbmodel.outliner?.push(outLinerElt);
                if (rootBone === undefined && !hasMultipleRoots) {
                    rootBone = bone;
                }
                else if (rootBone) {
                    rootBone = undefined;
                    hasMultipleRoots = true;
                }
            }
        }
        for (const bone of def.bones) {
            const thisOutlinerElt = outlinerEltsByName[bone.name];
            if (bone.cubes && bone.cubes.length >= 0) {
                for (const cube of bone.cubes) {
                    const id = Utilities_1.default.createUuid();
                    if (cube.origin && cube.origin.length === 3 && cube.size && cube.size.length === 3) {
                        const cubeFrom = cube.origin;
                        const cubeTo = new Array(3);
                        cubeTo[0] = cube.origin[0] + cube.size[0];
                        cubeTo[1] = cube.origin[1] + cube.size[1];
                        cubeTo[2] = cube.origin[2] + cube.size[2];
                        let rot = cube.rotation;
                        if (rot) {
                            if (rot.length === 3) {
                                rot[0] = -rot[0];
                                rot[1] = -rot[1];
                                rot[2] = -rot[2];
                            }
                        }
                        else if (bone.bind_pose_rotation) {
                            if (bone.bind_pose_rotation.length === 3) {
                                rot = [];
                                rot[0] = -bone.bind_pose_rotation[0];
                                rot[1] = -bone.bind_pose_rotation[1];
                                rot[2] = -bone.bind_pose_rotation[2];
                            }
                            else {
                                rot = bone.bind_pose_rotation;
                            }
                        }
                        else {
                            rot = [0, 0, 0];
                        }
                        let pivot = cube.pivot;
                        if (!pivot && bone.pivot) {
                            pivot = bone.pivot;
                        }
                        else if (!pivot) {
                            pivot = [0, 0, 0];
                        }
                        let uvOffset = undefined;
                        if (Array.isArray(cube.uv)) {
                            uvOffset = cube.uv;
                        }
                        bbmodel.elements?.push({
                            name: bone.name,
                            box_uv: true,
                            rescale: false,
                            locked: false,
                            light_emission: 0,
                            render_order: "default",
                            allow_mirror_modeling: true,
                            from: cubeFrom,
                            to: cubeTo,
                            inflate: cube.inflate,
                            autouv: 0,
                            color: 0,
                            rotation: rot,
                            origin: pivot,
                            uv_offset: uvOffset,
                            type: "cube",
                            faces: {
                                north: { uv: BlockbenchModel.getNorthBoxUvCoordinates(cube), texture: 0 },
                                east: { uv: BlockbenchModel.getEastBoxUvCoordinates(cube), texture: 0 },
                                south: { uv: BlockbenchModel.getSouthBoxUvCoordinates(cube), texture: 0 },
                                west: { uv: BlockbenchModel.getWestBoxUvCoordinates(cube), texture: 0 },
                                up: { uv: BlockbenchModel.getUpBoxUvCoordinates(cube), texture: 0 },
                                down: { uv: BlockbenchModel.getDownBoxUvCoordinates(cube), texture: 0 },
                            },
                            uuid: id,
                        });
                        thisOutlinerElt.children.push(id);
                    }
                }
            }
            if (bone.locators) {
                for (const locatorName in bone.locators) {
                    const locator = bone.locators[locatorName];
                    if (Array.isArray(locator) && locator.length === 3) {
                        const id = Utilities_1.default.createUuid();
                        bbmodel.elements?.push({
                            name: locatorName,
                            locked: false,
                            position: locator,
                            rotation: [0, 0, 0],
                            type: "locator",
                            uuid: id,
                        });
                        thisOutlinerElt.children.push(id);
                    }
                }
            }
            if (bone.parent !== undefined) {
                const parentOutlinerElt = outlinerEltsByName[bone.parent];
                if (parentOutlinerElt) {
                    parentOutlinerElt.children.push(thisOutlinerElt);
                }
            }
        }
        let textureList = [];
        bbmodel.textures = [];
        if (clientEntity && clientEntityItem && clientEntityItem.file) {
            const textures = clientEntity.getTextureItems(clientEntityItem);
            if (textures) {
                for (const textureName in textures) {
                    const textureItem = textures[textureName];
                    if (textureName && textureItem && textureItem.file) {
                        await textureItem.file.loadContent();
                        const exifr = new exifr_1.Exifr({});
                        if (textureItem.file.content) {
                            try {
                                await exifr.read(textureItem.file.content);
                                const results = await exifr.parse();
                                const relativePath = clientEntityItem.file.getRelativePathFor(textureItem.file);
                                const contentStr = StorageUtilities_1.default.getContentAsString(textureItem.file);
                                if (relativePath && contentStr) {
                                    textureList.push({
                                        path: textureItem.file.storageRelativePath,
                                        name: textureItem.file.name,
                                        folder: "",
                                        namespace: "",
                                        id: textureList.length.toString(),
                                        group: "",
                                        width: results.ImageWidth,
                                        height: results.ImageHeight,
                                        uv_width: results.ImageWidth,
                                        uv_height: results.ImageHeight,
                                        particle: false,
                                        use_as_default: false,
                                        layers_enabled: false,
                                        sync_to_project: "",
                                        render_mode: "default",
                                        render_sides: "auto",
                                        frame_time: 1,
                                        frame_order_type: "loop",
                                        frame_order: "",
                                        frame_interpolate: false,
                                        visible: true,
                                        internal: true,
                                        saved: true,
                                        uuid: Utilities_1.default.createUuid(),
                                        relative_path: relativePath,
                                        source: contentStr,
                                    });
                                    bbmodel.textures?.push(textureList[textureList.length - 1]);
                                }
                            }
                            catch (e) { }
                        }
                    }
                }
            }
        }
        return bbmodel;
    }
    /*
    Standard Box UV Mapping:
  
          +s0-+s0-+
          | u | d | < s2
      +s2-+s0-+s0-+s2-+
      | e | n | w | s | < s1
      +---+---+---+---+
  
      bb coordinates are: x1, y1, x2, y2
    */
    static getUpBoxUvCoordinates(cube) {
        let uv = cube.uv;
        if (Array.isArray(cube.uv)) {
            uv = cube.uv;
        }
        else if (cube.uv.up) {
            uv = cube.uv.up.uv;
        }
        else {
            Log_1.default.unexpectedContentState("BBMGUB");
            uv = [0, 0];
        }
        return [uv[0] + cube.size[2] + cube.size[0], uv[1] + cube.size[2], uv[0] + cube.size[2], uv[1]];
    }
    static getDownBoxUvCoordinates(cube) {
        let uv = cube.uv;
        if (Array.isArray(cube.uv)) {
            uv = cube.uv;
        }
        else if (cube.uv.down) {
            uv = cube.uv.down.uv;
        }
        else {
            Log_1.default.unexpectedContentState("BBMGDB");
            uv = [0, 0];
        }
        return [uv[0] + cube.size[2] + cube.size[0] * 2, uv[1], uv[0] + cube.size[0] + cube.size[2], uv[1] + cube.size[2]];
    }
    static getEastBoxUvCoordinates(cube) {
        let uv = cube.uv;
        if (Array.isArray(cube.uv)) {
            uv = cube.uv;
        }
        else if (cube.uv.east) {
            uv = cube.uv.east.uv;
        }
        else {
            Log_1.default.unexpectedContentState("BBMGEB");
            uv = [0, 0];
        }
        return [uv[0], uv[1] + cube.size[2], uv[0] + cube.size[2], uv[1] + cube.size[2] + cube.size[1]];
    }
    static getNorthBoxUvCoordinates(cube) {
        let uv = cube.uv;
        if (Array.isArray(cube.uv)) {
            uv = cube.uv;
        }
        else if (cube.uv.north) {
            uv = cube.uv.north.uv;
        }
        else {
            Log_1.default.unexpectedContentState("BBMGNB");
            uv = [0, 0];
        }
        return [
            uv[0] + cube.size[2],
            uv[1] + cube.size[2],
            uv[0] + cube.size[2] + cube.size[0],
            uv[1] + cube.size[2] + cube.size[1],
        ];
    }
    static getWestBoxUvCoordinates(cube) {
        let uv = cube.uv;
        if (Array.isArray(cube.uv)) {
            uv = cube.uv;
        }
        else if (cube.uv.west) {
            uv = cube.uv.west.uv;
        }
        else {
            Log_1.default.unexpectedContentState("BBMGWB");
            uv = [0, 0];
        }
        return [
            uv[0] + cube.size[2] + cube.size[0],
            uv[1] + cube.size[2],
            uv[0] + cube.size[2] * 2 + cube.size[0],
            uv[1] + cube.size[2] + cube.size[1],
        ];
    }
    static getSouthBoxUvCoordinates(cube) {
        let uv = cube.uv;
        if (Array.isArray(cube.uv)) {
            uv = cube.uv;
        }
        else if (cube.uv.south) {
            uv = cube.uv.south.uv;
        }
        else {
            Log_1.default.unexpectedContentState("BBMGWB");
            uv = [0, 0];
        }
        return [
            uv[0] + cube.size[2] * 2 + cube.size[0],
            uv[1] + cube.size[2],
            uv[0] + cube.size[2] * 2 + cube.size[0] * 2,
            uv[1] + cube.size[2] + cube.size[1],
        ];
    }
}
exports.default = BlockbenchModel;

//# sourceMappingURL=../maps/integrations/BlockbenchModel.js.map
