/*
 * Copyright (C) 2015 United States Government as represented by the Administrator of the
 * National Aeronautics and Space Administration. All Rights Reserved.
 */
/**
 * @exports TriangleMesh
 */
define([
        '../shapes/AbstractMesh',
        '../error/ArgumentError',
        '../shaders/BasicTextureProgram',
        '../geom/BoundingBox',
        '../util/Color',
        '../util/ImageSource',
        '../geom/Location',
        '../util/Logger',
        '../geom/Matrix',
        '../pick/PickedObject',
        '../geom/Position',
        '../shapes/ShapeAttributes',
        '../shapes/SurfacePolygon',
        '../geom/Vec2',
        '../geom/Vec3',
    ],
    function (AbstractMesh,
              ArgumentError,
              BasicTextureProgram,
              BoundingBox,
              Color,
              ImageSource,
              Location,
              Logger,
              Matrix,
              PickedObject,
              Position,
              ShapeAttributes,
              SurfacePolygon,
              Vec2,
              Vec3) {
        "use strict";

        /**
         * Constructs a triangle mesh.
         * @alias TriangleMesh
         * @constructor
         * @augments AbstractMesh
         * @classdesc Represents a 3D triangle mesh.
         * <p>
         *     Altitudes within the mesh's positions are interpreted according to the mesh's altitude mode, which
         *     can be one of the following:
         * <ul>
         *     <li>[WorldWind.ABSOLUTE]{@link WorldWind#ABSOLUTE}</li>
         *     <li>[WorldWind.RELATIVE_TO_GROUND]{@link WorldWind#RELATIVE_TO_GROUND}</li>
         *     <li>[WorldWind.CLAMP_TO_GROUND]{@link WorldWind#CLAMP_TO_GROUND}</li>
         * </ul>
         * If the latter, the mesh positions' altitudes are ignored. (If the mesh should be draped onto the
         * terrain, you might want to use {@link SurfacePolygon} instead.)
         * <p>
         *     Meshes have separate attributes for normal display and highlighted display. They use the interior and
         *     outline attributes of {@link ShapeAttributes}. If those attributes identify an image, that image is
         *     applied to the mesh. Texture coordinates for the image may be specified, but if not specified the full
         *     image is stretched over the full mesh. If texture coordinates are specified, there must be one texture
         *     coordinate for each vertex in the mesh.
         *
         * @param {Position[]} positions An array containing the mesh vertices.
         * There must be no more than 65536 positions. Use [split]{@link TriangleMesh#split} to subdivide large meshes
         * into smaller ones that fit this limit.
         * @param {Number[]} indices An array of integers identifying the positions of each mesh triangle.
         * Each sequence of three indices defines one triangle in the mesh. The indices identify the index of the
         * position in the associated positions array. The indices for each triangle should be in counter-clockwise
         * order to identify the triangles as front-facing.
         * @param {ShapeAttributes} attributes The attributes to associate with this mesh. May be null, in which case
         * default attributes are associated.
         *
         * @throws {ArgumentError} If the specified positions array is null, empty or undefined, the number of indices
         * is less than 3 or too many positions are specified (limit is 65536).
         */
        var TriangleMesh = function (positions, indices, attributes) {
            if (!positions) {
                throw new ArgumentError(
                    Logger.logMessage(Logger.LEVEL_SEVERE, "TriangleMesh", "constructor", "missingPositions"));
            }

            if (positions.length < 1) {
                throw new ArgumentError(
                    Logger.logMessage(Logger.LEVEL_SEVERE, "TriangleMesh", "constructor", "missingPositions"));
            }

            // Check for size limit, which is the max number of available indices for a 16-bit unsigned int.
            if (positions.length > 65536) {
                throw new ArgumentError(
                    Logger.logMessage(Logger.LEVEL_SEVERE, "TriangleMesh", "constructor",
                        "Too many positions. Must be fewer than 65537. Use TriangleMesh.split to split the shape."));
            }

            if (!indices) {
                throw new ArgumentError(
                    Logger.logMessage(Logger.LEVEL_SEVERE, "TriangleMesh", "constructor",
                        "Indices array is null or undefined"));
            }

            if (indices.length < 3) {
                throw new ArgumentError(
                    Logger.logMessage(Logger.LEVEL_SEVERE, "TriangleMesh", "constructor", "Too few indices."));
            }

            AbstractMesh.call(this, attributes);

            // Private. Documentation is with the defined property below and the constructor description above.
            this._positions = positions;

            // Private. Documentation is with the defined property below and the constructor description above.
            this._indices = indices;

            this.referencePosition = this._positions[0];
        };

        TriangleMesh.prototype = Object.create(AbstractMesh.prototype);

        Object.defineProperties(TriangleMesh.prototype, {
            /**
             * This mesh's positions.
             *
             * @type {Position[]}
             * @memberof TriangleMesh.prototype
             */
            positions: {
                get: function () {
                    return this._positions;
                },
                set: function (positions) {
                    if (!positions) {
                        throw new ArgumentError(
                            Logger.logMessage(Logger.LEVEL_SEVERE, "TriangleMesh", "positions", "missingPositions"));
                    }

                    this._positions = positions;
                    this.referencePosition = this._positions[0];
                    this.reset();
                }
            },

            /**
             * The mesh indices, an array of integers identifying the indexes of each triangle. Each index in this
             * array identifies the index of the corresponding position in the [positions]{@link TriangleMesh#positions}
             * array. Each group of three indices in this array identifies the positions of one triangle.
             *
             *
             * @type {Number[]}
             * @memberof TriangleMesh.prototype
             */
            indices: {
                get: function () {
                    return this._indices;
                },
                set: function (indices) {
                    if (!indices) {
                        throw new ArgumentError(
                            Logger.logMessage(Logger.LEVEL_SEVERE, "TriangleMesh", "indices",
                                "Indices array is null or undefined"));
                    }

                    this._indices = indices;
                    this.meshIndices = null;
                    this.reset();
                }
            },

            /**
             * The mesh outline indices, an array of integers identifying the positions in the outline. Each index in
             * this array identifies the index of the corresponding position in the
             * [positions]{@link TriangleMesh#positions} array. The collection of these positions forms the outline
             * of this mesh. May be null, in which case no outline is drawn.
             *
             * @type {Number[]}
             * @default null
             * @memberof TriangleMesh.prototype
             */
            outlineIndices: {
                get: function () {
                    return this._outlineIndices;
                },
                set: function (indices) {
                    this._outlineIndices = indices;
                    this.meshOutlineIndices = null;
                    this.reset();
                }
            },

            /**
             * This mesh's texture coordinates if this mesh is textured. A texture coordinate must be
             * provided for each mesh position. Each texture coordinate is a {@link Vec2} containing the s and t
             * coordinates, in that order. If no texture coordinates are specified then texture is not applied to
             * this mesh.
             * @type {Vec2[]}
             * @default null
             * @memberof TriangleMesh.prototype
             */
            textureCoordinates: {
                get: function () {
                    return this._textureCoordinates;
                },
                set: function (coords) {

                    if (coords && (coords.length != this._positions.length)) {
                        throw new ArgumentError(
                            Logger.logMessage(Logger.LEVEL_SEVERE, "TriangleMesh", "textureCoordinates",
                                "Number of texture coordinates is inconsistent with the currently specified positions."));
                    }

                    this._textureCoordinates = coords;
                    this.reset();
                    this.texCoords = null;
                }
            }
        });

        // Overridden from AbstractShape base class.
        TriangleMesh.prototype.createSurfaceShape = function () {
            if (this._outlineIndices) {
                var boundaries = [];

                for (var i = 0; i < this._outlineIndices.length; i++) {
                    boundaries.push(this._positions[this._outlineIndices[i]]);
                }

                return new SurfacePolygon(boundaries, null);
            } else {
                return null;
            }

        };

        // Overridden from AbstractShape base class.
        TriangleMesh.prototype.computeMeshPoints = function (dc, currentData) {
            var eyeDistSquared = Number.MAX_VALUE,
                eyePoint = dc.navigatorState.eyePoint,
                meshPoints = new Float32Array(this._positions.length * 3),
                pt = new Vec3(0, 0, 0),
                k = 0,
                pos, dSquared;

            for (var i = 0; i < this._positions.length; i++) {
                pos = this._positions[i];

                dc.surfacePointForMode(pos.latitude, pos.longitude, pos.altitude * this._altitudeScale,
                    this.altitudeMode, pt);

                dSquared = pt.distanceToSquared(eyePoint);
                if (dSquared < eyeDistSquared) {
                    eyeDistSquared = dSquared;
                }

                pt.subtract(this.currentData.referencePoint);

                meshPoints[k++] = pt[0];
                meshPoints[k++] = pt[1];
                meshPoints[k++] = pt[2];
            }

            currentData.eyeDistance = Math.sqrt(eyeDistSquared);

            return meshPoints;
        };

        // Overridden from AbstractShape base class.
        TriangleMesh.prototype.computeTexCoords = function () {

            if (!this._textureCoordinates) {
                return null;
            } else {
                // Capture the texture coordinates to a single array parallel to the mesh points array.
                var texCoords = new Float32Array(2 * this._textureCoordinates.length),
                    k = 0;

                for (var i = 0, len = this._textureCoordinates.length; i < len; i++) {
                    var texCoord = this._textureCoordinates[i];

                    texCoords[k++] = texCoord[0];
                    texCoords[k++] = texCoord[1];
                }

                return texCoords;
            }
        };

        // Overridden from AbstractShape base class.
        TriangleMesh.prototype.computeMeshIndices = function () {
            var meshIndices = new Uint16Array(this._indices.length);

            for (var i = 0, len = this._indices.length; i < len; i++) {
                meshIndices[i] = this._indices[i];
            }

            return meshIndices;
        };

        // Overridden from AbstractShape base class.
        TriangleMesh.prototype.computeOutlineIndices = function () {
            if (!this._outlineIndices) {
                return null;
            } else {
                var meshOutlineIndices = new Uint16Array(this._outlineIndices.length);

                for (var i = 0; i < this._outlineIndices.length; i++) {
                    meshOutlineIndices[i] = this._outlineIndices[i];
                }

                return meshOutlineIndices;
            }
        };

        /**
         * Splits a triangle mesh into several meshes, each of which contains fewer than 65536 positions.
         * @param {Position[]} positions An array containing the mesh vertices.
         * @param {Number[]} indices An array of integers identifying the positions of each mesh triangle.
         * Each sequence of three indices defines one triangle in the mesh. The indices identify the index of the
         * position in the associated positions array.
         * @param {Vec2[]} textureCoords The mesh's texture coordinates.
         * @param {Number[]} outlineIndices The mesh's outline indices.
         * @returns {Object[]} An array of objects, each of which defines one subdivision of the full mesh. Each object
         * in the array has the properties of the same name as the input arguments.
         */
        TriangleMesh.split = function (positions, indices, textureCoords, outlineIndices) {
            var splitPositions = [],
                splitTexCoords = [],
                splitIndices = [],
                indexMap = [],
                result = [],
                originalIndex, mappedIndex;

            for (var i = 0; i <= indices.length; i++) {
                if ((i === indices.length) || ((splitPositions.length > 65533) && splitIndices.length % 3 === 0)) {
                    if (splitPositions.length > 0) {
                        var shape = {
                            positions: splitPositions,
                            indices: splitIndices
                        };

                        if (textureCoords) {
                            shape.textureCoords = splitTexCoords;
                        }

                        if (outlineIndices) {
                            var splitOutline = [];
                            for (var j = 0; j < outlineIndices.length; j++) {
                                originalIndex = outlineIndices[j];
                                mappedIndex = indexMap[originalIndex];
                                if (mappedIndex) {
                                    splitOutline.push(indexMap[outlineIndices[j]]);
                                }
                            }

                            shape.outlineIndices = splitOutline;
                        }

                        result.push(shape);
                    }

                    if (i === indices.length) {
                        break;
                    }

                    splitPositions = [];
                    splitIndices = [];
                    indexMap = [];
                }

                originalIndex = indices[i];
                mappedIndex = indexMap[originalIndex];

                if (!mappedIndex) {
                    mappedIndex = splitPositions.length;
                    indexMap[originalIndex] = mappedIndex;

                    splitPositions.push(positions[originalIndex]);

                    if (textureCoords) {
                        splitTexCoords.push(textureCoords[originalIndex]);
                    }
                }

                splitIndices.push(mappedIndex);
            }

            return result;
        };

        return TriangleMesh;
    });