import BoundingSphere from "../Core/BoundingSphere.js";
import Cartesian2 from "../Core/Cartesian2.js";
import Cartesian3 from "../Core/Cartesian3.js";
import Cartesian4 from "../Core/Cartesian4.js";
import Cartographic from "../Core/Cartographic.js";
import clone from "../Core/clone.js";
import Color from "../Core/Color.js";
import combine from "../Core/combine.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 EncodedCartesian3 from "../Core/EncodedCartesian3.js";
import FeatureDetection from "../Core/FeatureDetection.js";
import Geometry from "../Core/Geometry.js";
import GeometryAttribute from "../Core/GeometryAttribute.js";
import GeometryAttributes from "../Core/GeometryAttributes.js";
import GeometryOffsetAttribute from "../Core/GeometryOffsetAttribute.js";
import Intersect from "../Core/Intersect.js";
import Matrix4 from "../Core/Matrix4.js";
import Plane from "../Core/Plane.js";
import RuntimeError from "../Core/RuntimeError.js";
import subdivideArray from "../Core/subdivideArray.js";
import TaskProcessor from "../Core/TaskProcessor.js";
import BufferUsage from "../Renderer/BufferUsage.js";
import ContextLimits from "../Renderer/ContextLimits.js";
import DrawCommand from "../Renderer/DrawCommand.js";
import Pass from "../Renderer/Pass.js";
import RenderState from "../Renderer/RenderState.js";
import ShaderProgram from "../Renderer/ShaderProgram.js";
import ShaderSource from "../Renderer/ShaderSource.js";
import VertexArray from "../Renderer/VertexArray.js";
import when from "../ThirdParty/when.js";
import BatchTable from "./BatchTable.js";
import CullFace from "./CullFace.js";
import DepthFunction from "./DepthFunction.js";
import PrimitivePipeline from "./PrimitivePipeline.js";
import PrimitiveState from "./PrimitiveState.js";
import SceneMode from "./SceneMode.js";
import ShadowMode from "./ShadowMode.js";

/**
 * A primitive represents geometry in the {@link Scene}.  The geometry can be from a single {@link GeometryInstance}
 * as shown in example 1 below, or from an array of instances, even if the geometry is from different
 * geometry types, e.g., an {@link RectangleGeometry} and an {@link EllipsoidGeometry} as shown in Code Example 2.
 * <p>
 * A primitive combines geometry instances with an {@link Appearance} that describes the full shading, including
 * {@link Material} and {@link RenderState}.  Roughly, the geometry instance defines the structure and placement,
 * and the appearance defines the visual characteristics.  Decoupling geometry and appearance allows us to mix
 * and match most of them and add a new geometry or appearance independently of each other.
 * </p>
 * <p>
 * Combining multiple instances into one primitive is called batching, and significantly improves performance for static data.
 * Instances can be individually picked; {@link Scene#pick} returns their {@link GeometryInstance#id}.  Using
 * per-instance appearances like {@link PerInstanceColorAppearance}, each instance can also have a unique color.
 * </p>
 * <p>
 * {@link Geometry} can either be created and batched on a web worker or the main thread. The first two examples
 * show geometry that will be created on a web worker by using the descriptions of the geometry. The third example
 * shows how to create the geometry on the main thread by explicitly calling the <code>createGeometry</code> method.
 * </p>
 *
 * @alias Primitive
 * @constructor
 *
 * @param {Object} [options] Object with the following properties:
 * @param {GeometryInstance[]|GeometryInstance} [options.geometryInstances] The geometry instances - or a single geometry instance - to render.
 * @param {Appearance} [options.appearance] The appearance used to render the primitive.
 * @param {Appearance} [options.depthFailAppearance] The appearance used to shade this primitive when it fails the depth test.
 * @param {Boolean} [options.show=true] Determines if this primitive will be shown.
 * @param {Matrix4} [options.modelMatrix=Matrix4.IDENTITY] The 4x4 transformation matrix that transforms the primitive (all geometry instances) from model to world coordinates.
 * @param {Boolean} [options.vertexCacheOptimize=false] When <code>true</code>, geometry vertices are optimized for the pre and post-vertex-shader caches.
 * @param {Boolean} [options.interleave=false] When <code>true</code>, geometry vertex attributes are interleaved, which can slightly improve rendering performance but increases load time.
 * @param {Boolean} [options.compressVertices=true] When <code>true</code>, the geometry vertices are compressed, which will save memory.
 * @param {Boolean} [options.releaseGeometryInstances=true] When <code>true</code>, the primitive does not keep a reference to the input <code>geometryInstances</code> to save memory.
 * @param {Boolean} [options.allowPicking=true] When <code>true</code>, each geometry instance will only be pickable with {@link Scene#pick}.  When <code>false</code>, GPU memory is saved.
 * @param {Boolean} [options.cull=true] When <code>true</code>, the renderer frustum culls and horizon culls the primitive's commands based on their bounding volume.  Set this to <code>false</code> for a small performance gain if you are manually culling the primitive.
 * @param {Boolean} [options.asynchronous=true] Determines if the primitive will be created asynchronously or block until ready.
 * @param {Boolean} [options.debugShowBoundingVolume=false] For debugging only. Determines if this primitive's commands' bounding spheres are shown.
 * @param {ShadowMode} [options.shadows=ShadowMode.DISABLED] Determines whether this primitive casts or receives shadows from light sources.
 *
 * @example
 * // 1. Draw a translucent ellipse on the surface with a checkerboard pattern
 * var instance = new Cesium.GeometryInstance({
 *   geometry : new Cesium.EllipseGeometry({
 *       center : Cesium.Cartesian3.fromDegrees(-100.0, 20.0),
 *       semiMinorAxis : 500000.0,
 *       semiMajorAxis : 1000000.0,
 *       rotation : Cesium.Math.PI_OVER_FOUR,
 *       vertexFormat : Cesium.VertexFormat.POSITION_AND_ST
 *   }),
 *   id : 'object returned when this instance is picked and to get/set per-instance attributes'
 * });
 * scene.primitives.add(new Cesium.Primitive({
 *   geometryInstances : instance,
 *   appearance : new Cesium.EllipsoidSurfaceAppearance({
 *     material : Cesium.Material.fromType('Checkerboard')
 *   })
 * }));
 *
 * @example
 * // 2. Draw different instances each with a unique color
 * var rectangleInstance = new Cesium.GeometryInstance({
 *   geometry : new Cesium.RectangleGeometry({
 *     rectangle : Cesium.Rectangle.fromDegrees(-140.0, 30.0, -100.0, 40.0),
 *     vertexFormat : Cesium.PerInstanceColorAppearance.VERTEX_FORMAT
 *   }),
 *   id : 'rectangle',
 *   attributes : {
 *     color : new Cesium.ColorGeometryInstanceAttribute(0.0, 1.0, 1.0, 0.5)
 *   }
 * });
 * var ellipsoidInstance = new Cesium.GeometryInstance({
 *   geometry : new Cesium.EllipsoidGeometry({
 *     radii : new Cesium.Cartesian3(500000.0, 500000.0, 1000000.0),
 *     vertexFormat : Cesium.VertexFormat.POSITION_AND_NORMAL
 *   }),
 *   modelMatrix : Cesium.Matrix4.multiplyByTranslation(Cesium.Transforms.eastNorthUpToFixedFrame(
 *     Cesium.Cartesian3.fromDegrees(-95.59777, 40.03883)), new Cesium.Cartesian3(0.0, 0.0, 500000.0), new Cesium.Matrix4()),
 *   id : 'ellipsoid',
 *   attributes : {
 *     color : Cesium.ColorGeometryInstanceAttribute.fromColor(Cesium.Color.AQUA)
 *   }
 * });
 * scene.primitives.add(new Cesium.Primitive({
 *   geometryInstances : [rectangleInstance, ellipsoidInstance],
 *   appearance : new Cesium.PerInstanceColorAppearance()
 * }));
 *
 * @example
 * // 3. Create the geometry on the main thread.
 * scene.primitives.add(new Cesium.Primitive({
 *   geometryInstances : new Cesium.GeometryInstance({
 *     geometry : Cesium.EllipsoidGeometry.createGeometry(new Cesium.EllipsoidGeometry({
 *       radii : new Cesium.Cartesian3(500000.0, 500000.0, 1000000.0),
 *       vertexFormat : Cesium.VertexFormat.POSITION_AND_NORMAL
 *     })),
 *     modelMatrix : Cesium.Matrix4.multiplyByTranslation(Cesium.Transforms.eastNorthUpToFixedFrame(
 *       Cesium.Cartesian3.fromDegrees(-95.59777, 40.03883)), new Cesium.Cartesian3(0.0, 0.0, 500000.0), new Cesium.Matrix4()),
 *     id : 'ellipsoid',
 *     attributes : {
 *       color : Cesium.ColorGeometryInstanceAttribute.fromColor(Cesium.Color.AQUA)
 *     }
 *   }),
 *   appearance : new Cesium.PerInstanceColorAppearance(),
 *   asynchronous : false
 * }));
 *
 * @see GeometryInstance
 * @see Appearance
 * @see ClassificationPrimitive
 * @see GroundPrimitive
 */
