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 clone from "../Core/clone.js";
import defined from "../Core/defined.js";
import Matrix2 from "../Core/Matrix2.js";
import Matrix3 from "../Core/Matrix3.js";
import Matrix4 from "../Core/Matrix4.js";
import Quaternion from "../Core/Quaternion.js";
import RuntimeError from "../Core/RuntimeError.js";
import WebGLConstants from "../Core/WebGLConstants.js";
import ShaderSource from "../Renderer/ShaderSource.js";
import addToArray from "./GltfPipeline/addToArray.js";
import ForEach from "./GltfPipeline/ForEach.js";
import usesExtension from "./GltfPipeline/usesExtension.js";
import AttributeType from "./AttributeType.js";
import Axis from "./Axis.js";

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

/**
 * Updates the model's forward axis if the model is not a 2.0 model.
 *
 * @param {Object} model The model to update.
 */
ModelUtility.updateForwardAxis = function (model) {
  var cachedSourceVersion = model.gltf.extras.sourceVersion;

  if (
    (defined(cachedSourceVersion) && cachedSourceVersion !== "2.0") ||
    ModelUtility.getAssetVersion(model.gltf) !== "2.0"
  ) {
    model._gltfForwardAxis = Axis.X;
  }
};

/**
 *  Gets the string representing the glTF asset version.
 *
 *  @param {Object} gltf A javascript object containing a glTF asset.
 *  @returns {String} The glTF asset version string.
 */
ModelUtility.getAssetVersion = function (gltf) {
  // In glTF 1.0 it was valid to omit the version number.
  if (!defined(gltf.asset) || !defined(gltf.asset.version)) {
    return "1.0";
  }

  return gltf.asset.version;
};

/**
 * Splits primitive materials with values incompatible for generating techniques.
 *
 * @param {Object} gltf A javascript object containing a glTF asset.
 * @returns {Object} The glTF asset with modified materials.
 */
ModelUtility.splitIncompatibleMaterials = function (gltf) {
  var accessors = gltf.accessors;
  var materials = gltf.materials;
  var primitiveInfoByMaterial = {};
  ForEach.mesh(gltf, function (mesh) {
    ForEach.meshPrimitive(mesh, function (primitive) {
      var materialIndex = primitive.material;
      var material = materials[materialIndex];

      var jointAccessorId = primitive.attributes.JOINTS_0;
      var componentType;
      var accessorType;
      if (defined(jointAccessorId)) {
        var jointAccessor = accessors[jointAccessorId];
        componentType = jointAccessor.componentType;
        accessorType = jointAccessor.type;
      }
      var isSkinned = defined(jointAccessorId) && accessorType === "VEC4";
      var hasVertexColors = defined(primitive.attributes.COLOR_0);
      var hasMorphTargets = defined(primitive.targets);
      var hasNormals = defined(primitive.attributes.NORMAL);
      var hasTangents = defined(primitive.attributes.TANGENT);
      var hasTexCoords = defined(primitive.attributes.TEXCOORD_0);
      var hasTexCoord1 =
        hasTexCoords && defined(primitive.attributes.TEXCOORD_1);
      var hasOutline =
        defined(primitive.extensions) &&
        defined(primitive.extensions.CESIUM_primitive_outline);

      var primitiveInfo = primitiveInfoByMaterial[materialIndex];
      if (!defined(primitiveInfo)) {
        primitiveInfoByMaterial[materialIndex] = {
          skinning: {
            skinned: isSkinned,
            componentType: componentType,
          },
          hasVertexColors: hasVertexColors,
          hasMorphTargets: hasMorphTargets,
          hasNormals: hasNormals,
          hasTangents: hasTangents,
          hasTexCoords: hasTexCoords,
          hasTexCoord1: hasTexCoord1,
          hasOutline: hasOutline,
        };
      } else if (
        primitiveInfo.skinning.skinned !== isSkinned ||
        primitiveInfo.hasVertexColors !== hasVertexColors ||
        primitiveInfo.hasMorphTargets !== hasMorphTargets ||
        primitiveInfo.hasNormals !== hasNormals ||
        primitiveInfo.hasTangents !== hasTangents ||
        primitiveInfo.hasTexCoords !== hasTexCoords ||
        primitiveInfo.hasTexCoord1 !== hasTexCoord1 ||
        primitiveInfo.hasOutline !== hasOutline
      ) {
        // This primitive uses the same material as another one that either:
        // * Isn't skinned
        // * Uses a different type to store joints and weights
        // * Doesn't have vertex colors, morph targets, normals, tangents, or texCoords
        // * Doesn't have a CESIUM_primitive_outline extension.
        var clonedMaterial = clone(material, true);
        // Split this off as a separate material
        materialIndex = addToArray(materials, clonedMaterial);
        primitive.material = materialIndex;
        primitiveInfoByMaterial[materialIndex] = {
          skinning: {
            skinned: isSkinned,
            componentType: componentType,
          },
          hasVertexColors: hasVertexColors,
          hasMorphTargets: hasMorphTargets,
          hasNormals: hasNormals,
          hasTangents: hasTangents,
          hasTexCoords: hasTexCoords,
          hasTexCoord1: hasTexCoord1,
          hasOutline: hasOutline,
        };
      }
    });
  });

  return primitiveInfoByMaterial;
};

