import { Direction } from '../geometry/geometry.js';
import { ImporterBase } from './importerbase.js';
import { GetFileExtension } from '../io/fileutils.js';
import { ConvertThreeGeometryToMesh } from '../threejs/threeutils.js';
import { ArrayBufferToUtf8String } from '../io/bufferutils.js';
import { Node } from '../model/node.js';
import { ColorToMaterialConverter, CreateOcctWorker } from './importerutils.js';
import { RGBAColor } from '../model/color.js';
import { Property, PropertyGroup, PropertyType } from '../model/property.js';
import { Loc } from '../core/localization.js';

import * as fflate from 'fflate';

const DocumentInitResult =
{
    Success : 0,
    NoDocumentXml : 1
};

class FreeCadObject
{
    constructor (name, type)
    {
        this.name = name;
        this.type = type;
        this.shapeName = null;
        this.isVisible = false;
        this.color = null;
        this.fileName = null;
        this.fileContent = null;
        this.inLinkCount = 0;
        this.properties = null;
    }

    IsConvertible ()
    {
        if (this.fileName === null || this.fileContent === null) {
            return false;
        }
        if (!this.isVisible) {
            return false;
        }
        if (this.inLinkCount > 0) {
            return false;
        }
        return true;
    }
}

class FreeCadDocument
{
    constructor ()
    {
        this.files = null;
        this.properties = null;
        this.objectNames = [];
        this.objectData = new Map ();
    }

    Init (fileContent)
    {
        let fileContentBuffer = new Uint8Array (fileContent);
        this.files = fflate.unzipSync (fileContentBuffer);
        if (!this.LoadDocumentXml ()) {
            return DocumentInitResult.NoDocumentXml;
        }

        this.LoadGuiDocumentXml ();
        return DocumentInitResult.Success;
    }

    GetObjectListToConvert ()
    {
        let objectList = [];
        for (let objectName of this.objectNames) {
            let object = this.objectData.get (objectName);
            if (!object.IsConvertible ()) {
                continue;
            }
            objectList.push (object);
        }
        return objectList;
    }

    IsSupportedType (type)
    {
        if (!type.startsWith ('Part::') && !type.startsWith ('PartDesign::')) {
            return false;
        }
        if (type.indexOf ('Part2D') !== -1) {
            return false;
        }
        return true;
    }

    HasFile (fileName)
    {
        return (fileName in this.files);
    }

    LoadDocumentXml ()
    {
        let documentXml = this.GetXMLContent ('Document.xml');
        if (documentXml === null) {
            return false;
        }

        this.properties = new PropertyGroup (Loc ('Properties'));
        let documentElements = documentXml.getElementsByTagName ('Document');
        for (let documentElement of documentElements) {
            for (let childNode of documentElement.childNodes) {
                if (childNode.tagName === 'Properties') {
                    this.GetPropertiesFromElement (childNode, this.properties);
                }
            }
        }

        let objectsElements = documentXml.getElementsByTagName ('Objects');
        for (let objectsElement of objectsElements) {
            let objectElements = objectsElement.getElementsByTagName ('Object');
            for (let objectElement of objectElements) {
                let name = objectElement.getAttribute ('name');
                let type = objectElement.getAttribute ('type');
                if (!this.IsSupportedType (type)) {
                    continue;
                }
                let object = new FreeCadObject (name, type);
                this.objectNames.push (name);
                this.objectData.set (name, object);
            }
        }

        let objectDataElements = documentXml.getElementsByTagName ('ObjectData');
        for (let objectDataElement of objectDataElements) {
            let objectElements = objectDataElement.getElementsByTagName ('Object');
            for (let objectElement of objectElements) {
                let name = objectElement.getAttribute ('name');
                if (!this.objectData.has (name)) {
                    continue;
                }

                let object = this.objectData.get (name);
                object.properties = new PropertyGroup (Loc ('Properties'));
                for (let childNode of objectElement.childNodes) {
                    if (childNode.tagName === 'Properties') {
                        this.GetPropertiesFromElement (childNode, object.properties);
                    }
                }

                let propertyElements = objectElement.getElementsByTagName ('Property');
                for (let propertyElement of propertyElements) {
                    let propertyName = propertyElement.getAttribute ('name');
                    if (propertyName === 'Label') {
                        object.shapeName = this.GetFirstChildValue (propertyElement, 'String', 'value');
                    } else if (propertyName === 'Visibility') {
                        let isVisibleString = this.GetFirstChildValue (propertyElement, 'Bool', 'value');
                        object.isVisible = (isVisibleString === 'true');
                    } else if (propertyName === 'Visible') {
                        let isVisibleString = this.GetFirstChildValue (propertyElement, 'Bool', 'value');
                        object.isVisible = (isVisibleString === 'true');
                    } else if (propertyName === 'Shape') {
                        let fileName = this.GetFirstChildValue (propertyElement, 'Part', 'file');
                        if (!this.HasFile (fileName)) {
                            continue;
                        }
                        let extension = GetFileExtension (fileName);
                        if (extension !== 'brp' && extension !== 'brep') {
                            continue;
                        }
                        object.fileName = fileName;
                        object.fileContent = this.files[fileName];
                    }
                }

                let linkElements = objectElement.getElementsByTagName ('Link');
                for (let linkElement of linkElements) {
                    let linkedName = linkElement.getAttribute ('value');
                    if (this.objectData.has (linkedName)) {
                        let linkedObject = this.objectData.get (linkedName);
                        linkedObject.inLinkCount += 1;
                    }
                }
            }
        }

        return true;
    }

