import { WaitWhile } from '../core/taskrunner.js';
import { Direction } from '../geometry/geometry.js';
import { Matrix } from '../geometry/matrix.js';
import { Transformation } from '../geometry/transformation.js';
import { Base64DataURIToArrayBuffer, CreateObjectUrl, GetFileExtensionFromMimeType } from '../io/bufferutils.js';
import { GetFileExtension, GetFileName } from '../io/fileutils.js';
import { PhongMaterial, TextureMap } from '../model/material.js';
import { Node, NodeType } from '../model/node.js';
import { ConvertThreeColorToColor, ConvertThreeGeometryToMesh } from '../threejs/threeutils.js';
import { ImporterBase } from './importerbase.js';

import * as THREE from 'three';
import { TGALoader } from 'three/examples/jsm/loaders/TGALoader.js';
import { FBXLoader } from 'three/examples/jsm/loaders/FBXLoader.js';
import { ColladaLoader } from 'three/examples/jsm/loaders/ColladaLoader.js';
import { VRMLLoader } from 'three/examples/jsm/loaders/VRMLLoader.js';
import { ThreeMFLoader } from 'three/examples/jsm/loaders/3MFLoader.js';

export class ImporterThreeBase extends ImporterBase
{
    constructor ()
    {
        super ();
    }

    CreateLoader (manager)
    {
        return null;
    }

    GetMainObject (loadedObject)
    {
        return loadedObject;
    }

    IsMeshVisible (mesh)
    {
        return true;
    }

    ClearContent ()
    {
        this.loader = null;
        this.materialIdToIndex = null;
        this.objectUrlToFileName = null;
    }

    ResetContent ()
    {
        this.loader = null;
        this.materialIdToIndex = new Map ();
        this.objectUrlToFileName = new Map ();
    }

    ImportContent (fileContent, onFinish)
    {
        this.LoadModel (fileContent, onFinish);
    }

    LoadModel (fileContent, onFinish)
    {
        let isAllLoadersDone = false;
        let loadingManager = new THREE.LoadingManager (() => {
            isAllLoadersDone = true;
        });

        const mainFileUrl = CreateObjectUrl (fileContent);
        loadingManager.setURLModifier ((url) => {
            if (url === mainFileUrl) {
                return url;
            }
            const name = GetFileName (url);
            const extension = GetFileExtension (url);
            if (extension.length > 0) {
                const buffer = this.callbacks.getFileBuffer (url);
                if (buffer !== null) {
                    let objectUrl = CreateObjectUrl (buffer);
                    this.objectUrlToFileName.set (objectUrl, name);
                    return objectUrl;
                }
            }
            return url;
        });

        const threeLoader = this.CreateLoader (loadingManager);
        if (threeLoader === null) {
            onFinish ();
            return;
        }

        threeLoader.load (mainFileUrl,
            (object) => {
                WaitWhile (() => {
                    if (isAllLoadersDone) {
                        this.OnThreeObjectsLoaded (object, onFinish);
                        return false;
                    }
                    return true;
                });
            },
            () => {
            },
            (err) => {
                this.SetError (err);
                onFinish ();
            }
        );
    }

    OnThreeObjectsLoaded (loadedObject, onFinish)
    {
        function GetObjectTransformation (threeObject)
        {
            let matrix = new Matrix ().CreateIdentity ();
            threeObject.updateMatrix ();
            if (threeObject.matrix !== undefined && threeObject.matrix !== null) {
                matrix.Set (threeObject.matrix.elements);
            }
            return new Transformation (matrix);
        }

        function AddObject (importer, model, threeObject, parentNode)
        {
            let node = new Node ();
            if (threeObject.name !== undefined) {
                node.SetName (threeObject.name);
            }
            node.SetTransformation (GetObjectTransformation (threeObject));
            parentNode.AddChildNode (node);

            for (let childObject of threeObject.children) {
                AddObject (importer, model, childObject, node);
            }
            if (threeObject.isMesh && importer.IsMeshVisible (threeObject)) {
                if (threeObject.children.length === 0) {
                    node.SetType (NodeType.MeshNode);
                }
                let mesh = importer.ConvertThreeMesh (threeObject);
                let meshIndex = model.AddMesh (mesh);
                node.AddMeshIndex (meshIndex);
            }
        }

        let mainObject = this.GetMainObject (loadedObject);
        let rootNode = this.model.GetRootNode ();
        rootNode.SetTransformation (GetObjectTransformation (mainObject));
        for (let childObject of mainObject.children) {
            AddObject (this, this.model, childObject, rootNode);
        }

        onFinish ();
    }

    ConvertThreeMesh (threeMesh)
    {
        let mesh = null;
        if (Array.isArray (threeMesh.material)) {
            mesh = ConvertThreeGeometryToMesh (threeMesh.geometry, null);
            if (threeMesh.geometry.attributes.color === undefined || threeMesh.geometry.attributes.color === null) {
                let materialIndices = [];
                for (let i = 0; i < threeMesh.material.length; i++) {
                    const material = threeMesh.material[i];
                    const materialIndex = this.FindOrCreateMaterial (material);
                    materialIndices.push (materialIndex);
                }
                for (let i = 0; i < threeMesh.geometry.groups.length; i++) {
                    let group = threeMesh.geometry.groups[i];
                    let groupEnd = null;
                    if (group.count === Infinity) {
                        groupEnd = mesh.TriangleCount ();
                    } else {
                        groupEnd = group.start / 3 + group.count / 3;
                    }
                    for (let j = group.start / 3; j < groupEnd; j++) {
                        let triangle = mesh.GetTriangle (j);
                        triangle.SetMaterial (materialIndices[group.materialIndex]);
                    }
                }
            }
        } else {
            const materialIndex = this.FindOrCreateMaterial (threeMesh.material);
            mesh = ConvertThreeGeometryToMesh (threeMesh.geometry, materialIndex);
        }
        if (threeMesh.name !== undefined && threeMesh.name !== null) {
            mesh.SetName (threeMesh.name);
        }
        return mesh;
    }