function Primitive(options) {
  options = defaultValue(options, defaultValue.EMPTY_OBJECT);

  /**
   * The geometry instances rendered with this primitive.  This may
   * be <code>undefined</code> if <code>options.releaseGeometryInstances</code>
   * is <code>true</code> when the primitive is constructed.
   * <p>
   * Changing this property after the primitive is rendered has no effect.
   * </p>
   *
   * @readonly
   * @type GeometryInstance[]|GeometryInstance
   *
   * @default undefined
   */
  this.geometryInstances = options.geometryInstances;

  /**
   * The {@link Appearance} used to shade this primitive. Each geometry
   * instance is shaded with the same appearance.  Some appearances, like
   * {@link PerInstanceColorAppearance} allow giving each instance unique
   * properties.
   *
   * @type Appearance
   *
   * @default undefined
   */
  this.appearance = options.appearance;
  this._appearance = undefined;
  this._material = undefined;

  /**
   * The {@link Appearance} used to shade this primitive when it fails the depth test. Each geometry
   * instance is shaded with the same appearance.  Some appearances, like
   * {@link PerInstanceColorAppearance} allow giving each instance unique
   * properties.
   *
   * <p>
   * When using an appearance that requires a color attribute, like PerInstanceColorAppearance,
   * add a depthFailColor per-instance attribute instead.
   * </p>
   *
   * <p>
   * Requires the EXT_frag_depth WebGL extension to render properly. If the extension is not supported,
   * there may be artifacts.
   * </p>
   * @type Appearance
   *
   * @default undefined
   */
  this.depthFailAppearance = options.depthFailAppearance;
  this._depthFailAppearance = undefined;
  this._depthFailMaterial = undefined;

  /**
   * The 4x4 transformation matrix that transforms the primitive (all geometry instances) from model to world coordinates.
   * When this is the identity matrix, the primitive is 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}.
   *
   * <p>
   * This property is only supported in 3D mode.
   * </p>
   *
   * @type Matrix4
   *
   * @default Matrix4.IDENTITY
   *
   * @example
   * var origin = Cesium.Cartesian3.fromDegrees(-95.0, 40.0, 200000.0);
   * p.modelMatrix = Cesium.Transforms.eastNorthUpToFixedFrame(origin);
   */
  this.modelMatrix = Matrix4.clone(
    defaultValue(options.modelMatrix, Matrix4.IDENTITY)
  );
  this._modelMatrix = new Matrix4();

  /**
   * Determines if the primitive will be shown.  This affects all geometry
   * instances in the primitive.
   *
   * @type Boolean
   *
   * @default true
   */
  this.show = defaultValue(options.show, true);

  this._vertexCacheOptimize = defaultValue(options.vertexCacheOptimize, false);
  this._interleave = defaultValue(options.interleave, false);
  this._releaseGeometryInstances = defaultValue(
    options.releaseGeometryInstances,
    true
  );
  this._allowPicking = defaultValue(options.allowPicking, true);
  this._asynchronous = defaultValue(options.asynchronous, true);
  this._compressVertices = defaultValue(options.compressVertices, true);

  /**
   * When <code>true</code>, the renderer frustum culls and horizon culls the primitive's commands
   * based on their bounding volume.  Set this to <code>false</code> for a small performance gain
   * if you are manually culling the primitive.
   *
   * @type {Boolean}
   *
   * @default true
   */
  this.cull = defaultValue(options.cull, true);

  /**
   * 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
  );

  /**
   * @private
   */
  this.rtcCenter = options.rtcCenter;

  //>>includeStart('debug', pragmas.debug);
  if (
    defined(this.rtcCenter) &&
    (!defined(this.geometryInstances) ||
      (Array.isArray(this.geometryInstances) &&
        this.geometryInstances.length !== 1))
  ) {
    throw new DeveloperError(
      "Relative-to-center rendering only supports one geometry instance."
    );
  }
  //>>includeEnd('debug');

  /**
   * Determines whether this primitive casts or receives shadows from light sources.
   *
   * @type {ShadowMode}
   *
   * @default ShadowMode.DISABLED
   */
  this.shadows = defaultValue(options.shadows, ShadowMode.DISABLED);

  this._translucent = undefined;

  this._state = PrimitiveState.READY;
  this._geometries = [];
  this._error = undefined;
  this._numberOfInstances = 0;

  this._boundingSpheres = [];
  this._boundingSphereWC = [];
  this._boundingSphereCV = [];
  this._boundingSphere2D = [];
  this._boundingSphereMorph = [];
  this._perInstanceAttributeCache = [];
  this._instanceIds = [];
  this._lastPerInstanceAttributeIndex = 0;

  this._va = [];
  this._attributeLocations = undefined;
  this._primitiveType = undefined;

  this._frontFaceRS = undefined;
  this._backFaceRS = undefined;
  this._sp = undefined;

  this._depthFailAppearance = undefined;
  this._spDepthFail = undefined;
  this._frontFaceDepthFailRS = undefined;
  this._backFaceDepthFailRS = undefined;

  this._pickIds = [];

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

  this._createBoundingVolumeFunction = options._createBoundingVolumeFunction;
  this._createRenderStatesFunction = options._createRenderStatesFunction;
  this._createShaderProgramFunction = options._createShaderProgramFunction;
  this._createCommandsFunction = options._createCommandsFunction;
  this._updateAndQueueCommandsFunction =
    options._updateAndQueueCommandsFunction;

  this._createPickOffsets = options._createPickOffsets;
  this._pickOffsets = undefined;

  this._createGeometryResults = undefined;
  this._ready = false;
  this._readyPromise = when.defer();

  this._batchTable = undefined;
  this._batchTableAttributeIndices = undefined;
  this._offsetInstanceExtend = undefined;
  this._batchTableOffsetAttribute2DIndex = undefined;
  this._batchTableOffsetsUpdated = false;
  this._instanceBoundingSpheres = undefined;
  this._instanceBoundingSpheresCV = undefined;
  this._tempBoundingSpheres = undefined;
  this._recomputeBoundingSpheres = false;
  this._batchTableBoundingSpheresUpdated = false;
  this._batchTableBoundingSphereAttributeIndices = undefined;
}