ModelUtility.getShaderVariable = function (type) {
  if (type === "SCALAR") {
    return "float";
  }
  return type.toLowerCase();
};

ModelUtility.ModelState = {
  NEEDS_LOAD: 0,
  LOADING: 1,
  LOADED: 2, // Renderable, but textures can still be pending when incrementallyLoadTextures is true.
  FAILED: 3,
};

ModelUtility.getFailedLoadFunction = function (model, type, path) {
  return function (error) {
    model._state = ModelUtility.ModelState.FAILED;
    var message = "Failed to load " + type + ": " + path;
    if (defined(error)) {
      message += "\n" + error.message;
    }
    model._readyPromise.reject(new RuntimeError(message));
  };
};

ModelUtility.parseBuffers = function (model, bufferLoad) {
  var loadResources = model._loadResources;
  ForEach.buffer(model.gltf, function (buffer, bufferViewId) {
    if (defined(buffer.extras._pipeline.source)) {
      loadResources.buffers[bufferViewId] = buffer.extras._pipeline.source;
    } else if (defined(bufferLoad)) {
      var bufferResource = model._resource.getDerivedResource({
        url: buffer.uri,
      });
      ++loadResources.pendingBufferLoads;
      bufferResource
        .fetchArrayBuffer()
        .then(bufferLoad(model, bufferViewId))
        .otherwise(
          ModelUtility.getFailedLoadFunction(
            model,
            "buffer",
            bufferResource.url
          )
        );
    }
  });
};

var aMinScratch = new Cartesian3();
var aMaxScratch = new Cartesian3();

ModelUtility.computeBoundingSphere = function (model) {
  var gltf = model.gltf;
  var gltfNodes = gltf.nodes;
  var gltfMeshes = gltf.meshes;
  var rootNodes = gltf.scenes[gltf.scene].nodes;
  var rootNodesLength = rootNodes.length;

  var nodeStack = [];

  var min = new Cartesian3(
    Number.MAX_VALUE,
    Number.MAX_VALUE,
    Number.MAX_VALUE
  );
  var max = new Cartesian3(
    -Number.MAX_VALUE,
    -Number.MAX_VALUE,
    -Number.MAX_VALUE
  );

  for (var i = 0; i < rootNodesLength; ++i) {
    var n = gltfNodes[rootNodes[i]];
    n._transformToRoot = ModelUtility.getTransform(n);
    nodeStack.push(n);

    while (nodeStack.length > 0) {
      n = nodeStack.pop();
      var transformToRoot = n._transformToRoot;

      var meshId = n.mesh;
      if (defined(meshId)) {
        var mesh = gltfMeshes[meshId];
        var primitives = mesh.primitives;
        var primitivesLength = primitives.length;
        for (var m = 0; m < primitivesLength; ++m) {
          var positionAccessor = primitives[m].attributes.POSITION;
          if (defined(positionAccessor)) {
            var minMax = ModelUtility.getAccessorMinMax(gltf, positionAccessor);
            if (defined(minMax.min) && defined(minMax.max)) {
              var aMin = Cartesian3.fromArray(minMax.min, 0, aMinScratch);
              var aMax = Cartesian3.fromArray(minMax.max, 0, aMaxScratch);

              Matrix4.multiplyByPoint(transformToRoot, aMin, aMin);
              Matrix4.multiplyByPoint(transformToRoot, aMax, aMax);
              Cartesian3.minimumByComponent(min, aMin, min);
              Cartesian3.maximumByComponent(max, aMax, max);
            }
          }
        }
      }

      var children = n.children;
      if (defined(children)) {
        var childrenLength = children.length;
        for (var k = 0; k < childrenLength; ++k) {
          var child = gltfNodes[children[k]];
          child._transformToRoot = ModelUtility.getTransform(child);
          Matrix4.multiplyTransformation(
            transformToRoot,
            child._transformToRoot,
            child._transformToRoot
          );
          nodeStack.push(child);
        }
      }
      delete n._transformToRoot;
    }
  }

  var boundingSphere = BoundingSphere.fromCornerPoints(min, max);
  if (model._forwardAxis === Axis.Z) {
    // glTF 2.0 has a Z-forward convention that must be adapted here to X-forward.
    BoundingSphere.transformWithoutScale(
      boundingSphere,
      Axis.Z_UP_TO_X_UP,
      boundingSphere
    );
  }
  if (model._upAxis === Axis.Y) {
    BoundingSphere.transformWithoutScale(
      boundingSphere,
      Axis.Y_UP_TO_Z_UP,
      boundingSphere
    );
  } else if (model._upAxis === Axis.X) {
    BoundingSphere.transformWithoutScale(
      boundingSphere,
      Axis.X_UP_TO_Z_UP,
      boundingSphere
    );
  }
  return boundingSphere;
};

function techniqueAttributeForSemantic(technique, semantic) {
  return ForEach.techniqueAttribute(technique, function (
    attribute,
    attributeName
  ) {
    if (attribute.semantic === semantic) {
      return attributeName;
    }
  });
}

