/*global define*/
define([
        '../Core/BoundingSphere',
        '../Core/Cartesian3',
        '../Core/Cartesian4',
        '../Core/Cartographic',
        '../Core/Color',
        '../Core/ComponentDatatype',
        '../Core/defaultValue',
        '../Core/defined',
        '../Core/defineProperties',
        '../Core/destroyObject',
        '../Core/DeveloperError',
        '../Core/EncodedCartesian3',
        '../Core/IndexDatatype',
        '../Core/Intersect',
        '../Core/Math',
        '../Core/Matrix4',
        '../Renderer/BufferUsage',
        '../Renderer/DrawCommand',
        '../Renderer/ShaderSource',
        '../Shaders/PolylineCommon',
        '../Shaders/PolylineFS',
        '../Shaders/PolylineVS',
        './BlendingState',
        './Material',
        './Pass',
        './Polyline',
        './SceneMode'
    ], function(
        BoundingSphere,
        Cartesian3,
        Cartesian4,
        Cartographic,
        Color,
        ComponentDatatype,
        defaultValue,
        defined,
        defineProperties,
        destroyObject,
        DeveloperError,
        EncodedCartesian3,
        IndexDatatype,
        Intersect,
        CesiumMath,
        Matrix4,
        BufferUsage,
        DrawCommand,
        ShaderSource,
        PolylineCommon,
        PolylineFS,
        PolylineVS,
        BlendingState,
        Material,
        Pass,
        Polyline,
        SceneMode) {
    "use strict";

    var SHOW_INDEX = Polyline.SHOW_INDEX;
    var WIDTH_INDEX = Polyline.WIDTH_INDEX;
    var POSITION_INDEX = Polyline.POSITION_INDEX;
    var MATERIAL_INDEX = Polyline.MATERIAL_INDEX;
    //POSITION_SIZE_INDEX is needed for when the polyline's position array changes size.
    //When it does, we need to recreate the indicesBuffer.
    var POSITION_SIZE_INDEX = Polyline.POSITION_SIZE_INDEX;
    var NUMBER_OF_PROPERTIES = Polyline.NUMBER_OF_PROPERTIES;

    var attributeLocations = {
        texCoordExpandWidthAndShow : 0,
        position3DHigh : 1,
        position3DLow : 2,
        position2DHigh : 3,
        position2DLow : 4,
        prevPosition3DHigh : 5,
        prevPosition3DLow : 6,
        prevPosition2DHigh : 7,
        prevPosition2DLow : 8,
        nextPosition3DHigh : 9,
        nextPosition3DLow : 10,
        nextPosition2DHigh : 11,
        nextPosition2DLow : 12,
        pickColor : 13
    };

    /**
     * A renderable collection of polylines.
     * <br /><br />
     * <div align="center">
     * <img src="images/Polyline.png" width="400" height="300" /><br />
     * Example polylines
     * </div>
     * <br /><br />
     * Polylines are added and removed from the collection using {@link PolylineCollection#add}
     * and {@link PolylineCollection#remove}.
     *
     * @alias PolylineCollection
     * @constructor
     *
     * @param {Object} [options] Object with the following properties:
     * @param {Matrix4} [options.modelMatrix=Matrix4.IDENTITY] The 4x4 transformation matrix that transforms each polyline from model to world coordinates.
     * @param {Boolean} [options.debugShowBoundingVolume=false] For debugging only. Determines if this primitive's commands' bounding spheres are shown.
     *
     * @performance For best performance, prefer a few collections, each with many polylines, to
     * many collections with only a few polylines each.  Organize collections so that polylines
     * with the same update frequency are in the same collection, i.e., polylines that do not
     * change should be in one collection; polylines that change every frame should be in another
     * collection; and so on.
     *
     * @see PolylineCollection#add
     * @see PolylineCollection#remove
     * @see Polyline
     * @see LabelCollection
     * @demo {@link http://cesiumjs.org/Cesium/Apps/Sandcastle/index.html?src=Polylines.html|Cesium Sandcastle Polyline Demo}
     *
     * @example
     * // Create a polyline collection with two polylines
     * var polylines = new Cesium.PolylineCollection();
     * polylines.add({
     *   position : Cesium.Cartesian3.fromDegreesArray([
     *     -75.10, 39.57,
     *     -77.02, 38.53,
     *     -80.50, 35.14,
     *     -80.12, 25.46]),
     *   width : 2
     * });
     *
     * polylines.add({
     *   positions : Cesium.Cartesian3.fromDegreesArray([
     *     -73.10, 37.57,
     *     -75.02, 36.53,
     *     -78.50, 33.14,
     *     -78.12, 23.46]),
     *   width : 4
     * });
     */
    var PolylineCollection = function(options) {
        options = defaultValue(options, defaultValue.EMPTY_OBJECT);

        /**
         * The 4x4 transformation matrix that transforms each polyline in this collection from model to world coordinates.
         * When this is the identity matrix, the polylines are drawn in world coordinates, i.e., Earth's WGS84 coordinates.
         * Local reference frames can be used by providing a different transformation matrix, like that returned
         * by {@link Transforms.eastNorthUpToFixedFrame}.
         *
         * @type {Matrix4}
         * @default {@link Matrix4.IDENTITY}
         */
        this.modelMatrix = Matrix4.clone(defaultValue(options.modelMatrix, Matrix4.IDENTITY));
        this._modelMatrix = Matrix4.clone(Matrix4.IDENTITY);

        /**
         * This property is for debugging only; it is not for production use nor is it optimized.
         * <p>
         * Draws the bounding sphere for each draw command in the primitive.
         * </p>
         *
         * @type {Boolean}
         *
         * @default false
         */
        this.debugShowBoundingVolume = defaultValue(options.debugShowBoundingVolume, false);

        this._opaqueRS = undefined;
        this._translucentRS = undefined;

        this._colorCommands = [];
        this._pickCommands = [];

        this._polylinesUpdated = false;
        this._polylinesRemoved = false;
        this._createVertexArray = false;
        this._propertiesChanged = new Uint32Array(NUMBER_OF_PROPERTIES);
        this._polylines = [];
        this._polylineBuckets = {};

        // The buffer usage for each attribute is determined based on the usage of the attribute over time.
        this._buffersUsage = [
                              {bufferUsage: BufferUsage.STATIC_DRAW, frameCount:0}, // SHOW_INDEX
                              {bufferUsage: BufferUsage.STATIC_DRAW, frameCount:0}, // WIDTH_INDEX
                              {bufferUsage: BufferUsage.STATIC_DRAW, frameCount:0}  // POSITION_INDEX
        ];

        this._mode = undefined;

        this._polylinesToUpdate = [];
        this._vertexArrays = [];
        this._positionBuffer = undefined;
        this._pickColorBuffer = undefined;
        this._texCoordExpandWidthAndShowBuffer = undefined;
    };

    defineProperties(PolylineCollection.prototype, {
        /**
         * Returns the number of polylines in this collection.  This is commonly used with
         * {@link PolylineCollection#get} to iterate over all the polylines
         * in the collection.
         * @memberof PolylineCollection.prototype
         * @type {Number}
         */
        length : {
            get : function() {
                removePolylines(this);
                return this._polylines.length;
            }
        }
    });

    /**
     * Creates and adds a polyline with the specified initial properties to the collection.
     * The added polyline is returned so it can be modified or removed from the collection later.
     *
     * @param {Object}[polyline] A template describing the polyline's properties as shown in Example 1.
     * @returns {Polyline} The polyline that was added to the collection.
     *
     * @performance After calling <code>add</code>, {@link PolylineCollection#update} is called and
     * the collection's vertex buffer is rewritten - an <code>O(n)</code> operation that also incurs CPU to GPU overhead.
     * For best performance, add as many polylines as possible before calling <code>update</code>.
     *
     * @exception {DeveloperError} This object was destroyed, i.e., destroy() was called.
     *
     * @see PolylineCollection#remove
     * @see PolylineCollection#removeAll
     * @see PolylineCollection#update
     *
     * @example
     * // Example 1:  Add a polyline, specifying all the default values.
     * var p = polylines.add({
     *   show : true,
     *   positions : ellipsoid.cartographicDegreesToCartesians([
     *     new Cesium.Cartographic2(-75.10, 39.57),
     *     new Cesium.Cartographic2(-77.02, 38.53)]),
     *     width : 1
     * });
     */
    PolylineCollection.prototype.add = function(polyline) {
        var p = new Polyline(polyline, this);
        p._index = this._polylines.length;
        this._polylines.push(p);
        this._createVertexArray = true;
        return p;
    };

    /**
     * Removes a polyline from the collection.
     *
     * @param {Polyline} polyline The polyline to remove.
     * @returns {Boolean} <code>true</code> if the polyline was removed; <code>false</code> if the polyline was not found in the collection.
     *
     * @performance After calling <code>remove</code>, {@link PolylineCollection#update} is called and
     * the collection's vertex buffer is rewritten - an <code>O(n)</code> operation that also incurs CPU to GPU overhead.
     * For best performance, remove as many polylines as possible before calling <code>update</code>.
     * If you intend to temporarily hide a polyline, it is usually more efficient to call
     * {@link Polyline#show} instead of removing and re-adding the polyline.
     *
     * @exception {DeveloperError} This object was destroyed, i.e., destroy() was called.
     *
     * @see PolylineCollection#add
     * @see PolylineCollection#removeAll
     * @see PolylineCollection#update
     * @see Polyline#show
     *
     * @example
     * var p = polylines.add(...);
     * polylines.remove(p);  // Returns true
     */
    PolylineCollection.prototype.remove = function(polyline) {
        if (this.contains(polyline)) {
            this._polylines[polyline._index] = undefined; // Removed later
            this._polylinesRemoved = true;
            this._createVertexArray = true;
            if (defined(polyline._bucket)) {
                var bucket = polyline._bucket;
                bucket.shaderProgram = bucket.shaderProgram && bucket.shaderProgram.destroy();
                bucket.pickShaderProgram = bucket.pickShaderProgram && bucket.pickShaderProgram.destroy();
            }
            polyline._destroy();
            return true;
        }

        return false;
    };

    /**
     * Removes all polylines from the collection.
     *
     * @performance <code>O(n)</code>.  It is more efficient to remove all the polylines
     * from a collection and then add new ones than to create a new collection entirely.
     *
     * @exception {DeveloperError} This object was destroyed, i.e., destroy() was called.
     *
     * @see PolylineCollection#add
     * @see PolylineCollection#remove
     * @see PolylineCollection#update
     *
     * @example
     * polylines.add(...);
     * polylines.add(...);
     * polylines.removeAll();
     */
    PolylineCollection.prototype.removeAll = function() {
        releaseShaders(this);
        destroyPolylines(this);
        this._polylineBuckets = {};
        this._polylinesRemoved = false;
        this._polylines.length = 0;
        this._polylinesToUpdate.length = 0;
        this._createVertexArray = true;
    };

    /**
     * Determines if this collection contains the specified polyline.
     *
     * @param {Polyline} polyline The polyline to check for.
     * @returns {Boolean} true if this collection contains the billboard, false otherwise.
     *
     * @see PolylineCollection#get
     */
    PolylineCollection.prototype.contains = function(polyline) {
        return defined(polyline) && polyline._polylineCollection === this;
    };

    /**
     * Returns the polyline in the collection at the specified index.  Indices are zero-based
     * and increase as polylines are added.  Removing a polyline shifts all polylines after
     * it to the left, changing their indices.  This function is commonly used with
     * {@link PolylineCollection#length} to iterate over all the polylines
     * in the collection.
     *
     * @param {Number} index The zero-based index of the polyline.
     * @returns {Polyline} The polyline at the specified index.
     *
     * @performance If polylines were removed from the collection and
     * {@link PolylineCollection#update} was not called, an implicit <code>O(n)</code>
     * operation is performed.
     *
     * @exception {DeveloperError} This object was destroyed, i.e., destroy() was called.
     *
     * @example
     * // Toggle the show property of every polyline in the collection
     * var len = polylines.length;
     * for (var i = 0; i < len; ++i) {
     *   var p = polylines.get(i);
     *   p.show = !p.show;
     * }
     *
     * @see PolylineCollection#length
     */
    PolylineCollection.prototype.get = function(index) {
        //>>includeStart('debug', pragmas.debug);
        if (!defined(index)) {
            throw new DeveloperError('index is required.');
        }
        //>>includeEnd('debug');

        removePolylines(this);
        return this._polylines[index];
    };

    /**
     * @private
     */
    PolylineCollection.prototype.update = function(context, frameState, commandList) {
        removePolylines(this);

        if (this._polylines.length === 0) {
            return;
        }

        updateMode(this, frameState);

        var projection = frameState.mapProjection;
        var polyline;
        var properties = this._propertiesChanged;

        if (this._createVertexArray || computeNewBuffersUsage(this)) {
            createVertexArrays(this, context, projection);
        } else if (this._polylinesUpdated) {
            // Polylines were modified, but no polylines were added or removed.
            var polylinesToUpdate = this._polylinesToUpdate;
            if (this._mode !== SceneMode.SCENE3D) {
                var updateLength = polylinesToUpdate.length;
                for ( var i = 0; i < updateLength; ++i) {
                    polyline = polylinesToUpdate[i];
                    polyline.update();
                }
            }

            // if a polyline's positions size changes, we need to recreate the vertex arrays and vertex buffers because the indices will be different.
            // if a polyline's material changes, we need to recreate the VAOs and VBOs because they will be batched differenty.
            if (properties[POSITION_SIZE_INDEX] || properties[MATERIAL_INDEX]) {
                createVertexArrays(this, context, projection);
            } else {
                var length = polylinesToUpdate.length;
                var polylineBuckets = this._polylineBuckets;
                for ( var ii = 0; ii < length; ++ii) {
                    polyline = polylinesToUpdate[ii];
                    properties = polyline._propertiesChanged;
                    var bucket = polyline._bucket;
                    var index = 0;
                    for ( var x in polylineBuckets) {
                        if (polylineBuckets.hasOwnProperty(x)) {
                            if (polylineBuckets[x] === bucket) {
                                if (properties[POSITION_INDEX] || properties[SHOW_INDEX] || properties[WIDTH_INDEX]) {
                                    bucket.writeUpdate(index, polyline, this._positionBuffer, this._texCoordExpandWidthAndShowBuffer, projection);
                                }
                                break;
                            }
                            index += polylineBuckets[x].lengthOfPositions;
                        }
                    }
                    polyline._clean();
                }
            }
            polylinesToUpdate.length = 0;
            this._polylinesUpdated = false;
        }

        properties = this._propertiesChanged;
        for ( var k = 0; k < NUMBER_OF_PROPERTIES; ++k) {
            properties[k] = 0;
        }

        var modelMatrix = Matrix4.IDENTITY;
        if (frameState.mode === SceneMode.SCENE3D) {
            modelMatrix = this.modelMatrix;
        }

        var pass = frameState.passes;
        var useDepthTest = (frameState.morphTime !== 0.0);

        if (!defined(this._opaqueRS) || this._opaqueRS.depthTest.enabled !== useDepthTest) {
            this._opaqueRS = context.createRenderState({
                depthMask : useDepthTest,
                depthTest : {
                    enabled : useDepthTest
                }
            });
        }

        if (!defined(this._translucentRS) || this._translucentRS.depthTest.enabled !== useDepthTest) {
            this._translucentRS = context.createRenderState({
                blending : BlendingState.ALPHA_BLEND,
                depthMask : !useDepthTest,
                depthTest : {
                    enabled : useDepthTest
                }
            });
        }

        if (pass.render) {
            var colorList = this._colorCommands;
            createCommandLists(this, context, frameState, colorList, commandList, modelMatrix, true);
        }

        if (pass.pick) {
            var pickList = this._pickCommands;
            createCommandLists(this, context, frameState, pickList, commandList, modelMatrix, false);
        }
    };

    var boundingSphereScratch = new BoundingSphere();
    var boundingSphereScratch2 = new BoundingSphere();

    function createCommandLists(polylineCollection, context, frameState, commands, commandList, modelMatrix, renderPass) {
        var commandsLength = commands.length;
        var commandIndex = 0;
        var cloneBoundingSphere = true;

        var vertexArrays = polylineCollection._vertexArrays;
        var debugShowBoundingVolume = polylineCollection.debugShowBoundingVolume;

        var length = vertexArrays.length;
        for ( var m = 0; m < length; ++m) {
            var va = vertexArrays[m];
            var buckets = va.buckets;
            var bucketLength = buckets.length;

            for ( var n = 0; n < bucketLength; ++n) {
                var bucketLocator = buckets[n];

                var offset = bucketLocator.offset;
                var sp = renderPass ? bucketLocator.bucket.shaderProgram : bucketLocator.bucket.pickShaderProgram;

                var polylines = bucketLocator.bucket.polylines;
                var polylineLength = polylines.length;
                var currentId;
                var currentMaterial;
                var count = 0;
                var command;

                for (var s = 0; s < polylineLength; ++s) {
                    var polyline = polylines[s];
                    var mId = createMaterialId(polyline._material);
                    if (mId !== currentId) {
                        if (defined(currentId) && count > 0) {
                            var translucent = currentMaterial.isTranslucent();

                            if (commandIndex >= commandsLength) {
                                command = new DrawCommand({
                                    owner : polylineCollection
                                });
                                commands.push(command);
                            } else {
                                command = commands[commandIndex];
                            }

                            ++commandIndex;

                            command.boundingVolume = BoundingSphere.clone(boundingSphereScratch, command.boundingVolume);
                            command.modelMatrix = modelMatrix;
                            command.shaderProgram = sp;
                            command.vertexArray = va.va;
                            command.renderState = translucent ? polylineCollection._translucentRS : polylineCollection._opaqueRS;
                            command.pass = translucent ? Pass.TRANSLUCENT : Pass.OPAQUE;
                            command.debugShowBoundingVolume = renderPass ? debugShowBoundingVolume : false;

                            command.uniformMap = currentMaterial._uniforms;
                            command.count = count;
                            command.offset = offset;

                            offset += count;
                            count = 0;
                            cloneBoundingSphere = true;

                            commandList.push(command);
                        }

                        currentMaterial = polyline._material;
                        currentMaterial.update(context);
                        currentId = mId;
                    }

                    var locators = polyline._locatorBuckets;
                    var locatorLength = locators.length;
                    for (var t = 0; t < locatorLength; ++t) {
                        var locator = locators[t];
                        if (locator.locator === bucketLocator) {
                            count += locator.count;
                        }
                    }

                    var boundingVolume;
                    if (frameState.mode === SceneMode.SCENE3D) {
                        boundingVolume = polyline._boundingVolumeWC;
                    } else if (frameState.mode === SceneMode.COLUMBUS_VIEW) {
                        boundingVolume = polyline._boundingVolume2D;
                    } else if (frameState.mode === SceneMode.SCENE2D) {
                        if (defined(polyline._boundingVolume2D)) {
                            boundingVolume = BoundingSphere.clone(polyline._boundingVolume2D, boundingSphereScratch2);
                            boundingVolume.center.x = 0.0;
                        }
                    } else if (defined(polyline._boundingVolumeWC) && defined(polyline._boundingVolume2D)) {
                        boundingVolume = BoundingSphere.union(polyline._boundingVolumeWC, polyline._boundingVolume2D, boundingSphereScratch2);
                    }

                    if (cloneBoundingSphere) {
                        cloneBoundingSphere = false;
                        BoundingSphere.clone(boundingVolume, boundingSphereScratch);
                    } else {
                        BoundingSphere.union(boundingVolume, boundingSphereScratch, boundingSphereScratch);
                    }
                }

                if (defined(currentId) && count > 0) {
                    if (commandIndex >= commandsLength) {
                        command = new DrawCommand({
                            owner : polylineCollection
                        });
                        commands.push(command);
                    } else {
                        command = commands[commandIndex];
                    }

                    ++commandIndex;

                    command.boundingVolume = BoundingSphere.clone(boundingSphereScratch, command.boundingVolume);
                    command.modelMatrix = modelMatrix;
                    command.shaderProgram = sp;
                    command.vertexArray = va.va;
                    command.renderState = currentMaterial.isTranslucent() ? polylineCollection._translucentRS : polylineCollection._opaqueRS;
                    command.pass = currentMaterial.isTranslucent() ? Pass.TRANSLUCENT : Pass.OPAQUE;
                    command.debugShowBoundingVolume = renderPass ? debugShowBoundingVolume : false;

                    command.uniformMap = currentMaterial._uniforms;
                    command.count = count;
                    command.offset = offset;

                    cloneBoundingSphere = true;

                    commandList.push(command);
                }

                currentId = undefined;
            }
        }

        commands.length = commandIndex;
    }

    /**
     * Returns true if this object was destroyed; otherwise, false.
     * <br /><br />
     * If this object was destroyed, it should not be used; calling any function other than
     * <code>isDestroyed</code> will result in a {@link DeveloperError} exception.
     *
     * @returns {Boolean} <code>true</code> if this object was destroyed; otherwise, <code>false</code>.
     *
     * @see PolylineCollection#destroy
     */
    PolylineCollection.prototype.isDestroyed = function() {
        return false;
    };

    /**
     * Destroys the WebGL resources held by this object.  Destroying an object allows for deterministic
     * release of WebGL resources, instead of relying on the garbage collector to destroy this object.
     * <br /><br />
     * Once an object is destroyed, it should not be used; calling any function other than
     * <code>isDestroyed</code> will result in a {@link DeveloperError} exception.  Therefore,
     * assign the return value (<code>undefined</code>) to the object as done in the example.
     *
     * @returns {undefined}
     *
     * @exception {DeveloperError} This object was destroyed, i.e., destroy() was called.
     *
     * @see PolylineCollection#isDestroyed
     *
     * @example
     * polylines = polylines && polylines.destroy();
     */
    PolylineCollection.prototype.destroy = function() {
        destroyVertexArrays(this);
        releaseShaders(this);
        destroyPolylines(this);
        return destroyObject(this);
    };

    function computeNewBuffersUsage(collection) {
        var buffersUsage = collection._buffersUsage;
        var usageChanged = false;

        var properties = collection._propertiesChanged;
        //subtract 2 from NUMBER_OF_PROPERTIES because we don't care about POSITION_SIZE_INDEX or MATERIAL_INDEX property change.
        for ( var k = 0; k < NUMBER_OF_PROPERTIES - 2; ++k) {
            var bufferUsage = buffersUsage[k];
            if (properties[k]) {
                if (bufferUsage.bufferUsage !== BufferUsage.STREAM_DRAW) {
                    usageChanged = true;
                    bufferUsage.bufferUsage = BufferUsage.STREAM_DRAW;
                    bufferUsage.frameCount = 100;
                } else {
                    bufferUsage.frameCount = 100;
                }
            } else {
                if (bufferUsage.bufferUsage !== BufferUsage.STATIC_DRAW) {
                    if (bufferUsage.frameCount === 0) {
                        usageChanged = true;
                        bufferUsage.bufferUsage = BufferUsage.STATIC_DRAW;
                    } else {
                        bufferUsage.frameCount--;
                    }
                }
            }
        }
        return usageChanged;
    }

    var emptyVertexBuffer = [0.0, 0.0, 0.0];

    function createVertexArrays(collection, context, projection) {
        collection._createVertexArray = false;
        releaseShaders(collection);
        destroyVertexArrays(collection);
        sortPolylinesIntoBuckets(collection);

        //stores all of the individual indices arrays.
        var totalIndices = [[]];
        var indices = totalIndices[0];

        //used to determine the vertexBuffer offset if the indicesArray goes over 64k.
        //if it's the same polyline while it goes over 64k, the offset needs to backtrack componentsPerAttribute * componentDatatype bytes
        //so that the polyline looks contiguous.
        //if the polyline ends at the 64k mark, then the offset is just 64k * componentsPerAttribute * componentDatatype
        var vertexBufferOffset = [0];
        var offset = 0;
        var vertexArrayBuckets = [[]];
        var totalLength = 0;
        var polylineBuckets = collection._polylineBuckets;
        var x;
        var bucket;
        for (x in polylineBuckets) {
            if (polylineBuckets.hasOwnProperty(x)) {
                bucket = polylineBuckets[x];
                bucket.updateShader(context);
                totalLength += bucket.lengthOfPositions;
            }
        }

        if (totalLength > 0) {
            var mode = collection._mode;

            var positionArray = new Float32Array(6 * totalLength * 3);
            var pickColorArray = new Uint8Array(totalLength * 4);
            var texCoordExpandWidthAndShowArray = new Float32Array(totalLength * 4);
            var position3DArray;

            var positionIndex = 0;
            var colorIndex = 0;
            var texCoordExpandWidthAndShowIndex = 0;
            for (x in polylineBuckets) {
                if (polylineBuckets.hasOwnProperty(x)) {
                    bucket = polylineBuckets[x];
                    bucket.write(positionArray, pickColorArray, texCoordExpandWidthAndShowArray, positionIndex, colorIndex, texCoordExpandWidthAndShowIndex, context, projection);

                    if (mode === SceneMode.MORPHING) {
                        if (!defined(position3DArray)) {
                            position3DArray = new Float32Array(6 * totalLength * 3);
                        }
                        bucket.writeForMorph(position3DArray, positionIndex);
                    }

                    var bucketLength = bucket.lengthOfPositions;
                    positionIndex += 6 * bucketLength * 3;
                    colorIndex += bucketLength * 4;
                    texCoordExpandWidthAndShowIndex += bucketLength * 4;
                    offset = bucket.updateIndices(totalIndices, vertexBufferOffset, vertexArrayBuckets, offset);
                }
            }

            var positionBufferUsage = collection._buffersUsage[POSITION_INDEX].bufferUsage;
            var showBufferUsage = collection._buffersUsage[SHOW_INDEX].bufferUsage;
            var widthBufferUsage = collection._buffersUsage[WIDTH_INDEX].bufferUsage;
            var texCoordExpandWidthAndShowBufferUsage = (showBufferUsage === BufferUsage.STREAM_DRAW || widthBufferUsage === BufferUsage.STREAM_DRAW) ? BufferUsage.STREAM_DRAW : BufferUsage.STATIC_DRAW;

            collection._positionBuffer = context.createVertexBuffer(positionArray, positionBufferUsage);
            var position3DBuffer;
            if (defined(position3DArray)) {
                position3DBuffer = context.createVertexBuffer(position3DArray, positionBufferUsage);
            }
            collection._pickColorBuffer = context.createVertexBuffer(pickColorArray, BufferUsage.STATIC_DRAW);
            collection._texCoordExpandWidthAndShowBuffer = context.createVertexBuffer(texCoordExpandWidthAndShowArray, texCoordExpandWidthAndShowBufferUsage);

            var pickColorSizeInBytes = 4 * Uint8Array.BYTES_PER_ELEMENT;
            var positionSizeInBytes = 3 * Float32Array.BYTES_PER_ELEMENT;
            var texCoordExpandWidthAndShowSizeInBytes = 4 * Float32Array.BYTES_PER_ELEMENT;

            var vbo = 0;
            var numberOfIndicesArrays = totalIndices.length;
            for ( var k = 0; k < numberOfIndicesArrays; ++k) {
                indices = totalIndices[k];

                if (indices.length > 0) {
                    var indicesArray = new Uint16Array(indices);
                    var indexBuffer = context.createIndexBuffer(indicesArray, BufferUsage.STATIC_DRAW, IndexDatatype.UNSIGNED_SHORT);

                    vbo += vertexBufferOffset[k];

                    var positionHighOffset = 6 * (k * (positionSizeInBytes * CesiumMath.SIXTY_FOUR_KILOBYTES) - vbo * positionSizeInBytes);//componentsPerAttribute(3) * componentDatatype(4)
                    var positionLowOffset = positionSizeInBytes + positionHighOffset;
                    var prevPositionHighOffset =  positionSizeInBytes + positionLowOffset;
                    var prevPositionLowOffset = positionSizeInBytes + prevPositionHighOffset;
                    var nextPositionHighOffset = positionSizeInBytes + prevPositionLowOffset;
                    var nextPositionLowOffset = positionSizeInBytes + nextPositionHighOffset;
                    var vertexPickColorBufferOffset = k * (pickColorSizeInBytes * CesiumMath.SIXTY_FOUR_KILOBYTES) - vbo * pickColorSizeInBytes;
                    var vertexTexCoordExpandWidthAndShowBufferOffset = k * (texCoordExpandWidthAndShowSizeInBytes * CesiumMath.SIXTY_FOUR_KILOBYTES) - vbo * texCoordExpandWidthAndShowSizeInBytes;

                    var attributes = [{
                        index : attributeLocations.position3DHigh,
                        componentsPerAttribute : 3,
                        componentDatatype : ComponentDatatype.FLOAT,
                        offsetInBytes : positionHighOffset,
                        strideInBytes : 6 * positionSizeInBytes
                    }, {
                        index : attributeLocations.position3DLow,
                        componentsPerAttribute : 3,
                        componentDatatype : ComponentDatatype.FLOAT,
                        offsetInBytes : positionLowOffset,
                        strideInBytes : 6 * positionSizeInBytes
                    }, {
                        index : attributeLocations.position2DHigh,
                        componentsPerAttribute : 3,
                        componentDatatype : ComponentDatatype.FLOAT,
                        offsetInBytes : positionHighOffset,
                        strideInBytes : 6 * positionSizeInBytes
                    }, {
                        index : attributeLocations.position2DLow,
                        componentsPerAttribute : 3,
                        componentDatatype : ComponentDatatype.FLOAT,
                        offsetInBytes : positionLowOffset,
                        strideInBytes : 6 * positionSizeInBytes
                    }, {
                        index : attributeLocations.prevPosition3DHigh,
                        componentsPerAttribute : 3,
                        componentDatatype : ComponentDatatype.FLOAT,
                        offsetInBytes : prevPositionHighOffset,
                        strideInBytes : 6 * positionSizeInBytes
                    }, {
                        index : attributeLocations.prevPosition3DLow,
                        componentsPerAttribute : 3,
                        componentDatatype : ComponentDatatype.FLOAT,
                        offsetInBytes : prevPositionLowOffset,
                        strideInBytes : 6 * positionSizeInBytes
                    }, {
                        index : attributeLocations.prevPosition2DHigh,
                        componentsPerAttribute : 3,
                        componentDatatype : ComponentDatatype.FLOAT,
                        offsetInBytes : prevPositionHighOffset,
                        strideInBytes : 6 * positionSizeInBytes
                    }, {
                        index : attributeLocations.prevPosition2DLow,
                        componentsPerAttribute : 3,
                        componentDatatype : ComponentDatatype.FLOAT,
                        offsetInBytes : prevPositionLowOffset,
                        strideInBytes : 6 * positionSizeInBytes
                    }, {
                        index : attributeLocations.nextPosition3DHigh,
                        componentsPerAttribute : 3,
                        componentDatatype : ComponentDatatype.FLOAT,
                        offsetInBytes : nextPositionHighOffset,
                        strideInBytes : 6 * positionSizeInBytes
                    }, {
                        index : attributeLocations.nextPosition3DLow,
                        componentsPerAttribute : 3,
                        componentDatatype : ComponentDatatype.FLOAT,
                        offsetInBytes : nextPositionLowOffset,
                        strideInBytes : 6 * positionSizeInBytes
                    }, {
                        index : attributeLocations.nextPosition2DHigh,
                        componentsPerAttribute : 3,
                        componentDatatype : ComponentDatatype.FLOAT,
                        offsetInBytes : nextPositionHighOffset,
                        strideInBytes : 6 * positionSizeInBytes
                    }, {
                        index : attributeLocations.nextPosition2DLow,
                        componentsPerAttribute : 3,
                        componentDatatype : ComponentDatatype.FLOAT,
                        offsetInBytes : nextPositionLowOffset,
                        strideInBytes : 6 * positionSizeInBytes
                    }, {
                        index : attributeLocations.texCoordExpandWidthAndShow,
                        componentsPerAttribute : 4,
                        componentDatatype : ComponentDatatype.FLOAT,
                        vertexBuffer : collection._texCoordExpandWidthAndShowBuffer,
                        offsetInBytes : vertexTexCoordExpandWidthAndShowBufferOffset
                    }, {
                        index : attributeLocations.pickColor,
                        componentsPerAttribute : 4,
                        componentDatatype : ComponentDatatype.UNSIGNED_BYTE,
                        vertexBuffer : collection._pickColorBuffer,
                        offsetInBytes : vertexPickColorBufferOffset,
                        normalize : true
                    }];

                    var buffer3D;
                    var bufferProperty3D;
                    var buffer2D;
                    var bufferProperty2D;

                    if (mode === SceneMode.SCENE3D) {
                        buffer3D = collection._positionBuffer;
                        bufferProperty3D = 'vertexBuffer';
                        buffer2D = emptyVertexBuffer;
                        bufferProperty2D = 'value';
                    } else if (mode === SceneMode.SCENE2D || mode === SceneMode.COLUMBUS_VIEW) {
                        buffer3D = emptyVertexBuffer;
                        bufferProperty3D = 'value';
                        buffer2D = collection._positionBuffer;
                        bufferProperty2D = 'vertexBuffer';
                    } else {
                        buffer3D = position3DBuffer;
                        bufferProperty3D = 'vertexBuffer';
                        buffer2D = collection._positionBuffer;
                        bufferProperty2D = 'vertexBuffer';
                    }

                    attributes[0][bufferProperty3D] = buffer3D;
                    attributes[1][bufferProperty3D] = buffer3D;
                    attributes[2][bufferProperty2D] = buffer2D;
                    attributes[3][bufferProperty2D] = buffer2D;
                    attributes[4][bufferProperty3D] = buffer3D;
                    attributes[5][bufferProperty3D] = buffer3D;
                    attributes[6][bufferProperty2D] = buffer2D;
                    attributes[7][bufferProperty2D] = buffer2D;
                    attributes[8][bufferProperty3D] = buffer3D;
                    attributes[9][bufferProperty3D] = buffer3D;
                    attributes[10][bufferProperty2D] = buffer2D;
                    attributes[11][bufferProperty2D] = buffer2D;

                    var va = context.createVertexArray(attributes, indexBuffer);
                    collection._vertexArrays.push({
                        va : va,
                        buckets : vertexArrayBuckets[k]
                    });
                }
            }
        }
    }

    var scratchUniformArray = [];
    function createMaterialId(material) {
        var uniforms = Material._uniformList[material.type];
        var length = uniforms.length;
        scratchUniformArray.length = 2.0 * length;

        var index = 0;
        for (var i = 0; i < length; ++i) {
            var uniform = uniforms[i];
            scratchUniformArray[index] = uniform;
            scratchUniformArray[index + 1] = material._uniforms[uniform]();
            index += 2;
        }

        return material.type + ':' + JSON.stringify(scratchUniformArray);
    }

    function sortPolylinesIntoBuckets(collection) {
        var mode = collection._mode;
        var modelMatrix = collection._modelMatrix;

        var polylineBuckets = collection._polylineBuckets = {};
        var polylines = collection._polylines;
        var length = polylines.length;
        for ( var i = 0; i < length; ++i) {
            var p = polylines[i];
            if (p._actualPositions.length > 1) {
                p.update();
                var material = p.material;
                var value = polylineBuckets[material.type];
                if (!defined(value)) {
                    value = polylineBuckets[material.type] = new PolylineBucket(material, mode, modelMatrix);
                }
                value.addPolyline(p);
            }
        }
    }

    function updateMode(collection, frameState) {
        var mode = frameState.mode;

        if (collection._mode !== mode || (!Matrix4.equals(collection._modelMatrix, collection.modelMatrix))) {
            collection._mode = mode;
            collection._modelMatrix = Matrix4.clone(collection.modelMatrix);
            collection._createVertexArray = true;
        }
    }

    function removePolylines(collection) {
        if (collection._polylinesRemoved) {
            collection._polylinesRemoved = false;

            var polylines = [];

            var length = collection._polylines.length;
            for ( var i = 0, j = 0; i < length; ++i) {
                var polyline = collection._polylines[i];
                if (defined(polyline)) {
                    polyline._index = j++;
                    polylines.push(polyline);
                }
            }

            collection._polylines = polylines;
        }
    }

    function releaseShaders(collection) {
        var polylines = collection._polylines;
        var length = polylines.length;
        for ( var i = 0; i < length; ++i) {
            if (defined(polylines[i])) {
                var bucket = polylines[i]._bucket;
                if (defined(bucket)) {
                    bucket.shaderProgram = bucket.shaderProgram && bucket.shaderProgram.destroy();
                }
            }
        }
    }

    function destroyVertexArrays(collection) {
        var length = collection._vertexArrays.length;
        for ( var t = 0; t < length; ++t) {
            collection._vertexArrays[t].va.destroy();
        }
        collection._vertexArrays.length = 0;
    }

    PolylineCollection.prototype._updatePolyline = function(polyline, propertyChanged) {
        this._polylinesUpdated = true;
        this._polylinesToUpdate.push(polyline);
        ++this._propertiesChanged[propertyChanged];
    };

    function destroyPolylines(collection) {
        var polylines = collection._polylines;
        var length = polylines.length;
        for ( var i = 0; i < length; ++i) {
            if (defined(polylines[i])) {
                polylines[i]._destroy();
            }
        }
    }

    function VertexArrayBucketLocator(count, offset, bucket) {
        this.count = count;
        this.offset = offset;
        this.bucket = bucket;
    }

    var PolylineBucket = function(material, mode, modelMatrix) {
        this.polylines = [];
        this.lengthOfPositions = 0;
        this.material = material;
        this.shaderProgram = undefined;
        this.pickShaderProgram = undefined;
        this.mode = mode;
        this.modelMatrix = modelMatrix;
    };

    PolylineBucket.prototype.addPolyline = function(p) {
        var polylines = this.polylines;
        polylines.push(p);
        p._actualLength = this.getPolylinePositionsLength(p);
        this.lengthOfPositions += p._actualLength;
        p._bucket = this;
    };

    PolylineBucket.prototype.updateShader = function(context) {
        if (defined(this.shaderProgram)) {
            return;
        }

        var vs = new ShaderSource({
            sources : [PolylineCommon, PolylineVS]
        });
        var fs = new ShaderSource({
            sources : [this.material.shaderSource, PolylineFS]
        });
        var fsPick = new ShaderSource({
            sources : fs.sources,
            pickColorQualifier : 'varying'
        });
        this.shaderProgram = context.createShaderProgram(vs, fs, attributeLocations);
        this.pickShaderProgram = context.createShaderProgram(vs, fsPick, attributeLocations);
    };

    function intersectsIDL(polyline) {
        return Cartesian3.dot(Cartesian3.UNIT_X, polyline._boundingVolume.center) < 0 ||
            polyline._boundingVolume.intersect(Cartesian4.UNIT_Y) === Intersect.INTERSECTING;
    }

    PolylineBucket.prototype.getPolylinePositionsLength = function(polyline) {
        var length;
        if (this.mode === SceneMode.SCENE3D || !intersectsIDL(polyline)) {
            length = polyline._actualPositions.length;
            return length * 4.0 - 4.0;
        }

        var count = 0;
        var segmentLengths = polyline._segments.lengths;
        length = segmentLengths.length;
        for (var i = 0; i < length; ++i) {
            count += segmentLengths[i] * 4.0 - 4.0;
        }

        return count;
    };

    var scratchWritePosition = new Cartesian3();
    var scratchWritePrevPosition = new Cartesian3();
    var scratchWriteNextPosition = new Cartesian3();
    var scratchWriteVector = new Cartesian3();

    PolylineBucket.prototype.write = function(positionArray, pickColorArray, texCoordExpandWidthAndShowArray, positionIndex, colorIndex, texCoordExpandWidthAndShowIndex, context, projection) {
        var mode = this.mode;
        var polylines = this.polylines;
        var length = polylines.length;
        for ( var i = 0; i < length; ++i) {
            var polyline = polylines[i];
            var width = polyline.width;
            var show = polyline.show && width > 0.0;
            var segments = this.getSegments(polyline, projection);
            var positions = segments.positions;
            var lengths = segments.lengths;
            var positionsLength = positions.length;

            var pickColor = polyline.getPickId(context).color;

            var segmentIndex = 0;
            var count = 0;
            var position;

            for ( var j = 0; j < positionsLength; ++j) {
                if (j === 0) {
                    if (polyline._loop) {
                        position = positions[positionsLength - 2];
                    } else {
                        position = scratchWriteVector;
                        Cartesian3.subtract(positions[0], positions[1], position);
                        Cartesian3.add(positions[0], position, position);
                    }
                } else {
                    position = positions[j - 1];
                }

                scratchWritePrevPosition.x = position.x;
                scratchWritePrevPosition.y = position.y;
                scratchWritePrevPosition.z = (mode !== SceneMode.SCENE2D) ? position.z : 0.0;

                position = positions[j];
                scratchWritePosition.x = position.x;
                scratchWritePosition.y = position.y;
                scratchWritePosition.z = (mode !== SceneMode.SCENE2D) ? position.z : 0.0;

                if (j === positionsLength - 1) {
                    if (polyline._loop) {
                        position = positions[1];
                    } else {
                        position = scratchWriteVector;
                        Cartesian3.subtract(positions[positionsLength - 1], positions[positionsLength - 2], position);
                        Cartesian3.add(positions[positionsLength - 1], position, position);
                    }
                } else {
                    position = positions[j + 1];
                }

                scratchWriteNextPosition.x = position.x;
                scratchWriteNextPosition.y = position.y;
                scratchWriteNextPosition.z = (mode !== SceneMode.SCENE2D) ? position.z : 0.0;

                var segmentLength = lengths[segmentIndex];
                if (j === count + segmentLength) {
                    count += segmentLength;
                    ++segmentIndex;
                }

                var segmentStart = j - count === 0;
                var segmentEnd = j === count + lengths[segmentIndex] - 1;

                var startK = (segmentStart) ? 2 : 0;
                var endK = (segmentEnd) ? 2 : 4;

                for (var k = startK; k < endK; ++k) {
                    EncodedCartesian3.writeElements(scratchWritePosition, positionArray, positionIndex);
                    EncodedCartesian3.writeElements(scratchWritePrevPosition, positionArray, positionIndex + 6);
                    EncodedCartesian3.writeElements(scratchWriteNextPosition, positionArray, positionIndex + 12);

                    pickColorArray[colorIndex] = Color.floatToByte(pickColor.red);
                    pickColorArray[colorIndex + 1] = Color.floatToByte(pickColor.green);
                    pickColorArray[colorIndex + 2] = Color.floatToByte(pickColor.blue);
                    pickColorArray[colorIndex + 3] = Color.floatToByte(pickColor.alpha);

                    var direction = (k - 2 < 0) ? -1.0 : 1.0;
                    texCoordExpandWidthAndShowArray[texCoordExpandWidthAndShowIndex] = j / (positionsLength - 1); // s tex coord
                    texCoordExpandWidthAndShowArray[texCoordExpandWidthAndShowIndex + 1] = 2 * (k % 2) - 1;       // expand direction
                    texCoordExpandWidthAndShowArray[texCoordExpandWidthAndShowIndex + 2] = direction * width;
                    texCoordExpandWidthAndShowArray[texCoordExpandWidthAndShowIndex + 3] = show;

                    positionIndex += 6 * 3;
                    colorIndex += 4;
                    texCoordExpandWidthAndShowIndex += 4;
                }
            }
        }
    };

    var morphPositionScratch = new Cartesian3();
    var morphPrevPositionScratch = new Cartesian3();
    var morphNextPositionScratch = new Cartesian3();
    var morphVectorScratch = new Cartesian3();

    PolylineBucket.prototype.writeForMorph = function(positionArray, positionIndex) {
        var modelMatrix = this.modelMatrix;
        var polylines = this.polylines;
        var length = polylines.length;
        for ( var i = 0; i < length; ++i) {
            var polyline = polylines[i];
            var positions = polyline._segments.positions;
            var lengths = polyline._segments.lengths;
            var positionsLength = positions.length;

            var segmentIndex = 0;
            var count = 0;

            for ( var j = 0; j < positionsLength; ++j) {
                var prevPosition;
                if (j === 0) {
                    if (polyline._loop) {
                        prevPosition = positions[positionsLength - 2];
                    } else {
                        prevPosition = morphVectorScratch;
                        Cartesian3.subtract(positions[0], positions[1], prevPosition);
                        Cartesian3.add(positions[0], prevPosition, prevPosition);
                    }
                } else {
                    prevPosition = positions[j - 1];
                }

                prevPosition = Matrix4.multiplyByPoint(modelMatrix, prevPosition, morphPrevPositionScratch);

                var position = Matrix4.multiplyByPoint(modelMatrix, positions[j], morphPositionScratch);

                var nextPosition;
                if (j === positionsLength - 1) {
                    if (polyline._loop) {
                        nextPosition = positions[1];
                    } else {
                        nextPosition = morphVectorScratch;
                        Cartesian3.subtract(positions[positionsLength - 1], positions[positionsLength - 2], nextPosition);
                        Cartesian3.add(positions[positionsLength - 1], nextPosition, nextPosition);
                    }
                } else {
                    nextPosition = positions[j + 1];
                }

                nextPosition = Matrix4.multiplyByPoint(modelMatrix, nextPosition, morphNextPositionScratch);

                var segmentLength = lengths[segmentIndex];
                if (j === count + segmentLength) {
                    count += segmentLength;
                    ++segmentIndex;
                }

                var segmentStart = j - count === 0;
                var segmentEnd = j === count + lengths[segmentIndex] - 1;

                var startK = (segmentStart) ? 2 : 0;
                var endK = (segmentEnd) ? 2 : 4;

                for (var k = startK; k < endK; ++k) {
                    EncodedCartesian3.writeElements(position, positionArray, positionIndex);
                    EncodedCartesian3.writeElements(prevPosition, positionArray, positionIndex + 6);
                    EncodedCartesian3.writeElements(nextPosition, positionArray, positionIndex + 12);

                    positionIndex += 6 * 3;
                }
            }
        }
    };

    var scratchSegmentLengths = new Array(1);

    PolylineBucket.prototype.updateIndices = function(totalIndices, vertexBufferOffset, vertexArrayBuckets, offset) {
        var vaCount = vertexArrayBuckets.length - 1;
        var bucketLocator = new VertexArrayBucketLocator(0, offset, this);
        vertexArrayBuckets[vaCount].push(bucketLocator);
        var count = 0;
        var indices = totalIndices[totalIndices.length - 1];
        var indicesCount = 0;
        if (indices.length > 0) {
            indicesCount = indices[indices.length - 1] + 1;
        }
        var polylines = this.polylines;
        var length = polylines.length;
        for ( var i = 0; i < length; ++i) {

            var polyline = polylines[i];
            polyline._locatorBuckets = [];

            var segments;
            if (this.mode === SceneMode.SCENE3D) {
                segments = scratchSegmentLengths;
                var positionsLength = polyline._actualPositions.length;
                if (positionsLength > 0) {
                    segments[0] = positionsLength;
                } else {
                    continue;
                }
            } else {
                segments = polyline._segments.lengths;
            }

            var numberOfSegments = segments.length;
            if (numberOfSegments > 0) {
                var segmentIndexCount = 0;
                for ( var j = 0; j < numberOfSegments; ++j) {
                    var segmentLength = segments[j] - 1.0;
                    for ( var k = 0; k < segmentLength; ++k) {
                        if (indicesCount + 4 >= CesiumMath.SIXTY_FOUR_KILOBYTES - 1) {
                            polyline._locatorBuckets.push({
                                locator : bucketLocator,
                                count : segmentIndexCount
                            });
                            segmentIndexCount = 0;
                            vertexBufferOffset.push(4);
                            indices = [];
                            totalIndices.push(indices);
                            indicesCount = 0;
                            bucketLocator.count = count;
                            count = 0;
                            offset = 0;
                            bucketLocator = new VertexArrayBucketLocator(0, 0, this);
                            vertexArrayBuckets[++vaCount] = [bucketLocator];
                        }

                        indices.push(indicesCount, indicesCount + 2, indicesCount + 1);
                        indices.push(indicesCount + 1, indicesCount + 2, indicesCount + 3);

                        segmentIndexCount += 6;
                        count += 6;
                        offset += 6;
                        indicesCount += 4;
                    }
                }

                polyline._locatorBuckets.push({
                    locator : bucketLocator,
                    count : segmentIndexCount
                });

                if (indicesCount + 4 >= CesiumMath.SIXTY_FOUR_KILOBYTES - 1) {
                    vertexBufferOffset.push(0);
                    indices = [];
                    totalIndices.push(indices);
                    indicesCount = 0;
                    bucketLocator.count = count;
                    offset = 0;
                    count = 0;
                    bucketLocator = new VertexArrayBucketLocator(0, 0, this);
                    vertexArrayBuckets[++vaCount] = [bucketLocator];
                }
            }
            polyline._clean();
        }
        bucketLocator.count = count;
        return offset;
    };

    PolylineBucket.prototype.getPolylineStartIndex = function(polyline) {
        var polylines = this.polylines;
        var positionIndex = 0;
        var length = polylines.length;
        for ( var i = 0; i < length; ++i) {
            var p = polylines[i];
            if (p === polyline) {
                break;
            }
            positionIndex += p._actualLength;
        }
        return positionIndex;
    };

    var scratchSegments = {
        positions : undefined,
        lengths : undefined
    };
    var scratchLengths = new Array(1);
    var pscratch = new Cartesian3();
    var scratchCartographic = new Cartographic();

    PolylineBucket.prototype.getSegments = function(polyline, projection) {
        var positions = polyline._actualPositions;

        if (this.mode === SceneMode.SCENE3D) {
            scratchLengths[0] = positions.length;
            scratchSegments.positions = positions;
            scratchSegments.lengths = scratchLengths;
            return scratchSegments;
        }

        if (intersectsIDL(polyline)) {
            positions = polyline._segments.positions;
        }

        var ellipsoid = projection.ellipsoid;
        var newPositions = [];
        var modelMatrix = this.modelMatrix;
        var length = positions.length;
        var position;
        var p = pscratch;

        for ( var n = 0; n < length; ++n) {
            position = positions[n];
            p = Matrix4.multiplyByPoint(modelMatrix, position, p);
            newPositions.push(projection.project(ellipsoid.cartesianToCartographic(p, scratchCartographic)));
        }

        if (newPositions.length > 0) {
            polyline._boundingVolume2D = BoundingSphere.fromPoints(newPositions, polyline._boundingVolume2D);
            var center2D = polyline._boundingVolume2D.center;
            polyline._boundingVolume2D.center = new Cartesian3(center2D.z, center2D.x, center2D.y);
        }

        scratchSegments.positions = newPositions;
        scratchSegments.lengths = polyline._segments.lengths;
        return scratchSegments;
    };

    var scratchPositionsArray;
    var scratchTexCoordArray;

    PolylineBucket.prototype.writeUpdate = function(index, polyline, positionBuffer, texCoordExpandWidthAndShowBuffer, projection) {
        var mode = this.mode;
        var positionsLength = polyline._actualLength;
        if (positionsLength) {
            index += this.getPolylineStartIndex(polyline);

            var positionArray = scratchPositionsArray;
            var texCoordExpandWidthAndShowArray = scratchTexCoordArray;

            var positionsArrayLength = 6 * positionsLength * 3;

            if (!defined(positionArray) || positionArray.length < positionsArrayLength) {
                positionArray = scratchPositionsArray = new Float32Array(positionsArrayLength);
                texCoordExpandWidthAndShowArray = scratchTexCoordArray = new Float32Array(positionsLength * 4);
            } else if (positionArray.length > positionsArrayLength) {
                positionArray = new Float32Array(positionArray.buffer, 0, positionsArrayLength);
                texCoordExpandWidthAndShowArray = new Float32Array(texCoordExpandWidthAndShowArray.buffer, 0, positionsLength * 4);
            }

            var positionIndex = 0;
            var texCoordExpandWidthAndShowIndex = 0;

            var segments = this.getSegments(polyline, projection);
            var positions = segments.positions;
            var lengths = segments.lengths;

            var segmentIndex = 0;
            var count = 0;
            var position;

            var width = polyline.width;
            var show = polyline.show && width > 0.0;

            positionsLength = positions.length;
            for ( var i = 0; i < positionsLength; ++i) {
                if (i === 0) {
                    if (polyline._loop) {
                        position = positions[positionsLength - 2];
                    } else {
                        position = scratchWriteVector;
                        Cartesian3.subtract(positions[0], positions[1], position);
                        Cartesian3.add(positions[0], position, position);
                    }
                } else {
                    position = positions[i - 1];
                }

                scratchWritePrevPosition.x = position.x;
                scratchWritePrevPosition.y = position.y;
                scratchWritePrevPosition.z = (mode !== SceneMode.SCENE2D) ? position.z : 0.0;

                position = positions[i];
                scratchWritePosition.x = position.x;
                scratchWritePosition.y = position.y;
                scratchWritePosition.z = (mode !== SceneMode.SCENE2D) ? position.z : 0.0;

                if (i === positionsLength - 1) {
                    if (polyline._loop) {
                        position = positions[1];
                    } else {
                        position = scratchWriteVector;
                        Cartesian3.subtract(positions[positionsLength - 1], positions[positionsLength - 2], position);
                        Cartesian3.add(positions[positionsLength - 1], position, position);
                    }
                } else {
                    position = positions[i + 1];
                }

                scratchWriteNextPosition.x = position.x;
                scratchWriteNextPosition.y = position.y;
                scratchWriteNextPosition.z = (mode !== SceneMode.SCENE2D) ? position.z : 0.0;

                var segmentLength = lengths[segmentIndex];
                if (i === count + segmentLength) {
                    count += segmentLength;
                    ++segmentIndex;
                }

                var segmentStart = i - count === 0;
                var segmentEnd = i === count + lengths[segmentIndex] - 1;

                var startJ = (segmentStart) ? 2 : 0;
                var endJ = (segmentEnd) ? 2 : 4;

                for (var j = startJ; j < endJ; ++j) {
                    EncodedCartesian3.writeElements(scratchWritePosition, positionArray, positionIndex);
                    EncodedCartesian3.writeElements(scratchWritePrevPosition, positionArray, positionIndex + 6);
                    EncodedCartesian3.writeElements(scratchWriteNextPosition, positionArray, positionIndex + 12);

                    var direction = (j - 2 < 0) ? -1.0 : 1.0;
                    texCoordExpandWidthAndShowArray[texCoordExpandWidthAndShowIndex] = i / (positionsLength - 1);  // s tex coord
                    texCoordExpandWidthAndShowArray[texCoordExpandWidthAndShowIndex + 1] = 2 * (j % 2) - 1;        // expand direction
                    texCoordExpandWidthAndShowArray[texCoordExpandWidthAndShowIndex + 2] = direction * width;
                    texCoordExpandWidthAndShowArray[texCoordExpandWidthAndShowIndex + 3] = show;

                    positionIndex += 6 * 3;
                    texCoordExpandWidthAndShowIndex += 4;
                }
            }

            positionBuffer.copyFromArrayView(positionArray, 6 * 3 * Float32Array.BYTES_PER_ELEMENT * index);
            texCoordExpandWidthAndShowBuffer.copyFromArrayView(texCoordExpandWidthAndShowArray, 4 * Float32Array.BYTES_PER_ELEMENT * index);
        }
    };

    return PolylineCollection;
});