import Check from "../Core/Check.js";
import ComponentDatatype from "../Core/ComponentDatatype.js";
import defaultValue from "../Core/defaultValue.js";
import defined from "../Core/defined.js";
import destroyObject from "../Core/destroyObject.js";
import DeveloperError from "../Core/DeveloperError.js";
import Geometry from "../Core/Geometry.js";
import IndexDatatype from "../Core/IndexDatatype.js";
import CesiumMath from "../Core/Math.js";
import RuntimeError from "../Core/RuntimeError.js";
import Buffer from "./Buffer.js";
import BufferUsage from "./BufferUsage.js";
import ContextLimits from "./ContextLimits.js";

function addAttribute(attributes, attribute, index, context) {
  var hasVertexBuffer = defined(attribute.vertexBuffer);
  var hasValue = defined(attribute.value);
  var componentsPerAttribute = attribute.value
    ? attribute.value.length
    : attribute.componentsPerAttribute;

  //>>includeStart('debug', pragmas.debug);
  if (!hasVertexBuffer && !hasValue) {
    throw new DeveloperError("attribute must have a vertexBuffer or a value.");
  }
  if (hasVertexBuffer && hasValue) {
    throw new DeveloperError(
      "attribute cannot have both a vertexBuffer and a value.  It must have either a vertexBuffer property defining per-vertex data or a value property defining data for all vertices."
    );
  }
  if (
    componentsPerAttribute !== 1 &&
    componentsPerAttribute !== 2 &&
    componentsPerAttribute !== 3 &&
    componentsPerAttribute !== 4
  ) {
    if (hasValue) {
      throw new DeveloperError(
        "attribute.value.length must be in the range [1, 4]."
      );
    }

    throw new DeveloperError(
      "attribute.componentsPerAttribute must be in the range [1, 4]."
    );
  }
  if (
    defined(attribute.componentDatatype) &&
    !ComponentDatatype.validate(attribute.componentDatatype)
  ) {
    throw new DeveloperError(
      "attribute must have a valid componentDatatype or not specify it."
    );
  }
  if (defined(attribute.strideInBytes) && attribute.strideInBytes > 255) {
    // WebGL limit.  Not in GL ES.
    throw new DeveloperError(
      "attribute must have a strideInBytes less than or equal to 255 or not specify it."
    );
  }
  if (
    defined(attribute.instanceDivisor) &&
    attribute.instanceDivisor > 0 &&
    !context.instancedArrays
  ) {
    throw new DeveloperError("instanced arrays is not supported");
  }
  if (defined(attribute.instanceDivisor) && attribute.instanceDivisor < 0) {
    throw new DeveloperError(
      "attribute must have an instanceDivisor greater than or equal to zero"
    );
  }
  if (defined(attribute.instanceDivisor) && hasValue) {
    throw new DeveloperError(
      "attribute cannot have have an instanceDivisor if it is not backed by a buffer"
    );
  }
  if (
    defined(attribute.instanceDivisor) &&
    attribute.instanceDivisor > 0 &&
    attribute.index === 0
  ) {
    throw new DeveloperError(
      "attribute zero cannot have an instanceDivisor greater than 0"
    );
  }
  //>>includeEnd('debug');

  // Shallow copy the attribute; we do not want to copy the vertex buffer.
  var attr = {
    index: defaultValue(attribute.index, index),
    enabled: defaultValue(attribute.enabled, true),
    vertexBuffer: attribute.vertexBuffer,
    value: hasValue ? attribute.value.slice(0) : undefined,
    componentsPerAttribute: componentsPerAttribute,
    componentDatatype: defaultValue(
      attribute.componentDatatype,
      ComponentDatatype.FLOAT
    ),
    normalize: defaultValue(attribute.normalize, false),
    offsetInBytes: defaultValue(attribute.offsetInBytes, 0),
    strideInBytes: defaultValue(attribute.strideInBytes, 0),
    instanceDivisor: defaultValue(attribute.instanceDivisor, 0),
  };

  if (hasVertexBuffer) {
    // Common case: vertex buffer for per-vertex data
    attr.vertexAttrib = function (gl) {
      var index = this.index;
      gl.bindBuffer(gl.ARRAY_BUFFER, this.vertexBuffer._getBuffer());
      gl.vertexAttribPointer(
        index,
        this.componentsPerAttribute,
        this.componentDatatype,
        this.normalize,
        this.strideInBytes,
        this.offsetInBytes
      );
      gl.enableVertexAttribArray(index);
      if (this.instanceDivisor > 0) {
        context.glVertexAttribDivisor(index, this.instanceDivisor);
        context._vertexAttribDivisors[index] = this.instanceDivisor;
        context._previousDrawInstanced = true;
      }
    };

    attr.disableVertexAttribArray = function (gl) {
      gl.disableVertexAttribArray(this.index);
      if (this.instanceDivisor > 0) {
        context.glVertexAttribDivisor(index, 0);
      }
    };
  } else {
    // Less common case: value array for the same data for each vertex
    switch (attr.componentsPerAttribute) {
      case 1:
        attr.vertexAttrib = function (gl) {
          gl.vertexAttrib1fv(this.index, this.value);
        };
        break;
      case 2:
        attr.vertexAttrib = function (gl) {
          gl.vertexAttrib2fv(this.index, this.value);
        };
        break;
      case 3:
        attr.vertexAttrib = function (gl) {
          gl.vertexAttrib3fv(this.index, this.value);
        };
        break;
      case 4:
        attr.vertexAttrib = function (gl) {
          gl.vertexAttrib4fv(this.index, this.value);
        };
        break;
    }

    attr.disableVertexAttribArray = function (gl) {};
  }

  attributes.push(attr);
}