function ensureSemanticExistenceForPrimitive(gltf, primitive) {
  var accessors = gltf.accessors;
  var materials = gltf.materials;
  var techniquesWebgl = gltf.extensions.KHR_techniques_webgl;

  var techniques = techniquesWebgl.techniques;
  var programs = techniquesWebgl.programs;
  var shaders = techniquesWebgl.shaders;
  var targets = primitive.targets;

  var attributes = primitive.attributes;
  for (var target in targets) {
    if (targets.hasOwnProperty(target)) {
      var targetAttributes = targets[target];
      for (var attribute in targetAttributes) {
        if (attribute !== "extras") {
          attributes[attribute + "_" + target] = targetAttributes[attribute];
        }
      }
    }
  }

  var material = materials[primitive.material];
  var technique =
    techniques[material.extensions.KHR_techniques_webgl.technique];
  var program = programs[technique.program];
  var vertexShader = shaders[program.vertexShader];

  for (var semantic in attributes) {
    if (attributes.hasOwnProperty(semantic)) {
      if (!defined(techniqueAttributeForSemantic(technique, semantic))) {
        var accessorId = attributes[semantic];
        var accessor = accessors[accessorId];
        var lowerCase = semantic.toLowerCase();
        if (lowerCase.charAt(0) === "_") {
          lowerCase = lowerCase.slice(1);
        }
        var attributeName = "a_" + lowerCase;
        technique.attributes[attributeName] = {
          semantic: semantic,
          type: accessor.componentType,
        };
        var pipelineExtras = vertexShader.extras._pipeline;
        var shaderText = pipelineExtras.source;
        shaderText =
          "attribute " +
          ModelUtility.getShaderVariable(accessor.type) +
          " " +
          attributeName +
          ";\n" +
          shaderText;
        pipelineExtras.source = shaderText;
      }
    }
  }
}

/**
 * Ensures all attributes present on the primitive are present in the technique and
 * vertex shader.
 *
 * @param {Object} gltf A javascript object containing a glTF asset.
 * @returns {Object} The glTF asset, including any additional attributes.
 */
ModelUtility.ensureSemanticExistence = function (gltf) {
  ForEach.mesh(gltf, function (mesh) {
    ForEach.meshPrimitive(mesh, function (primitive) {
      ensureSemanticExistenceForPrimitive(gltf, primitive);
    });
  });

  return gltf;
};

/**
 * Creates attribute location for all attributes required by a technique.
 *
 * @param {Object} technique A glTF KHR_techniques_webgl technique object.
 * @param {Object} precreatedAttributes A dictionary object of pre-created attributes for which to also create locations.
 * @returns {Object} A dictionary object containing attribute names and their locations.
 */
ModelUtility.createAttributeLocations = function (
  technique,
  precreatedAttributes
) {
  var attributeLocations = {};
  var hasIndex0 = false;
  var i = 1;

  ForEach.techniqueAttribute(technique, function (attribute, attributeName) {
    // Set the position attribute to the 0th index. In some WebGL implementations the shader
    // will not work correctly if the 0th attribute is not active. For example, some glTF models
    // list the normal attribute first but derived shaders like the cast-shadows shader do not use
    // the normal attribute.
    if (/pos/i.test(attributeName) && !hasIndex0) {
      attributeLocations[attributeName] = 0;
      hasIndex0 = true;
    } else {
      attributeLocations[attributeName] = i++;
    }
  });

  if (defined(precreatedAttributes)) {
    for (var attributeName in precreatedAttributes) {
      if (precreatedAttributes.hasOwnProperty(attributeName)) {
        attributeLocations[attributeName] = i++;
      }
    }
  }

  return attributeLocations;
};

ModelUtility.getAccessorMinMax = function (gltf, accessorId) {
  var accessor = gltf.accessors[accessorId];
  var extensions = accessor.extensions;
  var accessorMin = accessor.min;
  var accessorMax = accessor.max;
  // If this accessor is quantized, we should use the decoded min and max
  if (defined(extensions)) {
    var quantizedAttributes = extensions.WEB3D_quantized_attributes;
    if (defined(quantizedAttributes)) {
      accessorMin = quantizedAttributes.decodedMin;
      accessorMax = quantizedAttributes.decodedMax;
    }
  }
  return {
    min: accessorMin,
    max: accessorMax,
  };
};

function getTechniqueAttributeOrUniformFunction(
  gltf,
  technique,
  semantic,
  ignoreNodes
) {
  if (usesExtension(gltf, "KHR_techniques_webgl")) {
    return function (attributeOrUniform, attributeOrUniformName) {
      if (
        attributeOrUniform.semantic === semantic &&
        (!ignoreNodes || !defined(attributeOrUniform.node))
      ) {
        return attributeOrUniformName;
      }
    };
  }

  return function (parameterName, attributeOrUniformName) {
    var attributeOrUniform = technique.parameters[parameterName];
    if (
      attributeOrUniform.semantic === semantic &&
      (!ignoreNodes || !defined(attributeOrUniform.node))
    ) {
      return attributeOrUniformName;
    }
  };
}