Object.defineProperties(Primitive.prototype, {
  /**
   * When <code>true</code>, geometry vertices are optimized for the pre and post-vertex-shader caches.
   *
   * @memberof Primitive.prototype
   *
   * @type {Boolean}
   * @readonly
   *
   * @default true
   */
  vertexCacheOptimize: {
    get: function () {
      return this._vertexCacheOptimize;
    },
  },

  /**
   * Determines if geometry vertex attributes are interleaved, which can slightly improve rendering performance.
   *
   * @memberof Primitive.prototype
   *
   * @type {Boolean}
   * @readonly
   *
   * @default false
   */
  interleave: {
    get: function () {
      return this._interleave;
    },
  },

  /**
   * When <code>true</code>, the primitive does not keep a reference to the input <code>geometryInstances</code> to save memory.
   *
   * @memberof Primitive.prototype
   *
   * @type {Boolean}
   * @readonly
   *
   * @default true
   */
  releaseGeometryInstances: {
    get: function () {
      return this._releaseGeometryInstances;
    },
  },

  /**
   * When <code>true</code>, each geometry instance will only be pickable with {@link Scene#pick}.  When <code>false</code>, GPU memory is saved.         *
   *
   * @memberof Primitive.prototype
   *
   * @type {Boolean}
   * @readonly
   *
   * @default true
   */
  allowPicking: {
    get: function () {
      return this._allowPicking;
    },
  },

  /**
   * Determines if the geometry instances will be created and batched on a web worker.
   *
   * @memberof Primitive.prototype
   *
   * @type {Boolean}
   * @readonly
   *
   * @default true
   */
  asynchronous: {
    get: function () {
      return this._asynchronous;
    },
  },

  /**
   * When <code>true</code>, geometry vertices are compressed, which will save memory.
   *
   * @memberof Primitive.prototype
   *
   * @type {Boolean}
   * @readonly
   *
   * @default true
   */
  compressVertices: {
    get: function () {
      return this._compressVertices;
    },
  },

  /**
   * Determines if the primitive is complete and ready to render.  If this property is
   * true, the primitive will be rendered the next time that {@link Primitive#update}
   * is called.
   *
   * @memberof Primitive.prototype
   *
   * @type {Boolean}
   * @readonly
   */
  ready: {
    get: function () {
      return this._ready;
    },
  },

  /**
   * Gets a promise that resolves when the primitive is ready to render.
   * @memberof Primitive.prototype
   * @type {Promise.<Primitive>}
   * @readonly
   */
  readyPromise: {
    get: function () {
      return this._readyPromise.promise;
    },
  },
});

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

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

  for (name in attributes0) {
    if (attributes0.hasOwnProperty(name) && defined(attributes0[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;
}

var scratchGetAttributeCartesian2 = new Cartesian2();
var scratchGetAttributeCartesian3 = new Cartesian3();
var scratchGetAttributeCartesian4 = new Cartesian4();

function getAttributeValue(value) {
  var componentsPerAttribute = value.length;
  if (componentsPerAttribute === 1) {
    return value[0];
  } else if (componentsPerAttribute === 2) {
    return Cartesian2.unpack(value, 0, scratchGetAttributeCartesian2);
  } else if (componentsPerAttribute === 3) {
    return Cartesian3.unpack(value, 0, scratchGetAttributeCartesian3);
  } else if (componentsPerAttribute === 4) {
    return Cartesian4.unpack(value, 0, scratchGetAttributeCartesian4);
  }
}

function createBatchTable(primitive, context) {
  var geometryInstances = primitive.geometryInstances;
  var instances = Array.isArray(geometryInstances)
    ? geometryInstances
    : [geometryInstances];
  var numberOfInstances = instances.length;
  if (numberOfInstances === 0) {
    return;
  }

  var names = getCommonPerInstanceAttributeNames(instances);
  var length = names.length;

  var attributes = [];
  var attributeIndices = {};
  var boundingSphereAttributeIndices = {};
  var offset2DIndex;

  var firstInstance = instances[0];
  var instanceAttributes = firstInstance.attributes;

  var i;
  var name;
  var attribute;

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

    attributeIndices[name] = i;
    attributes.push({
      functionName: "czm_batchTable_" + name,
      componentDatatype: attribute.componentDatatype,
      componentsPerAttribute: attribute.componentsPerAttribute,
      normalize: attribute.normalize,
    });
  }

  if (names.indexOf("distanceDisplayCondition") !== -1) {
    attributes.push(
      {
        functionName: "czm_batchTable_boundingSphereCenter3DHigh",
        componentDatatype: ComponentDatatype.FLOAT,
        componentsPerAttribute: 3,
      },
      {
        functionName: "czm_batchTable_boundingSphereCenter3DLow",
        componentDatatype: ComponentDatatype.FLOAT,
        componentsPerAttribute: 3,
      },
      {
        functionName: "czm_batchTable_boundingSphereCenter2DHigh",
        componentDatatype: ComponentDatatype.FLOAT,
        componentsPerAttribute: 3,
      },
      {
        functionName: "czm_batchTable_boundingSphereCenter2DLow",
        componentDatatype: ComponentDatatype.FLOAT,
        componentsPerAttribute: 3,
      },
      {
        functionName: "czm_batchTable_boundingSphereRadius",
        componentDatatype: ComponentDatatype.FLOAT,
        componentsPerAttribute: 1,
      }
    );
    boundingSphereAttributeIndices.center3DHigh = attributes.length - 5;
    boundingSphereAttributeIndices.center3DLow = attributes.length - 4;
    boundingSphereAttributeIndices.center2DHigh = attributes.length - 3;
    boundingSphereAttributeIndices.center2DLow = attributes.length - 2;
    boundingSphereAttributeIndices.radius = attributes.length - 1;
  }

  if (names.indexOf("offset") !== -1) {
    attributes.push({
      functionName: "czm_batchTable_offset2D",
      componentDatatype: ComponentDatatype.FLOAT,
      componentsPerAttribute: 3,
    });
    offset2DIndex = attributes.length - 1;
  }

  attributes.push({
    functionName: "czm_batchTable_pickColor",
    componentDatatype: ComponentDatatype.UNSIGNED_BYTE,
    componentsPerAttribute: 4,
    normalize: true,
  });

  var attributesLength = attributes.length;
  var batchTable = new BatchTable(context, attributes, numberOfInstances);

  for (i = 0; i < numberOfInstances; ++i) {
    var instance = instances[i];
    instanceAttributes = instance.attributes;

    for (var j = 0; j < length; ++j) {
      name = names[j];
      attribute = instanceAttributes[name];
      var value = getAttributeValue(attribute.value);
      var attributeIndex = attributeIndices[name];
      batchTable.setBatchedAttribute(i, attributeIndex, value);
    }

    var pickObject = {
      primitive: defaultValue(instance.pickPrimitive, primitive),
    };

    if (defined(instance.id)) {
      pickObject.id = instance.id;
    }

    var pickId = context.createPickId(pickObject);
    primitive._pickIds.push(pickId);

    var pickColor = pickId.color;
    var color = scratchGetAttributeCartesian4;
    color.x = Color.floatToByte(pickColor.red);
    color.y = Color.floatToByte(pickColor.green);
    color.z = Color.floatToByte(pickColor.blue);
    color.w = Color.floatToByte(pickColor.alpha);

    batchTable.setBatchedAttribute(i, attributesLength - 1, color);
  }

  primitive._batchTable = batchTable;
  primitive._batchTableAttributeIndices = attributeIndices;
  primitive._batchTableBoundingSphereAttributeIndices = boundingSphereAttributeIndices;
  primitive._batchTableOffsetAttribute2DIndex = offset2DIndex;
}

function cloneAttribute(attribute) {
  var clonedValues;
  if (Array.isArray(attribute.values)) {
    clonedValues = attribute.values.slice(0);
  } else {
    clonedValues = new attribute.values.constructor(attribute.values);
  }
  return new GeometryAttribute({
    componentDatatype: attribute.componentDatatype,
    componentsPerAttribute: attribute.componentsPerAttribute,
    normalize: attribute.normalize,
    values: clonedValues,
  });
}

function cloneGeometry(geometry) {
  var attributes = geometry.attributes;
  var newAttributes = new GeometryAttributes();
  for (var property in attributes) {
    if (attributes.hasOwnProperty(property) && defined(attributes[property])) {
      newAttributes[property] = cloneAttribute(attributes[property]);
    }
  }

  var indices;
  if (defined(geometry.indices)) {
    var sourceValues = geometry.indices;
    if (Array.isArray(sourceValues)) {
      indices = sourceValues.slice(0);
    } else {
      indices = new sourceValues.constructor(sourceValues);
    }
  }

  return new Geometry({
    attributes: newAttributes,
    indices: indices,
    primitiveType: geometry.primitiveType,
    boundingSphere: BoundingSphere.clone(geometry.boundingSphere),
  });
}

function cloneInstance(instance, geometry) {
  return {
    geometry: geometry,
    attributes: instance.attributes,
    modelMatrix: Matrix4.clone(instance.modelMatrix),
    pickPrimitive: instance.pickPrimitive,
    id: instance.id,
  };
}

var positionRegex = /attribute\s+vec(?:3|4)\s+(.*)3DHigh;/g;

Primitive._modifyShaderPosition = function (
  primitive,
  vertexShaderSource,
  scene3DOnly
) {
  var match;

  var forwardDecl = "";
  var attributes = "";
  var computeFunctions = "";

  while ((match = positionRegex.exec(vertexShaderSource)) !== null) {
    var name = match[1];

    var functionName =
      "vec4 czm_compute" + name[0].toUpperCase() + name.substr(1) + "()";

    // Don't forward-declare czm_computePosition because computePosition.glsl already does.
    if (functionName !== "vec4 czm_computePosition()") {
      forwardDecl += functionName + ";\n";
    }

    if (!defined(primitive.rtcCenter)) {
      // Use GPU RTE
      if (!scene3DOnly) {
        attributes +=
          "attribute vec3 " +
          name +
          "2DHigh;\n" +
          "attribute vec3 " +
          name +
          "2DLow;\n";

        computeFunctions +=
          functionName +
          "\n" +
          "{\n" +
          "    vec4 p;\n" +
          "    if (czm_morphTime == 1.0)\n" +
          "    {\n" +
          "        p = czm_translateRelativeToEye(" +
          name +
          "3DHigh, " +
          name +
          "3DLow);\n" +
          "    }\n" +
          "    else if (czm_morphTime == 0.0)\n" +
          "    {\n" +
          "        p = czm_translateRelativeToEye(" +
          name +
          "2DHigh.zxy, " +
          name +
          "2DLow.zxy);\n" +
          "    }\n" +
          "    else\n" +
          "    {\n" +
          "        p = czm_columbusViewMorph(\n" +
          "                czm_translateRelativeToEye(" +
          name +
          "2DHigh.zxy, " +
          name +
          "2DLow.zxy),\n" +
          "                czm_translateRelativeToEye(" +
          name +
          "3DHigh, " +
          name +
          "3DLow),\n" +
          "                czm_morphTime);\n" +
          "    }\n" +
          "    return p;\n" +
          "}\n\n";
      } else {
        computeFunctions +=
          functionName +
          "\n" +
          "{\n" +
          "    return czm_translateRelativeToEye(" +
          name +
          "3DHigh, " +
          name +
          "3DLow);\n" +
          "}\n\n";
      }
    } else {
      // Use RTC
      vertexShaderSource = vertexShaderSource.replace(
        /attribute\s+vec(?:3|4)\s+position3DHigh;/g,
        ""
      );
      vertexShaderSource = vertexShaderSource.replace(
        /attribute\s+vec(?:3|4)\s+position3DLow;/g,
        ""
      );

      forwardDecl += "uniform mat4 u_modifiedModelView;\n";
      attributes += "attribute vec4 position;\n";

      computeFunctions +=
        functionName +
        "\n" +
        "{\n" +
        "    return u_modifiedModelView * position;\n" +
        "}\n\n";

      vertexShaderSource = vertexShaderSource.replace(
        /czm_modelViewRelativeToEye\s+\*\s+/g,
        ""
      );
      vertexShaderSource = vertexShaderSource.replace(
        /czm_modelViewProjectionRelativeToEye/g,
        "czm_projection"
      );
    }
  }

  return [forwardDecl, attributes, vertexShaderSource, computeFunctions].join(
    "\n"
  );
};

Primitive._appendShowToShader = function (primitive, vertexShaderSource) {
  if (!defined(primitive._batchTableAttributeIndices.show)) {
    return vertexShaderSource;
  }

  var renamedVS = ShaderSource.replaceMain(
    vertexShaderSource,
    "czm_non_show_main"
  );
  var showMain =
    "void main() \n" +
    "{ \n" +
    "    czm_non_show_main(); \n" +
    "    gl_Position *= czm_batchTable_show(batchId); \n" +
    "}";

  return renamedVS + "\n" + showMain;
};

Primitive._updateColorAttribute = function (
  primitive,
  vertexShaderSource,
  isDepthFail
) {
  // some appearances have a color attribute for per vertex color.
  // only remove if color is a per instance attribute.
  if (
    !defined(primitive._batchTableAttributeIndices.color) &&
    !defined(primitive._batchTableAttributeIndices.depthFailColor)
  ) {
    return vertexShaderSource;
  }

  if (vertexShaderSource.search(/attribute\s+vec4\s+color;/g) === -1) {
    return vertexShaderSource;
  }

  //>>includeStart('debug', pragmas.debug);
  if (
    isDepthFail &&
    !defined(primitive._batchTableAttributeIndices.depthFailColor)
  ) {
    throw new DeveloperError(
      "A depthFailColor per-instance attribute is required when using a depth fail appearance that uses a color attribute."
    );
  }
  //>>includeEnd('debug');

  var modifiedVS = vertexShaderSource;
  modifiedVS = modifiedVS.replace(/attribute\s+vec4\s+color;/g, "");
  if (!isDepthFail) {
    modifiedVS = modifiedVS.replace(
      /(\b)color(\b)/g,
      "$1czm_batchTable_color(batchId)$2"
    );
  } else {
    modifiedVS = modifiedVS.replace(
      /(\b)color(\b)/g,
      "$1czm_batchTable_depthFailColor(batchId)$2"
    );
  }
  return modifiedVS;
};

function appendPickToVertexShader(source) {
  var renamedVS = ShaderSource.replaceMain(source, "czm_non_pick_main");
  var pickMain =
    "varying vec4 v_pickColor; \n" +
    "void main() \n" +
    "{ \n" +
    "    czm_non_pick_main(); \n" +
    "    v_pickColor = czm_batchTable_pickColor(batchId); \n" +
    "}";

  return renamedVS + "\n" + pickMain;
}

function appendPickToFragmentShader(source) {
  return "varying vec4 v_pickColor;\n" + source;
}

Primitive._updatePickColorAttribute = function (source) {
  var vsPick = source.replace(/attribute\s+vec4\s+pickColor;/g, "");
  vsPick = vsPick.replace(
    /(\b)pickColor(\b)/g,
    "$1czm_batchTable_pickColor(batchId)$2"
  );
  return vsPick;
};

Primitive._appendOffsetToShader = function (primitive, vertexShaderSource) {
  if (!defined(primitive._batchTableAttributeIndices.offset)) {
    return vertexShaderSource;
  }

  var attr = "attribute float batchId;\n";
  attr += "attribute float applyOffset;";
  var modifiedShader = vertexShaderSource.replace(
    /attribute\s+float\s+batchId;/g,
    attr
  );

  var str = "vec4 $1 = czm_computePosition();\n";
  str += "    if (czm_sceneMode == czm_sceneMode3D)\n";
  str += "    {\n";
  str +=
    "        $1 = $1 + vec4(czm_batchTable_offset(batchId) * applyOffset, 0.0);";
  str += "    }\n";
  str += "    else\n";
  str += "    {\n";
  str +=
    "        $1 = $1 + vec4(czm_batchTable_offset2D(batchId) * applyOffset, 0.0);";
  str += "    }\n";
  modifiedShader = modifiedShader.replace(
    /vec4\s+([A-Za-z0-9_]+)\s+=\s+czm_computePosition\(\);/g,
    str
  );
  return modifiedShader;
};

Primitive._appendDistanceDisplayConditionToShader = function (
  primitive,
  vertexShaderSource,
  scene3DOnly
) {
  if (
    !defined(primitive._batchTableAttributeIndices.distanceDisplayCondition)
  ) {
    return vertexShaderSource;
  }

  var renamedVS = ShaderSource.replaceMain(
    vertexShaderSource,
    "czm_non_distanceDisplayCondition_main"
  );
  var distanceDisplayConditionMain =
    "void main() \n" +
    "{ \n" +
    "    czm_non_distanceDisplayCondition_main(); \n" +
    "    vec2 distanceDisplayCondition = czm_batchTable_distanceDisplayCondition(batchId);\n" +
    "    vec3 boundingSphereCenter3DHigh = czm_batchTable_boundingSphereCenter3DHigh(batchId);\n" +
    "    vec3 boundingSphereCenter3DLow = czm_batchTable_boundingSphereCenter3DLow(batchId);\n" +
    "    float boundingSphereRadius = czm_batchTable_boundingSphereRadius(batchId);\n";

  if (!scene3DOnly) {
    distanceDisplayConditionMain +=
      "    vec3 boundingSphereCenter2DHigh = czm_batchTable_boundingSphereCenter2DHigh(batchId);\n" +
      "    vec3 boundingSphereCenter2DLow = czm_batchTable_boundingSphereCenter2DLow(batchId);\n" +
      "    vec4 centerRTE;\n" +
      "    if (czm_morphTime == 1.0)\n" +
      "    {\n" +
      "        centerRTE = czm_translateRelativeToEye(boundingSphereCenter3DHigh, boundingSphereCenter3DLow);\n" +
      "    }\n" +
      "    else if (czm_morphTime == 0.0)\n" +
      "    {\n" +
      "        centerRTE = czm_translateRelativeToEye(boundingSphereCenter2DHigh.zxy, boundingSphereCenter2DLow.zxy);\n" +
      "    }\n" +
      "    else\n" +
      "    {\n" +
      "        centerRTE = czm_columbusViewMorph(\n" +
      "                czm_translateRelativeToEye(boundingSphereCenter2DHigh.zxy, boundingSphereCenter2DLow.zxy),\n" +
      "                czm_translateRelativeToEye(boundingSphereCenter3DHigh, boundingSphereCenter3DLow),\n" +
      "                czm_morphTime);\n" +
      "    }\n";
  } else {
    distanceDisplayConditionMain +=
      "    vec4 centerRTE = czm_translateRelativeToEye(boundingSphereCenter3DHigh, boundingSphereCenter3DLow);\n";
  }

  distanceDisplayConditionMain +=
    "    float radiusSq = boundingSphereRadius * boundingSphereRadius; \n" +
    "    float distanceSq; \n" +
    "    if (czm_sceneMode == czm_sceneMode2D) \n" +
    "    { \n" +
    "        distanceSq = czm_eyeHeight2D.y - radiusSq; \n" +
    "    } \n" +
    "    else \n" +
    "    { \n" +
    "        distanceSq = dot(centerRTE.xyz, centerRTE.xyz) - radiusSq; \n" +
    "    } \n" +
    "    distanceSq = max(distanceSq, 0.0); \n" +
    "    float nearSq = distanceDisplayCondition.x * distanceDisplayCondition.x; \n" +
    "    float farSq = distanceDisplayCondition.y * distanceDisplayCondition.y; \n" +
    "    float show = (distanceSq >= nearSq && distanceSq <= farSq) ? 1.0 : 0.0; \n" +
    "    gl_Position *= show; \n" +
    "}";
  return renamedVS + "\n" + distanceDisplayConditionMain;
};

function modifyForEncodedNormals(primitive, vertexShaderSource) {
  if (!primitive.compressVertices) {
    return vertexShaderSource;
  }

  var containsNormal =
    vertexShaderSource.search(/attribute\s+vec3\s+normal;/g) !== -1;
  var containsSt = vertexShaderSource.search(/attribute\s+vec2\s+st;/g) !== -1;
  if (!containsNormal && !containsSt) {
    return vertexShaderSource;
  }

  var containsTangent =
    vertexShaderSource.search(/attribute\s+vec3\s+tangent;/g) !== -1;
  var containsBitangent =
    vertexShaderSource.search(/attribute\s+vec3\s+bitangent;/g) !== -1;

  var numComponents = containsSt && containsNormal ? 2.0 : 1.0;
  numComponents += containsTangent || containsBitangent ? 1 : 0;

  var type = numComponents > 1 ? "vec" + numComponents : "float";

  var attributeName = "compressedAttributes";
  var attributeDecl = "attribute " + type + " " + attributeName + ";";

  var globalDecl = "";
  var decode = "";

  if (containsSt) {
    globalDecl += "vec2 st;\n";
    var stComponent = numComponents > 1 ? attributeName + ".x" : attributeName;
    decode +=
      "    st = czm_decompressTextureCoordinates(" + stComponent + ");\n";
  }

  if (containsNormal && containsTangent && containsBitangent) {
    globalDecl += "vec3 normal;\n" + "vec3 tangent;\n" + "vec3 bitangent;\n";
    decode +=
      "    czm_octDecode(" +
      attributeName +
      "." +
      (containsSt ? "yz" : "xy") +
      ", normal, tangent, bitangent);\n";
  } else {
    if (containsNormal) {
      globalDecl += "vec3 normal;\n";
      decode +=
        "    normal = czm_octDecode(" +
        attributeName +
        (numComponents > 1 ? "." + (containsSt ? "y" : "x") : "") +
        ");\n";
    }

    if (containsTangent) {
      globalDecl += "vec3 tangent;\n";
      decode +=
        "    tangent = czm_octDecode(" +
        attributeName +
        "." +
        (containsSt && containsNormal ? "z" : "y") +
        ");\n";
    }

    if (containsBitangent) {
      globalDecl += "vec3 bitangent;\n";
      decode +=
        "    bitangent = czm_octDecode(" +
        attributeName +
        "." +
        (containsSt && containsNormal ? "z" : "y") +
        ");\n";
    }
  }

  var modifiedVS = vertexShaderSource;
  modifiedVS = modifiedVS.replace(/attribute\s+vec3\s+normal;/g, "");
  modifiedVS = modifiedVS.replace(/attribute\s+vec2\s+st;/g, "");
  modifiedVS = modifiedVS.replace(/attribute\s+vec3\s+tangent;/g, "");
  modifiedVS = modifiedVS.replace(/attribute\s+vec3\s+bitangent;/g, "");
  modifiedVS = ShaderSource.replaceMain(modifiedVS, "czm_non_compressed_main");
  var compressedMain =
    "void main() \n" +
    "{ \n" +
    decode +
    "    czm_non_compressed_main(); \n" +
    "}";

  return [attributeDecl, globalDecl, modifiedVS, compressedMain].join("\n");
}

function depthClampVS(vertexShaderSource) {
  var modifiedVS = ShaderSource.replaceMain(
    vertexShaderSource,
    "czm_non_depth_clamp_main"
  );
  modifiedVS +=
    "void main() {\n" +
    "    czm_non_depth_clamp_main();\n" +
    "    gl_Position = czm_depthClamp(gl_Position);" +
    "}\n";
  return modifiedVS;
}

function depthClampFS(fragmentShaderSource) {
  var modifiedFS = ShaderSource.replaceMain(
    fragmentShaderSource,
    "czm_non_depth_clamp_main"
  );
  modifiedFS +=
    "void main() {\n" +
    "    czm_non_depth_clamp_main();\n" +
    "#if defined(GL_EXT_frag_depth)\n" +
    "    #if defined(LOG_DEPTH)\n" +
    "        czm_writeLogDepth();\n" +
    "    #else\n" +
    "        czm_writeDepthClamp();\n" +
    "    #endif\n" +
    "#endif\n" +
    "}\n";
  modifiedFS =
    "#ifdef GL_EXT_frag_depth\n" +
    "#extension GL_EXT_frag_depth : enable\n" +
    "#endif\n" +
    modifiedFS;
  return modifiedFS;
}

function validateShaderMatching(shaderProgram, attributeLocations) {
  // For a VAO and shader program to be compatible, the VAO must have
  // all active attribute in the shader program.  The VAO may have
  // extra attributes with the only concern being a potential
  // performance hit due to extra memory bandwidth and cache pollution.
  // The shader source could have extra attributes that are not used,
  // but there is no guarantee they will be optimized out.
  //
  // Here, we validate that the VAO has all attributes required
  // to match the shader program.
  var shaderAttributes = shaderProgram.vertexAttributes;

  //>>includeStart('debug', pragmas.debug);
  for (var name in shaderAttributes) {
    if (shaderAttributes.hasOwnProperty(name)) {
      if (!defined(attributeLocations[name])) {
        throw new DeveloperError(
          "Appearance/Geometry mismatch.  The appearance requires vertex shader attribute input '" +
            name +
            "', which was not computed as part of the Geometry.  Use the appearance's vertexFormat property when constructing the geometry."
        );
      }
    }
  }
  //>>includeEnd('debug');
}

function getUniformFunction(uniforms, name) {
  return function () {
    return uniforms[name];
  };
}

var numberOfCreationWorkers = Math.max(
  FeatureDetection.hardwareConcurrency - 1,
  1
);
var createGeometryTaskProcessors;
var combineGeometryTaskProcessor = new TaskProcessor("combineGeometry");

function loadAsynchronous(primitive, frameState) {
  var instances;
  var geometry;
  var i;
  var j;

  var instanceIds = primitive._instanceIds;

  if (primitive._state === PrimitiveState.READY) {
    instances = Array.isArray(primitive.geometryInstances)
      ? primitive.geometryInstances
      : [primitive.geometryInstances];
    var length = (primitive._numberOfInstances = instances.length);

    var promises = [];
    var subTasks = [];
    for (i = 0; i < length; ++i) {
      geometry = instances[i].geometry;
      instanceIds.push(instances[i].id);

      //>>includeStart('debug', pragmas.debug);
      if (!defined(geometry._workerName)) {
        throw new DeveloperError(
          "_workerName must be defined for asynchronous geometry."
        );
      }
      //>>includeEnd('debug');

      subTasks.push({
        moduleName: geometry._workerName,
        geometry: geometry,
      });
    }

    if (!defined(createGeometryTaskProcessors)) {
      createGeometryTaskProcessors = new Array(numberOfCreationWorkers);
      for (i = 0; i < numberOfCreationWorkers; i++) {
        createGeometryTaskProcessors[i] = new TaskProcessor("createGeometry");
      }
    }

    var subTask;
    subTasks = subdivideArray(subTasks, numberOfCreationWorkers);

    for (i = 0; i < subTasks.length; i++) {
      var packedLength = 0;
      var workerSubTasks = subTasks[i];
      var workerSubTasksLength = workerSubTasks.length;
      for (j = 0; j < workerSubTasksLength; ++j) {
        subTask = workerSubTasks[j];
        geometry = subTask.geometry;
        if (defined(geometry.constructor.pack)) {
          subTask.offset = packedLength;
          packedLength += defaultValue(
            geometry.constructor.packedLength,
            geometry.packedLength
          );
        }
      }

      var subTaskTransferableObjects;

      if (packedLength > 0) {
        var array = new Float64Array(packedLength);
        subTaskTransferableObjects = [array.buffer];

        for (j = 0; j < workerSubTasksLength; ++j) {
          subTask = workerSubTasks[j];
          geometry = subTask.geometry;
          if (defined(geometry.constructor.pack)) {
            geometry.constructor.pack(geometry, array, subTask.offset);
            subTask.geometry = array;
          }
        }
      }

      promises.push(
        createGeometryTaskProcessors[i].scheduleTask(
          {
            subTasks: subTasks[i],
          },
          subTaskTransferableObjects
        )
      );
    }

    primitive._state = PrimitiveState.CREATING;

    when
      .all(promises, function (results) {
        primitive._createGeometryResults = results;
        primitive._state = PrimitiveState.CREATED;
      })
      .otherwise(function (error) {
        setReady(primitive, frameState, PrimitiveState.FAILED, error);
      });
  } else if (primitive._state === PrimitiveState.CREATED) {
    var transferableObjects = [];
    instances = Array.isArray(primitive.geometryInstances)
      ? primitive.geometryInstances
      : [primitive.geometryInstances];

    var scene3DOnly = frameState.scene3DOnly;
    var projection = frameState.mapProjection;

    var promise = combineGeometryTaskProcessor.scheduleTask(
      PrimitivePipeline.packCombineGeometryParameters(
        {
          createGeometryResults: primitive._createGeometryResults,
          instances: instances,
          ellipsoid: projection.ellipsoid,
          projection: projection,
          elementIndexUintSupported: frameState.context.elementIndexUint,
          scene3DOnly: scene3DOnly,
          vertexCacheOptimize: primitive.vertexCacheOptimize,
          compressVertices: primitive.compressVertices,
          modelMatrix: primitive.modelMatrix,
          createPickOffsets: primitive._createPickOffsets,
        },
        transferableObjects
      ),
      transferableObjects
    );

    primitive._createGeometryResults = undefined;
    primitive._state = PrimitiveState.COMBINING;

    when(promise, function (packedResult) {
      var result = PrimitivePipeline.unpackCombineGeometryResults(packedResult);
      primitive._geometries = result.geometries;
      primitive._attributeLocations = result.attributeLocations;
      primitive.modelMatrix = Matrix4.clone(
        result.modelMatrix,
        primitive.modelMatrix
      );
      primitive._pickOffsets = result.pickOffsets;
      primitive._offsetInstanceExtend = result.offsetInstanceExtend;
      primitive._instanceBoundingSpheres = result.boundingSpheres;
      primitive._instanceBoundingSpheresCV = result.boundingSpheresCV;

      if (defined(primitive._geometries) && primitive._geometries.length > 0) {
        primitive._recomputeBoundingSpheres = true;
        primitive._state = PrimitiveState.COMBINED;
      } else {
        setReady(primitive, frameState, PrimitiveState.FAILED, undefined);
      }
    }).otherwise(function (error) {
      setReady(primitive, frameState, PrimitiveState.FAILED, error);
    });
  }
}

function loadSynchronous(primitive, frameState) {
  var instances = Array.isArray(primitive.geometryInstances)
    ? primitive.geometryInstances
    : [primitive.geometryInstances];
  var length = (primitive._numberOfInstances = instances.length);
  var clonedInstances = new Array(length);
  var instanceIds = primitive._instanceIds;

  var instance;
  var i;

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

    var createdGeometry;
    if (defined(geometry.attributes) && defined(geometry.primitiveType)) {
      createdGeometry = cloneGeometry(geometry);
    } else {
      createdGeometry = geometry.constructor.createGeometry(geometry);
    }

    clonedInstances[geometryIndex++] = cloneInstance(instance, createdGeometry);
    instanceIds.push(instance.id);
  }

  clonedInstances.length = geometryIndex;

  var scene3DOnly = frameState.scene3DOnly;
  var projection = frameState.mapProjection;

  var result = PrimitivePipeline.combineGeometry({
    instances: clonedInstances,
    ellipsoid: projection.ellipsoid,
    projection: projection,
    elementIndexUintSupported: frameState.context.elementIndexUint,
    scene3DOnly: scene3DOnly,
    vertexCacheOptimize: primitive.vertexCacheOptimize,
    compressVertices: primitive.compressVertices,
    modelMatrix: primitive.modelMatrix,
    createPickOffsets: primitive._createPickOffsets,
  });

  primitive._geometries = result.geometries;
  primitive._attributeLocations = result.attributeLocations;
  primitive.modelMatrix = Matrix4.clone(
    result.modelMatrix,
    primitive.modelMatrix
  );
  primitive._pickOffsets = result.pickOffsets;
  primitive._offsetInstanceExtend = result.offsetInstanceExtend;
  primitive._instanceBoundingSpheres = result.boundingSpheres;
  primitive._instanceBoundingSpheresCV = result.boundingSpheresCV;

  if (defined(primitive._geometries) && primitive._geometries.length > 0) {
    primitive._recomputeBoundingSpheres = true;
    primitive._state = PrimitiveState.COMBINED;
  } else {
    setReady(primitive, frameState, PrimitiveState.FAILED, undefined);
  }
}

function recomputeBoundingSpheres(primitive, frameState) {
  var offsetIndex = primitive._batchTableAttributeIndices.offset;
  if (!primitive._recomputeBoundingSpheres || !defined(offsetIndex)) {
    primitive._recomputeBoundingSpheres = false;
    return;
  }

  var i;
  var offsetInstanceExtend = primitive._offsetInstanceExtend;
  var boundingSpheres = primitive._instanceBoundingSpheres;
  var length = boundingSpheres.length;
  var newBoundingSpheres = primitive._tempBoundingSpheres;
  if (!defined(newBoundingSpheres)) {
    newBoundingSpheres = new Array(length);
    for (i = 0; i < length; i++) {
      newBoundingSpheres[i] = new BoundingSphere();
    }
    primitive._tempBoundingSpheres = newBoundingSpheres;
  }
  for (i = 0; i < length; ++i) {
    var newBS = newBoundingSpheres[i];
    var offset = primitive._batchTable.getBatchedAttribute(
      i,
      offsetIndex,
      new Cartesian3()
    );
    newBS = boundingSpheres[i].clone(newBS);
    transformBoundingSphere(newBS, offset, offsetInstanceExtend[i]);
  }
  var combinedBS = [];
  var combinedWestBS = [];
  var combinedEastBS = [];

  for (i = 0; i < length; ++i) {
    var bs = newBoundingSpheres[i];

    var minX = bs.center.x - bs.radius;
    if (
      minX > 0 ||
      BoundingSphere.intersectPlane(bs, Plane.ORIGIN_ZX_PLANE) !==
        Intersect.INTERSECTING
    ) {
      combinedBS.push(bs);
    } else {
      combinedWestBS.push(bs);
      combinedEastBS.push(bs);
    }
  }

  var resultBS1 = combinedBS[0];
  var resultBS2 = combinedEastBS[0];
  var resultBS3 = combinedWestBS[0];

  for (i = 1; i < combinedBS.length; i++) {
    resultBS1 = BoundingSphere.union(resultBS1, combinedBS[i]);
  }
  for (i = 1; i < combinedEastBS.length; i++) {
    resultBS2 = BoundingSphere.union(resultBS2, combinedEastBS[i]);
  }
  for (i = 1; i < combinedWestBS.length; i++) {
    resultBS3 = BoundingSphere.union(resultBS3, combinedWestBS[i]);
  }
  var result = [];
  if (defined(resultBS1)) {
    result.push(resultBS1);
  }
  if (defined(resultBS2)) {
    result.push(resultBS2);
  }
  if (defined(resultBS3)) {
    result.push(resultBS3);
  }

  for (i = 0; i < result.length; i++) {
    var boundingSphere = result[i].clone(primitive._boundingSpheres[i]);
    primitive._boundingSpheres[i] = boundingSphere;
    primitive._boundingSphereCV[i] = BoundingSphere.projectTo2D(
      boundingSphere,
      frameState.mapProjection,
      primitive._boundingSphereCV[i]
    );
  }

  Primitive._updateBoundingVolumes(
    primitive,
    frameState,
    primitive.modelMatrix,
    true
  );
  primitive._recomputeBoundingSpheres = false;
}

var scratchBoundingSphereCenterEncoded = new EncodedCartesian3();
var scratchBoundingSphereCartographic = new Cartographic();
var scratchBoundingSphereCenter2D = new Cartesian3();
var scratchBoundingSphere = new BoundingSphere();

function updateBatchTableBoundingSpheres(primitive, frameState) {
  var hasDistanceDisplayCondition = defined(
    primitive._batchTableAttributeIndices.distanceDisplayCondition
  );
  if (
    !hasDistanceDisplayCondition ||
    primitive._batchTableBoundingSpheresUpdated
  ) {
    return;
  }

  var indices = primitive._batchTableBoundingSphereAttributeIndices;
  var center3DHighIndex = indices.center3DHigh;
  var center3DLowIndex = indices.center3DLow;
  var center2DHighIndex = indices.center2DHigh;
  var center2DLowIndex = indices.center2DLow;
  var radiusIndex = indices.radius;

  var projection = frameState.mapProjection;
  var ellipsoid = projection.ellipsoid;

  var batchTable = primitive._batchTable;
  var boundingSpheres = primitive._instanceBoundingSpheres;
  var length = boundingSpheres.length;

  for (var i = 0; i < length; ++i) {
    var boundingSphere = boundingSpheres[i];
    if (!defined(boundingSphere)) {
      continue;
    }

    var modelMatrix = primitive.modelMatrix;
    if (defined(modelMatrix)) {
      boundingSphere = BoundingSphere.transform(
        boundingSphere,
        modelMatrix,
        scratchBoundingSphere
      );
    }

    var center = boundingSphere.center;
    var radius = boundingSphere.radius;

    var encodedCenter = EncodedCartesian3.fromCartesian(
      center,
      scratchBoundingSphereCenterEncoded
    );
    batchTable.setBatchedAttribute(i, center3DHighIndex, encodedCenter.high);
    batchTable.setBatchedAttribute(i, center3DLowIndex, encodedCenter.low);

    if (!frameState.scene3DOnly) {
      var cartographic = ellipsoid.cartesianToCartographic(
        center,
        scratchBoundingSphereCartographic
      );
      var center2D = projection.project(
        cartographic,
        scratchBoundingSphereCenter2D
      );
      encodedCenter = EncodedCartesian3.fromCartesian(
        center2D,
        scratchBoundingSphereCenterEncoded
      );
      batchTable.setBatchedAttribute(i, center2DHighIndex, encodedCenter.high);
      batchTable.setBatchedAttribute(i, center2DLowIndex, encodedCenter.low);
    }

    batchTable.setBatchedAttribute(i, radiusIndex, radius);
  }

  primitive._batchTableBoundingSpheresUpdated = true;
}

var offsetScratchCartesian = new Cartesian3();
var offsetCenterScratch = new Cartesian3();
function updateBatchTableOffsets(primitive, frameState) {
  var hasOffset = defined(primitive._batchTableAttributeIndices.offset);
  if (
    !hasOffset ||
    primitive._batchTableOffsetsUpdated ||
    frameState.scene3DOnly
  ) {
    return;
  }

  var index2D = primitive._batchTableOffsetAttribute2DIndex;

  var projection = frameState.mapProjection;
  var ellipsoid = projection.ellipsoid;

  var batchTable = primitive._batchTable;
  var boundingSpheres = primitive._instanceBoundingSpheres;
  var length = boundingSpheres.length;

  for (var i = 0; i < length; ++i) {
    var boundingSphere = boundingSpheres[i];
    if (!defined(boundingSphere)) {
      continue;
    }
    var offset = batchTable.getBatchedAttribute(
      i,
      primitive._batchTableAttributeIndices.offset
    );
    if (Cartesian3.equals(offset, Cartesian3.ZERO)) {
      batchTable.setBatchedAttribute(i, index2D, Cartesian3.ZERO);
      continue;
    }

    var modelMatrix = primitive.modelMatrix;
    if (defined(modelMatrix)) {
      boundingSphere = BoundingSphere.transform(
        boundingSphere,
        modelMatrix,
        scratchBoundingSphere
      );
    }

    var center = boundingSphere.center;
    center = ellipsoid.scaleToGeodeticSurface(center, offsetCenterScratch);
    var cartographic = ellipsoid.cartesianToCartographic(
      center,
      scratchBoundingSphereCartographic
    );
    var center2D = projection.project(
      cartographic,
      scratchBoundingSphereCenter2D
    );

    var newPoint = Cartesian3.add(offset, center, offsetScratchCartesian);
    cartographic = ellipsoid.cartesianToCartographic(newPoint, cartographic);

    var newPointProjected = projection.project(
      cartographic,
      offsetScratchCartesian
    );

    var newVector = Cartesian3.subtract(
      newPointProjected,
      center2D,
      offsetScratchCartesian
    );

    var x = newVector.x;
    newVector.x = newVector.z;
    newVector.z = newVector.y;
    newVector.y = x;

    batchTable.setBatchedAttribute(i, index2D, newVector);
  }

  primitive._batchTableOffsetsUpdated = true;
}

function createVertexArray(primitive, frameState) {
  var attributeLocations = primitive._attributeLocations;
  var geometries = primitive._geometries;
  var scene3DOnly = frameState.scene3DOnly;
  var context = frameState.context;

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

    va.push(
      VertexArray.fromGeometry({
        context: context,
        geometry: geometry,
        attributeLocations: attributeLocations,
        bufferUsage: BufferUsage.STATIC_DRAW,
        interleave: primitive._interleave,
      })
    );

    if (defined(primitive._createBoundingVolumeFunction)) {
      primitive._createBoundingVolumeFunction(frameState, geometry);
    } else {
      primitive._boundingSpheres.push(
        BoundingSphere.clone(geometry.boundingSphere)
      );
      primitive._boundingSphereWC.push(new BoundingSphere());

      if (!scene3DOnly) {
        var center = geometry.boundingSphereCV.center;
        var x = center.x;
        var y = center.y;
        var z = center.z;
        center.x = z;
        center.y = x;
        center.z = y;

        primitive._boundingSphereCV.push(
          BoundingSphere.clone(geometry.boundingSphereCV)
        );
        primitive._boundingSphere2D.push(new BoundingSphere());
        primitive._boundingSphereMorph.push(new BoundingSphere());
      }
    }
  }

  primitive._va = va;
  primitive._primitiveType = geometries[0].primitiveType;

  if (primitive.releaseGeometryInstances) {
    primitive.geometryInstances = undefined;
  }

  primitive._geometries = undefined;
  setReady(primitive, frameState, PrimitiveState.COMPLETE, undefined);
}