function bind(gl, attributes, indexBuffer) {
  for (var i = 0; i < attributes.length; ++i) {
    var attribute = attributes[i];
    if (attribute.enabled) {
      attribute.vertexAttrib(gl);
    }
  }

  if (defined(indexBuffer)) {
    gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, indexBuffer._getBuffer());
  }
}

/**
 * Creates a vertex array, which defines the attributes making up a vertex, and contains an optional index buffer
 * to select vertices for rendering.  Attributes are defined using object literals as shown in Example 1 below.
 *
 * @param {Object} options Object with the following properties:
 * @param {Context} options.context The context in which the VertexArray gets created.
 * @param {Object[]} options.attributes An array of attributes.
 * @param {IndexBuffer} [options.indexBuffer] An optional index buffer.
 *
 * @returns {VertexArray} The vertex array, ready for use with drawing.
 *
 * @exception {DeveloperError} Attribute must have a <code>vertexBuffer</code>.
 * @exception {DeveloperError} Attribute must have a <code>componentsPerAttribute</code>.
 * @exception {DeveloperError} Attribute must have a valid <code>componentDatatype</code> or not specify it.
 * @exception {DeveloperError} Attribute must have a <code>strideInBytes</code> less than or equal to 255 or not specify it.
 * @exception {DeveloperError} Index n is used by more than one attribute.
 *
 *
 * @example
 * // Example 1. Create a vertex array with vertices made up of three floating point
 * // values, e.g., a position, from a single vertex buffer.  No index buffer is used.
 * var positionBuffer = Buffer.createVertexBuffer({
 *     context : context,
 *     sizeInBytes : 12,
 *     usage : BufferUsage.STATIC_DRAW
 * });
 * var attributes = [
 *     {
 *         index                  : 0,
 *         enabled                : true,
 *         vertexBuffer           : positionBuffer,
 *         componentsPerAttribute : 3,
 *         componentDatatype      : ComponentDatatype.FLOAT,
 *         normalize              : false,
 *         offsetInBytes          : 0,
 *         strideInBytes          : 0 // tightly packed
 *         instanceDivisor        : 0 // not instanced
 *     }
 * ];
 * var va = new VertexArray({
 *     context : context,
 *     attributes : attributes
 * });
 *
 * @example
 * // Example 2. Create a vertex array with vertices from two different vertex buffers.
 * // Each vertex has a three-component position and three-component normal.
 * var positionBuffer = Buffer.createVertexBuffer({
 *     context : context,
 *     sizeInBytes : 12,
 *     usage : BufferUsage.STATIC_DRAW
 * });
 * var normalBuffer = Buffer.createVertexBuffer({
 *     context : context,
 *     sizeInBytes : 12,
 *     usage : BufferUsage.STATIC_DRAW
 * });
 * var attributes = [
 *     {
 *         index                  : 0,
 *         vertexBuffer           : positionBuffer,
 *         componentsPerAttribute : 3,
 *         componentDatatype      : ComponentDatatype.FLOAT
 *     },
 *     {
 *         index                  : 1,
 *         vertexBuffer           : normalBuffer,
 *         componentsPerAttribute : 3,
 *         componentDatatype      : ComponentDatatype.FLOAT
 *     }
 * ];
 * var va = new VertexArray({
 *     context : context,
 *     attributes : attributes
 * });
 *
 * @example
 * // Example 3. Creates the same vertex layout as Example 2 using a single
 * // vertex buffer, instead of two.
 * var buffer = Buffer.createVertexBuffer({
 *     context : context,
 *     sizeInBytes : 24,
 *     usage : BufferUsage.STATIC_DRAW
 * });
 * var attributes = [
 *     {
 *         vertexBuffer           : buffer,
 *         componentsPerAttribute : 3,
 *         componentDatatype      : ComponentDatatype.FLOAT,
 *         offsetInBytes          : 0,
 *         strideInBytes          : 24
 *     },
 *     {
 *         vertexBuffer           : buffer,
 *         componentsPerAttribute : 3,
 *         componentDatatype      : ComponentDatatype.FLOAT,
 *         normalize              : true,
 *         offsetInBytes          : 12,
 *         strideInBytes          : 24
 *     }
 * ];
 * var va = new VertexArray({
 *     context : context,
 *     attributes : attributes
 * });
 *
 * @see Buffer#createVertexBuffer
 * @see Buffer#createIndexBuffer
 * @see Context#draw
 *
 * @private
 */