ModelUtility.getAttributeOrUniformBySemantic = function (
  gltf,
  semantic,
  programId,
  ignoreNodes
) {
  return ForEach.technique(gltf, function (technique) {
    if (defined(programId) && technique.program !== programId) {
      return;
    }

    var value = ForEach.techniqueAttribute(
      technique,
      getTechniqueAttributeOrUniformFunction(
        gltf,
        technique,
        semantic,
        ignoreNodes
      )
    );

    if (defined(value)) {
      return value;
    }

    return ForEach.techniqueUniform(
      technique,
      getTechniqueAttributeOrUniformFunction(
        gltf,
        technique,
        semantic,
        ignoreNodes
      )
    );
  });
};

ModelUtility.getDiffuseAttributeOrUniform = function (gltf, programId) {
  var diffuseUniformName = ModelUtility.getAttributeOrUniformBySemantic(
    gltf,
    "COLOR_0",
    programId
  );
  if (!defined(diffuseUniformName)) {
    diffuseUniformName = ModelUtility.getAttributeOrUniformBySemantic(
      gltf,
      "_3DTILESDIFFUSE",
      programId
    );
  }
  return diffuseUniformName;
};

var nodeTranslationScratch = new Cartesian3();
var nodeQuaternionScratch = new Quaternion();
var nodeScaleScratch = new Cartesian3();

ModelUtility.getTransform = function (node, result) {
  if (defined(node.matrix)) {
    return Matrix4.fromColumnMajorArray(node.matrix, result);
  }

  return Matrix4.fromTranslationQuaternionRotationScale(
    Cartesian3.fromArray(node.translation, 0, nodeTranslationScratch),
    Quaternion.unpack(node.rotation, 0, nodeQuaternionScratch),
    Cartesian3.fromArray(node.scale, 0, nodeScaleScratch),
    result
  );
};

ModelUtility.getUsedExtensions = function (gltf) {
  var extensionsUsed = gltf.extensionsUsed;
  var cachedExtensionsUsed = {};

  if (defined(extensionsUsed)) {
    var extensionsUsedLength = extensionsUsed.length;
    for (var i = 0; i < extensionsUsedLength; i++) {
      var extension = extensionsUsed[i];
      cachedExtensionsUsed[extension] = true;
    }
  }
  return cachedExtensionsUsed;
};

ModelUtility.getRequiredExtensions = function (gltf) {
  var extensionsRequired = gltf.extensionsRequired;
  var cachedExtensionsRequired = {};

  if (defined(extensionsRequired)) {
    var extensionsRequiredLength = extensionsRequired.length;
    for (var i = 0; i < extensionsRequiredLength; i++) {
      var extension = extensionsRequired[i];
      cachedExtensionsRequired[extension] = true;
    }
  }

  return cachedExtensionsRequired;
};

ModelUtility.supportedExtensions = {
  AGI_articulations: true,
  CESIUM_RTC: true,
  EXT_texture_webp: true,
  KHR_blend: true,
  KHR_binary_glTF: true,
  KHR_texture_basisu: true,
  KHR_draco_mesh_compression: true,
  KHR_materials_common: true,
  KHR_techniques_webgl: true,
  KHR_materials_unlit: true,
  KHR_materials_pbrSpecularGlossiness: true,
  KHR_texture_transform: true,
  WEB3D_quantized_attributes: true,
};

ModelUtility.checkSupportedExtensions = function (
  extensionsRequired,
  browserSupportsWebp
) {
  for (var extension in extensionsRequired) {
    if (extensionsRequired.hasOwnProperty(extension)) {
      if (!ModelUtility.supportedExtensions[extension]) {
        throw new RuntimeError("Unsupported glTF Extension: " + extension);
      }

      if (extension === "EXT_texture_webp" && browserSupportsWebp === false) {
        throw new RuntimeError(
          "Loaded model requires WebP but browser does not support it."
        );
      }
    }
  }
};

ModelUtility.checkSupportedGlExtensions = function (extensionsUsed, context) {
  if (defined(extensionsUsed)) {
    var glExtensionsUsedLength = extensionsUsed.length;
    for (var i = 0; i < glExtensionsUsedLength; i++) {
      var extension = extensionsUsed[i];
      if (extension !== "OES_element_index_uint") {
        throw new RuntimeError("Unsupported WebGL Extension: " + extension);
      } else if (!context.elementIndexUint) {
        throw new RuntimeError(
          "OES_element_index_uint WebGL extension is not enabled."
        );
      }
    }
  }
};

function replaceAllButFirstInString(string, find, replace) {
  // Limit search to strings that are not a subset of other tokens.
  find += "(?!\\w)";
  find = new RegExp(find, "g");

  var index = string.search(find);
  return string.replace(find, function (match, offset) {
    return index === offset ? match : replace;
  });
}

function getQuantizedAttributes(gltf, accessorId) {
  var accessor = gltf.accessors[accessorId];
  var extensions = accessor.extensions;
  if (defined(extensions)) {
    return extensions.WEB3D_quantized_attributes;
  }
  return undefined;
}