    LoadGuiDocumentXml ()
    {
        let documentXml = this.GetXMLContent ('GuiDocument.xml');
        if (documentXml === null) {
            return false;
        }

        let viewProviderElements = documentXml.getElementsByTagName ('ViewProvider');
        for (let viewProviderElement of viewProviderElements) {
            let name = viewProviderElement.getAttribute ('name');
            if (!this.objectData.has (name)) {
                continue;
            }

            let object = this.objectData.get (name);
            let propertyElements = viewProviderElement.getElementsByTagName ('Property');
            for (let propertyElement of propertyElements) {
                let propertyName = propertyElement.getAttribute ('name');
                if (propertyName === 'Visibility') {
                    let isVisibleString = this.GetFirstChildValue (propertyElement, 'Bool', 'value');
                    object.isVisible = (isVisibleString === 'true');
                } else if (propertyName === 'ShapeColor') {
                    let colorString = this.GetFirstChildValue (propertyElement, 'PropertyColor', 'value');
                    let rgba = parseInt (colorString, 10);
                    object.color = new RGBAColor (
                        rgba >> 24 & 0xff,
                        rgba >> 16 & 0xff,
                        rgba >> 8 & 0xff,
                        255
                    );
                }
            }
        }

        return true;
    }

    GetPropertiesFromElement (propertiesElement, propertyGroup)
    {
        let propertyElements = propertiesElement.getElementsByTagName ('Property');
        for (let propertyElement of propertyElements) {
            let propertyName = propertyElement.getAttribute ('name');
            let propertyType = propertyElement.getAttribute ('type');

            let property = null;
            if (propertyType === 'App::PropertyBool') {
                let propertyValue = this.GetFirstChildValue (propertyElement, 'String', 'bool');
                if (propertyValue !== null && propertyValue.length > 0) {
                    property = new Property (PropertyType.Boolean, propertyName, propertyValue === 'true');
                }
            } else if (propertyType === 'App::PropertyInteger') {
                let propertyValue = this.GetFirstChildValue (propertyElement, 'Integer', 'value');
                if (propertyValue !== null && propertyValue.length > 0) {
                    property = new Property (PropertyType.Integer, propertyName, parseInt (propertyValue));
                }
            } else if (propertyType === 'App::PropertyString') {
                let propertyValue = this.GetFirstChildValue (propertyElement, 'String', 'value');
                if (propertyValue !== null && propertyValue.length > 0) {
                    property = new Property (PropertyType.Text, propertyName, propertyValue);
                }
            } else if (propertyType === 'App::PropertyUUID') {
                let propertyValue = this.GetFirstChildValue (propertyElement, 'Uuid', 'value');
                if (propertyValue !== null && propertyValue.length > 0) {
                    property = new Property (PropertyType.Text, propertyName, propertyValue);
                }
            } else if (propertyType === 'App::PropertyFloat' || propertyType === 'App::PropertyLength' || propertyType === 'App::PropertyDistance' || propertyType === 'App::PropertyArea' || propertyType === 'App::PropertyVolume') {
                let propertyValue = this.GetFirstChildValue (propertyElement, 'Float', 'value');
                if (propertyValue !== null && propertyValue.length > 0) {
                    property = new Property (PropertyType.Number, propertyName, parseFloat (propertyValue));
                }
            }
            if (property !== null) {
                propertyGroup.AddProperty (property);
            }
        }
    }