function createRenderStates(primitive, context, appearance, twoPasses) {
  var renderState = appearance.getRenderState();
  var rs;

  if (twoPasses) {
    rs = clone(renderState, false);
    rs.cull = {
      enabled: true,
      face: CullFace.BACK,
    };
    primitive._frontFaceRS = RenderState.fromCache(rs);

    rs.cull.face = CullFace.FRONT;
    primitive._backFaceRS = RenderState.fromCache(rs);
  } else {
    primitive._frontFaceRS = RenderState.fromCache(renderState);
    primitive._backFaceRS = primitive._frontFaceRS;
  }

  rs = clone(renderState, false);
  if (defined(primitive._depthFailAppearance)) {
    rs.depthTest.enabled = false;
  }

  if (defined(primitive._depthFailAppearance)) {
    renderState = primitive._depthFailAppearance.getRenderState();
    rs = clone(renderState, false);
    rs.depthTest.func = DepthFunction.GREATER;
    if (twoPasses) {
      rs.cull = {
        enabled: true,
        face: CullFace.BACK,
      };
      primitive._frontFaceDepthFailRS = RenderState.fromCache(rs);

      rs.cull.face = CullFace.FRONT;
      primitive._backFaceDepthFailRS = RenderState.fromCache(rs);
    } else {
      primitive._frontFaceDepthFailRS = RenderState.fromCache(rs);
      primitive._backFaceDepthFailRS = primitive._frontFaceRS;
    }
  }
}