function VertexArray(options) {
  options = defaultValue(options, defaultValue.EMPTY_OBJECT);

  //>>includeStart('debug', pragmas.debug);
  Check.defined("options.context", options.context);
  Check.defined("options.attributes", options.attributes);
  //>>includeEnd('debug');

  var context = options.context;
  var gl = context._gl;
  var attributes = options.attributes;
  var indexBuffer = options.indexBuffer;

  var i;
  var vaAttributes = [];
  var numberOfVertices = 1; // if every attribute is backed by a single value
  var hasInstancedAttributes = false;
  var hasConstantAttributes = false;

  var length = attributes.length;
  for (i = 0; i < length; ++i) {
    addAttribute(vaAttributes, attributes[i], i, context);
  }

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

    if (defined(attribute.vertexBuffer) && attribute.instanceDivisor === 0) {
      // This assumes that each vertex buffer in the vertex array has the same number of vertices.
      var bytes =
        attribute.strideInBytes ||
        attribute.componentsPerAttribute *
          ComponentDatatype.getSizeInBytes(attribute.componentDatatype);
      numberOfVertices = attribute.vertexBuffer.sizeInBytes / bytes;
      break;
    }
  }

  for (i = 0; i < length; ++i) {
    if (vaAttributes[i].instanceDivisor > 0) {
      hasInstancedAttributes = true;
    }
    if (defined(vaAttributes[i].value)) {
      hasConstantAttributes = true;
    }
  }

  //>>includeStart('debug', pragmas.debug);
  // Verify all attribute names are unique
  var uniqueIndices = {};
  for (i = 0; i < length; ++i) {
    var index = vaAttributes[i].index;
    if (uniqueIndices[index]) {
      throw new DeveloperError(
        "Index " + index + " is used by more than one attribute."
      );
    }
    uniqueIndices[index] = true;
  }
  //>>includeEnd('debug');

  var vao;

  // Setup VAO if supported
  if (context.vertexArrayObject) {
    vao = context.glCreateVertexArray();
    context.glBindVertexArray(vao);
    bind(gl, vaAttributes, indexBuffer);
    context.glBindVertexArray(null);
  }

  this._numberOfVertices = numberOfVertices;
  this._hasInstancedAttributes = hasInstancedAttributes;
  this._hasConstantAttributes = hasConstantAttributes;
  this._context = context;
  this._gl = gl;
  this._vao = vao;
  this._attributes = vaAttributes;
  this._indexBuffer = indexBuffer;
}