    GetXMLContent (xmlFileName)
    {
        if (!this.HasFile (xmlFileName)) {
            return null;
        }

        let xmlParser = new DOMParser ();
        let xmlString = ArrayBufferToUtf8String (this.files[xmlFileName]);
        return xmlParser.parseFromString (xmlString, 'text/xml');
    }

    GetFirstChildValue (element, childTagName, childAttribute)
    {
        let childObjects = element.getElementsByTagName (childTagName);
        if (childObjects.length === 0) {
            return null;
        }
        return childObjects[0].getAttribute (childAttribute);
    }
}

export class ImporterFcstd extends ImporterBase
{
    constructor ()
    {
        super ();
        this.worker = null;
        this.document = null;
    }

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

    GetUpDirection ()
    {
        return Direction.Z;
    }

	ClearContent ()
	{
        if (this.worker !== null) {
            this.worker.terminate ();
            this.worker = null;
        }
        this.document = null;
	}

    ResetContent ()
    {
        this.worker = null;
        this.document = new FreeCadDocument ();
    }

    ImportContent (fileContent, onFinish)
    {
        let result = this.document.Init (fileContent);
        if (result === DocumentInitResult.NoDocumentXml) {
            this.SetError (Loc ('No Document.xml found.'));
            onFinish ();
            return;
        }

        if (this.document.properties !== null && this.document.properties.PropertyCount () > 0) {
            this.model.AddPropertyGroup (this.document.properties);
        }

        let objectsToConvert = this.document.GetObjectListToConvert ();
        if (objectsToConvert.length === 0) {
            this.SetError (Loc ('No importable object found.'));
            onFinish ();
            return;
        }

        this.ConvertObjects (objectsToConvert, onFinish);
    }

    ConvertObjects (objects, onFinish)
    {
        CreateOcctWorker ().then ((worker) => {
            this.worker = worker;
            this.worker.addEventListener ('message', (ev) => {
                onFileConverted (ev.data);
            });

            this.worker.addEventListener ('error', (ev) => {
                onFileConverted (null);
            });

            let convertedObjectCount = 0;
            let colorToMaterial = new ColorToMaterialConverter (this.model);
            let onFileConverted = (resultContent) => {
                if (resultContent !== null) {
                    let currentObject = objects[convertedObjectCount];
                    this.OnFileConverted (currentObject, resultContent, colorToMaterial);
                }
                convertedObjectCount += 1;
                if (convertedObjectCount === objects.length) {
                    onFinish ();
                } else {
                    let currentObject = objects[convertedObjectCount];
                    this.worker.postMessage ({
                        format : 'brep',
                        buffer : currentObject.fileContent
                    });
                }
            };

            let currentObject = objects[convertedObjectCount];
            this.worker.postMessage ({
                format : 'brep',
                buffer : currentObject.fileContent
            });
        }).catch (() => {
            this.SetError (Loc ('Failed to load occt-import-js.'));
            onFinish ();
        });
    }

    OnFileConverted (object, resultContent, colorToMaterial)
    {
        if (!resultContent.success || resultContent.meshes.length === 0) {
            return;
        }

        let objectNode = new Node ();
        if (object.shapeName !== null) {
            objectNode.SetName (object.shapeName);
        }

        let objectMeshIndex = 1;
        for (let resultMesh of resultContent.meshes) {
            let materialIndex = null;
            if (object.color !== null) {
                materialIndex = colorToMaterial.GetMaterialIndex (
                    object.color.r,
                    object.color.g,
                    object.color.b,
                    object.color.a
                );
            }
            let mesh = ConvertThreeGeometryToMesh (resultMesh, materialIndex, null);
            if (object.shapeName !== null) {
                let indexString = objectMeshIndex.toString ().padStart (3, '0');
                mesh.SetName (object.shapeName + ' ' + indexString);
            }

            if (object.properties !== null && object.properties.PropertyCount () > 0) {
                mesh.AddPropertyGroup (object.properties);
            }

            let meshIndex = this.model.AddMesh (mesh);
            objectNode.AddMeshIndex (meshIndex);
            objectMeshIndex += 1;
        }

        let rootNode = this.model.GetRootNode ();
        rootNode.AddChildNode (objectNode);
    }
}