function createShaderProgram(primitive, frameState, appearance) {
  var context = frameState.context;

  var attributeLocations = primitive._attributeLocations;

  var vs = primitive._batchTable.getVertexShaderCallback()(
    appearance.vertexShaderSource
  );
  vs = Primitive._appendOffsetToShader(primitive, vs);
  vs = Primitive._appendShowToShader(primitive, vs);
  vs = Primitive._appendDistanceDisplayConditionToShader(
    primitive,
    vs,
    frameState.scene3DOnly
  );
  vs = appendPickToVertexShader(vs);
  vs = Primitive._updateColorAttribute(primitive, vs, false);
  vs = modifyForEncodedNormals(primitive, vs);
  vs = Primitive._modifyShaderPosition(primitive, vs, frameState.scene3DOnly);
  var fs = appearance.getFragmentShaderSource();
  fs = appendPickToFragmentShader(fs);

  primitive._sp = ShaderProgram.replaceCache({
    context: context,
    shaderProgram: primitive._sp,
    vertexShaderSource: vs,
    fragmentShaderSource: fs,
    attributeLocations: attributeLocations,
  });
  validateShaderMatching(primitive._sp, attributeLocations);

  if (defined(primitive._depthFailAppearance)) {
    vs = primitive._batchTable.getVertexShaderCallback()(
      primitive._depthFailAppearance.vertexShaderSource
    );
    vs = Primitive._appendShowToShader(primitive, vs);
    vs = Primitive._appendDistanceDisplayConditionToShader(
      primitive,
      vs,
      frameState.scene3DOnly
    );
    vs = appendPickToVertexShader(vs);
    vs = Primitive._updateColorAttribute(primitive, vs, true);
    vs = modifyForEncodedNormals(primitive, vs);
    vs = Primitive._modifyShaderPosition(primitive, vs, frameState.scene3DOnly);
    vs = depthClampVS(vs);

    fs = primitive._depthFailAppearance.getFragmentShaderSource();
    fs = appendPickToFragmentShader(fs);
    fs = depthClampFS(fs);

    primitive._spDepthFail = ShaderProgram.replaceCache({
      context: context,
      shaderProgram: primitive._spDepthFail,
      vertexShaderSource: vs,
      fragmentShaderSource: fs,
      attributeLocations: attributeLocations,
    });
    validateShaderMatching(primitive._spDepthFail, attributeLocations);
  }
}

