import { Coord3D } from '../geometry/coord3d.js';
import { Direction } from '../geometry/geometry.js';
import { BinaryReader } from '../io/binaryreader.js';
import { ArrayBufferToUtf8String } from '../io/bufferutils.js';
import { RGBColor, IntegerToHexString } from '../model/color.js';
import { PhongMaterial } from '../model/material.js';
import { Mesh } from '../model/mesh.js';
import { Triangle } from '../model/triangle.js';
import { ImporterBase } from './importerbase.js';
import { ParametersFromLine, ReadLines, UpdateMaterialTransparency } from './importerutils.js';
import { Loc } from '../core/localization.js';

const PlyHeaderCheckResult =
{
    Ok : 1,
    NoVertices : 2,
    NoFaces : 3,
    UnknownError : 4
};

class PlyHeader
{
    constructor ()
    {
        this.format = null;
        this.elements = [];
    }

    SetFormat (format)
    {
        this.format = format;
    }

    AddElement (name, count)
    {
        this.elements.push ({
            name : name,
            count : count,
            format : []
        });
    }

    GetElements ()
    {
        return this.elements;
    }

    AddSingleFormat (elemType, name)
    {
        let lastElement = this.elements[this.elements.length - 1];
        lastElement.format.push ({
            name : name,
            isSingle : true,
            elemType : elemType
        });
    }

    AddListFormat (countType, elemType, name)
    {
        let lastElement = this.elements[this.elements.length - 1];
        lastElement.format.push ({
            name : name,
            isSingle : false,
            countType : countType,
            elemType : elemType
        });
    }

    GetElement (name)
    {
        for (let i = 0; i < this.elements.length; i++) {
            let element = this.elements[i];
            if (element.name === name) {
                return element;
            }
        }
        return null;
    }

    Check ()
    {
        let vertex = this.GetElement ('vertex');
        if (vertex === null || vertex.length === 0 || vertex.format.length < 3) {
            return PlyHeaderCheckResult.NoVertices;
        }

        let face = this.GetElement ('face');
        if (this.format === 'ascii') {
            if (face === null || face.count === 0 || face.format.length < 0) {
                return PlyHeaderCheckResult.NoFaces;
            }
        } else if (this.format === 'binary_little_endian' || this.format === 'binary_big_endian') {
            let triStrips = this.GetElement ('tristrips');
            let hasFaces = (face !== null && face.count > 0 && face.format.length > 0);
            let hasTriStrips = (triStrips !== null && triStrips.count > 0 && triStrips.format.length > 0);
            if (!hasFaces && !hasTriStrips) {
                return PlyHeaderCheckResult.NoFaces;
            }
        } else {
            return PlyHeaderCheckResult.UnknownError;
        }

        return PlyHeaderCheckResult.Ok;
    }
}

class PlyMaterialHandler
{
    constructor (model)
    {
        this.model = model;
        this.colorToMaterial = new Map ();
    }