function getAttributeVariableName(gltf, primitive, attributeSemantic) {
  var materialId = primitive.material;
  var material = gltf.materials[materialId];

  if (
    !usesExtension(gltf, "KHR_techniques_webgl") ||
    !defined(material.extensions) ||
    !defined(material.extensions.KHR_techniques_webgl)
  ) {
    return;
  }

  var techniqueId = material.extensions.KHR_techniques_webgl.technique;
  var techniquesWebgl = gltf.extensions.KHR_techniques_webgl;
  var technique = techniquesWebgl.techniques[techniqueId];
  return ForEach.techniqueAttribute(technique, function (
    attribute,
    attributeName
  ) {
    var semantic = attribute.semantic;
    if (semantic === attributeSemantic) {
      return attributeName;
    }
  });
}

ModelUtility.modifyShaderForDracoQuantizedAttributes = function (
  gltf,
  primitive,
  shader,
  decodedAttributes
) {
  var quantizedUniforms = {};
  for (var attributeSemantic in decodedAttributes) {
    if (decodedAttributes.hasOwnProperty(attributeSemantic)) {
      var attribute = decodedAttributes[attributeSemantic];
      var quantization = attribute.quantization;
      if (!defined(quantization)) {
        continue;
      }

      var attributeVarName = getAttributeVariableName(
        gltf,
        primitive,
        attributeSemantic
      );

      if (attributeSemantic.charAt(0) === "_") {
        attributeSemantic = attributeSemantic.substring(1);
      }
      var decodeUniformVarName =
        "gltf_u_dec_" + attributeSemantic.toLowerCase();

      if (!defined(quantizedUniforms[decodeUniformVarName])) {
        var newMain = "gltf_decoded_" + attributeSemantic;
        var decodedAttributeVarName = attributeVarName.replace(
          "a_",
          "gltf_a_dec_"
        );
        var size = attribute.componentsPerAttribute;

        // replace usages of the original attribute with the decoded version, but not the declaration
        shader = replaceAllButFirstInString(
          shader,
          attributeVarName,
          decodedAttributeVarName
        );

        // declare decoded attribute
        var variableType;
        if (quantization.octEncoded) {
          variableType = "vec3";
        } else if (size > 1) {
          variableType = "vec" + size;
        } else {
          variableType = "float";
        }
        shader = variableType + " " + decodedAttributeVarName + ";\n" + shader;

        // The gltf 2.0 COLOR_0 vertex attribute can be VEC4 or VEC3
        var vec3Color = size === 3 && attributeSemantic === "COLOR_0";
        if (vec3Color) {
          shader = replaceAllButFirstInString(
            shader,
            decodedAttributeVarName,
            "vec4(" + decodedAttributeVarName + ", 1.0)"
          );
        }

        // splice decode function into the shader
        var decode = "";
        if (quantization.octEncoded) {
          var decodeUniformVarNameRangeConstant =
            decodeUniformVarName + "_rangeConstant";
          shader =
            "uniform float " +
            decodeUniformVarNameRangeConstant +
            ";\n" +
            shader;
          decode =
            "\n" +
            "void main() {\n" +
            // Draco oct-encoding decodes to zxy order
            "    " +
            decodedAttributeVarName +
            " = czm_octDecode(" +
            attributeVarName +
            ".xy, " +
            decodeUniformVarNameRangeConstant +
            ").zxy;\n" +
            "    " +
            newMain +
            "();\n" +
            "}\n";
        } else {
          var decodeUniformVarNameNormConstant =
            decodeUniformVarName + "_normConstant";
          var decodeUniformVarNameMin = decodeUniformVarName + "_min";
          shader =
            "uniform float " +
            decodeUniformVarNameNormConstant +
            ";\n" +
            "uniform " +
            variableType +
            " " +
            decodeUniformVarNameMin +
            ";\n" +
            shader;
          var attributeVarAccess = vec3Color ? ".xyz" : "";
          decode =
            "\n" +
            "void main() {\n" +
            "    " +
            decodedAttributeVarName +
            " = " +
            decodeUniformVarNameMin +
            " + " +
            attributeVarName +
            attributeVarAccess +
            " * " +
            decodeUniformVarNameNormConstant +
            ";\n" +
            "    " +
            newMain +
            "();\n" +
            "}\n";
        }

        shader = ShaderSource.replaceMain(shader, newMain);
        shader += decode;
      }
    }
  }
  return {
    shader: shader,
  };
};

