text: define([
          '../Core/ComponentDatatype',
          '../Core/defaultValue',
          '../Core/defined',
          '../Core/destroyObject',
          '../Core/DeveloperError',
          '../Core/Math',
          './Buffer',
          './BufferUsage',
          './VertexArray'
      ], function(
          ComponentDatatype,
          defaultValue,
          defined,
          destroyObject,
          DeveloperError,
          CesiumMath,
          Buffer,
          BufferUsage,
          VertexArray) {
      'use strict';
  
      /**
       * @private
       */
      function VertexArrayFacade(context, attributes, sizeInVertices, instanced) {
          //>>includeStart('debug', pragmas.debug);
          if (!context) {
              throw new DeveloperError('context is required.');
          }
          if (!attributes || (attributes.length === 0)) {
              throw new DeveloperError('At least one attribute is required.');
          }
          //>>includeEnd('debug');
  
          var attrs = VertexArrayFacade._verifyAttributes(attributes);
          sizeInVertices = defaultValue(sizeInVertices, 0);
          var precreatedAttributes = [];
          var attributesByUsage = {};
          var attributesForUsage;
          var usage;
  
          // Bucket the attributes by usage.
          var length = attrs.length;
          for (var i = 0; i < length; ++i) {
              var attribute = attrs[i];
  
              // If the attribute already has a vertex buffer, we do not need
              // to manage a vertex buffer or typed array for it.
              if (attribute.vertexBuffer) {
                  precreatedAttributes.push(attribute);
                  continue;
              }
  
              usage = attribute.usage;
              attributesForUsage = attributesByUsage[usage];
              if (!defined(attributesForUsage)) {
                  attributesForUsage = attributesByUsage[usage] = [];
              }
  
              attributesForUsage.push(attribute);
          }
  
          // A function to sort attributes by the size of their components.  From left to right, a vertex
          // stores floats, shorts, and then bytes.
          function compare(left, right) {
              return ComponentDatatype.getSizeInBytes(right.componentDatatype) - ComponentDatatype.getSizeInBytes(left.componentDatatype);
          }
  
          this._allBuffers = [];
  
          for (usage in attributesByUsage) {
              if (attributesByUsage.hasOwnProperty(usage)) {
                  attributesForUsage = attributesByUsage[usage];
  
                  attributesForUsage.sort(compare);
                  var vertexSizeInBytes = VertexArrayFacade._vertexSizeInBytes(attributesForUsage);
  
                  var bufferUsage = attributesForUsage[0].usage;
  
                  var buffer = {
                      vertexSizeInBytes : vertexSizeInBytes,
                      vertexBuffer : undefined,
                      usage : bufferUsage,
                      needsCommit : false,
                      arrayBuffer : undefined,
                      arrayViews : VertexArrayFacade._createArrayViews(attributesForUsage, vertexSizeInBytes)
                  };
  
                  this._allBuffers.push(buffer);
              }
          }
  
          this._size = 0;
          this._instanced = defaultValue(instanced, false);
  
          this._precreated = precreatedAttributes;
          this._context = context;
  
          this.writers = undefined;
          this.va = undefined;
  
          this.resize(sizeInVertices);
      }
      VertexArrayFacade._verifyAttributes = function(attributes) {
          var attrs = [];
  
          for ( var i = 0; i < attributes.length; ++i) {
              var attribute = attributes[i];
  
              var attr = {
                  index : defaultValue(attribute.index, i),
                  enabled : defaultValue(attribute.enabled, true),
                  componentsPerAttribute : attribute.componentsPerAttribute,
                  componentDatatype : defaultValue(attribute.componentDatatype, ComponentDatatype.FLOAT),
                  normalize : defaultValue(attribute.normalize, false),
  
                  // There will be either a vertexBuffer or an [optional] usage.
                  vertexBuffer : attribute.vertexBuffer,
                  usage : defaultValue(attribute.usage, BufferUsage.STATIC_DRAW)
              };
              attrs.push(attr);
  
              //>>includeStart('debug', pragmas.debug);
              if ((attr.componentsPerAttribute !== 1) && (attr.componentsPerAttribute !== 2) && (attr.componentsPerAttribute !== 3) && (attr.componentsPerAttribute !== 4)) {
                  throw new DeveloperError('attribute.componentsPerAttribute must be in the range [1, 4].');
              }
  
              var datatype = attr.componentDatatype;
              if (!ComponentDatatype.validate(datatype)) {
                  throw new DeveloperError('Attribute must have a valid componentDatatype or not specify it.');
              }
  
              if (!BufferUsage.validate(attr.usage)) {
                  throw new DeveloperError('Attribute must have a valid usage or not specify it.');
              }
              //>>includeEnd('debug');
          }
  
          // Verify all attribute names are unique.
          var uniqueIndices = new Array(attrs.length);
          for ( var j = 0; j < attrs.length; ++j) {
              var currentAttr = attrs[j];
              var index = currentAttr.index;
              //>>includeStart('debug', pragmas.debug);
              if (uniqueIndices[index]) {
                  throw new DeveloperError('Index ' + index + ' is used by more than one attribute.');
              }
              //>>includeEnd('debug');
              uniqueIndices[index] = true;
          }
  
          return attrs;
      };
  
      VertexArrayFacade._vertexSizeInBytes = function(attributes) {
          var sizeInBytes = 0;
  
          var length = attributes.length;
          for ( var i = 0; i < length; ++i) {
              var attribute = attributes[i];
              sizeInBytes += (attribute.componentsPerAttribute * ComponentDatatype.getSizeInBytes(attribute.componentDatatype));
          }
  
          var maxComponentSizeInBytes = (length > 0) ? ComponentDatatype.getSizeInBytes(attributes[0].componentDatatype) : 0; // Sorted by size
          var remainder = (maxComponentSizeInBytes > 0) ? (sizeInBytes % maxComponentSizeInBytes) : 0;
          var padding = (remainder === 0) ? 0 : (maxComponentSizeInBytes - remainder);
          sizeInBytes += padding;
  
          return sizeInBytes;
      };
  
      VertexArrayFacade._createArrayViews = function(attributes, vertexSizeInBytes) {
          var views = [];
          var offsetInBytes = 0;
  
          var length = attributes.length;
          for ( var i = 0; i < length; ++i) {
              var attribute = attributes[i];
              var componentDatatype = attribute.componentDatatype;
  
              views.push({
                  index : attribute.index,
                  enabled : attribute.enabled,
                  componentsPerAttribute : attribute.componentsPerAttribute,
                  componentDatatype : componentDatatype,
                  normalize : attribute.normalize,
  
                  offsetInBytes : offsetInBytes,
                  vertexSizeInComponentType : vertexSizeInBytes / ComponentDatatype.getSizeInBytes(componentDatatype),
  
                  view : undefined
              });
  
              offsetInBytes += (attribute.componentsPerAttribute * ComponentDatatype.getSizeInBytes(componentDatatype));
          }
  
          return views;
      };
  
      /**
       * Invalidates writers.  Can't render again until commit is called.
       */
      VertexArrayFacade.prototype.resize = function(sizeInVertices) {
          this._size = sizeInVertices;
  
          var allBuffers = this._allBuffers;
          this.writers = [];
  
          for (var i = 0, len = allBuffers.length; i < len; ++i) {
              var buffer = allBuffers[i];
  
              VertexArrayFacade._resize(buffer, this._size);
  
              // Reserving invalidates the writers, so if client's cache them, they need to invalidate their cache.
              VertexArrayFacade._appendWriters(this.writers, buffer);
          }
  
          // VAs are recreated next time commit is called.
          destroyVA(this);
      };
  
      VertexArrayFacade._resize = function(buffer, size) {
          if (buffer.vertexSizeInBytes > 0) {
              // Create larger array buffer
              var arrayBuffer = new ArrayBuffer(size * buffer.vertexSizeInBytes);
  
              // Copy contents from previous array buffer
              if (defined(buffer.arrayBuffer)) {
                  var destView = new Uint8Array(arrayBuffer);
                  var sourceView = new Uint8Array(buffer.arrayBuffer);
                  var sourceLength = sourceView.length;
                  for ( var j = 0; j < sourceLength; ++j) {
                      destView[j] = sourceView[j];
                  }
              }
  
              // Create typed views into the new array buffer
              var views = buffer.arrayViews;
              var length = views.length;
              for ( var i = 0; i < length; ++i) {
                  var view = views[i];
                  view.view = ComponentDatatype.createArrayBufferView(view.componentDatatype, arrayBuffer, view.offsetInBytes);
              }
  
              buffer.arrayBuffer = arrayBuffer;
          }
      };
  
      var createWriters = [
      // 1 component per attribute
      function(buffer, view, vertexSizeInComponentType) {
          return function(index, attribute) {
              view[index * vertexSizeInComponentType] = attribute;
              buffer.needsCommit = true;
          };
      },
  
      // 2 component per attribute
      function(buffer, view, vertexSizeInComponentType) {
          return function(index, component0, component1) {
              var i = index * vertexSizeInComponentType;
              view[i] = component0;
              view[i + 1] = component1;
              buffer.needsCommit = true;
          };
      },
  
      // 3 component per attribute
      function(buffer, view, vertexSizeInComponentType) {
          return function(index, component0, component1, component2) {
              var i = index * vertexSizeInComponentType;
              view[i] = component0;
              view[i + 1] = component1;
              view[i + 2] = component2;
              buffer.needsCommit = true;
          };
      },
  
      // 4 component per attribute
      function(buffer, view, vertexSizeInComponentType) {
          return function(index, component0, component1, component2, component3) {
              var i = index * vertexSizeInComponentType;
              view[i] = component0;
              view[i + 1] = component1;
              view[i + 2] = component2;
              view[i + 3] = component3;
              buffer.needsCommit = true;
          };
      }];
  
      VertexArrayFacade._appendWriters = function(writers, buffer) {
          var arrayViews = buffer.arrayViews;
          var length = arrayViews.length;
          for ( var i = 0; i < length; ++i) {
              var arrayView = arrayViews[i];
              writers[arrayView.index] = createWriters[arrayView.componentsPerAttribute - 1](buffer, arrayView.view, arrayView.vertexSizeInComponentType);
          }
      };
  
      VertexArrayFacade.prototype.commit = function(indexBuffer) {
          var recreateVA = false;
  
          var allBuffers = this._allBuffers;
          var buffer;
          var i;
          var length;
  
          for (i = 0, length = allBuffers.length; i < length; ++i) {
              buffer = allBuffers[i];
              recreateVA = commit(this, buffer) || recreateVA;
          }
  
          ///////////////////////////////////////////////////////////////////////
  
          if (recreateVA || !defined(this.va)) {
              destroyVA(this);
              var va = this.va = [];
  
              var numberOfVertexArrays = defined(indexBuffer) ? Math.ceil(this._size / (CesiumMath.SIXTY_FOUR_KILOBYTES - 1)) : 1;
              for ( var k = 0; k < numberOfVertexArrays; ++k) {
                  var attributes = [];
                  for (i = 0, length = allBuffers.length; i < length; ++i) {
                      buffer = allBuffers[i];
                      var offset = k * (buffer.vertexSizeInBytes * (CesiumMath.SIXTY_FOUR_KILOBYTES - 1));
                      VertexArrayFacade._appendAttributes(attributes, buffer, offset, this._instanced);
                  }
  
                  attributes = attributes.concat(this._precreated);
  
                  va.push({
                      va : new VertexArray({
                          context : this._context,
                          attributes : attributes,
                          indexBuffer : indexBuffer
                      }),
                      indicesCount : 1.5 * ((k !== (numberOfVertexArrays - 1)) ? (CesiumMath.SIXTY_FOUR_KILOBYTES - 1) : (this._size % (CesiumMath.SIXTY_FOUR_KILOBYTES - 1)))
                  // TODO: not hardcode 1.5, this assumes 6 indices per 4 vertices (as for Billboard quads).
                  });
              }
          }
      };
  
      function commit(vertexArrayFacade, buffer) {
          if (buffer.needsCommit && (buffer.vertexSizeInBytes > 0)) {
              buffer.needsCommit = false;
  
              var vertexBuffer = buffer.vertexBuffer;
              var vertexBufferSizeInBytes = vertexArrayFacade._size * buffer.vertexSizeInBytes;
              var vertexBufferDefined = defined(vertexBuffer);
              if (!vertexBufferDefined || (vertexBuffer.sizeInBytes < vertexBufferSizeInBytes)) {
                  if (vertexBufferDefined) {
                      vertexBuffer.destroy();
                  }
                  buffer.vertexBuffer = Buffer.createVertexBuffer({
                      context : vertexArrayFacade._context,
                      typedArray : buffer.arrayBuffer,
                      usage : buffer.usage
                  });
                  buffer.vertexBuffer.vertexArrayDestroyable = false;
  
                  return true; // Created new vertex buffer
              }
  
              buffer.vertexBuffer.copyFromArrayView(buffer.arrayBuffer);
          }
  
          return false; // Did not create new vertex buffer
      }
  
      VertexArrayFacade._appendAttributes = function(attributes, buffer, vertexBufferOffset, instanced) {
          var arrayViews = buffer.arrayViews;
          var length = arrayViews.length;
          for ( var i = 0; i < length; ++i) {
              var view = arrayViews[i];
  
              attributes.push({
                  index : view.index,
                  enabled : view.enabled,
                  componentsPerAttribute : view.componentsPerAttribute,
                  componentDatatype : view.componentDatatype,
                  normalize : view.normalize,
                  vertexBuffer : buffer.vertexBuffer,
                  offsetInBytes : vertexBufferOffset + view.offsetInBytes,
                  strideInBytes : buffer.vertexSizeInBytes,
                  instanceDivisor : instanced ? 1 : 0
              });
          }
      };
  
      VertexArrayFacade.prototype.subCommit = function(offsetInVertices, lengthInVertices) {
          //>>includeStart('debug', pragmas.debug);
          if (offsetInVertices < 0 || offsetInVertices >= this._size) {
              throw new DeveloperError('offsetInVertices must be greater than or equal to zero and less than the vertex array size.');
          }
          if (offsetInVertices + lengthInVertices > this._size) {
              throw new DeveloperError('offsetInVertices + lengthInVertices cannot exceed the vertex array size.');
          }
          //>>includeEnd('debug');
  
          var allBuffers = this._allBuffers;
          for (var i = 0, len = allBuffers.length; i < len; ++i) {
              subCommit(allBuffers[i], offsetInVertices, lengthInVertices);
          }
      };
  
      function subCommit(buffer, offsetInVertices, lengthInVertices) {
          if (buffer.needsCommit && (buffer.vertexSizeInBytes > 0)) {
              var byteOffset = buffer.vertexSizeInBytes * offsetInVertices;
              var byteLength = buffer.vertexSizeInBytes * lengthInVertices;
  
              // PERFORMANCE_IDEA: If we want to get really crazy, we could consider updating
              // individual attributes instead of the entire (sub-)vertex.
              //
              // PERFORMANCE_IDEA: Does creating the typed view add too much GC overhead?
              buffer.vertexBuffer.copyFromArrayView(new Uint8Array(buffer.arrayBuffer, byteOffset, byteLength), byteOffset);
          }
      }
  
      VertexArrayFacade.prototype.endSubCommits = function() {
          var allBuffers = this._allBuffers;
  
          for (var i = 0, len = allBuffers.length; i < len; ++i) {
              allBuffers[i].needsCommit = false;
          }
      };
  
      function destroyVA(vertexArrayFacade) {
          var va = vertexArrayFacade.va;
          if (!defined(va)) {
              return;
          }
  
          var length = va.length;
          for (var i = 0; i < length; ++i) {
              va[i].va.destroy();
          }
  
          vertexArrayFacade.va = undefined;
      }
  
      VertexArrayFacade.prototype.isDestroyed = function() {
          return false;
      };
  
      VertexArrayFacade.prototype.destroy = function() {
          var allBuffers = this._allBuffers;
          for (var i = 0, len = allBuffers.length; i < len; ++i) {
              var buffer = allBuffers[i];
              buffer.vertexBuffer = buffer.vertexBuffer && buffer.vertexBuffer.destroy();
          }
  
          destroyVA(this);
  
          return destroyObject(this);
      };
  
      return VertexArrayFacade;
  });
  