OV.ThreeLoader = class
{
    constructor ()
    {

    }

    GetExtension ()
    {
        return null;
    }

    GetExternalLibraries ()
    {
        return null;
    }

    CreateLoader (manager)
    {
        return null;
    }

    EnumerateMeshes (loadedObject, processor)
    {
        
    }

    GetUpDirection ()
    {
        return null;
    }
};

OV.ThreeLoaderFbx = class extends OV.ThreeLoader
{
    constructor ()
    {
        super ();
    }

    GetExtension ()
    {
        return 'fbx';
    }

    GetExternalLibraries ()
    {
        return [
            'loaders/fflate.min.js',
            'three_loaders/TGALoader.js',
            'three_loaders/FBXLoader.js'
        ];
    }

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

    EnumerateMeshes (loadedObject, processor)
    {
        loadedObject.traverse ((child) => {
            if (child.isMesh) {
                processor (child);
            }
        });
    }

    GetUpDirection ()
    {
        return OV.Direction.Y;
    }
};

OV.ThreeLoaderDae = class extends OV.ThreeLoader
{
    constructor ()
    {
        super ();
    }

    GetExtension ()
    {
        return 'dae';
    }

    GetExternalLibraries ()
    {
        return [
            'three_loaders/TGALoader.js',
            'three_loaders/ColladaLoader.js'
        ];
    }

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

    EnumerateMeshes (loadedObject, processor)
    {
        loadedObject.scene.traverse ((child) => {
            if (child.isMesh) {
                processor (child);
            }
        });
    }

    GetUpDirection ()
    {
        return OV.Direction.Y;
    }
};

OV.ThreeLoaderVrml = class extends OV.ThreeLoader
{
    constructor ()
    {
        super ();
    }

    GetExtension ()
    {
        return 'wrl';
    }

    GetExternalLibraries ()
    {
        return [
            'three_loaders/chevrotain.min.js',
            'three_loaders/VRMLLoader.js'
        ];
    }

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

    EnumerateMeshes (loadedObject, processor)
    {
        loadedObject.traverse ((child) => {
            if (child.isMesh) {
                let needToProcess = true;
                if (Array.isArray (child.material)) {
                    for (let i = 0; i < child.material.length; i++) {
                        if (child.material[i].side === THREE.BackSide) {
                            needToProcess = false;
                            break;
                        }
                    }
                } else {
                    needToProcess = (child.material.side !== THREE.BackSide);
                }
                if (needToProcess) {
                    processor (child);
                }
            }
        });
    }

    GetUpDirection ()
    {
        return OV.Direction.Y;
    }
};

OV.ThreeLoader3mf = class extends OV.ThreeLoader
{
    constructor ()
    {
        super ();
    }

    GetExtension ()
    {
        return '3mf';
    }

    GetExternalLibraries ()
    {
        return [
            'loaders/fflate.min.js',
            'three_loaders/3MFLoader.js'
        ];
    }

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

    EnumerateMeshes (loadedObject, processor)
    {
        loadedObject.traverse ((child) => {
            if (child.isMesh) {
                processor (child);
            }
        });
    }

    GetUpDirection ()
    {
        return OV.Direction.Z;
    }
};

