/*
 * Copyright (C) 2014 United States Government as represented by the Administrator of the
 * National Aeronautics and Space Administration. All Rights Reserved.
 */
/**
 * @exports Path
 * @version $Id: Path.js 3345 2015-07-28 20:28:35Z dcollins $
 */
define([
        '../shapes/AbstractShape',
        '../error/ArgumentError',
        '../shaders/BasicTextureProgram',
        '../geom/BoundingBox',
        '../util/Color',
        '../geom/Location',
        '../util/Logger',
        '../geom/Matrix',
        '../pick/PickedObject',
        '../geom/Position',
        '../shapes/ShapeAttributes',
        '../shapes/SurfacePolyline',
        '../geom/Vec2',
        '../geom/Vec3'
    ],
    function (AbstractShape,
              ArgumentError,
              BasicTextureProgram,
              BoundingBox,
              Color,
              Location,
              Logger,
              Matrix,
              PickedObject,
              Position,
              ShapeAttributes,
              SurfacePolyline,
              Vec2,
              Vec3) {
        "use strict";

        /**
         * Constructs a path.
         * @alias Path
         * @constructor
         * @augments AbstractShape
         * @classdesc Represents a line, curve or curtain between specified positions. The path is drawn between input
         * positions to achieve a specified path type, which can be one of the following:
         * <ul>
         *     <li>[WorldWind.GREAT_CIRCLE]{@link WorldWind#GREAT_CIRCLE}</li>
         *     <li>[WorldWind.RHUMB_LINE]{@link WorldWind#RHUMB_LINE}</li>
         *     <li>[WorldWind.LINEAR]{@link WorldWind#LINEAR}</li>
         * </ul>
         * <p>
         *     Paths conform to the terrain if the path's [followTerrain]{@link Path#followTerrain} property is true.
         * <p>
         *     Altitudes within the path's positions are interpreted according to the path'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 path positions' altitudes are ignored.
         * <p>
         *     Paths have separate attributes for normal display and highlighted display. They use the interior and
         *     outline attributes of {@link ShapeAttributes} but do not use the image attributes.
         * <p>
         *     A path displays as a curtain if its [extrude]{@link Path#extrude} property is true. A curtain extends
         *     from the line formed by the path positions to the ground.
         * <p>
         *     This shape uses a {@link SurfacePolyline} when drawing on 2D globes and this shape's
         *     [useSurfaceShapeFor2D]{@link AbstractShape#useSurfaceShapeFor2D} is true.
         *
         * @param {Position[]} positions An array containing the path positions.
         * @param {ShapeAttributes} attributes The attributes to associate with this path. May be null, in which case
         * default attributes are associated.
         * @throws {ArgumentError} If the specified positions array is null or undefined.
         */
        var Path = function (positions, attributes) {
            if (!positions) {
                throw new ArgumentError(
                    Logger.logMessage(Logger.LEVEL_SEVERE, "Path", "constructor", "missingPositions"));
            }

            AbstractShape.call(this, attributes);

            // Private. Documentation is with the defined property below.
            this._positions = positions;

            // Private. Documentation is with the defined property below.
            this._pathType = WorldWind.GREAT_CIRCLE;

            // Private. Documentation is with the defined property below.
            this._terrainConformance = 10;

            // Private. Documentation is with the defined property below.
            this._numSubSegments = 10;

            this.referencePosition = this.determineReferencePosition(this._positions);

            this.scratchPoint = new Vec3(0, 0, 0); // scratch variable
        };

        Path.prototype = Object.create(AbstractShape.prototype);

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

                    this._positions = positions;
                    this.referencePosition = this.determineReferencePosition(this._positions);
                    this.reset();
                }
            },

            /**
             * Indicates whether this path should conform to the terrain.
             * @type {Boolean}
             * @default false
             * @memberof Path.prototype
             */
            followTerrain: {
                get: function () {
                    return this._followTerrain;
                },
                set: function (followTerrain) {
                    this._followTerrain = followTerrain;
                    this.reset();
                }
            },

            /**
             * Specifies how accurately this path must adhere to the terrain when the path is terrain following. The value
             * specifies the maximum number of pixels between tessellation points. Lower values increase accuracy but decrease
             * performance.
             * @type {Number}
             * @default 10
             * @memberof Path.prototype
             */
            terrainConformance: {
                get: function () {
                    return this._terrainConformance;
                },
                set: function (terrainConformance) {
                    this._terrainConformance = terrainConformance;
                    this.reset();
                }
            },

            /**
             * Specifies the number of segments used between specified positions to achieve this path's path type. Higher values
             * cause the path to conform more closely to the path type but decrease performance.
             * <p/>
             * Note: The sub-segments number is ignored when the path follows terrain or when the path type is
             * WorldWind.LINEAR.
             * @type {Number}
             * @default 10
             * @memberof Path.prototype
             */
            numSubSegments: {
                get: function () {
                    return this._numSubSegments;
                },
                set: function (numSubSegments) {
                    this._numSubSegments = numSubSegments >= 0 ? numSubSegments : 0;
                    this.reset();
                }
            },

            /**
             * The type of path to follow when drawing the path. Recognized values are:
             * <ul>
             * <li>[WorldWind.GREAT_CIRCLE]{@link WorldWind#GREAT_CIRCLE}</li>
             * <li>[WorldWind.RHUMB_LINE]{@link WorldWind#RHUMB_LINE}</li>
             * <li>[WorldWind.LINEAR]{@link WorldWind#LINEAR}</li>
             * </ul>
             * @type {String}
             * @default WorldWind.GREAT_CIRCLE
             * @memberof Path.prototype
             */
            pathType: {
                get: function () {
                    return this._pathType;
                },
                set: function (pathType) {
                    this._pathType = pathType;
                    this.reset();
                }
            },

            /**
             * Specifies whether to extrude this path to the ground by drawing a filled interior from the path to the
             * terrain. The filled interior uses this path's interior attributes.
             * @type {Boolean}
             * @default false
             * @memberof Path.prototype
             */
            extrude: {
                get: function () {
                    return this._extrude;
                },
                set: function (extrude) {
                    this._extrude = extrude;
                    this.reset();
                }
            }
        });

        // Intentionally not documented.
        Path.prototype.determineReferencePosition = function (positions) {
            // Assign the first position as the reference position.
            return (positions.length > 0) ? positions[0] : null;
        };

        // Internal. Determines whether this shape's geometry must be re-computed.
        Path.prototype.mustGenerateGeometry = function (dc) {
            if (!this.currentData.tessellatedPoints) {
                return true;
            }

            if (this.currentData.drawInterior !== this.activeAttributes.drawInterior
                || this.currentData.drawVerticals !== this.activeAttributes.drawVerticals) {
                return true;
            }

            if (!this.followTerrain && this.currentData.numSubSegments !== this.numSubSegments) {
                return true;
            }

            if (this.followTerrain && this.currentData.terrainConformance !== this.terrainConformance) {
                return true;
            }

            if (this.altitudeMode === WorldWind.ABSOLUTE) {
                return false;
            }

            return this.currentData.isExpired
        };

        Path.prototype.createSurfaceShape = function () {
            return new SurfacePolyline(this.positions, null);
        };

        // Overridden from AbstractShape base class.
        Path.prototype.doMakeOrderedRenderable = function (dc) {
            // A null reference position is a signal that there are no positions to render.
            if (!this.referencePosition) {
                return null;
            }

            // See if the current shape data can be re-used.
            if (!this.mustGenerateGeometry(dc)) {
                return this;
            }

            // Set the transformation matrix to correspond to the reference position.
            var refPt = this.currentData.referencePoint;
            dc.surfacePointForMode(this.referencePosition.latitude, this.referencePosition.longitude,
                this.referencePosition.altitude, this._altitudeMode, refPt);
            this.currentData.transformationMatrix.setToTranslation(refPt[0], refPt[1], refPt[2]);

            // Tessellate the path in geographic coordinates.
            var tessellatedPositions = this.makeTessellatedPositions(dc);
            if (tessellatedPositions.length < 2) {
                return null;
            }

            // Convert the tessellated geographic coordinates to the Cartesian coordinates that will be rendered.
            var tessellatedPoints = this.computeRenderedPath(dc, tessellatedPositions);

            this.currentData.tessellatedPoints = tessellatedPoints;
            this.currentData.drawInterior = this.activeAttributes.drawInterior;
            this.currentData.drawVerticals = this.activeAttributes.drawVerticals;
            this.currentData.numSubSegments = this.numSubSegments;
            this.currentData.terrainConformance = this.terrainConformance;
            this.resetExpiration(this.currentData);
            this.currentData.fillVbo = true;

            // Create the extent from the Cartesian points. Those points are relative to this path's reference point, so
            // translate the computed extent to the reference point.
            if (!this.currentData.extent) {
                this.currentData.extent = new BoundingBox();
            }
            this.currentData.extent.setToPoints(tessellatedPoints);
            this.currentData.extent.translate(this.currentData.referencePoint);

            return this;
        };

        // Private. Intentionally not documented.
        Path.prototype.makeTessellatedPositions = function (dc) {
            var tessellatedPositions = [],
                navState = dc.navigatorState,
                showVerticals = this.mustDrawVerticals(dc),
                ptA = new Vec3(0, 0, 0),
                ptB = new Vec3(0, 0, 0),
                posA = this._positions[0],
                posB, eyeDistance, pixelSize;

            if (showVerticals) {
                this.currentData.verticalIndices = new Int16Array(this.positions.length * 2);
                this.currentData.verticalIndices[0] = 0;
                this.currentData.verticalIndices[1] = 1;
            }

            tessellatedPositions.push(posA);

            dc.surfacePointForMode(posA.latitude, posA.longitude, posA.altitude, this._altitudeMode, ptA);

            for (var i = 1, len = this._positions.length; i < len; i++) {
                posB = this._positions[i];
                dc.surfacePointForMode(posB.latitude, posB.longitude, posB.altitude, this._altitudeMode, ptB);
                eyeDistance = navState.eyePoint.distanceTo(ptA);
                pixelSize = navState.pixelSizeAtDistance(eyeDistance);
                if (ptA.distanceTo(ptB) < pixelSize * 8 && this.altitudeMode !== WorldWind.ABSOLUTE) {
                    tessellatedPositions.push(posB); // distance is short so no need for sub-segments
                } else {
                    this.makeSegment(dc, posA, posB, ptA, ptB, tessellatedPositions);
                }

                posA = posB;
                ptA.copy(ptB);

                if (showVerticals) {
                    var k = 2 * (tessellatedPositions.length - 1);
                    this.currentData.verticalIndices[i * 2] = k;
                    this.currentData.verticalIndices[i * 2 + 1] = k + 1;
                }
            }

            return tessellatedPositions;
        };

        // Private. Intentionally not documented.
        Path.prototype.makeSegment = function (dc, posA, posB, ptA, ptB, tessellatedPositions) {
            var navState = dc.navigatorState,
                eyePoint = navState.eyePoint,
                pos = new Location(0, 0),
                height = 0,
                arcLength, segmentAzimuth, segmentDistance, s, p, distance;

            // If it's just a straight line and not terrain following, then the segment is just two points.
            if (this._pathType === WorldWind.LINEAR && !this._followTerrain) {
                if (!ptA.equals(ptB)) {
                    tessellatedPositions.push(posB);
                }
                return;
            }

            // Compute the segment length.

            if (this._pathType === WorldWind.LINEAR) {
                segmentDistance = Location.linearDistance(posA, posB);
            } else if (this._pathType === WorldWind.RHUMB_LINE) {
                segmentDistance = Location.rhumbDistance(posA, posB);
            } else {
                segmentDistance = Location.greatCircleDistance(posA, posB);
            }

            if (this._altitudeMode !== WorldWind.CLAMP_TO_GROUND) {
                height = 0.5 * (posA.altitude + posB.altitude);
            }

            arcLength = segmentDistance * (dc.globe.equatorialRadius + height * dc.verticalExaggeration);

            if (arcLength <= 0) { // segment is 0 length
                return;
            }

            // Compute the azimuth to apply while tessellating the segment.

            if (this._pathType === WorldWind.LINEAR) {
                segmentAzimuth = Location.linearAzimuth(posA, posB);
            } else if (this._pathType === WorldWind.RHUMB_LINE) {
                segmentAzimuth = Location.rhumbAzimuth(posA, posB);
            } else {
                segmentAzimuth = Location.greatCircleAzimuth(posA, posB);
            }

            this.scratchPoint.copy(ptA);
            for (s = 0, p = 0; s < 1;) {
                if (this._followTerrain) {
                    p += this._terrainConformance * navState.pixelSizeAtDistance(this.scratchPoint.distanceTo(eyePoint));
                } else {
                    p += arcLength / this._numSubSegments;
                }

                // Stop adding intermediate positions when we reach the arc length, or the remaining distance is in
                // millimeters on Earth.
                if (arcLength < p || arcLength - p < 1e-9)
                    break;

                s = p / arcLength;
                distance = s * segmentDistance;

                if (this._pathType === WorldWind.LINEAR) {
                    Location.linearLocation(posA, segmentAzimuth, distance, pos);
                } else if (this._pathType === WorldWind.RHUMB_LINE) {
                    Location.rhumbLocation(posA, segmentAzimuth, distance, pos);
                } else {
                    Location.greatCircleLocation(posA, segmentAzimuth, distance, pos);
                }

                pos.altitude = (1 - s) * posA.altitude + s * posB.altitude;
                tessellatedPositions.push(new Position(pos.latitude, pos.longitude, pos.altitude));

                if (this._followTerrain) {
                    // Compute a new reference point for eye distance.
                    dc.surfacePointForMode(pos.latitude, pos.longitude, pos.altitude,
                        WorldWind.CLAMP_TO_GROUND, this.scratchPoint);
                }
            }

            tessellatedPositions.push(posB);
        };

        // Private. Intentionally not documented.
        Path.prototype.computeRenderedPath = function (dc, tessellatedPositions) {
            var capturePoles = this.mustDrawInterior(dc) || this.mustDrawVerticals(dc),
                eyeDistSquared = Number.MAX_VALUE,
                eyePoint = dc.navigatorState.eyePoint,
                numPoints = (capturePoles ? 2 : 1) * tessellatedPositions.length,
                tessellatedPoints = new Float32Array(numPoints * 3),
                stride = capturePoles ? 6 : 3,
                pt = new Vec3(0, 0, 0),
                altitudeMode, pos, k, dSquared;

            if (this._followTerrain && this.altitudeMode !== WorldWind.CLAMP_TO_GROUND) {
                altitudeMode = WorldWind.RELATIVE_TO_GROUND;
            } else {
                altitudeMode = this.altitudeMode;
            }

            for (var i = 0, len = tessellatedPositions.length; i < len; i++) {
                pos = tessellatedPositions[i];

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

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

                pt.subtract(this.currentData.referencePoint);

                k = stride * i;
                tessellatedPoints[k] = pt[0];
                tessellatedPoints[k + 1] = pt[1];
                tessellatedPoints[k + 2] = pt[2];

                if (capturePoles) {
                    dc.surfacePointForMode(pos.latitude, pos.longitude, 0, WorldWind.CLAMP_TO_GROUND, pt);

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

                    pt.subtract(this.currentData.referencePoint);

                    tessellatedPoints[k + 3] = pt[0];
                    tessellatedPoints[k + 4] = pt[1];
                    tessellatedPoints[k + 5] = pt[2];
                }
            }

            this.currentData.pointBufferHasExtrusionPoints = capturePoles;
            this.currentData.eyeDistance = Math.sqrt(eyeDistSquared);

            return tessellatedPoints;
        };

        // Private. Intentionally not documented.
        Path.prototype.mustDrawInterior = function (dc) {
            return this.activeAttributes.drawInterior
                && this._extrude
                && this._altitudeMode !== WorldWind.CLAMP_TO_GROUND;
        };

        // Private. Intentionally not documented.
        Path.prototype.mustDrawVerticals = function (dc) {
            return this.activeAttributes.drawOutline && this.activeAttributes.drawVerticals
                && this.altitudeMode !== WorldWind.CLAMP_TO_GROUND;
        };

        // Overridden from AbstractShape base class.
        Path.prototype.doRenderOrdered = function (dc) {
            var gl = dc.currentGlContext,
                program = dc.currentProgram,
                currentData = this.currentData,
                numPoints = currentData.tessellatedPoints.length / 3,
                vboId, opacity, color, pickColor, stride, nPts;

            this.applyMvpMatrix(dc);

            if (!currentData.vboCacheKey) {
                currentData.vboCacheKey = dc.gpuResourceCache.generateCacheKey();
            }

            vboId = dc.gpuResourceCache.resourceForKey(currentData.vboCacheKey);
            if (!vboId) {
                vboId = gl.createBuffer();
                dc.gpuResourceCache.putResource(this.currentData.vboCacheKey, vboId,
                    currentData.tessellatedPoints.length * 4);
                currentData.fillVbo = true;
            }

            // Bind and if necessary fill the VBO. We fill the VBO here rather than in doMakeOrderedRenderable so that
            // there's no possibility of the VBO being ejected from the cache between the time it's filled and
            // the time it's used.
            gl.bindBuffer(gl.ARRAY_BUFFER, vboId);
            if (currentData.fillVbo) {
                gl.bufferData(gl.ARRAY_BUFFER, currentData.tessellatedPoints,
                    gl.STATIC_DRAW);
                dc.frameStatistics.incrementVboLoadCount(1);
            }

            program.loadTextureEnabled(gl, false);

            if (dc.pickingMode) {
                pickColor = dc.uniquePickColor();
            }

            if (this.mustDrawInterior(dc)) {
                color = this.activeAttributes.interiorColor;
                opacity = color.alpha * dc.currentLayer.opacity;
                // Disable writing the shape's fragments to the depth buffer when the interior is semi-transparent.
                if (opacity < 1 && !dc.pickingMode) {
                    gl.depthMask(false);
                }
                program.loadColor(gl, dc.pickingMode ? pickColor : color);
                program.loadOpacity(gl, dc.pickingMode ? (opacity > 0 ? 1 : 0) : opacity);

                gl.vertexAttribPointer(program.vertexPointLocation, 3, gl.FLOAT, false, 0, 0);
                gl.drawArrays(gl.TRIANGLE_STRIP, 0, numPoints);
            }

            if (this.activeAttributes.drawOutline) {
                if ((this.mustDrawVerticals(dc) && this.mustDrawInterior(dc))
                    || this.altitudeMode === WorldWind.CLAMP_TO_GROUND) {
                    // Make the verticals stand out from the interior, or the outline stand out from the terrain.
                    this.applyMvpMatrixForOutline(dc);
                }

                color = this.activeAttributes.outlineColor;
                opacity = color.alpha * dc.currentLayer.opacity;
                // Disable writing the shape's fragments to the depth buffer when the interior is semi-transparent.
                if (opacity < 1 && !dc.pickingMode) {
                    gl.depthMask(false);
                }
                program.loadColor(gl, dc.pickingMode ? pickColor : color);
                program.loadOpacity(gl, dc.pickingMode ? 1 : opacity);

                gl.lineWidth(this.activeAttributes.outlineWidth);

                if (this.currentData.pointBufferHasExtrusionPoints) {
                    stride = 24;
                    nPts = numPoints / 2;
                } else {
                    stride = 12;
                    nPts = numPoints;
                }

                gl.vertexAttribPointer(program.vertexPointLocation, 3, gl.FLOAT, false, stride, 0);
                gl.drawArrays(gl.LINE_STRIP, 0, nPts);

                if (this.mustDrawVerticals(dc)) {
                    if (!currentData.verticalIndicesVboCacheKey) {
                        currentData.verticalIndicesVboCacheKey = dc.gpuResourceCache.generateCacheKey();
                    }

                    vboId = dc.gpuResourceCache.resourceForKey(currentData.verticalIndicesVboCacheKey);
                    if (!vboId) {
                        vboId = gl.createBuffer();
                        dc.gpuResourceCache.putResource(currentData.verticalIndicesVboCacheKey, vboId,
                            currentData.verticalIndices.length * 4);
                        currentData.fillVbo = true;
                    }

                    gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, vboId);
                    if (currentData.fillVbo) {
                        gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, currentData.verticalIndices,
                            gl.STATIC_DRAW);
                        dc.frameStatistics.incrementVboLoadCount(1);
                    }

                    gl.vertexAttribPointer(program.vertexPointLocation, 3, gl.FLOAT, false, 0, 0);
                    gl.drawElements(gl.LINES, currentData.verticalIndices.length,
                        gl.UNSIGNED_SHORT, 0);
                }
            }
            currentData.fillVbo = false;

            if (dc.pickingMode) {
                var po = new PickedObject(pickColor, this.pickDelegate ? this.pickDelegate : this, null, dc.currentLayer,
                    false);
                dc.resolvePick(po);
            }
        };

        // Overridden from AbstractShape base class.
        Path.prototype.beginDrawing = function (dc) {
            var gl = dc.currentGlContext;

            if (this.mustDrawInterior(dc)) {
                gl.disable(gl.CULL_FACE);
            }

            dc.findAndBindProgram(BasicTextureProgram);
            gl.enableVertexAttribArray(dc.currentProgram.vertexPointLocation);
        };

        // Overridden from AbstractShape base class.
        Path.prototype.endDrawing = function (dc) {
            var gl = dc.currentGlContext;

            gl.disableVertexAttribArray(dc.currentProgram.vertexPointLocation);
            gl.depthMask(true);
            gl.lineWidth(1);
            gl.enable(gl.CULL_FACE);
        };

        return Path;
    });