var modifiedModelViewScratch = new Matrix4();
var rtcScratch = new Cartesian3();

function getUniforms(primitive, appearance, material, frameState) {
  // Create uniform map by combining uniforms from the appearance and material if either have uniforms.
  var materialUniformMap = defined(material) ? material._uniforms : undefined;
  var appearanceUniformMap = {};
  var appearanceUniforms = appearance.uniforms;
  if (defined(appearanceUniforms)) {
    // Convert to uniform map of functions for the renderer
    for (var name in appearanceUniforms) {
      if (appearanceUniforms.hasOwnProperty(name)) {
        //>>includeStart('debug', pragmas.debug);
        if (defined(materialUniformMap) && defined(materialUniformMap[name])) {
          // Later, we could rename uniforms behind-the-scenes if needed.
          throw new DeveloperError(
            "Appearance and material have a uniform with the same name: " + name
          );
        }
        //>>includeEnd('debug');

        appearanceUniformMap[name] = getUniformFunction(
          appearanceUniforms,
          name
        );
      }
    }
  }
  var uniforms = combine(appearanceUniformMap, materialUniformMap);
  uniforms = primitive._batchTable.getUniformMapCallback()(uniforms);

  if (defined(primitive.rtcCenter)) {
    uniforms.u_modifiedModelView = function () {
      var viewMatrix = frameState.context.uniformState.view;
      Matrix4.multiply(
        viewMatrix,
        primitive._modelMatrix,
        modifiedModelViewScratch
      );
      Matrix4.multiplyByPoint(
        modifiedModelViewScratch,
        primitive.rtcCenter,
        rtcScratch
      );
      Matrix4.setTranslation(
        modifiedModelViewScratch,
        rtcScratch,
        modifiedModelViewScratch
      );
      return modifiedModelViewScratch;
    };
  }

  return uniforms;
}