    FindOrCreateMaterial (threeMaterial)
    {
        if (this.materialIdToIndex.has (threeMaterial.id)) {
            return this.materialIdToIndex.get (threeMaterial.id);
        }
        let material = this.ConvertThreeMaterial (threeMaterial);
        let materialIndex = this.model.AddMaterial (material);
        this.materialIdToIndex.set (threeMaterial.id, materialIndex);
        return materialIndex;
    }

    ConvertThreeMaterial (threeMaterial)
    {
        function CreateTexture (threeMap, objectUrlToFileName)
        {
            function GetDataUrl (img)
            {
                if (img.data !== undefined && img.data !== null) {
                    let imageData = new ImageData (img.width, img.height);
                    let imageSize = img.width * img.height * 4;
                    for (let i = 0; i < imageSize; i++) {
                        imageData.data[i] = img.data[i];
                    }
                    return THREE.ImageUtils.getDataURL (imageData);
                } else {
                    return THREE.ImageUtils.getDataURL (img);
                }
            }

            if (threeMap === undefined || threeMap === null) {
                return null;
            }

            if (threeMap.image === undefined || threeMap.image === null) {
                return null;
            }

            try {
                const dataUrl = GetDataUrl (threeMap.image);
                const base64Buffer = Base64DataURIToArrayBuffer (dataUrl);
                let texture = new TextureMap ();
                let textureName = null;
                if (objectUrlToFileName.has (threeMap.image.src)) {
                    textureName = objectUrlToFileName.get (threeMap.image.src);
                } else if (threeMap.name !== undefined && threeMap.name !== null) {
                    textureName = threeMap.name + '.' + GetFileExtensionFromMimeType (base64Buffer.mimeType);
                } else {
                    textureName = 'Embedded_' + threeMap.id.toString () + '.' + GetFileExtensionFromMimeType (base64Buffer.mimeType);
                }
                texture.name = textureName;
                texture.mimeType = base64Buffer.mimeType;
                texture.buffer = base64Buffer.buffer;
                texture.rotation = threeMap.rotation;
                texture.offset.x = threeMap.offset.x;
                texture.offset.y = threeMap.offset.y;
                texture.scale.x = threeMap.repeat.x;
                texture.scale.y = threeMap.repeat.y;
                return texture;
            } catch (err) {
                return null;
            }
        }

        let material = new PhongMaterial ();
        material.name = threeMaterial.name;
        material.color = ConvertThreeColorToColor (threeMaterial.color);
        material.opacity = threeMaterial.opacity;
        material.transparent = threeMaterial.transparent;
        material.alphaTest = threeMaterial.alphaTest;
        if (threeMaterial.type === 'MeshPhongMaterial') {
            material.specular = ConvertThreeColorToColor (threeMaterial.specular);
            material.shininess = threeMaterial.shininess / 100.0;
        }
        material.diffuseMap = CreateTexture (threeMaterial.map, this.objectUrlToFileName);
        material.normalMap = CreateTexture (threeMaterial.normalMap, this.objectUrlToFileName);
        material.bumpMap = CreateTexture (threeMaterial.bumpMap, this.objectUrlToFileName);

        return material;
    }
}

export class ImporterThreeFbx extends ImporterThreeBase
{
    constructor ()
    {
        super ();
    }

    CanImportExtension (extension)
    {
        return extension === 'fbx';
    }

    GetUpDirection ()
    {
        return Direction.Y;
    }

    CreateLoader (manager)
    {
        manager.addHandler (/\.tga$/i, new TGALoader (manager));
        return new FBXLoader (manager);
    }

    GetMainObject (loadedObject)
    {
        return loadedObject;
    }
}

export class ImporterThreeDae extends ImporterThreeBase
{
    constructor ()
    {
        super ();
    }

    CanImportExtension (extension)
    {
        return extension === 'dae';
    }

    GetUpDirection ()
    {
        return Direction.Y;
    }

    CreateLoader (manager)
    {
        manager.addHandler (/\.tga$/i, new TGALoader (manager));
        return new ColladaLoader (manager);
    }

    GetMainObject (loadedObject)
    {
        return loadedObject.scene;
    }
}

export class ImporterThreeWrl extends ImporterThreeBase
{
    constructor ()
    {
        super ();
    }

    CanImportExtension (extension)
    {
        return extension === 'wrl';
    }

    GetUpDirection ()
    {
        return Direction.Y;
    }

    CreateLoader (manager)
    {
        return new VRMLLoader (manager);
    }

    GetMainObject (loadedObject)
    {
        return loadedObject;
    }

    IsMeshVisible (mesh)
    {
        let isVisible = true;
        if (Array.isArray (mesh.material)) {
            for (let i = 0; i < mesh.material.length; i++) {
                if (mesh.material[i].side === THREE.BackSide) {
                    isVisible = false;
                    break;
                }
            }
        } else {
            isVisible = (mesh.material.side !== THREE.BackSide);
        }
        return isVisible;
    }
}

export class ImporterThree3mf extends ImporterThreeBase
{
    constructor ()
    {
        super ();
    }

    CanImportExtension (extension)
    {
        return extension === '3mf';
    }

    GetUpDirection ()
    {
        return Direction.Z;
    }

    CreateLoader (manager)
    {
        return new ThreeMFLoader (manager);
    }

    GetMainObject (loadedObject)
    {
        return loadedObject;
    }
}
