import { PmxObject } from "../Loader/Parser/pmxObject";
/**
 * The MmdMorphControllerBase uses `MorphTargetManager` to handle position uv morphs, while the material, bone, and group morphs are handled by CPU bound
 *
 * As a result, it reproduces the behavior of the MMD morph system
 */
export class MmdMorphControllerBase {
    _logger;
    _runtimeBones;
    _materials;
    _morphs;
    _morphIndexMap;
    _morphWeights;
    _activeMorphs;
    /**
     * MorphTargetManagers used to handle position uv morphs
     */
    morphTargetManagers;
    /**
     * Creates a new MmdMorphController
     * @param runtimeBones MMD runtime bones which are original order
     * @param materials MMD materials which are order of mmd metadata
     * @param meshes MMD meshes which are order of mmd metadata
     * @param materialProxyConstructor The constructor of `IMmdMaterialProxy`
     * @param morphsMetadata Morphs metadata
     * @param morphTargetManagers MorphTargetManagers
     * @param logger Logger
     */
    constructor(runtimeBones, materials, meshes, materialProxyConstructor, morphsMetadata, morphTargetManagers, logger) {
        this._logger = logger;
        this._runtimeBones = runtimeBones ?? [];
        if (materialProxyConstructor !== null) {
            const materialProxies = this._materials = new Array(materials.length);
            for (let i = 0; i < materials.length; ++i) {
                const material = materials[i];
                const referencedMeshes = [];
                for (let j = 0; j < meshes.length; ++j) {
                    // TODO: handle multi-material meshes
                    if (meshes[j].material === material)
                        referencedMeshes.push(meshes[j]);
                }
                materialProxies[i] = new materialProxyConstructor(material, referencedMeshes);
            }
        }
        else {
            this._materials = [];
        }
        const morphs = this._morphs = this._createRuntimeMorphData(morphsMetadata, runtimeBones !== null, materialProxyConstructor !== null);
        const morphIndexMap = this._morphIndexMap = new Map();
        for (let i = 0; i < morphs.length; ++i) {
            const morph = morphs[i];
            let morphIndices = morphIndexMap.get(morph.name);
            if (morphIndices === undefined) {
                morphIndices = [];
                morphIndexMap.set(morph.name, morphIndices);
            }
            morphIndices.push(i);
        }
        this._morphWeights = new Float32Array(morphs.length);
        this._activeMorphs = new Set();
        this.morphTargetManagers = morphTargetManagers;
    }
    /**
     * Gets the weight of the morph
     *
     * If there are multiple morphs with the same name, the weight of the first one will be returned
     * @param morphName Name of the morph
     * @returns Weight of the morph
     */
    getMorphWeight(morphName) {
        const morphIndexMap = this._morphIndexMap;
        const morphIndices = morphIndexMap.get(morphName);
        if (morphIndices === undefined)
            return 0;
        return this._morphWeights[morphIndices[0]];
    }
    /**
     * Gets the indices of the morph with the given name
     *
     * The index array is returned because multiple morphs can have the same name
     * @param morphName Name of the morph
     * @returns Indices of the morph
     */
    getMorphIndices(morphName) {
        const morphIndexMap = this._morphIndexMap;
        const morphIndices = morphIndexMap.get(morphName);
        return morphIndices;
    }
    /**
     * Gets the weight of the morph from the index
     * @param morphIndex Index of the morph
     * @returns Weight of the morph
     */
    getMorphWeightFromIndex(morphIndex) {
        return this._morphWeights[morphIndex];
    }
    /**
     * Gets the weights of all morphs
     * @returns Weights of all morphs
     */
    getMorphWeights() {
        return this._morphWeights;
    }
    /**
     * Set the weights of all morphs to 0
     */
    resetMorphWeights() {
        this._morphWeights.fill(0);
    }
    _updatedMaterials = new Set();
    /**
     * Apply the morphs to mesh
     */
    update() {
        const morphs = this._morphs;
        const morphIndexMap = this._morphIndexMap;
        const morphWeights = this._morphWeights;
        const activeMorphs = this._activeMorphs;
        const morphTargetManagers = this.morphTargetManagers;
        for (let i = 0; i < morphTargetManagers.length; ++i)
            morphTargetManagers[i].areUpdatesFrozen = true;
        for (const morphName of activeMorphs) {
            const morphIndices = morphIndexMap.get(morphName);
            for (let i = 0; i < morphIndices.length; ++i) {
                this._resetMorph(morphs[morphIndices[i]]);
            }
        }
        for (const morphName of activeMorphs) {
            const morphIndices = morphIndexMap.get(morphName);
            for (let i = 0; i < morphIndices.length; ++i) {
                const morphIndex = morphIndices[i];
                const morphWeight = morphWeights[morphIndex];
                this._applyMorph(morphs[morphIndex], morphWeight);
                if (morphWeight === 0) {
                    activeMorphs.delete(morphName);
                }
            }
        }
        for (let i = 0; i < morphTargetManagers.length; ++i)
            morphTargetManagers[i].areUpdatesFrozen = false;
        const updatedMaterials = this._updatedMaterials;
        for (const updatedMaterial of updatedMaterials) {
            updatedMaterial.applyChanges();
        }
        updatedMaterials.clear();
    }
    /**
     * Gets the morph data
     */
    get morphs() {
        return this._morphs;
    }
    _createRuntimeMorphData(morphsMetadata, createBoneMorphs, createMaterialMorphs) {
        const morphs = [];
        for (let i = 0; i < morphsMetadata.length; ++i) {
            const morphMetadata = morphsMetadata[i];
            let runtimeMorphMaterialElements = null;
            let runtimeMorphElements = null;
            let runtimeMorphElements2 = null;
            let runtimeMorphElements3 = null;
            switch (morphMetadata.type) {
                case PmxObject.Morph.Type.GroupMorph:
                    {
                        runtimeMorphElements = morphMetadata.indices;
                        runtimeMorphElements2 = morphMetadata.ratios;
                    }
                    break;
                case PmxObject.Morph.Type.BoneMorph:
                    if (!createBoneMorphs) {
                        runtimeMorphElements = new Int32Array(0);
                        runtimeMorphElements2 = new Float32Array(0);
                        runtimeMorphElements3 = new Float32Array(0);
                    }
                    else {
                        runtimeMorphElements = morphMetadata.indices;
                        runtimeMorphElements2 = morphMetadata.positions;
                        runtimeMorphElements3 = morphMetadata.rotations;
                    }
                    break;
                case PmxObject.Morph.Type.MaterialMorph:
                    if (!createMaterialMorphs) {
                        runtimeMorphMaterialElements = [];
                    }
                    else {
                        const elements = morphMetadata.elements;
                        const morphElements = new Array(elements.length);
                        for (let j = 0; j < elements.length; ++j) {
                            const element = elements[j];
                            if (element.type === PmxObject.Morph.MaterialMorph.Type.Multiply) {
                                morphElements[j] = {
                                    index: element.index,
                                    type: element.type,
                                    diffuse: element.diffuse[0] !== 1 || element.diffuse[1] !== 1 || element.diffuse[2] !== 1 || element.diffuse[3] !== 1 ? element.diffuse : null,
                                    specular: element.specular[0] !== 1 || element.specular[1] !== 1 || element.specular[2] !== 1 ? element.specular : null,
                                    shininess: element.shininess !== 1 ? element.shininess : null,
                                    ambient: element.ambient[0] !== 1 || element.ambient[1] !== 1 || element.ambient[2] !== 1 ? element.ambient : null,
                                    edgeColor: element.edgeColor[0] !== 1 || element.edgeColor[1] !== 1 || element.edgeColor[2] !== 1 || element.edgeColor[3] !== 1 ? element.edgeColor : null,
                                    edgeSize: element.edgeSize !== 1 ? element.edgeSize : null,
                                    textureColor: element.textureColor[0] !== 1 || element.textureColor[1] !== 1 || element.textureColor[2] !== 1 || element.textureColor[3] !== 1 ? element.textureColor : null,
                                    sphereTextureColor: element.sphereTextureColor[0] !== 1 || element.sphereTextureColor[1] !== 1 || element.sphereTextureColor[2] !== 1 || element.sphereTextureColor[3] !== 1 ? element.sphereTextureColor : null,
                                    toonTextureColor: element.toonTextureColor[0] !== 1 || element.toonTextureColor[1] !== 1 || element.toonTextureColor[2] !== 1 || element.toonTextureColor[3] !== 1 ? element.toonTextureColor : null
                                };
                            }
                            else /* if (element.type === PmxObject.Morph.MaterialMorph.Type.Add) */ {
                                morphElements[j] = {
                                    index: element.index,
                                    type: element.type,
                                    diffuse: element.diffuse[0] !== 0 || element.diffuse[1] !== 0 || element.diffuse[2] !== 0 || element.diffuse[3] !== 0 ? element.diffuse : null,
                                    specular: element.specular[0] !== 0 || element.specular[1] !== 0 || element.specular[2] !== 0 ? element.specular : null,
                                    shininess: element.shininess !== 0 ? element.shininess : null,
                                    ambient: element.ambient[0] !== 0 || element.ambient[1] !== 0 || element.ambient[2] !== 0 ? element.ambient : null,
                                    edgeColor: element.edgeColor[0] !== 0 || element.edgeColor[1] !== 0 || element.edgeColor[2] !== 0 || element.edgeColor[3] !== 0 ? element.edgeColor : null,
                                    edgeSize: element.edgeSize !== 0 ? element.edgeSize : null,
                                    textureColor: element.textureColor[0] !== 0 || element.textureColor[1] !== 0 || element.textureColor[2] !== 0 || element.textureColor[3] !== 0 ? element.textureColor : null,
                                    sphereTextureColor: element.sphereTextureColor[0] !== 0 || element.sphereTextureColor[1] !== 0 || element.sphereTextureColor[2] !== 0 || element.sphereTextureColor[3] !== 0 ? element.sphereTextureColor : null,
                                    toonTextureColor: element.toonTextureColor[0] !== 0 || element.toonTextureColor[1] !== 0 || element.toonTextureColor[2] !== 0 || element.toonTextureColor[3] !== 0 ? element.toonTextureColor : null
                                };
                            }
                        }
                        runtimeMorphMaterialElements = morphElements;
                    }
                    break;
                case PmxObject.Morph.Type.UvMorph:
                case PmxObject.Morph.Type.AdditionalUvMorph1:
                case PmxObject.Morph.Type.AdditionalUvMorph2:
                case PmxObject.Morph.Type.AdditionalUvMorph3:
                case PmxObject.Morph.Type.AdditionalUvMorph4:
                case PmxObject.Morph.Type.VertexMorph:
                    runtimeMorphElements = morphMetadata.morphTargets;
                    break;
            }
            const morph = {
                name: morphMetadata.name,
                type: morphMetadata.type,
                materialElements: runtimeMorphMaterialElements,
                elements: runtimeMorphElements,
                elements2: runtimeMorphElements2,
                elements3: runtimeMorphElements3
            };
            morphs.push(morph);
        }
        {
            const groupMorphStack = [];
            const fixLoopingGroupMorphs = (morphIndex) => {
                const morph = morphs[morphIndex];
                if (morph.type !== PmxObject.Morph.Type.GroupMorph)
                    return;
                const indices = morph.elements;
                for (let i = 0; i < indices.length; ++i) {
                    const index = indices[i];
                    if (groupMorphStack.includes(index)) {
                        this._logger.warn(`Looping group morph detected resolves to -1: ${morph.name} -> ${morphs[index].name}`);
                        indices[i] = -1;
                    }
                    else {
                        if (0 <= index && index < morphs.length) {
                            groupMorphStack.push(morphIndex);
                            fixLoopingGroupMorphs(index);
                            groupMorphStack.pop();
                        }
                    }
                }
            };
            for (let i = 0; i < morphs.length; ++i) {
                groupMorphStack.push(i);
                fixLoopingGroupMorphs(i);
                groupMorphStack.length = 0;
            }
        }
        return morphs;
    }
    /**
     * Iterates all sub morphs of the group morph
     *
     * @param groupMorph Group morph
     * @param callback Callback
     */
    groupMorphFlatForeach(groupMorph, callback) {
        this._groupMorphFlatForeach(groupMorph, callback);
    }
    _groupMorphFlatForeach(groupMorph, callback, accumulatedRatio = 1) {
        const morphs = this._morphs;
        const indices = groupMorph.elements;
        const ratios = groupMorph.elements2;
        for (let i = 0; i < indices.length; ++i) {
            const index = indices[i];
            const ratio = ratios[i];
            const childMorph = morphs[index];
            if (childMorph.type === PmxObject.Morph.Type.GroupMorph) {
                this._groupMorphFlatForeach(childMorph, callback, ratio * accumulatedRatio);
            }
            else {
                callback(index, ratio * accumulatedRatio);
            }
        }
    }
    _resetMorph(morph) {
        switch (morph.type) {
            case PmxObject.Morph.Type.GroupMorph:
                {
                    const morphs = this._morphs;
                    this._groupMorphFlatForeach(morph, (index, _ratio) => {
                        const childMorph = morphs[index];
                        if (childMorph !== undefined)
                            this._resetMorph(childMorph);
                    });
                }
                break;
            case PmxObject.Morph.Type.BoneMorph:
                this._resetBoneMorph(morph);
                break;
            case PmxObject.Morph.Type.MaterialMorph:
                {
                    const elements = morph.materialElements;
                    for (let i = 0; i < elements.length; ++i) {
                        const element = elements[i];
                        const materials = this._materials;
                        if (element.index === -1) { // -1 means "all materials"
                            for (let i = 0; i < materials.length; ++i) {
                                materials[i]?.reset();
                            }
                        }
                        else {
                            materials[element.index]?.reset();
                        }
                    }
                }
                break;
            case PmxObject.Morph.Type.VertexMorph:
            case PmxObject.Morph.Type.UvMorph:
            case PmxObject.Morph.Type.AdditionalUvMorph1:
            case PmxObject.Morph.Type.AdditionalUvMorph2:
            case PmxObject.Morph.Type.AdditionalUvMorph3:
            case PmxObject.Morph.Type.AdditionalUvMorph4:
                {
                    const morphTargets = morph.elements;
                    for (let i = 0; i < morphTargets.length; ++i)
                        morphTargets[i].influence = 0;
                }
                break;
        }
    }
    _applyMorph(morph, weight) {
        switch (morph.type) {
            case PmxObject.Morph.Type.GroupMorph:
                {
                    const morphs = this._morphs;
                    this._groupMorphFlatForeach(morph, (index, ratio) => {
                        const childMorph = morphs[index];
                        if (childMorph !== undefined)
                            this._applyMorph(childMorph, weight * ratio);
                    });
                }
                break;
            case PmxObject.Morph.Type.BoneMorph:
                this._applyBoneMorph(morph, weight);
                break;
            case PmxObject.Morph.Type.MaterialMorph:
                {
                    const elements = morph.materialElements;
                    for (let i = 0; i < elements.length; ++i) {
                        const element = elements[i];
                        const materials = this._materials;
                        if (element.index === -1) { // -1 means "all materials"
                            for (let i = 0; i < materials.length; ++i) {
                                const material = materials[i];
                                if (material !== undefined) {
                                    this._applyMaterialMorph(element, material, weight);
                                }
                            }
                        }
                        else {
                            const material = materials[element.index];
                            if (material !== undefined) {
                                this._applyMaterialMorph(element, material, weight);
                            }
                        }
                    }
                }
                break;
            case PmxObject.Morph.Type.VertexMorph:
            case PmxObject.Morph.Type.UvMorph:
            case PmxObject.Morph.Type.AdditionalUvMorph1:
            case PmxObject.Morph.Type.AdditionalUvMorph2:
            case PmxObject.Morph.Type.AdditionalUvMorph3:
            case PmxObject.Morph.Type.AdditionalUvMorph4:
                {
                    const morphTargets = morph.elements;
                    for (let i = 0; i < morphTargets.length; ++i)
                        morphTargets[i].influence += weight;
                }
                break;
        }
    }
    _applyMaterialMorph(materialMorph, material, weight) {
        if (materialMorph.type === PmxObject.Morph.MaterialMorph.Type.Multiply) {
            if (materialMorph.diffuse !== null) {
                const diffuse = material.diffuse;
                for (let i = 0; i < 4; ++i) {
                    diffuse[i] = diffuse[i] + (diffuse[i] * materialMorph.diffuse[i] - diffuse[i]) * weight;
                }
            }
            if (materialMorph.specular !== null) {
                const specular = material.specular;
                for (let i = 0; i < 3; ++i) {
                    specular[i] = specular[i] + (specular[i] * materialMorph.specular[i] - specular[i]) * weight;
                }
            }
            if (materialMorph.shininess !== null) {
                material.shininess = material.shininess + (material.shininess * materialMorph.shininess - material.shininess) * weight;
            }
            if (materialMorph.ambient !== null) {
                const ambient = material.ambient;
                for (let i = 0; i < 3; ++i) {
                    ambient[i] = ambient[i] + (ambient[i] * materialMorph.ambient[i] - ambient[i]) * weight;
                }
            }
            if (materialMorph.edgeColor !== null) {
                const edgeColor = material.edgeColor;
                for (let i = 0; i < 4; ++i) {
                    edgeColor[i] = edgeColor[i] + (edgeColor[i] * materialMorph.edgeColor[i] - edgeColor[i]) * weight;
                }
            }
            if (materialMorph.edgeSize !== null) {
                material.edgeSize = material.edgeSize + (material.edgeSize * materialMorph.edgeSize - material.edgeSize) * weight;
            }
            if (materialMorph.textureColor !== null) {
                const textureColor = material.textureMultiplicativeColor;
                for (let i = 0; i < 4; ++i) {
                    textureColor[i] = textureColor[i] + (textureColor[i] * materialMorph.textureColor[i] - textureColor[i]) * weight;
                }
            }
            if (materialMorph.sphereTextureColor !== null) {
                const sphereTextureColor = material.sphereTextureMultiplicativeColor;
                for (let i = 0; i < 4; ++i) {
                    sphereTextureColor[i] = sphereTextureColor[i] + (sphereTextureColor[i] * materialMorph.sphereTextureColor[i] - sphereTextureColor[i]) * weight;
                }
            }
            if (materialMorph.toonTextureColor !== null) {
                const toonTextureColor = material.toonTextureMultiplicativeColor;
                for (let i = 0; i < 4; ++i) {
                    toonTextureColor[i] = toonTextureColor[i] + (toonTextureColor[i] * materialMorph.toonTextureColor[i] - toonTextureColor[i]) * weight;
                }
            }
        }
        else /* if (materialMorph.type === PmxObject.Morph.MaterialMorph.Type.Add) */ {
            if (materialMorph.diffuse !== null) {
                const diffuse = material.diffuse;
                for (let i = 0; i < 4; ++i) {
                    diffuse[i] += materialMorph.diffuse[i] * weight;
                }
            }
            if (materialMorph.specular !== null) {
                const specular = material.specular;
                for (let i = 0; i < 3; ++i) {
                    specular[i] += materialMorph.specular[i] * weight;
                }
            }
            if (materialMorph.shininess !== null) {
                material.shininess += materialMorph.shininess * weight;
            }
            if (materialMorph.ambient !== null) {
                const ambient = material.ambient;
                for (let i = 0; i < 3; ++i) {
                    ambient[i] += materialMorph.ambient[i] * weight;
                }
            }
            if (materialMorph.edgeColor !== null) {
                const edgeColor = material.edgeColor;
                for (let i = 0; i < 4; ++i) {
                    edgeColor[i] += materialMorph.edgeColor[i] * weight;
                }
            }
            if (materialMorph.edgeSize !== null) {
                material.edgeSize += materialMorph.edgeSize * weight;
            }
            if (materialMorph.textureColor !== null) {
                const textureColor = material.textureAdditiveColor;
                for (let i = 0; i < 4; ++i) {
                    textureColor[i] += materialMorph.textureColor[i] * weight;
                }
            }
            if (materialMorph.sphereTextureColor !== null) {
                const sphereTextureColor = material.sphereTextureAdditiveColor;
                for (let i = 0; i < 4; ++i) {
                    sphereTextureColor[i] += materialMorph.sphereTextureColor[i] * weight;
                }
            }
            if (materialMorph.toonTextureColor !== null) {
                const toonTextureColor = material.toonTextureAdditiveColor;
                for (let i = 0; i < 4; ++i) {
                    toonTextureColor[i] += materialMorph.toonTextureColor[i] * weight;
                }
            }
        }
        this._updatedMaterials.add(material);
    }
}