function computeNumberOfVertices(attribute) {
  return attribute.values.length / attribute.componentsPerAttribute;
}

function computeAttributeSizeInBytes(attribute) {
  return (
    ComponentDatatype.getSizeInBytes(attribute.componentDatatype) *
    attribute.componentsPerAttribute
  );
}

function interleaveAttributes(attributes) {
  var j;
  var name;
  var attribute;

  // Extract attribute names.
  var names = [];
  for (name in attributes) {
    // Attribute needs to have per-vertex values; not a constant value for all vertices.
    if (
      attributes.hasOwnProperty(name) &&
      defined(attributes[name]) &&
      defined(attributes[name].values)
    ) {
      names.push(name);

      if (attributes[name].componentDatatype === ComponentDatatype.DOUBLE) {
        attributes[name].componentDatatype = ComponentDatatype.FLOAT;
        attributes[name].values = ComponentDatatype.createTypedArray(
          ComponentDatatype.FLOAT,
          attributes[name].values
        );
      }
    }
  }

  // Validation.  Compute number of vertices.
  var numberOfVertices;
  var namesLength = names.length;

  if (namesLength > 0) {
    numberOfVertices = computeNumberOfVertices(attributes[names[0]]);

    for (j = 1; j < namesLength; ++j) {
      var currentNumberOfVertices = computeNumberOfVertices(
        attributes[names[j]]
      );

      if (currentNumberOfVertices !== numberOfVertices) {
        throw new RuntimeError(
          "Each attribute list must have the same number of vertices.  " +
            "Attribute " +
            names[j] +
            " has a different number of vertices " +
            "(" +
            currentNumberOfVertices.toString() +
            ")" +
            " than attribute " +
            names[0] +
            " (" +
            numberOfVertices.toString() +
            ")."
        );
      }
    }
  }

  // Sort attributes by the size of their components.  From left to right, a vertex stores floats, shorts, and then bytes.
  names.sort(function (left, right) {
    return (
      ComponentDatatype.getSizeInBytes(attributes[right].componentDatatype) -
      ComponentDatatype.getSizeInBytes(attributes[left].componentDatatype)
    );
  });

  // Compute sizes and strides.
  var vertexSizeInBytes = 0;
  var offsetsInBytes = {};

  for (j = 0; j < namesLength; ++j) {
    name = names[j];
    attribute = attributes[name];

    offsetsInBytes[name] = vertexSizeInBytes;
    vertexSizeInBytes += computeAttributeSizeInBytes(attribute);
  }

  if (vertexSizeInBytes > 0) {
    // Pad each vertex to be a multiple of the largest component datatype so each
    // attribute can be addressed using typed arrays.
    var maxComponentSizeInBytes = ComponentDatatype.getSizeInBytes(
      attributes[names[0]].componentDatatype
    ); // Sorted large to small
    var remainder = vertexSizeInBytes % maxComponentSizeInBytes;
    if (remainder !== 0) {
      vertexSizeInBytes += maxComponentSizeInBytes - remainder;
    }

    // Total vertex buffer size in bytes, including per-vertex padding.
    var vertexBufferSizeInBytes = numberOfVertices * vertexSizeInBytes;

    // Create array for interleaved vertices.  Each attribute has a different view (pointer) into the array.
    var buffer = new ArrayBuffer(vertexBufferSizeInBytes);
    var views = {};

    for (j = 0; j < namesLength; ++j) {
      name = names[j];
      var sizeInBytes = ComponentDatatype.getSizeInBytes(
        attributes[name].componentDatatype
      );

      views[name] = {
        pointer: ComponentDatatype.createTypedArray(
          attributes[name].componentDatatype,
          buffer
        ),
        index: offsetsInBytes[name] / sizeInBytes, // Offset in ComponentType
        strideInComponentType: vertexSizeInBytes / sizeInBytes,
      };
    }

    // Copy attributes into one interleaved array.
    // PERFORMANCE_IDEA:  Can we optimize these loops?
    for (j = 0; j < numberOfVertices; ++j) {
      for (var n = 0; n < namesLength; ++n) {
        name = names[n];
        attribute = attributes[name];
        var values = attribute.values;
        var view = views[name];
        var pointer = view.pointer;

        var numberOfComponents = attribute.componentsPerAttribute;
        for (var k = 0; k < numberOfComponents; ++k) {
          pointer[view.index + k] = values[j * numberOfComponents + k];
        }

        view.index += view.strideInComponentType;
      }
    }

    return {
      buffer: buffer,
      offsetsInBytes: offsetsInBytes,
      vertexSizeInBytes: vertexSizeInBytes,
    };
  }

  // No attributes to interleave.
  return undefined;
}

