/*global define*/
define([
        '../Core/BoundingSphere',
        '../Core/Color',
        '../Core/ComponentDatatype',
        '../Core/defaultValue',
        '../Core/defined',
        '../Core/DeveloperError',
        '../Core/Ellipsoid',
        '../Core/FeatureDetection',
        '../Core/GeographicProjection',
        '../Core/Geometry',
        '../Core/GeometryAttribute',
        '../Core/GeometryAttributes',
        '../Core/GeometryPipeline',
        '../Core/IndexDatatype',
        '../Core/Matrix4',
        '../Core/WebMercatorProjection'
    ], function(
        BoundingSphere,
        Color,
        ComponentDatatype,
        defaultValue,
        defined,
        DeveloperError,
        Ellipsoid,
        FeatureDetection,
        GeographicProjection,
        Geometry,
        GeometryAttribute,
        GeometryAttributes,
        GeometryPipeline,
        IndexDatatype,
        Matrix4,
        WebMercatorProjection) {
    "use strict";

    // Bail out if the browser doesn't support typed arrays, to prevent the setup function
    // from failing, since we won't be able to create a WebGL context anyway.
    if (!FeatureDetection.supportsTypedArrays()) {
        return {};
    }

    function transformToWorldCoordinates(instances, primitiveModelMatrix, scene3DOnly) {
        var toWorld = !scene3DOnly;
        var length = instances.length;
        var i;

        if (!toWorld && (length > 1)) {
            var modelMatrix = instances[0].modelMatrix;

            for (i = 1; i < length; ++i) {
                if (!Matrix4.equals(modelMatrix, instances[i].modelMatrix)) {
                    toWorld = true;
                    break;
                }
            }
        }

        if (toWorld) {
            for (i = 0; i < length; ++i) {
                GeometryPipeline.transformToWorldCoordinates(instances[i]);
            }
        } else {
            // Leave geometry in local coordinate system; auto update model-matrix.
            Matrix4.multiplyTransformation(primitiveModelMatrix, instances[0].modelMatrix, primitiveModelMatrix);
        }
    }

    function addGeometryPickColor(geometry, pickColor) {
        var attributes = geometry.attributes;
        var positionAttr = attributes.position;
        var numberOfComponents = 4 * (positionAttr.values.length / positionAttr.componentsPerAttribute);

        attributes.pickColor = new GeometryAttribute({
            componentDatatype : ComponentDatatype.UNSIGNED_BYTE,
            componentsPerAttribute : 4,
            normalize : true,
            values : new Uint8Array(numberOfComponents)
        });

        var red = Color.floatToByte(pickColor.red);
        var green = Color.floatToByte(pickColor.green);
        var blue = Color.floatToByte(pickColor.blue);
        var alpha = Color.floatToByte(pickColor.alpha);
        var values = attributes.pickColor.values;

        for (var j = 0; j < numberOfComponents; j += 4) {
            values[j] = red;
            values[j + 1] = green;
            values[j + 2] = blue;
            values[j + 3] = alpha;
        }
    }

    function addPickColorAttribute(instances, pickIds) {
        var length = instances.length;

        for (var i = 0; i < length; ++i) {
            var instance = instances[i];
            var pickColor = pickIds[i];

            if (defined(instance.geometry)) {
                addGeometryPickColor(instance.geometry, pickColor);
            } else {
                addGeometryPickColor(instance.westHemisphereGeometry, pickColor);
                addGeometryPickColor(instance.eastHemisphereGeometry, pickColor);
            }
        }
    }

    function getCommonPerInstanceAttributeNames(instances) {
        var length = instances.length;

        var attributesInAllInstances = [];
        var attributes0 = instances[0].attributes;
        var name;

        for (name in attributes0) {
            if (attributes0.hasOwnProperty(name)) {
                var attribute = attributes0[name];
                var inAllInstances = true;

                // Does this same attribute exist in all instances?
                for (var i = 1; i < length; ++i) {
                    var otherAttribute = instances[i].attributes[name];

                    if (!defined(otherAttribute) ||
                        (attribute.componentDatatype !== otherAttribute.componentDatatype) ||
                        (attribute.componentsPerAttribute !== otherAttribute.componentsPerAttribute) ||
                        (attribute.normalize !== otherAttribute.normalize)) {

                        inAllInstances = false;
                        break;
                    }
                }

                if (inAllInstances) {
                    attributesInAllInstances.push(name);
                }
            }
        }

        return attributesInAllInstances;
    }

    function addPerInstanceAttributesToGeometry(instanceAttributes, geometry, names) {
        var numberOfVertices = Geometry.computeNumberOfVertices(geometry);

        var namesLength = names.length;
        for (var j = 0; j < namesLength; ++j) {
            var name = names[j];
            var attribute = instanceAttributes[name];
            var componentDatatype = attribute.componentDatatype;
            var value = attribute.value;
            var componentsPerAttribute = value.length;

            var buffer = ComponentDatatype.createTypedArray(componentDatatype, numberOfVertices * componentsPerAttribute);
            for (var k = 0; k < numberOfVertices; ++k) {
                buffer.set(value, k * componentsPerAttribute);
            }

            geometry.attributes[name] = new GeometryAttribute({
                componentDatatype : componentDatatype,
                componentsPerAttribute : componentsPerAttribute,
                normalize : attribute.normalize,
                values : buffer
            });
        }
    }

    function addPerInstanceAttributes(instances, names) {
        var length = instances.length;
        for (var i = 0; i < length; ++i) {
            var instance = instances[i];
            var instanceAttributes = instance.attributes;

            if (defined(instance.geometry)) {
                addPerInstanceAttributesToGeometry(instanceAttributes, instance.geometry, names);
            } else {
                addPerInstanceAttributesToGeometry(instanceAttributes, instance.westHemisphereGeometry, names);
                addPerInstanceAttributesToGeometry(instanceAttributes, instance.eastHemisphereGeometry, names);
            }
        }
    }

    function geometryPipeline(parameters) {
        var instances = parameters.instances;
        var pickIds = parameters.pickIds;
        var projection = parameters.projection;
        var uintIndexSupport = parameters.elementIndexUintSupported;
        var scene3DOnly = parameters.scene3DOnly;
        var allowPicking = parameters.allowPicking;
        var vertexCacheOptimize = parameters.vertexCacheOptimize;
        var compressVertices = parameters.compressVertices;
        var modelMatrix = parameters.modelMatrix;

        var i;
        var geometry;
        var length = instances.length;
        var primitiveType = instances[0].geometry.primitiveType;

        //>>includeStart('debug', pragmas.debug);
        for (i = 1; i < length; ++i) {
            if (instances[i].geometry.primitiveType !== primitiveType) {
                throw new DeveloperError('All instance geometries must have the same primitiveType.');
            }
        }
        //>>includeEnd('debug');

        // Unify to world coordinates before combining.
        transformToWorldCoordinates(instances, modelMatrix, scene3DOnly);

        // Clip to IDL
        if (!scene3DOnly) {
            for (i = 0; i < length; ++i) {
                GeometryPipeline.splitLongitude(instances[i]);
            }
        }

        // Add pickColor attribute for picking individual instances
        if (allowPicking) {
            addPickColorAttribute(instances, pickIds);
        }

        // add attributes to the geometry for each per-instance attribute
        var perInstanceAttributeNames = getCommonPerInstanceAttributeNames(instances);
        addPerInstanceAttributes(instances, perInstanceAttributeNames);

        // Optimize for vertex shader caches
        if (vertexCacheOptimize) {
            for (i = 0; i < length; ++i) {
                var instance = instances[i];
                if (defined(instance.geometry)) {
                    GeometryPipeline.reorderForPostVertexCache(instance.geometry);
                    GeometryPipeline.reorderForPreVertexCache(instance.geometry);
                } else {
                    GeometryPipeline.reorderForPostVertexCache(instance.westHemisphereGeometry);
                    GeometryPipeline.reorderForPreVertexCache(instance.westHemisphereGeometry);

                    GeometryPipeline.reorderForPostVertexCache(instance.eastHemisphereGeometry);
                    GeometryPipeline.reorderForPreVertexCache(instance.eastHemisphereGeometry);
                }
            }
        }

        // Combine into single geometry for better rendering performance.
        var geometries = GeometryPipeline.combineInstances(instances);

        length = geometries.length;
        for (i = 0; i < length; ++i) {
            geometry = geometries[i];

            // Split positions for GPU RTE
            var attributes = geometry.attributes;
            var name;
            if (!scene3DOnly) {
                for (name in attributes) {
                    if (attributes.hasOwnProperty(name) && attributes[name].componentDatatype === ComponentDatatype.DOUBLE) {
                        var name3D = name + '3D';
                        var name2D = name + '2D';

                        // Compute 2D positions
                        GeometryPipeline.projectTo2D(geometry, name, name3D, name2D, projection);
                        if (defined(geometry.boundingSphere) && name === 'position') {
                            geometry.boundingSphereCV = BoundingSphere.fromVertices(geometry.attributes.position2D.values);
                        }

                        GeometryPipeline.encodeAttribute(geometry, name3D, name3D + 'High', name3D + 'Low');
                        GeometryPipeline.encodeAttribute(geometry, name2D, name2D + 'High', name2D + 'Low');
                    }
                }
            } else {
                for (name in attributes) {
                    if (attributes.hasOwnProperty(name) && attributes[name].componentDatatype === ComponentDatatype.DOUBLE) {
                        GeometryPipeline.encodeAttribute(geometry, name, name + '3DHigh', name + '3DLow');
                    }
                }
            }

            // oct encode and pack normals, compress texture coordinates
            if (compressVertices) {
                GeometryPipeline.compressVertices(geometry);
            }
        }

        if (!uintIndexSupport) {
            // Break into multiple geometries to fit within unsigned short indices if needed
            var splitGeometries = [];
            length = geometries.length;
            for (i = 0; i < length; ++i) {
                geometry = geometries[i];
                splitGeometries = splitGeometries.concat(GeometryPipeline.fitToUnsignedShortIndices(geometry));
            }

            geometries = splitGeometries;
        }

        return geometries;
    }

    function createPerInstanceVAAttributes(geometry, attributeLocations, names) {
        var vaAttributes = [];
        var attributes = geometry.attributes;

        var length = names.length;
        for (var i = 0; i < length; ++i) {
            var name = names[i];
            var attribute = attributes[name];

            var componentDatatype = attribute.componentDatatype;
            if (componentDatatype === ComponentDatatype.DOUBLE) {
                componentDatatype = ComponentDatatype.FLOAT;
            }

            var typedArray = ComponentDatatype.createTypedArray(componentDatatype, attribute.values);
            vaAttributes.push({
                index : attributeLocations[name],
                componentDatatype : componentDatatype,
                componentsPerAttribute : attribute.componentsPerAttribute,
                normalize : attribute.normalize,
                values : typedArray
            });

            delete attributes[name];
        }

        return vaAttributes;
    }

    function computePerInstanceAttributeLocationsForGeometry(instanceIndex, geometry, instanceAttributes, names, attributeLocations, vertexArrays, indices, offsets, vaIndices) {
        var numberOfVertices = Geometry.computeNumberOfVertices(geometry);

        if (!defined(indices[instanceIndex])) {
            indices[instanceIndex] = {
                boundingSphere : geometry.boundingSphere,
                boundingSphereCV : geometry.boundingSphereCV
            };
        }

        var namesLength = names.length;
        for (var j = 0; j < namesLength; ++j) {
            var name = names[j];
            var index = attributeLocations[name];

            var tempVertexCount = numberOfVertices;
            while (tempVertexCount > 0) {
                var vaIndex = defaultValue(vaIndices[name], 0);
                var va = vertexArrays[vaIndex];
                var vaLength = va.length;

                var attribute;
                for (var k = 0; k < vaLength; ++k) {
                    attribute = va[k];
                    if (attribute.index === index) {
                        break;
                    }
                }

                if (!defined(indices[instanceIndex][name])) {
                    indices[instanceIndex][name] = {
                        dirty : false,
                        valid : true,
                        value : instanceAttributes[name].value,
                        indices : []
                    };
                }

                var size = attribute.values.length / attribute.componentsPerAttribute;
                var offset = defaultValue(offsets[name], 0);

                var count;
                if (offset + tempVertexCount < size) {
                    count = tempVertexCount;
                    indices[instanceIndex][name].indices.push({
                        attribute : attribute,
                        offset : offset,
                        count : count
                    });
                    offsets[name] = offset + tempVertexCount;
                } else {
                    count = size - offset;
                    indices[instanceIndex][name].indices.push({
                        attribute : attribute,
                        offset : offset,
                        count : count
                    });
                    offsets[name] = 0;
                    vaIndices[name] = vaIndex + 1;
                }

                tempVertexCount -= count;
            }
        }
    }

    function computePerInstanceAttributeLocations(instances, invalidInstances, vertexArrays, attributeLocations, names) {
        var indices = [];

        var length = instances.length;
        var offsets = {};
        var vaIndices = {};

        var i;
        var instance;
        var attributes;

        for (i = 0; i < length; ++i) {
            instance = instances[i];
            attributes = instance.attributes;
            if (defined(instance.geometry)) {
                computePerInstanceAttributeLocationsForGeometry(i, instance.geometry, attributes, names, attributeLocations, vertexArrays, indices, offsets, vaIndices);
            }
        }

        for (i = 0; i < length; ++i) {
            instance = instances[i];
            attributes = instance.attributes;
            if (defined(instance.westHemisphereGeometry)) {
                computePerInstanceAttributeLocationsForGeometry(i, instance.westHemisphereGeometry, attributes, names, attributeLocations, vertexArrays, indices, offsets, vaIndices);
            }
        }

        for (i = 0; i < length; ++i) {
            instance = instances[i];
            attributes = instance.attributes;
            if (defined(instance.eastHemisphereGeometry)) {
                computePerInstanceAttributeLocationsForGeometry(i, instance.eastHemisphereGeometry, attributes, names, attributeLocations, vertexArrays, indices, offsets, vaIndices);
            }
        }

        length = invalidInstances.length;
        for (i = 0; i < length; ++i) {
            instance = invalidInstances[i];
            attributes = instance.attributes;

            var instanceAttributes = {};
            indices.push(instanceAttributes);

            var namesLength = names.length;
            for (var j = 0; j < namesLength; ++j) {
                var name = names[j];
                instanceAttributes[name] = {
                    dirty : false,
                    valid : false,
                    value : attributes[name].value,
                    indices : []
                };
            }
        }

        return indices;
    }

    /**
     * @private
     */
    var PrimitivePipeline = {};

    /**
     * @private
     */
    PrimitivePipeline.combineGeometry = function(parameters) {
        var geometries;
        var attributeLocations;
        var perInstanceAttributes;
        var perInstanceAttributeNames;
        var length;

        var instances = parameters.instances;
        var invalidInstances = parameters.invalidInstances;

        if (instances.length > 0) {
            geometries = geometryPipeline(parameters);
            attributeLocations = GeometryPipeline.createAttributeLocations(geometries[0]);

            perInstanceAttributeNames = getCommonPerInstanceAttributeNames(instances);

            perInstanceAttributes = [];
            length = geometries.length;
            for (var i = 0; i < length; ++i) {
                var geometry = geometries[i];
                perInstanceAttributes.push(createPerInstanceVAAttributes(geometry, attributeLocations, perInstanceAttributeNames));
            }
        }

        perInstanceAttributeNames = defined(perInstanceAttributeNames) ? perInstanceAttributeNames : getCommonPerInstanceAttributeNames(invalidInstances);
        var indices = computePerInstanceAttributeLocations(instances, invalidInstances, perInstanceAttributes, attributeLocations, perInstanceAttributeNames);

        return {
            geometries : geometries,
            modelMatrix : parameters.modelMatrix,
            attributeLocations : attributeLocations,
            vaAttributes : perInstanceAttributes,
            vaAttributeLocations : indices,
            validInstancesIndices : parameters.validInstancesIndices,
            invalidInstancesIndices : parameters.invalidInstancesIndices
        };
    };

    function transferGeometry(geometry, transferableObjects) {
        var attributes = geometry.attributes;
        for ( var name in attributes) {
            if (attributes.hasOwnProperty(name)) {
                var attribute = attributes[name];

                if (defined(attribute) && defined(attribute.values)) {
                    transferableObjects.push(attribute.values.buffer);
                }
            }
        }

        if (defined(geometry.indices)) {
            transferableObjects.push(geometry.indices.buffer);
        }
    }

    function transferGeometries(geometries, transferableObjects) {
        var length = geometries.length;
        for (var i = 0; i < length; ++i) {
            transferGeometry(geometries[i], transferableObjects);
        }
    }

    /**
     * @private
     */
    function transferPerInstanceAttributes(perInstanceAttributes, transferableObjects) {
        var length = perInstanceAttributes.length;
        for (var i = 0; i < length; ++i) {
            var vaAttributes = perInstanceAttributes[i];
            var vaLength = vaAttributes.length;
            for (var j = 0; j < vaLength; ++j) {
                transferableObjects.push(vaAttributes[j].values.buffer);
            }
        }
    }

    // This function was created by simplifying packCreateGeometryResults into a count-only operation.
    function countCreateGeometryResults(items) {
        var count = 1;
        var length = items.length;
        for (var i = 0; i < length; i++) {
            var geometry = items[i];
            ++count;

            if (!defined(geometry)) {
                continue;
            }

            var attributes = geometry.attributes;

            count += 6 + 2 * BoundingSphere.packedLength + (defined(geometry.indices) ? geometry.indices.length : 0);

            for ( var property in attributes) {
                if (attributes.hasOwnProperty(property) && defined(attributes[property])) {
                    var attribute = attributes[property];
                    count += 5 + attribute.values.length;
                }
            }
        }

        return count;
    }

    /**
     * @private
     */
    PrimitivePipeline.packCreateGeometryResults = function(items, transferableObjects) {
        var packedData = new Float64Array(countCreateGeometryResults(items));
        var stringTable = [];
        var stringHash = {};

        var length = items.length;
        var count = 0;
        packedData[count++] = length;
        for (var i = 0; i < length; i++) {
            var geometry = items[i];

            var validGeometry = defined(geometry);
            packedData[count++] = validGeometry ? 1.0 : 0.0;

            if (!validGeometry) {
                continue;
            }

            packedData[count++] = geometry.primitiveType;
            packedData[count++] = geometry.geometryType;

            var validBoundingSphere = defined(geometry.boundingSphere) ? 1.0 : 0.0;
            packedData[count++] = validBoundingSphere;
            if (validBoundingSphere) {
                BoundingSphere.pack(geometry.boundingSphere, packedData, count);
            }

            count += BoundingSphere.packedLength;

            var validBoundingSphereCV = defined(geometry.boundingSphereCV) ? 1.0 : 0.0;
            packedData[count++] = validBoundingSphereCV;
            if (validBoundingSphereCV) {
                BoundingSphere.pack(geometry.boundingSphereCV, packedData, count);
            }

            count += BoundingSphere.packedLength;

            var attributes = geometry.attributes;
            var attributesToWrite = [];
            for ( var property in attributes) {
                if (attributes.hasOwnProperty(property) && defined(attributes[property])) {
                    attributesToWrite.push(property);
                    if (!defined(stringHash[property])) {
                        stringHash[property] = stringTable.length;
                        stringTable.push(property);
                    }
                }
            }

            packedData[count++] = attributesToWrite.length;
            for (var q = 0; q < attributesToWrite.length; q++) {
                var name = attributesToWrite[q];
                var attribute = attributes[name];
                packedData[count++] = stringHash[name];
                packedData[count++] = attribute.componentDatatype;
                packedData[count++] = attribute.componentsPerAttribute;
                packedData[count++] = attribute.normalize ? 1 : 0;
                packedData[count++] = attribute.values.length;
                packedData.set(attribute.values, count);
                count += attribute.values.length;
            }

            var indicesLength = defined(geometry.indices) ? geometry.indices.length : 0;
            packedData[count++] = indicesLength;

            if (indicesLength > 0) {
                packedData.set(geometry.indices, count);
                count += indicesLength;
            }
        }

        transferableObjects.push(packedData.buffer);

        return {
            stringTable : stringTable,
            packedData : packedData
        };
    };

    /**
     * @private
     */
    PrimitivePipeline.unpackCreateGeometryResults = function(createGeometryResult) {
        var stringTable = createGeometryResult.stringTable;
        var packedGeometry = createGeometryResult.packedData;

        var i;
        var result = new Array(packedGeometry[0]);
        var resultIndex = 0;

        var packedGeometryIndex = 1;
        while (packedGeometryIndex < packedGeometry.length) {
            var valid = packedGeometry[packedGeometryIndex++] === 1.0;
            if (!valid) {
                result[resultIndex++] = undefined;
                continue;
            }

            var primitiveType = packedGeometry[packedGeometryIndex++];
            var geometryType = packedGeometry[packedGeometryIndex++];

            var boundingSphere;
            var boundingSphereCV;

            var validBoundingSphere = packedGeometry[packedGeometryIndex++] === 1.0;
            if (validBoundingSphere) {
                boundingSphere = BoundingSphere.unpack(packedGeometry, packedGeometryIndex);
            }

            packedGeometryIndex += BoundingSphere.packedLength;

            var validBoundingSphereCV = packedGeometry[packedGeometryIndex++] === 1.0;
            if (validBoundingSphereCV) {
                boundingSphereCV = BoundingSphere.unpack(packedGeometry, packedGeometryIndex);
            }

            packedGeometryIndex += BoundingSphere.packedLength;

            var length;
            var values;
            var componentsPerAttribute;
            var attributes = new GeometryAttributes();
            var numAttributes = packedGeometry[packedGeometryIndex++];
            for (i = 0; i < numAttributes; i++) {
                var name = stringTable[packedGeometry[packedGeometryIndex++]];
                var componentDatatype = packedGeometry[packedGeometryIndex++];
                componentsPerAttribute = packedGeometry[packedGeometryIndex++];
                var normalize = packedGeometry[packedGeometryIndex++] !== 0;

                length = packedGeometry[packedGeometryIndex++];
                values = ComponentDatatype.createTypedArray(componentDatatype, length);
                for (var valuesIndex = 0; valuesIndex < length; valuesIndex++) {
                    values[valuesIndex] = packedGeometry[packedGeometryIndex++];
                }

                attributes[name] = new GeometryAttribute({
                    componentDatatype : componentDatatype,
                    componentsPerAttribute : componentsPerAttribute,
                    normalize : normalize,
                    values : values
                });
            }

            var indices;
            length = packedGeometry[packedGeometryIndex++];

            if (length > 0) {
                var numberOfVertices = values.length / componentsPerAttribute;
                indices = IndexDatatype.createTypedArray(numberOfVertices, length);
                for (i = 0; i < length; i++) {
                    indices[i] = packedGeometry[packedGeometryIndex++];
                }
            }

            result[resultIndex++] = new Geometry({
                primitiveType : primitiveType,
                geometryType : geometryType,
                boundingSphere : boundingSphere,
                indices : indices,
                attributes : attributes
            });
        }

        return result;
    };

    function packPickIds(pickIds, transferableObjects) {
        var length = pickIds.length;
        var packedPickIds = new Uint32Array(pickIds.length);
        for (var i = 0; i < length; ++i) {
            packedPickIds[i] = pickIds[i].toRgba();
        }
        transferableObjects.push(packedPickIds.buffer);
        return packedPickIds;
    }

    function unpackPickIds(packedPickIds) {
        var length = packedPickIds.length;
        var pickIds = new Array(length);
        for (var i = 0; i < length; i++) {
            pickIds[i] = Color.fromRgba(packedPickIds[i]);
        }
        return pickIds;
    }

    // This function was created by simplifying packInstancesForCombine into a count-only operation.
    function countInstancesForCombine(instances) {
        var length = instances.length;
        var count = 1 + (length * 17);
        for (var i = 0; i < length; i++) {
            var attributes = instances[i].attributes;
            for ( var property in attributes) {
                if (attributes.hasOwnProperty(property) && defined(attributes[property])) {
                    var attribute = attributes[property];
                    count += 5 + attribute.value.length;
                }
            }
        }
        return count;
    }

    function packInstancesForCombine(instances, transferableObjects) {
        var packedData = new Float64Array(countInstancesForCombine(instances));
        var stringHash = {};
        var stringTable = [];

        var length = instances.length;
        var count = 0;
        packedData[count++] = length;
        for (var i = 0; i < length; i++) {
            var instance = instances[i];

            Matrix4.pack(instance.modelMatrix, packedData, count);
            count += Matrix4.packedLength;

            var attributes = instance.attributes;
            var attributesToWrite = [];
            for ( var property in attributes) {
                if (attributes.hasOwnProperty(property) && defined(attributes[property])) {
                    attributesToWrite.push(property);
                    if (!defined(stringHash[property])) {
                        stringHash[property] = stringTable.length;
                        stringTable.push(property);
                    }
                }
            }

            packedData[count++] = attributesToWrite.length;
            for (var q = 0; q < attributesToWrite.length; q++) {
                var name = attributesToWrite[q];
                var attribute = attributes[name];
                packedData[count++] = stringHash[name];
                packedData[count++] = attribute.componentDatatype;
                packedData[count++] = attribute.componentsPerAttribute;
                packedData[count++] = attribute.normalize;
                packedData[count++] = attribute.value.length;
                packedData.set(attribute.value, count);
                count += attribute.value.length;
            }
        }
        transferableObjects.push(packedData.buffer);

        return {
            stringTable : stringTable,
            packedData : packedData
        };
    }

    function unpackInstancesForCombine(data) {
        var packedInstances = data.packedData;
        var stringTable = data.stringTable;
        var result = new Array(packedInstances[0]);
        var count = 0;

        var i = 1;
        while (i < packedInstances.length) {
            var modelMatrix = Matrix4.unpack(packedInstances, i);
            i += Matrix4.packedLength;

            var attributes = {};
            var numAttributes = packedInstances[i++];
            for (var x = 0; x < numAttributes; x++) {
                var name = stringTable[packedInstances[i++]];
                var componentDatatype = packedInstances[i++];
                var componentsPerAttribute = packedInstances[i++];
                var normalize = packedInstances[i++] !== 0;
                var length = packedInstances[i++];
                var value = ComponentDatatype.createTypedArray(componentDatatype, length);
                for (var valueIndex = 0; valueIndex < length; valueIndex++) {
                    value[valueIndex] = packedInstances[i++];
                }

                attributes[name] = {
                    componentDatatype : componentDatatype,
                    componentsPerAttribute : componentsPerAttribute,
                    normalize : normalize,
                    value : value
                };
            }

            result[count++] = {
                attributes : attributes,
                modelMatrix : modelMatrix
            };
        }

        return result;
    }

    // This function was created by simplifying packAttributeLocations into a count-only operation.
    function countAttributeLocations(attributeLocations) {
        var length = attributeLocations.length;
        var count = 1 + length;
        for (var i = 0; i < length; i++) {
            var instance = attributeLocations[i];

            count += 2;
            count += defined(instance.boundingSphere) ? BoundingSphere.packedLength : 0.0;
            count += defined(instance.boundingSphereCV) ? BoundingSphere.packedLength : 0.0;

            for ( var propertyName in instance) {
                if (instance.hasOwnProperty(propertyName) && defined(instance[propertyName]) &&
                        propertyName !== 'boundingSphere' && propertyName !== 'boundingSphereCV') {
                    var property = instance[propertyName];
                    count += 4 + (property.indices.length * 3) + property.value.length;
                }
            }
        }
        return count;
    }

    function packAttributeLocations(attributeLocations, transferableObjects) {
        var packedData = new Float64Array(countAttributeLocations(attributeLocations));
        var stringTable = [];
        var attributeTable = [];

        var stringHash = {};
        var length = attributeLocations.length;
        var count = 0;
        packedData[count++] = length;
        for (var i = 0; i < length; i++) {
            var instance = attributeLocations[i];

            var boundingSphere = instance.boundingSphere;
            var hasBoundingSphere = defined(boundingSphere);
            packedData[count++] = hasBoundingSphere ? 1.0 : 0.0;
            if (hasBoundingSphere) {
                BoundingSphere.pack(boundingSphere, packedData, count);
                count += BoundingSphere.packedLength;
            }

            boundingSphere = instance.boundingSphereCV;
            hasBoundingSphere = defined(boundingSphere);
            packedData[count++] = hasBoundingSphere ? 1.0 : 0.0;
            if (hasBoundingSphere) {
                BoundingSphere.pack(boundingSphere, packedData, count);
                count += BoundingSphere.packedLength;
            }

            var propertiesToWrite = [];
            for ( var propertyName in instance) {
                if (instance.hasOwnProperty(propertyName) && defined(instance[propertyName]) &&
                        propertyName !== 'boundingSphere' && propertyName !== 'boundingSphereCV') {
                    propertiesToWrite.push(propertyName);
                    if (!defined(stringHash[propertyName])) {
                        stringHash[propertyName] = stringTable.length;
                        stringTable.push(propertyName);
                    }
                }
            }

            packedData[count++] = propertiesToWrite.length;
            for (var q = 0; q < propertiesToWrite.length; q++) {
                var name = propertiesToWrite[q];
                var property = instance[name];
                packedData[count++] = stringHash[name];
                packedData[count++] = property.valid ? 1.0 : 0.0;

                var indices = property.indices;
                var indicesLength = indices.length;
                packedData[count++] = indicesLength;
                for (var x = 0; x < indicesLength; x++) {
                    var index = indices[x];
                    packedData[count++] = index.count;
                    packedData[count++] = index.offset;
                    var tableIndex = attributeTable.indexOf(index.attribute);
                    if (tableIndex === -1) {
                        tableIndex = attributeTable.length;
                        attributeTable.push(index.attribute);
                    }
                    packedData[count++] = tableIndex;
                }

                packedData[count++] = property.value.length;
                packedData.set(property.value, count);
                count += property.value.length;
            }
        }

        transferableObjects.push(packedData.buffer);

        return {
            stringTable : stringTable,
            packedData : packedData,
            attributeTable : attributeTable
        };
    }

    function unpackAttributeLocations(packedAttributeLocations, vaAttributes) {
        var stringTable = packedAttributeLocations.stringTable;
        var attributeTable = packedAttributeLocations.attributeTable;
        var packedData = packedAttributeLocations.packedData;

        var attributeLocations = new Array(packedData[0]);
        var attributeLocationsIndex = 0;
        var i = 1;
        var packedDataLength = packedData.length;
        while (i < packedDataLength) {
            var instance = {};

            var hasBoundingSphere = packedData[i++] === 1.0;
            if (hasBoundingSphere) {
                instance.boundingSphere = BoundingSphere.unpack(packedData, i);
                i += BoundingSphere.packedLength;
            }

            hasBoundingSphere = packedData[i++] === 1.0;
            if (hasBoundingSphere) {
                instance.boundingSphereCV = BoundingSphere.unpack(packedData, i);
                i += BoundingSphere.packedLength;
            }

            var numAttributes = packedData[i++];
            for (var x = 0; x < numAttributes; x++) {
                var name = stringTable[packedData[i++]];
                var valid = packedData[i++] === 1.0;

                var indicesLength = packedData[i++];
                var indices = indicesLength > 0 ? new Array(indicesLength) : undefined;
                for (var indicesIndex = 0; indicesIndex < indicesLength; indicesIndex++) {
                    var index = {};
                    index.count = packedData[i++];
                    index.offset = packedData[i++];
                    index.attribute = attributeTable[packedData[i++]];
                    indices[indicesIndex] = index;
                }

                var valueLength = packedData[i++];
                var value = valid ? ComponentDatatype.createTypedArray(indices[0].attribute.componentDatatype, valueLength) : new Array(valueLength);
                for (var valueIndex = 0; valueIndex < valueLength; valueIndex++) {
                    value[valueIndex] = packedData[i++];
                }

                instance[name] = {
                    dirty : false,
                    valid : valid,
                    indices : indices,
                    value : value
                };
            }
            attributeLocations[attributeLocationsIndex++] = instance;
        }

        return attributeLocations;
    }

    /**
     * @private
     */
    PrimitivePipeline.packCombineGeometryParameters = function(parameters, transferableObjects) {
        var createGeometryResults = parameters.createGeometryResults;
        var length = createGeometryResults.length;

        for (var i = 0; i < length; i++) {
            transferableObjects.push(createGeometryResults[i].packedData.buffer);
        }

        var packedPickIds;
        if (parameters.allowPicking) {
            packedPickIds = packPickIds(parameters.pickIds, transferableObjects);
        }

        return {
            createGeometryResults : parameters.createGeometryResults,
            packedInstances : packInstancesForCombine(parameters.instances, transferableObjects),
            packedPickIds : packedPickIds,
            ellipsoid : parameters.ellipsoid,
            isGeographic : parameters.projection instanceof GeographicProjection,
            elementIndexUintSupported : parameters.elementIndexUintSupported,
            scene3DOnly : parameters.scene3DOnly,
            allowPicking : parameters.allowPicking,
            vertexCacheOptimize : parameters.vertexCacheOptimize,
            compressVertices : parameters.compressVertices,
            modelMatrix : parameters.modelMatrix
        };
    };

    /**
     * @private
     */
    PrimitivePipeline.unpackCombineGeometryParameters = function(packedParameters) {
        var instances = unpackInstancesForCombine(packedParameters.packedInstances);
        var allowPicking = packedParameters.allowPicking;
        var pickIds = allowPicking ? unpackPickIds(packedParameters.packedPickIds) : undefined;
        var createGeometryResults = packedParameters.createGeometryResults;
        var length = createGeometryResults.length;
        var instanceIndex = 0;

        var validInstances = [];
        var invalidInstances = [];
        var validInstancesIndices = [];
        var invalidInstancesIndices = [];
        var validPickIds = [];

        for (var resultIndex = 0; resultIndex < length; resultIndex++) {
            var geometries = PrimitivePipeline.unpackCreateGeometryResults(createGeometryResults[resultIndex]);
            var geometriesLength = geometries.length;
            for (var geometryIndex = 0; geometryIndex < geometriesLength; geometryIndex++) {
                var geometry = geometries[geometryIndex];
                var instance = instances[instanceIndex];

                if (defined(geometry)) {
                    instance.geometry = geometry;
                    validInstances.push(instance);
                    validInstancesIndices.push(instanceIndex);
                    if (allowPicking) {
                        validPickIds.push(pickIds[instanceIndex]);
                    }
                } else {
                    invalidInstances.push(instance);
                    invalidInstancesIndices.push(instanceIndex);
                }

                ++instanceIndex;
            }
        }

        var ellipsoid = Ellipsoid.clone(packedParameters.ellipsoid);
        var projection = packedParameters.isGeographic ? new GeographicProjection(ellipsoid) : new WebMercatorProjection(ellipsoid);

        return {
            instances : validInstances,
            invalidInstances : invalidInstances,
            validInstancesIndices : validInstancesIndices,
            invalidInstancesIndices : invalidInstancesIndices,
            pickIds : validPickIds,
            ellipsoid : ellipsoid,
            projection : projection,
            elementIndexUintSupported : packedParameters.elementIndexUintSupported,
            scene3DOnly : packedParameters.scene3DOnly,
            allowPicking : packedParameters.allowPicking,
            vertexCacheOptimize : packedParameters.vertexCacheOptimize,
            compressVertices : packedParameters.compressVertices,
            modelMatrix : Matrix4.clone(packedParameters.modelMatrix)
        };
    };

    /**
     * @private
     */
    PrimitivePipeline.packCombineGeometryResults = function(results, transferableObjects) {
        if (defined(results.geometries)) {
            transferGeometries(results.geometries, transferableObjects);
            transferPerInstanceAttributes(results.vaAttributes, transferableObjects);
        }

        return {
            geometries : results.geometries,
            attributeLocations : results.attributeLocations,
            vaAttributes : results.vaAttributes,
            packedVaAttributeLocations : packAttributeLocations(results.vaAttributeLocations, transferableObjects),
            modelMatrix : results.modelMatrix,
            validInstancesIndices : results.validInstancesIndices,
            invalidInstancesIndices : results.invalidInstancesIndices
        };
    };

    /**
     * @private
     */
    PrimitivePipeline.unpackCombineGeometryResults = function(packedResult) {
        return {
            geometries : packedResult.geometries,
            attributeLocations : packedResult.attributeLocations,
            vaAttributes : packedResult.vaAttributes,
            perInstanceAttributeLocations : unpackAttributeLocations(packedResult.packedVaAttributeLocations, packedResult.vaAttributes),
            modelMatrix : packedResult.modelMatrix
        };
    };

    return PrimitivePipeline;
});