    GetMaterialIndexByColor (color)
    {
        let materialName = 'Color ' +
            IntegerToHexString (color[0]) +
            IntegerToHexString (color[1]) +
            IntegerToHexString (color[2]) +
            IntegerToHexString (color[3]);

        if (this.colorToMaterial.has (materialName)) {
            return this.colorToMaterial.get (materialName);
        } else {
            let material = new PhongMaterial ();
            material.name = materialName;
            material.color = new RGBColor (color[0], color[1], color[2]);
            material.opacity = color[3] / 255.0;
            UpdateMaterialTransparency (material);
            let materialIndex = this.model.AddMaterial (material);
            this.colorToMaterial.set (materialName, materialIndex);
            return materialIndex;
        }
    }
}

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

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

    GetUpDirection ()
    {
        return Direction.Y;
    }

    ClearContent ()
    {
        this.mesh = null;
    }

    ResetContent ()
    {
        this.mesh = new Mesh ();
        this.model.AddMeshToRootNode (this.mesh);
    }

    ImportContent (fileContent, onFinish)
    {
        let headerString = this.GetHeaderContent (fileContent);
        let header = this.ReadHeader (headerString);
        let checkResult = header.Check ();
        if (checkResult === PlyHeaderCheckResult.Ok) {
            if (header.format === 'ascii') {
                let contentString = ArrayBufferToUtf8String (fileContent);
                contentString = contentString.substring (headerString.length);
                this.ReadAsciiContent (header, contentString);
            } else if (header.format === 'binary_little_endian' || header.format === 'binary_big_endian') {
                this.ReadBinaryContent (header, fileContent, headerString.length);
            }
        } else {
            if (checkResult === PlyHeaderCheckResult.NoVertices) {
                this.SetError (Loc ('The model contains no vertices.'));
            } else if (checkResult === PlyHeaderCheckResult.NoFaces) {
                this.SetError (Loc ('The model contains no faces.'));
            } else {
                this.SetError (Loc ('Invalid header information.'));
            }
        }
        onFinish ();
    }

    GetHeaderContent (fileContent)
    {
        let headerContent = '';
        let bufferView = new Uint8Array (fileContent);
        let bufferIndex = 0;
        for (bufferIndex = 0; bufferIndex < fileContent.byteLength; bufferIndex++) {
            headerContent += String.fromCharCode (bufferView[bufferIndex]);
            if (headerContent.endsWith ('end_header')) {
                break;
            }
        }
        bufferIndex += 1;
        while (bufferIndex < fileContent.byteLength) {
            let char = String.fromCharCode (bufferView[bufferIndex]);
            headerContent += char;
            bufferIndex += 1;
            if (char === '\n') {
                break;
            }
        }
        return headerContent;
    }

    ReadHeader (headerContent)
    {
        let header = new PlyHeader ();
        ReadLines (headerContent, (line) => {
            let parameters = ParametersFromLine (line, null);
            if (parameters.length === 0 || parameters[0] === 'comment') {
                return;
            }

            if (parameters[0] === 'ply') {
                return;
            } else if (parameters[0] === 'format' && parameters.length >= 2) {
                header.SetFormat (parameters[1]);
            } else if (parameters[0] === 'element' && parameters.length >= 3) {
                header.AddElement (parameters[1], parseInt (parameters[2], 10));
            } else if (parameters[0] === 'property' && parameters.length >= 3) {
                if (parameters[1] === 'list' && parameters.length >= 5) {
                    header.AddListFormat (parameters[2], parameters[3], parameters[4]);
                } else {
                    header.AddSingleFormat (parameters[1], parameters[2]);
                }
            }
        });

        return header;
    }

    ReadAsciiContent (header, fileContent)
    {
        let vertex = header.GetElement ('vertex');
        let face = header.GetElement ('face');
        let foundVertex = 0;
        let foundFace = 0;
        ReadLines (fileContent, (line) => {
            if (this.WasError ()) {
                return;
            }

            let parameters = ParametersFromLine (line, null);
            if (parameters.length === 0 || parameters[0] === 'comment') {
                return;
            }

            if (foundVertex < vertex.count) {
                if (parameters.length >= 3) {
                    this.mesh.AddVertex (new Coord3D (
                        parseFloat (parameters[0]),
                        parseFloat (parameters[1]),
                        parseFloat (parameters[2])
                    ));
                    foundVertex += 1;
                }
                return;
            }

            if (face !== null && foundFace < face.count) {
                if (parameters.length >= 4) {
                    let vertexCount = parseInt (parameters[0], 10);
                    if (parameters.length < vertexCount + 1) {
                        return;
                    }
                    for (let i = 0; i < vertexCount - 2; i++) {
                        let v0 = parseInt (parameters[1]);
                        let v1 = parseInt (parameters[i + 2]);
                        let v2 = parseInt (parameters[i + 3]);
                        let triangle = new Triangle (v0, v1, v2);
                        this.mesh.AddTriangle (triangle);
                    }
                    foundFace += 1;
                }
                return;
            }
        });
    }

    ReadBinaryContent (header, fileContent, headerLength)
    {
        function ReadByFormat (reader, format)
        {
            function ReadType (reader, type)
            {
                if (type === 'char' || type === 'int8') {
                    return reader.ReadCharacter8 ();
                } else if (type === 'uchar' || type === 'uint8') {
                    return reader.ReadUnsignedCharacter8 ();
                } else if (type === 'short' || type === 'int16') {
                    return reader.ReadInteger16 ();
                } else if (type === 'ushort' || type === 'uint16') {
                    return reader.ReadUnsignedInteger16 ();
                } else if (type === 'int' || type === 'int32') {
                    return reader.ReadInteger32 ();
                } else if (type === 'uint' || type === 'uint32') {
                    return reader.ReadUnsignedInteger32 ();
                } else if (type === 'float' || type === 'float32') {
                    return reader.ReadFloat32 ();
                } else if (type === 'double' || type === 'double64') {
                    return reader.ReadDouble64 ();
                }
                return null;
            }

            if (format.isSingle) {
                return ReadType (reader, format.elemType);
            } else {
                let list = [];
                let count = ReadType (reader, format.countType);
                for (let i = 0; i < count; i++) {
                    list.push (ReadType (reader, format.elemType));
                }
                return list;
            }
        }

        function SkipFormat (reader, format, startIndex)
        {
            for (let i = startIndex; i < format.length; i++) {
                ReadByFormat (reader, format[i]);
            }
        }

        function SkipAndGetColor (reader, format, startIndex)
        {
            let r = null;
            let g = null;
            let b = null;
            let a = 255;

            for (let i = startIndex; i < format.length; i++) {
                let currFormat = format[i];
                let val = ReadByFormat (reader, currFormat);
                if (currFormat.name === 'red') {
                    r = val;
                } else if (currFormat.name === 'green') {
                    g = val;
                } else if (currFormat.name === 'blue') {
                    b = val;
                } else if (currFormat.name === 'alpha') {
                    a = val;
                }
            }

            if (r !== null && g !== null && b !== null) {
                return [r, g, b, a];
            }

            return null;
        }

        let reader = null;
        if (header.format === 'binary_little_endian') {
            reader = new BinaryReader (fileContent, true);
        } else if (header.format === 'binary_big_endian') {
            reader = new BinaryReader (fileContent, false);
        } else {
            return;
        }
        reader.Skip (headerLength);

        let materialHandler = new PlyMaterialHandler (this.model);
        let elements = header.GetElements ();
        for (let elementIndex = 0; elementIndex < elements.length; elementIndex++) {
            let element = elements[elementIndex];
            if (element.name === 'vertex') {
                for (let vertexIndex = 0; vertexIndex < element.count; vertexIndex++) {
                    let x = ReadByFormat (reader, element.format[0]);
                    let y = ReadByFormat (reader, element.format[1]);
                    let z = ReadByFormat (reader, element.format[2]);
                    let color = SkipAndGetColor (reader, element.format, 3);
                    if (color !== null) {
                        this.mesh.AddVertexColor (new RGBColor (color[0], color[1], color[2]));
                    }
                    this.mesh.AddVertex (new Coord3D (x, y, z));
                }
            } else if (element.name === 'face') {
                for (let faceIndex = 0; faceIndex < element.count; faceIndex++) {
                    let vertices = ReadByFormat (reader, element.format[0]);
                    let faceColor = SkipAndGetColor (reader, element.format, 1);
                    for (let i = 0; i < vertices.length - 2; i++) {
                        let v0 = vertices[0];
                        let v1 = vertices[i + 1];
                        let v2 = vertices[i + 2];
                        let triangle = new Triangle (v0, v1, v2);
                        if (faceColor !== null) {
                            triangle.mat = materialHandler.GetMaterialIndexByColor (faceColor);
                        } else if (this.mesh.VertexColorCount () > 0) {
                            triangle.SetVertexColors (v0, v1, v2);
                        }
                        this.mesh.AddTriangle (triangle);
                    }
                }
            } else if (element.name === 'tristrips') {
                for (let triStripIndex = 0; triStripIndex < element.count; triStripIndex++) {
                    let vertices = ReadByFormat (reader, element.format[0]);
                    SkipFormat (reader, element.format, 1);
                    let ccw = true;
                    for (let i = 0; i < vertices.length - 2; i++) {
                        let v0 = vertices[i];
                        let v1 = vertices[i + 1];
                        let v2 = vertices[i + 2];
                        if (v2 === -1) {
                            i += 2;
                            ccw = true;
                            continue;
                        }
                        if (!ccw) {
                            let tmp = v1;
                            v1 = v2;
                            v2 = tmp;
                        }
                        ccw = !ccw;
                        let triangle = new Triangle (v0, v1, v2);
                        this.mesh.AddTriangle (triangle);
                    }
                }
            } else {
                SkipFormat (reader, element.format, 0);
            }
        }
    }
}