/**
 * Creates a vertex array from a geometry.  A geometry contains vertex attributes and optional index data
 * in system memory, whereas a vertex array contains vertex buffers and an optional index buffer in WebGL
 * memory for use with rendering.
 * <br /><br />
 * The <code>geometry</code> argument should use the standard layout like the geometry returned by {@link BoxGeometry}.
 * <br /><br />
 * <code>options</code> can have four properties:
 * <ul>
 *   <li><code>geometry</code>:  The source geometry containing data used to create the vertex array.</li>
 *   <li><code>attributeLocations</code>:  An object that maps geometry attribute names to vertex shader attribute locations.</li>
 *   <li><code>bufferUsage</code>:  The expected usage pattern of the vertex array's buffers.  On some WebGL implementations, this can significantly affect performance.  See {@link BufferUsage}.  Default: <code>BufferUsage.DYNAMIC_DRAW</code>.</li>
 *   <li><code>interleave</code>:  Determines if all attributes are interleaved in a single vertex buffer or if each attribute is stored in a separate vertex buffer.  Default: <code>false</code>.</li>
 * </ul>
 * <br />
 * If <code>options</code> is not specified or the <code>geometry</code> contains no data, the returned vertex array is empty.
 *
 * @param {Object} options An object defining the geometry, attribute indices, buffer usage, and vertex layout used to create the vertex array.
 *
 * @exception {RuntimeError} Each attribute list must have the same number of vertices.
 * @exception {DeveloperError} The geometry must have zero or one index lists.
 * @exception {DeveloperError} Index n is used by more than one attribute.
 *
 *
 * @example
 * // Example 1. Creates a vertex array for rendering a box.  The default dynamic draw
 * // usage is used for the created vertex and index buffer.  The attributes are not
 * // interleaved by default.
 * var geometry = new BoxGeometry();
 * var va = VertexArray.fromGeometry({
 *     context            : context,
 *     geometry           : geometry,
 *     attributeLocations : GeometryPipeline.createAttributeLocations(geometry),
 * });
 *
 * @example
 * // Example 2. Creates a vertex array with interleaved attributes in a
 * // single vertex buffer.  The vertex and index buffer have static draw usage.
 * var va = VertexArray.fromGeometry({
 *     context            : context,
 *     geometry           : geometry,
 *     attributeLocations : GeometryPipeline.createAttributeLocations(geometry),
 *     bufferUsage        : BufferUsage.STATIC_DRAW,
 *     interleave         : true
 * });
 *
 * @example
 * // Example 3.  When the caller destroys the vertex array, it also destroys the
 * // attached vertex buffer(s) and index buffer.
 * va = va.destroy();
 *
 * @see Buffer#createVertexBuffer
 * @see Buffer#createIndexBuffer
 * @see GeometryPipeline.createAttributeLocations
 * @see ShaderProgram
 */