ModelUtility.modifyShaderForQuantizedAttributes = function (
  gltf,
  primitive,
  shader
) {
  var quantizedUniforms = {};
  var attributes = primitive.attributes;
  for (var attributeSemantic in attributes) {
    if (attributes.hasOwnProperty(attributeSemantic)) {
      var attributeVarName = getAttributeVariableName(
        gltf,
        primitive,
        attributeSemantic
      );
      var accessorId = primitive.attributes[attributeSemantic];

      if (attributeSemantic.charAt(0) === "_") {
        attributeSemantic = attributeSemantic.substring(1);
      }
      var decodeUniformVarName =
        "gltf_u_dec_" + attributeSemantic.toLowerCase();

      var decodeUniformVarNameScale = decodeUniformVarName + "_scale";
      var decodeUniformVarNameTranslate = decodeUniformVarName + "_translate";
      if (
        !defined(quantizedUniforms[decodeUniformVarName]) &&
        !defined(quantizedUniforms[decodeUniformVarNameScale])
      ) {
        var quantizedAttributes = getQuantizedAttributes(gltf, accessorId);
        if (defined(quantizedAttributes)) {
          var decodeMatrix = quantizedAttributes.decodeMatrix;
          var newMain = "gltf_decoded_" + attributeSemantic;
          var decodedAttributeVarName = attributeVarName.replace(
            "a_",
            "gltf_a_dec_"
          );
          var size = Math.floor(Math.sqrt(decodeMatrix.length));

          // replace usages of the original attribute with the decoded version, but not the declaration
          shader = replaceAllButFirstInString(
            shader,
            attributeVarName,
            decodedAttributeVarName
          );
          // declare decoded attribute
          var variableType;
          if (size > 2) {
            variableType = "vec" + (size - 1);
          } else {
            variableType = "float";
          }
          shader =
            variableType + " " + decodedAttributeVarName + ";\n" + shader;
          // splice decode function into the shader - attributes are pre-multiplied with the decode matrix
          // uniform in the shader (32-bit floating point)
          var decode = "";
          if (size === 5) {
            // separate scale and translate since glsl doesn't have mat5
            shader =
              "uniform mat4 " + decodeUniformVarNameScale + ";\n" + shader;
            shader =
              "uniform vec4 " + decodeUniformVarNameTranslate + ";\n" + shader;
            decode =
              "\n" +
              "void main() {\n" +
              "    " +
              decodedAttributeVarName +
              " = " +
              decodeUniformVarNameScale +
              " * " +
              attributeVarName +
              " + " +
              decodeUniformVarNameTranslate +
              ";\n" +
              "    " +
              newMain +
              "();\n" +
              "}\n";

            quantizedUniforms[decodeUniformVarNameScale] = { mat: 4 };
            quantizedUniforms[decodeUniformVarNameTranslate] = { vec: 4 };
          } else {
            shader =
              "uniform mat" +
              size +
              " " +
              decodeUniformVarName +
              ";\n" +
              shader;
            decode =
              "\n" +
              "void main() {\n" +
              "    " +
              decodedAttributeVarName +
              " = " +
              variableType +
              "(" +
              decodeUniformVarName +
              " * vec" +
              size +
              "(" +
              attributeVarName +
              ",1.0));\n" +
              "    " +
              newMain +
              "();\n" +
              "}\n";

            quantizedUniforms[decodeUniformVarName] = { mat: size };
          }
          shader = ShaderSource.replaceMain(shader, newMain);
          shader += decode;
        }
      }
    }
  }
  return {
    shader: shader,
    uniforms: quantizedUniforms,
  };
};

function getScalarUniformFunction(value) {
  var that = {
    value: value,
    clone: function (source, result) {
      return source;
    },
    func: function () {
      return that.value;
    },
  };
  return that;
}

function getVec2UniformFunction(value) {
  var that = {
    value: Cartesian2.fromArray(value),
    clone: Cartesian2.clone,
    func: function () {
      return that.value;
    },
  };
  return that;
}

function getVec3UniformFunction(value) {
  var that = {
    value: Cartesian3.fromArray(value),
    clone: Cartesian3.clone,
    func: function () {
      return that.value;
    },
  };
  return that;
}

function getVec4UniformFunction(value) {
  var that = {
    value: Cartesian4.fromArray(value),
    clone: Cartesian4.clone,
    func: function () {
      return that.value;
    },
  };
  return that;
}

function getMat2UniformFunction(value) {
  var that = {
    value: Matrix2.fromColumnMajorArray(value),
    clone: Matrix2.clone,
    func: function () {
      return that.value;
    },
  };
  return that;
}

function getMat3UniformFunction(value) {
  var that = {
    value: Matrix3.fromColumnMajorArray(value),
    clone: Matrix3.clone,
    func: function () {
      return that.value;
    },
  };
  return that;
}

function getMat4UniformFunction(value) {
  var that = {
    value: Matrix4.fromColumnMajorArray(value),
    clone: Matrix4.clone,
    func: function () {
      return that.value;
    },
  };
  return that;
}

///////////////////////////////////////////////////////////////////////////

function DelayLoadedTextureUniform(value, textures, defaultTexture) {
  this._value = undefined;
  this._textureId = value.index;
  this._textures = textures;
  this._defaultTexture = defaultTexture;
}

Object.defineProperties(DelayLoadedTextureUniform.prototype, {
  value: {
    get: function () {
      // Use the default texture (1x1 white) until the model's texture is loaded
      if (!defined(this._value)) {
        var texture = this._textures[this._textureId];
        if (defined(texture)) {
          this._value = texture;
        } else {
          return this._defaultTexture;
        }
      }

      return this._value;
    },
    set: function (value) {
      this._value = value;
    },
  },
});

DelayLoadedTextureUniform.prototype.clone = function (source) {
  return source;
};

DelayLoadedTextureUniform.prototype.func = undefined;

///////////////////////////////////////////////////////////////////////////

function getTextureUniformFunction(value, textures, defaultTexture) {
  var uniform = new DelayLoadedTextureUniform(value, textures, defaultTexture);
  // Define function here to access closure since 'this' can't be
  // used when the Renderer sets uniforms.
  uniform.func = function () {
    return uniform.value;
  };
  return uniform;
}