function createCommands(
  primitive,
  appearance,
  material,
  translucent,
  twoPasses,
  colorCommands,
  pickCommands,
  frameState
) {
  var uniforms = getUniforms(primitive, appearance, material, frameState);

  var depthFailUniforms;
  if (defined(primitive._depthFailAppearance)) {
    depthFailUniforms = getUniforms(
      primitive,
      primitive._depthFailAppearance,
      primitive._depthFailAppearance.material,
      frameState
    );
  }

  var pass = translucent ? Pass.TRANSLUCENT : Pass.OPAQUE;

  var multiplier = twoPasses ? 2 : 1;
  multiplier *= defined(primitive._depthFailAppearance) ? 2 : 1;
  colorCommands.length = primitive._va.length * multiplier;

  var length = colorCommands.length;
  var vaIndex = 0;
  for (var i = 0; i < length; ++i) {
    var colorCommand;

    if (twoPasses) {
      colorCommand = colorCommands[i];
      if (!defined(colorCommand)) {
        colorCommand = colorCommands[i] = new DrawCommand({
          owner: primitive,
          primitiveType: primitive._primitiveType,
        });
      }
      colorCommand.vertexArray = primitive._va[vaIndex];
      colorCommand.renderState = primitive._backFaceRS;
      colorCommand.shaderProgram = primitive._sp;
      colorCommand.uniformMap = uniforms;
      colorCommand.pass = pass;

      ++i;
    }

    colorCommand = colorCommands[i];
    if (!defined(colorCommand)) {
      colorCommand = colorCommands[i] = new DrawCommand({
        owner: primitive,
        primitiveType: primitive._primitiveType,
      });
    }
    colorCommand.vertexArray = primitive._va[vaIndex];
    colorCommand.renderState = primitive._frontFaceRS;
    colorCommand.shaderProgram = primitive._sp;
    colorCommand.uniformMap = uniforms;
    colorCommand.pass = pass;

    if (defined(primitive._depthFailAppearance)) {
      if (twoPasses) {
        ++i;

        colorCommand = colorCommands[i];
        if (!defined(colorCommand)) {
          colorCommand = colorCommands[i] = new DrawCommand({
            owner: primitive,
            primitiveType: primitive._primitiveType,
          });
        }
        colorCommand.vertexArray = primitive._va[vaIndex];
        colorCommand.renderState = primitive._backFaceDepthFailRS;
        colorCommand.shaderProgram = primitive._spDepthFail;
        colorCommand.uniformMap = depthFailUniforms;
        colorCommand.pass = pass;
      }

      ++i;

      colorCommand = colorCommands[i];
      if (!defined(colorCommand)) {
        colorCommand = colorCommands[i] = new DrawCommand({
          owner: primitive,
          primitiveType: primitive._primitiveType,
        });
      }
      colorCommand.vertexArray = primitive._va[vaIndex];
      colorCommand.renderState = primitive._frontFaceDepthFailRS;
      colorCommand.shaderProgram = primitive._spDepthFail;
      colorCommand.uniformMap = depthFailUniforms;
      colorCommand.pass = pass;
    }

    ++vaIndex;
  }
}

Primitive._updateBoundingVolumes = function (
  primitive,
  frameState,
  modelMatrix,
  forceUpdate
) {
  var i;
  var length;
  var boundingSphere;

  if (forceUpdate || !Matrix4.equals(modelMatrix, primitive._modelMatrix)) {
    Matrix4.clone(modelMatrix, primitive._modelMatrix);
    length = primitive._boundingSpheres.length;
    for (i = 0; i < length; ++i) {
      boundingSphere = primitive._boundingSpheres[i];
      if (defined(boundingSphere)) {
        primitive._boundingSphereWC[i] = BoundingSphere.transform(
          boundingSphere,
          modelMatrix,
          primitive._boundingSphereWC[i]
        );
        if (!frameState.scene3DOnly) {
          primitive._boundingSphere2D[i] = BoundingSphere.clone(
            primitive._boundingSphereCV[i],
            primitive._boundingSphere2D[i]
          );
          primitive._boundingSphere2D[i].center.x = 0.0;
          primitive._boundingSphereMorph[i] = BoundingSphere.union(
            primitive._boundingSphereWC[i],
            primitive._boundingSphereCV[i]
          );
        }
      }
    }
  }

  // Update bounding volumes for primitives that are sized in pixels.
  // The pixel size in meters varies based on the distance from the camera.
  var pixelSize = primitive.appearance.pixelSize;
  if (defined(pixelSize)) {
    length = primitive._boundingSpheres.length;
    for (i = 0; i < length; ++i) {
      boundingSphere = primitive._boundingSpheres[i];
      var boundingSphereWC = primitive._boundingSphereWC[i];
      var pixelSizeInMeters = frameState.camera.getPixelSize(
        boundingSphere,
        frameState.context.drawingBufferWidth,
        frameState.context.drawingBufferHeight
      );
      var sizeInMeters = pixelSizeInMeters * pixelSize;
      boundingSphereWC.radius = boundingSphere.radius + sizeInMeters;
    }
  }
};

function updateAndQueueCommands(
  primitive,
  frameState,
  colorCommands,
  pickCommands,
  modelMatrix,
  cull,
  debugShowBoundingVolume,
  twoPasses
) {
  //>>includeStart('debug', pragmas.debug);
  if (
    frameState.mode !== SceneMode.SCENE3D &&
    !Matrix4.equals(modelMatrix, Matrix4.IDENTITY)
  ) {
    throw new DeveloperError(
      "Primitive.modelMatrix is only supported in 3D mode."
    );
  }
  //>>includeEnd('debug');

  Primitive._updateBoundingVolumes(primitive, frameState, modelMatrix);

  var boundingSpheres;
  if (frameState.mode === SceneMode.SCENE3D) {
    boundingSpheres = primitive._boundingSphereWC;
  } else if (frameState.mode === SceneMode.COLUMBUS_VIEW) {
    boundingSpheres = primitive._boundingSphereCV;
  } else if (
    frameState.mode === SceneMode.SCENE2D &&
    defined(primitive._boundingSphere2D)
  ) {
    boundingSpheres = primitive._boundingSphere2D;
  } else if (defined(primitive._boundingSphereMorph)) {
    boundingSpheres = primitive._boundingSphereMorph;
  }

  var commandList = frameState.commandList;
  var passes = frameState.passes;
  if (passes.render || passes.pick) {
    var allowPicking = primitive.allowPicking;
    var castShadows = ShadowMode.castShadows(primitive.shadows);
    var receiveShadows = ShadowMode.receiveShadows(primitive.shadows);
    var colorLength = colorCommands.length;

    var factor = twoPasses ? 2 : 1;
    factor *= defined(primitive._depthFailAppearance) ? 2 : 1;

    for (var j = 0; j < colorLength; ++j) {
      var sphereIndex = Math.floor(j / factor);
      var colorCommand = colorCommands[j];
      colorCommand.modelMatrix = modelMatrix;
      colorCommand.boundingVolume = boundingSpheres[sphereIndex];
      colorCommand.cull = cull;
      colorCommand.debugShowBoundingVolume = debugShowBoundingVolume;
      colorCommand.castShadows = castShadows;
      colorCommand.receiveShadows = receiveShadows;

      if (allowPicking) {
        colorCommand.pickId = "v_pickColor";
      } else {
        colorCommand.pickId = undefined;
      }

      commandList.push(colorCommand);
    }
  }
}

/**
 * Called when {@link Viewer} or {@link CesiumWidget} render the scene to
 * get the draw commands needed to render this primitive.
 * <p>
 * Do not call this function directly.  This is documented just to
 * list the exceptions that may be propagated when the scene is rendered:
 * </p>
 *
 * @exception {DeveloperError} All instance geometries must have the same primitiveType.
 * @exception {DeveloperError} Appearance and material have a uniform with the same name.
 * @exception {DeveloperError} Primitive.modelMatrix is only supported in 3D mode.
 * @exception {RuntimeError} Vertex texture fetch support is required to render primitives with per-instance attributes. The maximum number of vertex texture image units must be greater than zero.
 */