VertexArray.fromGeometry = function (options) {
  options = defaultValue(options, defaultValue.EMPTY_OBJECT);

  //>>includeStart('debug', pragmas.debug);
  Check.defined("options.context", options.context);
  //>>includeEnd('debug');

  var context = options.context;
  var geometry = defaultValue(options.geometry, defaultValue.EMPTY_OBJECT);

  var bufferUsage = defaultValue(options.bufferUsage, BufferUsage.DYNAMIC_DRAW);

  var attributeLocations = defaultValue(
    options.attributeLocations,
    defaultValue.EMPTY_OBJECT
  );
  var interleave = defaultValue(options.interleave, false);
  var createdVAAttributes = options.vertexArrayAttributes;

  var name;
  var attribute;
  var vertexBuffer;
  var vaAttributes = defined(createdVAAttributes) ? createdVAAttributes : [];
  var attributes = geometry.attributes;

  if (interleave) {
    // Use a single vertex buffer with interleaved vertices.
    var interleavedAttributes = interleaveAttributes(attributes);
    if (defined(interleavedAttributes)) {
      vertexBuffer = Buffer.createVertexBuffer({
        context: context,
        typedArray: interleavedAttributes.buffer,
        usage: bufferUsage,
      });
      var offsetsInBytes = interleavedAttributes.offsetsInBytes;
      var strideInBytes = interleavedAttributes.vertexSizeInBytes;

      for (name in attributes) {
        if (attributes.hasOwnProperty(name) && defined(attributes[name])) {
          attribute = attributes[name];

          if (defined(attribute.values)) {
            // Common case: per-vertex attributes
            vaAttributes.push({
              index: attributeLocations[name],
              vertexBuffer: vertexBuffer,
              componentDatatype: attribute.componentDatatype,
              componentsPerAttribute: attribute.componentsPerAttribute,
              normalize: attribute.normalize,
              offsetInBytes: offsetsInBytes[name],
              strideInBytes: strideInBytes,
            });
          } else {
            // Constant attribute for all vertices
            vaAttributes.push({
              index: attributeLocations[name],
              value: attribute.value,
              componentDatatype: attribute.componentDatatype,
              normalize: attribute.normalize,
            });
          }
        }
      }
    }
  } else {
    // One vertex buffer per attribute.
    for (name in attributes) {
      if (attributes.hasOwnProperty(name) && defined(attributes[name])) {
        attribute = attributes[name];

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

        vertexBuffer = undefined;
        if (defined(attribute.values)) {
          vertexBuffer = Buffer.createVertexBuffer({
            context: context,
            typedArray: ComponentDatatype.createTypedArray(
              componentDatatype,
              attribute.values
            ),
            usage: bufferUsage,
          });
        }

        vaAttributes.push({
          index: attributeLocations[name],
          vertexBuffer: vertexBuffer,
          value: attribute.value,
          componentDatatype: componentDatatype,
          componentsPerAttribute: attribute.componentsPerAttribute,
          normalize: attribute.normalize,
        });
      }
    }
  }

  var indexBuffer;
  var indices = geometry.indices;
  if (defined(indices)) {
    if (
      Geometry.computeNumberOfVertices(geometry) >=
        CesiumMath.SIXTY_FOUR_KILOBYTES &&
      context.elementIndexUint
    ) {
      indexBuffer = Buffer.createIndexBuffer({
        context: context,
        typedArray: new Uint32Array(indices),
        usage: bufferUsage,
        indexDatatype: IndexDatatype.UNSIGNED_INT,
      });
    } else {
      indexBuffer = Buffer.createIndexBuffer({
        context: context,
        typedArray: new Uint16Array(indices),
        usage: bufferUsage,
        indexDatatype: IndexDatatype.UNSIGNED_SHORT,
      });
    }
  }

  return new VertexArray({
    context: context,
    attributes: vaAttributes,
    indexBuffer: indexBuffer,
  });
};