var gltfUniformFunctions = {};
gltfUniformFunctions[WebGLConstants.FLOAT] = getScalarUniformFunction;
gltfUniformFunctions[WebGLConstants.FLOAT_VEC2] = getVec2UniformFunction;
gltfUniformFunctions[WebGLConstants.FLOAT_VEC3] = getVec3UniformFunction;
gltfUniformFunctions[WebGLConstants.FLOAT_VEC4] = getVec4UniformFunction;
gltfUniformFunctions[WebGLConstants.INT] = getScalarUniformFunction;
gltfUniformFunctions[WebGLConstants.INT_VEC2] = getVec2UniformFunction;
gltfUniformFunctions[WebGLConstants.INT_VEC3] = getVec3UniformFunction;
gltfUniformFunctions[WebGLConstants.INT_VEC4] = getVec4UniformFunction;
gltfUniformFunctions[WebGLConstants.BOOL] = getScalarUniformFunction;
gltfUniformFunctions[WebGLConstants.BOOL_VEC2] = getVec2UniformFunction;
gltfUniformFunctions[WebGLConstants.BOOL_VEC3] = getVec3UniformFunction;
gltfUniformFunctions[WebGLConstants.BOOL_VEC4] = getVec4UniformFunction;
gltfUniformFunctions[WebGLConstants.FLOAT_MAT2] = getMat2UniformFunction;
gltfUniformFunctions[WebGLConstants.FLOAT_MAT3] = getMat3UniformFunction;
gltfUniformFunctions[WebGLConstants.FLOAT_MAT4] = getMat4UniformFunction;
gltfUniformFunctions[WebGLConstants.SAMPLER_2D] = getTextureUniformFunction;
// GLTF_SPEC: Support SAMPLER_CUBE. https://github.com/KhronosGroup/glTF/issues/40

ModelUtility.createUniformFunction = function (
  type,
  value,
  textures,
  defaultTexture
) {
  return gltfUniformFunctions[type](value, textures, defaultTexture);
};

function scaleFromMatrix5Array(matrix) {
  return [
    matrix[0],
    matrix[1],
    matrix[2],
    matrix[3],
    matrix[5],
    matrix[6],
    matrix[7],
    matrix[8],
    matrix[10],
    matrix[11],
    matrix[12],
    matrix[13],
    matrix[15],
    matrix[16],
    matrix[17],
    matrix[18],
  ];
}

function translateFromMatrix5Array(matrix) {
  return [matrix[20], matrix[21], matrix[22], matrix[23]];
}

ModelUtility.createUniformsForDracoQuantizedAttributes = function (
  decodedAttributes
) {
  var uniformMap = {};
  for (var attribute in decodedAttributes) {
    if (decodedAttributes.hasOwnProperty(attribute)) {
      var decodedData = decodedAttributes[attribute];
      var quantization = decodedData.quantization;

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

      if (attribute.charAt(0) === "_") {
        attribute = attribute.substring(1);
      }

      var uniformVarName = "gltf_u_dec_" + attribute.toLowerCase();

      if (quantization.octEncoded) {
        var uniformVarNameRangeConstant = uniformVarName + "_rangeConstant";
        var rangeConstant = (1 << quantization.quantizationBits) - 1.0;
        uniformMap[uniformVarNameRangeConstant] = getScalarUniformFunction(
          rangeConstant
        ).func;
        continue;
      }

      var uniformVarNameNormConstant = uniformVarName + "_normConstant";
      var normConstant =
        quantization.range / (1 << quantization.quantizationBits);
      uniformMap[uniformVarNameNormConstant] = getScalarUniformFunction(
        normConstant
      ).func;

      var uniformVarNameMin = uniformVarName + "_min";
      switch (decodedData.componentsPerAttribute) {
        case 1:
          uniformMap[uniformVarNameMin] = getScalarUniformFunction(
            quantization.minValues
          ).func;
          break;
        case 2:
          uniformMap[uniformVarNameMin] = getVec2UniformFunction(
            quantization.minValues
          ).func;
          break;
        case 3:
          uniformMap[uniformVarNameMin] = getVec3UniformFunction(
            quantization.minValues
          ).func;
          break;
        case 4:
          uniformMap[uniformVarNameMin] = getVec4UniformFunction(
            quantization.minValues
          ).func;
          break;
      }
    }
  }

  return uniformMap;
};