OV.ImporterThree = class extends OV.ImporterBase
{
    constructor ()
    {
        super ();
        this.loaders = [
            new OV.ThreeLoaderFbx (),
            new OV.ThreeLoaderDae (),
            new OV.ThreeLoaderVrml (),
            new OV.ThreeLoader3mf ()
        ];
    }

    CanImportExtension (extension)
    {
        for (let i = 0; i < this.loaders.length; i++)  {
            let loader = this.loaders[i];
            if (loader.GetExtension () === extension) {
                return true;
            }
        }
        return false;
    }

    GetUpDirection ()
    {
        return this.loader.GetUpDirection ();
    }    
    
    ClearContent ()
    {
        this.loader = null;
    }

    ResetContent ()
    {
        this.loader = null;
    }

    ImportContent (fileContent, onFinish)
    {
        async function LoadLibraries (libraries, onFinish, onError)
        {
            try {
                for (let i = 0; i < libraries.length; i++) {
                    await OV.LoadExternalLibrary (libraries[i]);
                }
            } catch (err) {
                onError ();
            }
            onFinish ();
        }

        this.loader = this.FindLoader ();
        if (this.loader === null) {
            onFinish ();
            return;
        }
        
        const libraries = this.loader.GetExternalLibraries ();
        if (libraries === null) {
            onFinish ();
            return;
        }

        LoadLibraries (libraries, () => {
            this.LoadModel (fileContent, onFinish);    
        }, () => {
            onFinish ();
        });
    }

    FindLoader ()
    {
        for (let i = 0; i < this.loaders.length; i++)  {
            let loader = this.loaders[i];
            if (loader.GetExtension () === this.extension) {
                return loader;
            }
        }
        return null;
    }

    LoadModel (fileContent, onFinish)
    {
        let loadedObject = null;
        let externalFileNames = {};
        let loadingManager = new THREE.LoadingManager (() => {
            if (loadedObject !== null) {
                this.OnThreeObjectsLoaded (loadedObject, externalFileNames, onFinish);
            }
        });

        const mainFileUrl = OV.CreateObjectUrl (fileContent);
        loadingManager.setURLModifier ((url) => {
            if (url === mainFileUrl) {
                return url;
            }
            if (url.startsWith ('blob:')) {
                const name = OV.GetFileName (url);
                const extension = OV.GetFileExtension (url);
                if (extension.length > 0) {
                    const buffer = this.callbacks.getFileBuffer (url);
                    if (buffer !== null) {
                        let objectUrl = OV.CreateObjectUrl (buffer);
                        externalFileNames[objectUrl] = name;
                        return objectUrl;
                    }
                }
            }
            return url;
        });

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

        threeLoader.load (mainFileUrl,
            (object) => {
                loadedObject = object;
            },
            () => {
            },
            (err) => {
                this.SetError (err);
                onFinish ();
            }
        );
    }

    OnThreeObjectsLoaded (loadedObject, externalFileNames, onFinish)
    {
        function ConvertThreeMaterialToMaterial (threeMaterial, externalFileNames)
        {
            function SetColor (color, threeColor)
            {
                color.Set (
                    parseInt (threeColor.r * 255.0, 10),
                    parseInt (threeColor.g * 255.0, 10),
                    parseInt (threeColor.b * 255.0, 10)
                );
            }
        
            function CreateTexture (threeMap, externalFileNames)
            {
                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 (threeMap.image);
                    }
                }

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

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

                try {
                    const dataUrl = GetDataUrl (threeMap.image);
                    const base64Buffer = OV.Base64DataURIToArrayBuffer (dataUrl);
                    let texture = new OV.TextureMap ();
                    let textureName = externalFileNames[threeMap.image.src];
                    if (textureName === undefined) {
                        textureName = 'Embedded_' + threeMap.id.toString () + '.' + OV.GetFileExtensionFromMimeType (base64Buffer.mimeType);
                    }
                    texture.name = textureName;
                    texture.url = dataUrl;
                    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 OV.Material (OV.MaterialType.Phong);
            material.name = threeMaterial.name;
            SetColor (material.color, threeMaterial.color);
            material.opacity = threeMaterial.opacity;
            material.transparent = threeMaterial.transparent;
            material.alphaTest = threeMaterial.alphaTest;
            if (threeMaterial.type === 'MeshPhongMaterial') {
                SetColor (material.specular, threeMaterial.specular);
                material.shininess = threeMaterial.shininess / 100.0;
            }
            material.diffuseMap = CreateTexture (threeMaterial.map, externalFileNames);
            material.normalMap = CreateTexture (threeMaterial.normalMap, externalFileNames);
            material.bumpMap = CreateTexture (threeMaterial.bumpMap, externalFileNames);

            return material;
        }

        function FindMatchingMaterialIndex (model, threeMaterial, materialIdToIndex, externalFileNames)
        {
            let index = materialIdToIndex[threeMaterial.id];
            if (index !== undefined) {
                return index;
            }
            let material = ConvertThreeMaterialToMaterial (threeMaterial, externalFileNames);
            index = model.AddMaterial (material);
            materialIdToIndex[threeMaterial.id] = index;
            return index;
        }

        let materialIdToIndex = {};
        this.loader.EnumerateMeshes (loadedObject, (child) => {
            let materialIndex = null;
            let mesh = null;
            if (Array.isArray (child.material)) {
                mesh = OV.ConvertThreeGeometryToMesh (child.geometry, null);
                if (child.geometry.attributes.color === undefined || child.geometry.attributes.color === null) {
                    let materialIndices = [];
                    for (let i = 0; i < child.material.length; i++) {
                        let material = child.material[i];
                        materialIndices.push (FindMatchingMaterialIndex (this.model, material, materialIdToIndex, externalFileNames));
                    }
                    for (let i = 0; i < child.geometry.groups.length; i++) {
                        let group = child.geometry.groups[i];
                        for (let j = group.start / 3; j < group.start / 3 + group.count / 3; j++) {
                            let triangle = mesh.GetTriangle (j);
                            triangle.SetMaterial (materialIndices[group.materialIndex]);
                        }
                    }
                }
            } else {
                materialIndex = FindMatchingMaterialIndex (this.model, child.material, materialIdToIndex, externalFileNames);
                mesh = OV.ConvertThreeGeometryToMesh (child.geometry, materialIndex);
            }
            if (child.name !== undefined && child.name !== null) {
                mesh.SetName (child.name);
            }
            child.updateWorldMatrix (true, true);
            if (child.matrixWorld !== undefined && child.matrixWorld !== null) {
                const matrix = new OV.Matrix (child.matrixWorld.elements);
                const transformation = new OV.Transformation (matrix);
                const determinant = matrix.Determinant ();
                const mirrorByX = OV.IsNegative (determinant);
                OV.TransformMesh (mesh, transformation);
                if (mirrorByX) {
                    OV.FlipMeshTrianglesOrientation (mesh);
                }
            }
            this.model.AddMesh (mesh);
            child.geometry.dispose ();
        });

        onFinish ();
    }
};