Object.defineProperties(VertexArray.prototype, {
  numberOfAttributes: {
    get: function () {
      return this._attributes.length;
    },
  },
  numberOfVertices: {
    get: function () {
      return this._numberOfVertices;
    },
  },
  indexBuffer: {
    get: function () {
      return this._indexBuffer;
    },
  },
});

/**
 * index is the location in the array of attributes, not the index property of an attribute.
 */
VertexArray.prototype.getAttribute = function (index) {
  //>>includeStart('debug', pragmas.debug);
  Check.defined("index", index);
  //>>includeEnd('debug');

  return this._attributes[index];
};

// Workaround for ANGLE, where the attribute divisor seems to be part of the global state instead
// of the VAO state. This function is called when the vao is bound, and should be removed
// once the ANGLE issue is resolved. Setting the divisor should normally happen in vertexAttrib and
// disableVertexAttribArray.
function setVertexAttribDivisor(vertexArray) {
  var context = vertexArray._context;
  var hasInstancedAttributes = vertexArray._hasInstancedAttributes;
  if (!hasInstancedAttributes && !context._previousDrawInstanced) {
    return;
  }
  context._previousDrawInstanced = hasInstancedAttributes;

  var divisors = context._vertexAttribDivisors;
  var attributes = vertexArray._attributes;
  var maxAttributes = ContextLimits.maximumVertexAttributes;
  var i;

  if (hasInstancedAttributes) {
    var length = attributes.length;
    for (i = 0; i < length; ++i) {
      var attribute = attributes[i];
      if (attribute.enabled) {
        var divisor = attribute.instanceDivisor;
        var index = attribute.index;
        if (divisor !== divisors[index]) {
          context.glVertexAttribDivisor(index, divisor);
          divisors[index] = divisor;
        }
      }
    }
  } else {
    for (i = 0; i < maxAttributes; ++i) {
      if (divisors[i] > 0) {
        context.glVertexAttribDivisor(i, 0);
        divisors[i] = 0;
      }
    }
  }
}

// Vertex attributes backed by a constant value go through vertexAttrib[1234]f[v]
// which is part of context state rather than VAO state.
function setConstantAttributes(vertexArray, gl) {
  var attributes = vertexArray._attributes;
  var length = attributes.length;
  for (var i = 0; i < length; ++i) {
    var attribute = attributes[i];
    if (attribute.enabled && defined(attribute.value)) {
      attribute.vertexAttrib(gl);
    }
  }
}

VertexArray.prototype._bind = function () {
  if (defined(this._vao)) {
    this._context.glBindVertexArray(this._vao);
    if (this._context.instancedArrays) {
      setVertexAttribDivisor(this);
    }
    if (this._hasConstantAttributes) {
      setConstantAttributes(this, this._gl);
    }
  } else {
    bind(this._gl, this._attributes, this._indexBuffer);
  }
};

VertexArray.prototype._unBind = function () {
  if (defined(this._vao)) {
    this._context.glBindVertexArray(null);
  } else {
    var attributes = this._attributes;
    var gl = this._gl;

    for (var i = 0; i < attributes.length; ++i) {
      var attribute = attributes[i];
      if (attribute.enabled) {
        attribute.disableVertexAttribArray(gl);
      }
    }
    if (this._indexBuffer) {
      gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, null);
    }
  }
};

VertexArray.prototype.isDestroyed = function () {
  return false;
};

VertexArray.prototype.destroy = function () {
  var attributes = this._attributes;
  for (var i = 0; i < attributes.length; ++i) {
    var vertexBuffer = attributes[i].vertexBuffer;
    if (
      defined(vertexBuffer) &&
      !vertexBuffer.isDestroyed() &&
      vertexBuffer.vertexArrayDestroyable
    ) {
      vertexBuffer.destroy();
    }
  }

  var indexBuffer = this._indexBuffer;
  if (
    defined(indexBuffer) &&
    !indexBuffer.isDestroyed() &&
    indexBuffer.vertexArrayDestroyable
  ) {
    indexBuffer.destroy();
  }

  if (defined(this._vao)) {
    this._context.glDeleteVertexArray(this._vao);
  }

  return destroyObject(this);
};
export default VertexArray;