ModelUtility.createUniformsForQuantizedAttributes = function (
  gltf,
  primitive,
  quantizedUniforms
) {
  var accessors = gltf.accessors;
  var setUniforms = {};
  var uniformMap = {};

  var attributes = primitive.attributes;
  for (var attribute in attributes) {
    if (attributes.hasOwnProperty(attribute)) {
      var accessorId = attributes[attribute];
      var a = accessors[accessorId];
      var extensions = a.extensions;

      if (attribute.charAt(0) === "_") {
        attribute = attribute.substring(1);
      }

      if (defined(extensions)) {
        var quantizedAttributes = extensions.WEB3D_quantized_attributes;
        if (defined(quantizedAttributes)) {
          var decodeMatrix = quantizedAttributes.decodeMatrix;
          var uniformVariable = "gltf_u_dec_" + attribute.toLowerCase();

          switch (a.type) {
            case AttributeType.SCALAR:
              uniformMap[uniformVariable] = getMat2UniformFunction(
                decodeMatrix
              ).func;
              setUniforms[uniformVariable] = true;
              break;
            case AttributeType.VEC2:
              uniformMap[uniformVariable] = getMat3UniformFunction(
                decodeMatrix
              ).func;
              setUniforms[uniformVariable] = true;
              break;
            case AttributeType.VEC3:
              uniformMap[uniformVariable] = getMat4UniformFunction(
                decodeMatrix
              ).func;
              setUniforms[uniformVariable] = true;
              break;
            case AttributeType.VEC4:
              // VEC4 attributes are split into scale and translate because there is no mat5 in GLSL
              var uniformVariableScale = uniformVariable + "_scale";
              var uniformVariableTranslate = uniformVariable + "_translate";
              uniformMap[uniformVariableScale] = getMat4UniformFunction(
                scaleFromMatrix5Array(decodeMatrix)
              ).func;
              uniformMap[uniformVariableTranslate] = getVec4UniformFunction(
                translateFromMatrix5Array(decodeMatrix)
              ).func;
              setUniforms[uniformVariableScale] = true;
              setUniforms[uniformVariableTranslate] = true;
              break;
          }
        }
      }
    }
  }

  // If there are any unset quantized uniforms in this program, they should be set to the identity
  for (var quantizedUniform in quantizedUniforms) {
    if (quantizedUniforms.hasOwnProperty(quantizedUniform)) {
      if (!setUniforms[quantizedUniform]) {
        var properties = quantizedUniforms[quantizedUniform];
        if (defined(properties.mat)) {
          if (properties.mat === 2) {
            uniformMap[quantizedUniform] = getMat2UniformFunction(
              Matrix2.IDENTITY
            ).func;
          } else if (properties.mat === 3) {
            uniformMap[quantizedUniform] = getMat3UniformFunction(
              Matrix3.IDENTITY
            ).func;
          } else if (properties.mat === 4) {
            uniformMap[quantizedUniform] = getMat4UniformFunction(
              Matrix4.IDENTITY
            ).func;
          }
        }
        if (defined(properties.vec)) {
          if (properties.vec === 4) {
            uniformMap[quantizedUniform] = getVec4UniformFunction([
              0,
              0,
              0,
              0,
            ]).func;
          }
        }
      }
    }
  }
  return uniformMap;
};

// This doesn't support LOCAL, which we could add if it is ever used.
var scratchTranslationRtc = new Cartesian3();
var gltfSemanticUniforms = {
  MODEL: function (uniformState, model) {
    return function () {
      return uniformState.model;
    };
  },
  VIEW: function (uniformState, model) {
    return function () {
      return uniformState.view;
    };
  },
  PROJECTION: function (uniformState, model) {
    return function () {
      return uniformState.projection;
    };
  },
  MODELVIEW: function (uniformState, model) {
    return function () {
      return uniformState.modelView;
    };
  },
  CESIUM_RTC_MODELVIEW: function (uniformState, model) {
    // CESIUM_RTC extension
    var mvRtc = new Matrix4();
    return function () {
      if (defined(model._rtcCenter)) {
        Matrix4.getTranslation(uniformState.model, scratchTranslationRtc);
        Cartesian3.add(
          scratchTranslationRtc,
          model._rtcCenter,
          scratchTranslationRtc
        );
        Matrix4.multiplyByPoint(
          uniformState.view,
          scratchTranslationRtc,
          scratchTranslationRtc
        );
        return Matrix4.setTranslation(
          uniformState.modelView,
          scratchTranslationRtc,
          mvRtc
        );
      }
      return uniformState.modelView;
    };
  },
  MODELVIEWPROJECTION: function (uniformState, model) {
    return function () {
      return uniformState.modelViewProjection;
    };
  },
  MODELINVERSE: function (uniformState, model) {
    return function () {
      return uniformState.inverseModel;
    };
  },
  VIEWINVERSE: function (uniformState, model) {
    return function () {
      return uniformState.inverseView;
    };
  },
  PROJECTIONINVERSE: function (uniformState, model) {
    return function () {
      return uniformState.inverseProjection;
    };
  },
  MODELVIEWINVERSE: function (uniformState, model) {
    return function () {
      return uniformState.inverseModelView;
    };
  },
  MODELVIEWPROJECTIONINVERSE: function (uniformState, model) {
    return function () {
      return uniformState.inverseModelViewProjection;
    };
  },
  MODELINVERSETRANSPOSE: function (uniformState, model) {
    return function () {
      return uniformState.inverseTransposeModel;
    };
  },
  MODELVIEWINVERSETRANSPOSE: function (uniformState, model) {
    return function () {
      return uniformState.normal;
    };
  },
  VIEWPORT: function (uniformState, model) {
    return function () {
      return uniformState.viewportCartesian4;
    };
  },
  // JOINTMATRIX created in createCommand()
};

ModelUtility.getGltfSemanticUniforms = function () {
  return gltfSemanticUniforms;
};
export default ModelUtility;