Primitive.prototype.update = function (frameState) {
  if (
    (!defined(this.geometryInstances) && this._va.length === 0) ||
    (defined(this.geometryInstances) &&
      Array.isArray(this.geometryInstances) &&
      this.geometryInstances.length === 0) ||
    !defined(this.appearance) ||
    (frameState.mode !== SceneMode.SCENE3D && frameState.scene3DOnly) ||
    (!frameState.passes.render && !frameState.passes.pick)
  ) {
    return;
  }

  if (defined(this._error)) {
    throw this._error;
  }

  //>>includeStart('debug', pragmas.debug);
  if (defined(this.rtcCenter) && !frameState.scene3DOnly) {
    throw new DeveloperError(
      "RTC rendering is only available for 3D only scenes."
    );
  }
  //>>includeEnd('debug');

  if (this._state === PrimitiveState.FAILED) {
    return;
  }

  var context = frameState.context;
  if (!defined(this._batchTable)) {
    createBatchTable(this, context);
  }
  if (this._batchTable.attributes.length > 0) {
    if (ContextLimits.maximumVertexTextureImageUnits === 0) {
      throw new RuntimeError(
        "Vertex texture fetch support is required to render primitives with per-instance attributes. The maximum number of vertex texture image units must be greater than zero."
      );
    }
    this._batchTable.update(frameState);
  }

  if (
    this._state !== PrimitiveState.COMPLETE &&
    this._state !== PrimitiveState.COMBINED
  ) {
    if (this.asynchronous) {
      loadAsynchronous(this, frameState);
    } else {
      loadSynchronous(this, frameState);
    }
  }

  if (this._state === PrimitiveState.COMBINED) {
    updateBatchTableBoundingSpheres(this, frameState);
    updateBatchTableOffsets(this, frameState);
    createVertexArray(this, frameState);
  }

  if (!this.show || this._state !== PrimitiveState.COMPLETE) {
    return;
  }

  if (!this._batchTableOffsetsUpdated) {
    updateBatchTableOffsets(this, frameState);
  }
  if (this._recomputeBoundingSpheres) {
    recomputeBoundingSpheres(this, frameState);
  }

  // Create or recreate render state and shader program if appearance/material changed
  var appearance = this.appearance;
  var material = appearance.material;
  var createRS = false;
  var createSP = false;

  if (this._appearance !== appearance) {
    this._appearance = appearance;
    this._material = material;
    createRS = true;
    createSP = true;
  } else if (this._material !== material) {
    this._material = material;
    createSP = true;
  }

  var depthFailAppearance = this.depthFailAppearance;
  var depthFailMaterial = defined(depthFailAppearance)
    ? depthFailAppearance.material
    : undefined;

  if (this._depthFailAppearance !== depthFailAppearance) {
    this._depthFailAppearance = depthFailAppearance;
    this._depthFailMaterial = depthFailMaterial;
    createRS = true;
    createSP = true;
  } else if (this._depthFailMaterial !== depthFailMaterial) {
    this._depthFailMaterial = depthFailMaterial;
    createSP = true;
  }

  var translucent = this._appearance.isTranslucent();
  if (this._translucent !== translucent) {
    this._translucent = translucent;
    createRS = true;
  }

  if (defined(this._material)) {
    this._material.update(context);
  }

  var twoPasses = appearance.closed && translucent;

  if (createRS) {
    var rsFunc = defaultValue(
      this._createRenderStatesFunction,
      createRenderStates
    );
    rsFunc(this, context, appearance, twoPasses);
  }

  if (createSP) {
    var spFunc = defaultValue(
      this._createShaderProgramFunction,
      createShaderProgram
    );
    spFunc(this, frameState, appearance);
  }

  if (createRS || createSP) {
    var commandFunc = defaultValue(
      this._createCommandsFunction,
      createCommands
    );
    commandFunc(
      this,
      appearance,
      material,
      translucent,
      twoPasses,
      this._colorCommands,
      this._pickCommands,
      frameState
    );
  }

  var updateAndQueueCommandsFunc = defaultValue(
    this._updateAndQueueCommandsFunction,
    updateAndQueueCommands
  );
  updateAndQueueCommandsFunc(
    this,
    frameState,
    this._colorCommands,
    this._pickCommands,
    this.modelMatrix,
    this.cull,
    this.debugShowBoundingVolume,
    twoPasses
  );
};

var offsetBoundingSphereScratch1 = new BoundingSphere();
var offsetBoundingSphereScratch2 = new BoundingSphere();
function transformBoundingSphere(boundingSphere, offset, offsetAttribute) {
  if (offsetAttribute === GeometryOffsetAttribute.TOP) {
    var origBS = BoundingSphere.clone(
      boundingSphere,
      offsetBoundingSphereScratch1
    );
    var offsetBS = BoundingSphere.clone(
      boundingSphere,
      offsetBoundingSphereScratch2
    );
    offsetBS.center = Cartesian3.add(offsetBS.center, offset, offsetBS.center);
    boundingSphere = BoundingSphere.union(origBS, offsetBS, boundingSphere);
  } else if (offsetAttribute === GeometryOffsetAttribute.ALL) {
    boundingSphere.center = Cartesian3.add(
      boundingSphere.center,
      offset,
      boundingSphere.center
    );
  }

  return boundingSphere;
}

function createGetFunction(batchTable, instanceIndex, attributeIndex) {
  return function () {
    var attributeValue = batchTable.getBatchedAttribute(
      instanceIndex,
      attributeIndex
    );
    var attribute = batchTable.attributes[attributeIndex];
    var componentsPerAttribute = attribute.componentsPerAttribute;
    var value = ComponentDatatype.createTypedArray(
      attribute.componentDatatype,
      componentsPerAttribute
    );
    if (defined(attributeValue.constructor.pack)) {
      attributeValue.constructor.pack(attributeValue, value, 0);
    } else {
      value[0] = attributeValue;
    }
    return value;
  };
}

function createSetFunction(
  batchTable,
  instanceIndex,
  attributeIndex,
  primitive,
  name
) {
  return function (value) {
    //>>includeStart('debug', pragmas.debug);
    if (
      !defined(value) ||
      !defined(value.length) ||
      value.length < 1 ||
      value.length > 4
    ) {
      throw new DeveloperError(
        "value must be and array with length between 1 and 4."
      );
    }
    //>>includeEnd('debug');
    var attributeValue = getAttributeValue(value);
    batchTable.setBatchedAttribute(
      instanceIndex,
      attributeIndex,
      attributeValue
    );
    if (name === "offset") {
      primitive._recomputeBoundingSpheres = true;
      primitive._batchTableOffsetsUpdated = false;
    }
  };
}

var offsetScratch = new Cartesian3();

function createBoundingSphereProperties(primitive, properties, index) {
  properties.boundingSphere = {
    get: function () {
      var boundingSphere = primitive._instanceBoundingSpheres[index];
      if (defined(boundingSphere)) {
        boundingSphere = boundingSphere.clone();
        var modelMatrix = primitive.modelMatrix;
        var offset = properties.offset;
        if (defined(offset)) {
          transformBoundingSphere(
            boundingSphere,
            Cartesian3.fromArray(offset.get(), 0, offsetScratch),
            primitive._offsetInstanceExtend[index]
          );
        }
        if (defined(modelMatrix)) {
          boundingSphere = BoundingSphere.transform(
            boundingSphere,
            modelMatrix
          );
        }
      }

      return boundingSphere;
    },
  };
  properties.boundingSphereCV = {
    get: function () {
      return primitive._instanceBoundingSpheresCV[index];
    },
  };
}

function createPickIdProperty(primitive, properties, index) {
  properties.pickId = {
    get: function () {
      return primitive._pickIds[index];
    },
  };
}

/**
 * Returns the modifiable per-instance attributes for a {@link GeometryInstance}.
 *
 * @param {*} id The id of the {@link GeometryInstance}.
 * @returns {Object} The typed array in the attribute's format or undefined if the is no instance with id.
 *
 * @exception {DeveloperError} must call update before calling getGeometryInstanceAttributes.
 *
 * @example
 * var attributes = primitive.getGeometryInstanceAttributes('an id');
 * attributes.color = Cesium.ColorGeometryInstanceAttribute.toValue(Cesium.Color.AQUA);
 * attributes.show = Cesium.ShowGeometryInstanceAttribute.toValue(true);
 * attributes.distanceDisplayCondition = Cesium.DistanceDisplayConditionGeometryInstanceAttribute.toValue(100.0, 10000.0);
 * attributes.offset = Cesium.OffsetGeometryInstanceAttribute.toValue(Cartesian3.IDENTITY);
 */
Primitive.prototype.getGeometryInstanceAttributes = function (id) {
  //>>includeStart('debug', pragmas.debug);
  if (!defined(id)) {
    throw new DeveloperError("id is required");
  }
  if (!defined(this._batchTable)) {
    throw new DeveloperError(
      "must call update before calling getGeometryInstanceAttributes"
    );
  }
  //>>includeEnd('debug');

  var index = -1;
  var lastIndex = this._lastPerInstanceAttributeIndex;
  var ids = this._instanceIds;
  var length = ids.length;
  for (var i = 0; i < length; ++i) {
    var curIndex = (lastIndex + i) % length;
    if (id === ids[curIndex]) {
      index = curIndex;
      break;
    }
  }

  if (index === -1) {
    return undefined;
  }

  var attributes = this._perInstanceAttributeCache[index];
  if (defined(attributes)) {
    return attributes;
  }

  var batchTable = this._batchTable;
  var perInstanceAttributeIndices = this._batchTableAttributeIndices;
  attributes = {};
  var properties = {};

  for (var name in perInstanceAttributeIndices) {
    if (perInstanceAttributeIndices.hasOwnProperty(name)) {
      var attributeIndex = perInstanceAttributeIndices[name];
      properties[name] = {
        get: createGetFunction(batchTable, index, attributeIndex),
        set: createSetFunction(batchTable, index, attributeIndex, this, name),
      };
    }
  }

  createBoundingSphereProperties(this, properties, index);
  createPickIdProperty(this, properties, index);
  Object.defineProperties(attributes, properties);

  this._lastPerInstanceAttributeIndex = index;
  this._perInstanceAttributeCache[index] = attributes;
  return attributes;
};

/**
 * Returns true if this object was destroyed; otherwise, false.
 * <p>
 * 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.
 * </p>
 *
 * @returns {Boolean} <code>true</code> if this object was destroyed; otherwise, <code>false</code>.
 *
 * @see Primitive#destroy
 */
Primitive.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.
 * <p>
 * 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.
 * </p>
 *
 * @exception {DeveloperError} This object was destroyed, i.e., destroy() was called.
 *
 *
 * @example
 * e = e && e.destroy();
 *
 * @see Primitive#isDestroyed
 */
Primitive.prototype.destroy = function () {
  var length;
  var i;

  this._sp = this._sp && this._sp.destroy();
  this._spDepthFail = this._spDepthFail && this._spDepthFail.destroy();

  var va = this._va;
  length = va.length;
  for (i = 0; i < length; ++i) {
    va[i].destroy();
  }
  this._va = undefined;

  var pickIds = this._pickIds;
  length = pickIds.length;
  for (i = 0; i < length; ++i) {
    pickIds[i].destroy();
  }
  this._pickIds = undefined;

  this._batchTable = this._batchTable && this._batchTable.destroy();

  //These objects may be fairly large and reference other large objects (like Entities)
  //We explicitly set them to undefined here so that the memory can be freed
  //even if a reference to the destroyed Primitive has been kept around.
  this._instanceIds = undefined;
  this._perInstanceAttributeCache = undefined;
  this._attributeLocations = undefined;

  return destroyObject(this);
};

function setReady(primitive, frameState, state, error) {
  primitive._error = error;
  primitive._state = state;
  frameState.afterRender.push(function () {
    primitive._ready =
      primitive._state === PrimitiveState.COMPLETE ||
      primitive._state === PrimitiveState.FAILED;
    if (!defined(error)) {
      primitive._readyPromise.resolve(primitive);
    } else {
      primitive._readyPromise.reject(error);
    }
  });
}
export default Primitive;
