import defaultValue from "../Core/defaultValue.js";
import defined from "../Core/defined.js";
import WebGLConstants from "../Core/WebGLConstants.js";
import webGLConstantToGlslType from "../Core/webGLConstantToGlslType.js";
import addToArray from "./GltfPipeline/addToArray.js";
import ForEach from "./GltfPipeline/ForEach.js";
import usesExtension from "./GltfPipeline/usesExtension.js";
import ModelUtility from "./ModelUtility.js";

/**
 * @private
 */
function processPbrMaterials(gltf, options) {
  options = defaultValue(options, defaultValue.EMPTY_OBJECT);

  // No need to create new techniques if they already exist,
  // the shader should handle these values
  if (usesExtension(gltf, "KHR_techniques_webgl")) {
    return gltf;
  }

  // All materials in glTF are PBR by default,
  // so we should apply PBR unless no materials are found.
  if (!defined(gltf.materials) || gltf.materials.length === 0) {
    return gltf;
  }

  if (!defined(gltf.extensions)) {
    gltf.extensions = {};
  }

  if (!defined(gltf.extensionsUsed)) {
    gltf.extensionsUsed = [];
  }

  if (!defined(gltf.extensionsRequired)) {
    gltf.extensionsRequired = [];
  }

  gltf.extensions.KHR_techniques_webgl = {
    programs: [],
    shaders: [],
    techniques: [],
  };

  gltf.extensionsUsed.push("KHR_techniques_webgl");
  gltf.extensionsRequired.push("KHR_techniques_webgl");

  const primitiveByMaterial = ModelUtility.splitIncompatibleMaterials(gltf);

  ForEach.material(gltf, function (material, materialIndex) {
    const generatedMaterialValues = {};
    const technique = generateTechnique(
      gltf,
      material,
      materialIndex,
      generatedMaterialValues,
      primitiveByMaterial,
      options
    );

    if (!defined(material.extensions)) {
      material.extensions = {};
    }

    material.extensions.KHR_techniques_webgl = {
      values: generatedMaterialValues,
      technique: technique,
    };
  });

  // If any primitives have semantics that aren't declared in the generated
  // shaders, we want to preserve them.
  ModelUtility.ensureSemanticExistence(gltf);

  return gltf;
}

function isSpecularGlossinessMaterial(material) {
  return (
    defined(material.extensions) &&
    defined(material.extensions.KHR_materials_pbrSpecularGlossiness)
  );
}

function addTextureCoordinates(
  gltf,
  textureName,
  generatedMaterialValues,
  defaultTexCoord,
  result
) {
  let texCoord;
  const texInfo = generatedMaterialValues[textureName];
  if (defined(texInfo) && defined(texInfo.texCoord) && texInfo.texCoord === 1) {
    defaultTexCoord = defaultTexCoord.replace("0", "1");
  }
  if (defined(generatedMaterialValues[`${textureName}Offset`])) {
    texCoord = `${textureName}Coord`;
    result.fragmentShaderMain += `    vec2 ${texCoord} = computeTexCoord(${defaultTexCoord}, ${textureName}Offset, ${textureName}Rotation, ${textureName}Scale);\n`;
  } else {
    texCoord = defaultTexCoord;
  }
  return texCoord;
}

const DEFAULT_TEXTURE_OFFSET = [0.0, 0.0];
const DEFAULT_TEXTURE_ROTATION = [0.0];
const DEFAULT_TEXTURE_SCALE = [1.0, 1.0];

function handleKHRTextureTransform(
  parameterName,
  value,
  generatedMaterialValues
) {
  if (
    parameterName.indexOf("Texture") === -1 ||
    !defined(value.extensions) ||
    !defined(value.extensions.KHR_texture_transform)
  ) {
    return;
  }

  const uniformName = `u_${parameterName}`;
  const extension = value.extensions.KHR_texture_transform;
  generatedMaterialValues[`${uniformName}Offset`] = defaultValue(
    extension.offset,
    DEFAULT_TEXTURE_OFFSET
  );
  generatedMaterialValues[`${uniformName}Rotation`] = defaultValue(
    extension.rotation,
    DEFAULT_TEXTURE_ROTATION
  );
  generatedMaterialValues[`${uniformName}Scale`] = defaultValue(
    extension.scale,
    DEFAULT_TEXTURE_SCALE
  );

  if (defined(value.texCoord) && defined(extension.texCoord)) {
    generatedMaterialValues[uniformName].texCoord = extension.texCoord;
  }
}

function generateTechnique(
  gltf,
  material,
  materialIndex,
  generatedMaterialValues,
  primitiveByMaterial,
  options
) {
  const addBatchIdToGeneratedShaders = defaultValue(
    options.addBatchIdToGeneratedShaders,
    false
  );

  const techniquesWebgl = gltf.extensions.KHR_techniques_webgl;
  const techniques = techniquesWebgl.techniques;
  const shaders = techniquesWebgl.shaders;
  const programs = techniquesWebgl.programs;

  const useSpecGloss = isSpecularGlossinessMaterial(material);

  let uniformName;
  let parameterName;
  let value;
  if (!useSpecGloss) {
    const pbrMetallicRoughness = material.pbrMetallicRoughness;
    if (defined(pbrMetallicRoughness)) {
      for (parameterName in pbrMetallicRoughness) {
        if (pbrMetallicRoughness.hasOwnProperty(parameterName)) {
          value = pbrMetallicRoughness[parameterName];
          uniformName = `u_${parameterName}`;
          generatedMaterialValues[uniformName] = value;
          handleKHRTextureTransform(
            parameterName,
            value,
            generatedMaterialValues
          );
        }
      }
    } else {
      // Add a uniform for baseColorFactor and set the default value. Otherwise
      // glTFs without a pbrMetallicRoughness object will not be styled correctly
      // when using Cesium3DTileColorBlendMode.REPLACE.
      generatedMaterialValues["u_baseColorFactor"] = [1.0, 1.0, 1.0, 1.0];
    }
  }

  if (useSpecGloss) {
    const pbrSpecularGlossiness =
      material.extensions.KHR_materials_pbrSpecularGlossiness;
    for (parameterName in pbrSpecularGlossiness) {
      if (pbrSpecularGlossiness.hasOwnProperty(parameterName)) {
        value = pbrSpecularGlossiness[parameterName];
        uniformName = `u_${parameterName}`;
        generatedMaterialValues[uniformName] = value;
        handleKHRTextureTransform(
          parameterName,
          value,
          generatedMaterialValues
        );
      }
    }
  }

  for (const additional in material) {
    if (
      material.hasOwnProperty(additional) &&
      (additional.indexOf("Texture") >= 0 || additional.indexOf("Factor") >= 0)
    ) {
      value = material[additional];
      uniformName = `u_${additional}`;
      generatedMaterialValues[uniformName] = value;
      handleKHRTextureTransform(additional, value, generatedMaterialValues);
    }
  }

  let vertexShader = "precision highp float;\n";
  let fragmentShader = "precision highp float;\n";

  let skin;
  if (defined(gltf.skins)) {
    skin = gltf.skins[0];
  }
  const joints = defined(skin) ? skin.joints : [];
  const jointCount = joints.length;

  const primitiveInfo = primitiveByMaterial[materialIndex];

  let skinningInfo;
  let hasSkinning = false;
  let hasVertexColors = false;
  let hasMorphTargets = false;
  let hasNormals = false;
  let hasTangents = false;
  let hasTexCoords = false;
  let hasTexCoord1 = false;
  let hasOutline = false;
  let isUnlit = false;

  if (defined(primitiveInfo)) {
    skinningInfo = primitiveInfo.skinning;
    hasSkinning = skinningInfo.skinned && joints.length > 0;
    hasVertexColors = primitiveInfo.hasVertexColors;
    hasMorphTargets = primitiveInfo.hasMorphTargets;
    hasNormals = primitiveInfo.hasNormals;
    hasTangents = primitiveInfo.hasTangents;
    hasTexCoords = primitiveInfo.hasTexCoords;
    hasTexCoord1 = primitiveInfo.hasTexCoord1;
    hasOutline = primitiveInfo.hasOutline;
  }

  let morphTargets;
  if (hasMorphTargets) {
    ForEach.mesh(gltf, function (mesh) {
      ForEach.meshPrimitive(mesh, function (primitive) {
        if (primitive.material === materialIndex) {
          const targets = primitive.targets;
          if (defined(targets)) {
            morphTargets = targets;
          }
        }
      });
    });
  }

  // Add techniques
  const techniqueUniforms = {
    // Add matrices
    u_modelViewMatrix: {
      semantic: usesExtension(gltf, "CESIUM_RTC")
        ? "CESIUM_RTC_MODELVIEW"
        : "MODELVIEW",
      type: WebGLConstants.FLOAT_MAT4,
    },
    u_projectionMatrix: {
      semantic: "PROJECTION",
      type: WebGLConstants.FLOAT_MAT4,
    },
  };

  if (
    defined(material.extensions) &&
    defined(material.extensions.KHR_materials_unlit)
  ) {
    isUnlit = true;
  }

  if (hasNormals) {
    techniqueUniforms.u_normalMatrix = {
      semantic: "MODELVIEWINVERSETRANSPOSE",
      type: WebGLConstants.FLOAT_MAT3,
    };
  }

  if (hasSkinning) {
    techniqueUniforms.u_jointMatrix = {
      count: jointCount,
      semantic: "JOINTMATRIX",
      type: WebGLConstants.FLOAT_MAT4,
    };
  }

  if (hasMorphTargets) {
    techniqueUniforms.u_morphWeights = {
      count: morphTargets.length,
      semantic: "MORPHWEIGHTS",
      type: WebGLConstants.FLOAT,
    };
  }

  const alphaMode = material.alphaMode;
  if (defined(alphaMode) && alphaMode === "MASK") {
    techniqueUniforms.u_alphaCutoff = {
      semantic: "ALPHACUTOFF",
      type: WebGLConstants.FLOAT,
    };
  }

  // Add material values
  for (uniformName in generatedMaterialValues) {
    if (generatedMaterialValues.hasOwnProperty(uniformName)) {
      techniqueUniforms[uniformName] = {
        type: getPBRValueType(uniformName),
      };
    }
  }

  const baseColorUniform = defaultValue(
    techniqueUniforms.u_baseColorTexture,
    techniqueUniforms.u_baseColorFactor
  );
  if (defined(baseColorUniform)) {
    baseColorUniform.semantic = "_3DTILESDIFFUSE";
  }

  // Add uniforms to shaders
  for (uniformName in techniqueUniforms) {
    if (techniqueUniforms.hasOwnProperty(uniformName)) {
      const uniform = techniqueUniforms[uniformName];
      const arraySize = defined(uniform.count) ? `[${uniform.count}]` : "";
      if (
        (uniform.type !== WebGLConstants.FLOAT_MAT3 &&
          uniform.type !== WebGLConstants.FLOAT_MAT4 &&
          uniformName !== "u_morphWeights") ||
        uniform.useInFragment
      ) {
        fragmentShader += `uniform ${webGLConstantToGlslType(
          uniform.type
        )} ${uniformName}${arraySize};\n`;
        delete uniform.useInFragment;
      } else {
        vertexShader += `uniform ${webGLConstantToGlslType(
          uniform.type
        )} ${uniformName}${arraySize};\n`;
      }
    }
  }

  if (hasOutline) {
    fragmentShader += "uniform sampler2D u_outlineTexture;\n";
  }

  // Add attributes with semantics
  let vertexShaderMain = "";
  if (hasSkinning) {
    vertexShaderMain +=
      "    mat4 skinMatrix =\n" +
      "        a_weight.x * u_jointMatrix[int(a_joint.x)] +\n" +
      "        a_weight.y * u_jointMatrix[int(a_joint.y)] +\n" +
      "        a_weight.z * u_jointMatrix[int(a_joint.z)] +\n" +
      "        a_weight.w * u_jointMatrix[int(a_joint.w)];\n";
  }

  // Add position always
  const techniqueAttributes = {
    a_position: {
      semantic: "POSITION",
    },
  };

  if (hasOutline) {
    techniqueAttributes.a_outlineCoordinates = {
      semantic: "_OUTLINE_COORDINATES",
    };
  }

  vertexShader += "attribute vec3 a_position;\n";
  if (hasNormals) {
    vertexShader += "varying vec3 v_positionEC;\n";
  }
  if (hasOutline) {
    vertexShader += "attribute vec3 a_outlineCoordinates;\n";
    vertexShader += "varying vec3 v_outlineCoordinates;\n";
  }

  // Morph Target Weighting
  vertexShaderMain += "    vec3 weightedPosition = a_position;\n";
  if (hasNormals) {
    vertexShaderMain += "    vec3 weightedNormal = a_normal;\n";
  }
  if (hasTangents) {
    vertexShaderMain += "    vec4 weightedTangent = a_tangent;\n";
  }
  if (hasMorphTargets) {
    for (let k = 0; k < morphTargets.length; k++) {
      const targetAttributes = morphTargets[k];
      for (const targetAttribute in targetAttributes) {
        if (
          targetAttributes.hasOwnProperty(targetAttribute) &&
          targetAttribute !== "extras"
        ) {
          const attributeName = `a_${targetAttribute}_${k}`;
          techniqueAttributes[attributeName] = {
            semantic: `${targetAttribute}_${k}`,
          };
          vertexShader += `attribute vec3 ${attributeName};\n`;
          if (targetAttribute === "POSITION") {
            vertexShaderMain += `    weightedPosition += u_morphWeights[${k}] * ${attributeName};\n`;
          } else if (targetAttribute === "NORMAL") {
            vertexShaderMain += `    weightedNormal += u_morphWeights[${k}] * ${attributeName};\n`;
          } else if (hasTangents && targetAttribute === "TANGENT") {
            vertexShaderMain += `    weightedTangent.xyz += u_morphWeights[${k}] * ${attributeName};\n`;
          }
        }
      }
    }
  }

  // Final position computation
  if (hasSkinning) {
    vertexShaderMain +=
      "    vec4 position = skinMatrix * vec4(weightedPosition, 1.0);\n";
  } else {
    vertexShaderMain += "    vec4 position = vec4(weightedPosition, 1.0);\n";
  }
  vertexShaderMain += "    position = u_modelViewMatrix * position;\n";
  if (hasNormals) {
    vertexShaderMain += "    v_positionEC = position.xyz;\n";
  }
  vertexShaderMain += "    gl_Position = u_projectionMatrix * position;\n";

  if (hasOutline) {
    vertexShaderMain += "    v_outlineCoordinates = a_outlineCoordinates;\n";
  }

  // Final normal computation
  if (hasNormals) {
    techniqueAttributes.a_normal = {
      semantic: "NORMAL",
    };
    vertexShader += "attribute vec3 a_normal;\n";
    if (!isUnlit) {
      vertexShader += "varying vec3 v_normal;\n";
      if (hasSkinning) {
        vertexShaderMain +=
          "    v_normal = u_normalMatrix * mat3(skinMatrix) * weightedNormal;\n";
      } else {
        vertexShaderMain += "    v_normal = u_normalMatrix * weightedNormal;\n";
      }
      fragmentShader += "varying vec3 v_normal;\n";
    }
    fragmentShader += "varying vec3 v_positionEC;\n";
  }

  // Read tangents if available
  if (hasTangents) {
    techniqueAttributes.a_tangent = {
      semantic: "TANGENT",
    };
    vertexShader += "attribute vec4 a_tangent;\n";
    vertexShader += "varying vec4 v_tangent;\n";
    vertexShaderMain +=
      "    v_tangent.xyz = u_normalMatrix * weightedTangent.xyz;\n";
    vertexShaderMain += "    v_tangent.w = weightedTangent.w;\n";

    fragmentShader += "varying vec4 v_tangent;\n";
  }

  if (hasOutline) {
    fragmentShader += "varying vec3 v_outlineCoordinates;\n";
  }

  let fragmentShaderMain = "";

  // Add texture coordinates if the material uses them
  let v_texCoord;
  let normalTexCoord;
  let baseColorTexCoord;
  let specularGlossinessTexCoord;
  let diffuseTexCoord;
  let metallicRoughnessTexCoord;
  let occlusionTexCoord;
  let emissiveTexCoord;

  if (hasTexCoords) {
    techniqueAttributes.a_texcoord_0 = {
      semantic: "TEXCOORD_0",
    };

    v_texCoord = "v_texcoord_0";
    vertexShader += "attribute vec2 a_texcoord_0;\n";
    vertexShader += `varying vec2 ${v_texCoord};\n`;
    vertexShaderMain += `    ${v_texCoord} = a_texcoord_0;\n`;

    fragmentShader += `varying vec2 ${v_texCoord};\n`;

    if (hasTexCoord1) {
      techniqueAttributes.a_texcoord_1 = {
        semantic: "TEXCOORD_1",
      };

      const v_texCoord1 = v_texCoord.replace("0", "1");
      vertexShader += "attribute vec2 a_texcoord_1;\n";
      vertexShader += `varying vec2 ${v_texCoord1};\n`;
      vertexShaderMain += `    ${v_texCoord1} = a_texcoord_1;\n`;

      fragmentShader += `varying vec2 ${v_texCoord1};\n`;
    }

    const result = {
      fragmentShaderMain: fragmentShaderMain,
    };
    normalTexCoord = addTextureCoordinates(
      gltf,
      "u_normalTexture",
      generatedMaterialValues,
      v_texCoord,
      result
    );
    baseColorTexCoord = addTextureCoordinates(
      gltf,
      "u_baseColorTexture",
      generatedMaterialValues,
      v_texCoord,
      result
    );
    specularGlossinessTexCoord = addTextureCoordinates(
      gltf,
      "u_specularGlossinessTexture",
      generatedMaterialValues,
      v_texCoord,
      result
    );
    diffuseTexCoord = addTextureCoordinates(
      gltf,
      "u_diffuseTexture",
      generatedMaterialValues,
      v_texCoord,
      result
    );
    metallicRoughnessTexCoord = addTextureCoordinates(
      gltf,
      "u_metallicRoughnessTexture",
      generatedMaterialValues,
      v_texCoord,
      result
    );
    occlusionTexCoord = addTextureCoordinates(
      gltf,
      "u_occlusionTexture",
      generatedMaterialValues,
      v_texCoord,
      result
    );
    emissiveTexCoord = addTextureCoordinates(
      gltf,
      "u_emissiveTexture",
      generatedMaterialValues,
      v_texCoord,
      result
    );

    fragmentShaderMain = result.fragmentShaderMain;
  }

  // Add skinning information if available
  if (hasSkinning) {
    techniqueAttributes.a_joint = {
      semantic: "JOINTS_0",
    };
    techniqueAttributes.a_weight = {
      semantic: "WEIGHTS_0",
    };

    vertexShader += "attribute vec4 a_joint;\n";
    vertexShader += "attribute vec4 a_weight;\n";
  }

  if (hasVertexColors) {
    techniqueAttributes.a_vertexColor = {
      semantic: "COLOR_0",
    };
    vertexShader += "attribute vec4 a_vertexColor;\n";
    vertexShader += "varying vec4 v_vertexColor;\n";
    vertexShaderMain += "  v_vertexColor = a_vertexColor;\n";
    fragmentShader += "varying vec4 v_vertexColor;\n";
  }

  if (addBatchIdToGeneratedShaders) {
    techniqueAttributes.a_batchId = {
      semantic: "_BATCHID",
    };
    vertexShader += "attribute float a_batchId;\n";
  }

  vertexShader += "void main(void) \n{\n";
  vertexShader += vertexShaderMain;
  vertexShader += "}\n";

  // Fragment shader lighting
  if (hasNormals && !isUnlit) {
    fragmentShader += "const float M_PI = 3.141592653589793;\n";

    fragmentShader +=
      "vec3 lambertianDiffuse(vec3 diffuseColor) \n" +
      "{\n" +
      "    return diffuseColor / M_PI;\n" +
      "}\n\n";

    fragmentShader +=
      "vec3 fresnelSchlick2(vec3 f0, vec3 f90, float VdotH) \n" +
      "{\n" +
      "    return f0 + (f90 - f0) * pow(clamp(1.0 - VdotH, 0.0, 1.0), 5.0);\n" +
      "}\n\n";

    fragmentShader +=
      "vec3 fresnelSchlick(float metalness, float VdotH) \n" +
      "{\n" +
      "    return metalness + (vec3(1.0) - metalness) * pow(1.0 - VdotH, 5.0);\n" +
      "}\n\n";

    fragmentShader +=
      "float smithVisibilityG1(float NdotV, float roughness) \n" +
      "{\n" +
      "    float k = (roughness + 1.0) * (roughness + 1.0) / 8.0;\n" +
      "    return NdotV / (NdotV * (1.0 - k) + k);\n" +
      "}\n\n";

    fragmentShader +=
      "float smithVisibilityGGX(float roughness, float NdotL, float NdotV) \n" +
      "{\n" +
      "    return smithVisibilityG1(NdotL, roughness) * smithVisibilityG1(NdotV, roughness);\n" +
      "}\n\n";

    fragmentShader +=
      "float GGX(float roughness, float NdotH) \n" +
      "{\n" +
      "    float roughnessSquared = roughness * roughness;\n" +
      "    float f = (NdotH * roughnessSquared - NdotH) * NdotH + 1.0;\n" +
      "    return roughnessSquared / (M_PI * f * f);\n" +
      "}\n\n";
  }

  fragmentShader +=
    "vec3 SRGBtoLINEAR3(vec3 srgbIn) \n" +
    "{\n" +
    "    return pow(srgbIn, vec3(2.2));\n" +
    "}\n\n";

  fragmentShader +=
    "vec4 SRGBtoLINEAR4(vec4 srgbIn) \n" +
    "{\n" +
    "    vec3 linearOut = pow(srgbIn.rgb, vec3(2.2));\n" +
    "    return vec4(linearOut, srgbIn.a);\n" +
    "}\n\n";

  fragmentShader +=
    "vec3 applyTonemapping(vec3 linearIn) \n" +
    "{\n" +
    "#ifndef HDR \n" +
    "    return czm_acesTonemapping(linearIn);\n" +
    "#else \n" +
    "    return linearIn;\n" +
    "#endif \n" +
    "}\n\n";

  fragmentShader +=
    "vec3 LINEARtoSRGB(vec3 linearIn) \n" +
    "{\n" +
    "#ifndef HDR \n" +
    "    return pow(linearIn, vec3(1.0/2.2));\n" +
    "#else \n" +
    "    return linearIn;\n" +
    "#endif \n" +
    "}\n\n";

  fragmentShader +=
    "vec2 computeTexCoord(vec2 texCoords, vec2 offset, float rotation, vec2 scale) \n" +
    "{\n" +
    "    rotation = -rotation; \n" +
    "    mat3 transform = mat3(\n" +
    "        cos(rotation) * scale.x, sin(rotation) * scale.x, 0.0, \n" +
    "       -sin(rotation) * scale.y, cos(rotation) * scale.y, 0.0, \n" +
    "        offset.x, offset.y, 1.0); \n" +
    "    vec2 transformedTexCoords = (transform * vec3(fract(texCoords), 1.0)).xy; \n" +
    "    return transformedTexCoords; \n" +
    "}\n\n";

  fragmentShader += "#ifdef USE_IBL_LIGHTING \n";
  fragmentShader += "uniform vec2 gltf_iblFactor; \n";
  fragmentShader += "#endif \n";
  fragmentShader += "#ifdef USE_CUSTOM_LIGHT_COLOR \n";
  fragmentShader += "uniform vec3 gltf_lightColor; \n";
  fragmentShader += "#endif \n";

  fragmentShader += "void main(void) \n{\n";
  fragmentShader += fragmentShaderMain;

  // Add normal mapping to fragment shader
  if (hasNormals && !isUnlit) {
    fragmentShader += "    vec3 ng = normalize(v_normal);\n";
    fragmentShader +=
      "    vec3 positionWC = vec3(czm_inverseView * vec4(v_positionEC, 1.0));\n";
    if (defined(generatedMaterialValues.u_normalTexture)) {
      if (hasTangents) {
        // Read tangents from varying
        fragmentShader += "    vec3 t = normalize(v_tangent.xyz);\n";
        fragmentShader +=
          "    vec3 b = normalize(cross(ng, t) * v_tangent.w);\n";
        fragmentShader += "    mat3 tbn = mat3(t, b, ng);\n";
        fragmentShader += `    vec3 n = texture2D(u_normalTexture, ${normalTexCoord}).rgb;\n`;
        fragmentShader += "    n = normalize(tbn * (2.0 * n - 1.0));\n";
      } else {
        // Add standard derivatives extension
        fragmentShader = `${
          "#ifdef GL_OES_standard_derivatives\n" +
          "#extension GL_OES_standard_derivatives : enable\n" +
          "#endif\n"
        }${fragmentShader}`;
        // Compute tangents
        fragmentShader += "#ifdef GL_OES_standard_derivatives\n";
        fragmentShader += "    vec3 pos_dx = dFdx(v_positionEC);\n";
        fragmentShader += "    vec3 pos_dy = dFdy(v_positionEC);\n";
        fragmentShader += `    vec3 tex_dx = dFdx(vec3(${normalTexCoord},0.0));\n`;
        fragmentShader += `    vec3 tex_dy = dFdy(vec3(${normalTexCoord},0.0));\n`;
        fragmentShader +=
          "    vec3 t = (tex_dy.t * pos_dx - tex_dx.t * pos_dy) / (tex_dx.s * tex_dy.t - tex_dy.s * tex_dx.t);\n";
        fragmentShader += "    t = normalize(t - ng * dot(ng, t));\n";
        fragmentShader += "    vec3 b = normalize(cross(ng, t));\n";
        fragmentShader += "    mat3 tbn = mat3(t, b, ng);\n";
        fragmentShader += `    vec3 n = texture2D(u_normalTexture, ${normalTexCoord}).rgb;\n`;
        fragmentShader += "    n = normalize(tbn * (2.0 * n - 1.0));\n";
        fragmentShader += "#else\n";
        fragmentShader += "    vec3 n = ng;\n";
        fragmentShader += "#endif\n";
      }
    } else {
      fragmentShader += "    vec3 n = ng;\n";
    }
    if (material.doubleSided) {
      fragmentShader += "    if (czm_backFacing())\n";
      fragmentShader += "    {\n";
      fragmentShader += "        n = -n;\n";
      fragmentShader += "    }\n";
    }
  }

  // Add base color to fragment shader
  if (defined(generatedMaterialValues.u_baseColorTexture)) {
    fragmentShader += `    vec4 baseColorWithAlpha = SRGBtoLINEAR4(texture2D(u_baseColorTexture, ${baseColorTexCoord}));\n`;
    if (defined(generatedMaterialValues.u_baseColorFactor)) {
      fragmentShader += "    baseColorWithAlpha *= u_baseColorFactor;\n";
    }
  } else if (defined(generatedMaterialValues.u_baseColorFactor)) {
    fragmentShader += "    vec4 baseColorWithAlpha = u_baseColorFactor;\n";
  } else {
    fragmentShader += "    vec4 baseColorWithAlpha = vec4(1.0);\n";
  }

  if (hasVertexColors) {
    fragmentShader += "    baseColorWithAlpha *= v_vertexColor;\n";
  }

  fragmentShader += "    vec3 baseColor = baseColorWithAlpha.rgb;\n";

  if (hasNormals && !isUnlit) {
    if (useSpecGloss) {
      if (defined(generatedMaterialValues.u_specularGlossinessTexture)) {
        fragmentShader += `    vec4 specularGlossiness = SRGBtoLINEAR4(texture2D(u_specularGlossinessTexture, ${specularGlossinessTexCoord}));\n`;
        fragmentShader += "    vec3 specular = specularGlossiness.rgb;\n";
        fragmentShader += "    float glossiness = specularGlossiness.a;\n";
        if (defined(generatedMaterialValues.u_specularFactor)) {
          fragmentShader += "    specular *= u_specularFactor;\n";
        }
        if (defined(generatedMaterialValues.u_glossinessFactor)) {
          fragmentShader += "    glossiness *= u_glossinessFactor;\n";
        }
      } else {
        if (defined(generatedMaterialValues.u_specularFactor)) {
          fragmentShader +=
            "    vec3 specular = clamp(u_specularFactor, vec3(0.0), vec3(1.0));\n";
        } else {
          fragmentShader += "    vec3 specular = vec3(1.0);\n";
        }
        if (defined(generatedMaterialValues.u_glossinessFactor)) {
          fragmentShader +=
            "    float glossiness = clamp(u_glossinessFactor, 0.0, 1.0);\n";
        } else {
          fragmentShader += "    float glossiness = 1.0;\n";
        }
      }
      if (defined(generatedMaterialValues.u_diffuseTexture)) {
        fragmentShader += `    vec4 diffuse = SRGBtoLINEAR4(texture2D(u_diffuseTexture, ${diffuseTexCoord}));\n`;
        if (defined(generatedMaterialValues.u_diffuseFactor)) {
          fragmentShader += "    diffuse *= u_diffuseFactor;\n";
        }
      } else if (defined(generatedMaterialValues.u_diffuseFactor)) {
        fragmentShader +=
          "    vec4 diffuse = clamp(u_diffuseFactor, vec4(0.0), vec4(1.0));\n";
      } else {
        fragmentShader += "    vec4 diffuse = vec4(1.0);\n";
      }

      // the specular glossiness extension's alpha takes precedence over
      // the base color alpha.
      fragmentShader += "    baseColorWithAlpha.a = diffuse.a;\n";
    } else if (defined(generatedMaterialValues.u_metallicRoughnessTexture)) {
      fragmentShader += `    vec3 metallicRoughness = texture2D(u_metallicRoughnessTexture, ${metallicRoughnessTexCoord}).rgb;\n`;
      fragmentShader +=
        "    float metalness = clamp(metallicRoughness.b, 0.0, 1.0);\n";
      fragmentShader +=
        "    float roughness = clamp(metallicRoughness.g, 0.04, 1.0);\n";
      if (defined(generatedMaterialValues.u_metallicFactor)) {
        fragmentShader += "    metalness *= u_metallicFactor;\n";
      }
      if (defined(generatedMaterialValues.u_roughnessFactor)) {
        fragmentShader += "    roughness *= u_roughnessFactor;\n";
      }
    } else {
      if (defined(generatedMaterialValues.u_metallicFactor)) {
        fragmentShader +=
          "    float metalness = clamp(u_metallicFactor, 0.0, 1.0);\n";
      } else {
        fragmentShader += "    float metalness = 1.0;\n";
      }
      if (defined(generatedMaterialValues.u_roughnessFactor)) {
        fragmentShader +=
          "    float roughness = clamp(u_roughnessFactor, 0.04, 1.0);\n";
      } else {
        fragmentShader += "    float roughness = 1.0;\n";
      }
    }

    fragmentShader += "    vec3 v = -normalize(v_positionEC);\n";

    // Generate fragment shader's lighting block
    fragmentShader += "#ifndef USE_CUSTOM_LIGHT_COLOR \n";
    fragmentShader += "    vec3 lightColorHdr = czm_lightColorHdr;\n";
    fragmentShader += "#else \n";
    fragmentShader += "    vec3 lightColorHdr = gltf_lightColor;\n";
    fragmentShader += "#endif \n";
    fragmentShader += "    vec3 l = normalize(czm_lightDirectionEC);\n";
    fragmentShader += "    vec3 h = normalize(v + l);\n";
    fragmentShader += "    float NdotL = clamp(dot(n, l), 0.001, 1.0);\n";
    fragmentShader += "    float NdotV = abs(dot(n, v)) + 0.001;\n";
    fragmentShader += "    float NdotH = clamp(dot(n, h), 0.0, 1.0);\n";
    fragmentShader += "    float LdotH = clamp(dot(l, h), 0.0, 1.0);\n";
    fragmentShader += "    float VdotH = clamp(dot(v, h), 0.0, 1.0);\n";
    fragmentShader += "    vec3 f0 = vec3(0.04);\n";
    // Whether the material uses metallic-roughness or specular-glossiness changes how the BRDF inputs are computed.
    // It does not change the implementation of the BRDF itself.
    if (useSpecGloss) {
      fragmentShader += "    float roughness = 1.0 - glossiness;\n";
      fragmentShader +=
        "    vec3 diffuseColor = diffuse.rgb * (1.0 - max(max(specular.r, specular.g), specular.b));\n";
      fragmentShader += "    vec3 specularColor = specular;\n";
    } else {
      fragmentShader +=
        "    vec3 diffuseColor = baseColor * (1.0 - metalness) * (1.0 - f0);\n";
      fragmentShader +=
        "    vec3 specularColor = mix(f0, baseColor, metalness);\n";
    }

    fragmentShader += "    float alpha = roughness * roughness;\n";
    fragmentShader +=
      "    float reflectance = max(max(specularColor.r, specularColor.g), specularColor.b);\n";
    fragmentShader +=
      "    vec3 r90 = vec3(clamp(reflectance * 25.0, 0.0, 1.0));\n";
    fragmentShader += "    vec3 r0 = specularColor.rgb;\n";

    fragmentShader += "    vec3 F = fresnelSchlick2(r0, r90, VdotH);\n";
    fragmentShader +=
      "    float G = smithVisibilityGGX(alpha, NdotL, NdotV);\n";
    fragmentShader += "    float D = GGX(alpha, NdotH);\n";

    fragmentShader +=
      "    vec3 diffuseContribution = (1.0 - F) * lambertianDiffuse(diffuseColor);\n";
    fragmentShader +=
      "    vec3 specularContribution = F * G * D / (4.0 * NdotL * NdotV);\n";
    fragmentShader +=
      "    vec3 color = NdotL * lightColorHdr * (diffuseContribution + specularContribution);\n";

    // Use the procedural IBL if there are no environment maps
    fragmentShader +=
      "#if defined(USE_IBL_LIGHTING) && !defined(DIFFUSE_IBL) && !defined(SPECULAR_IBL) \n";

    fragmentShader +=
      "    vec3 r = normalize(czm_inverseViewRotation * normalize(reflect(v, n)));\n";
    // Figure out if the reflection vector hits the ellipsoid
    fragmentShader += "    float vertexRadius = length(positionWC);\n";
    fragmentShader +=
      "    float horizonDotNadir = 1.0 - min(1.0, czm_ellipsoidRadii.x / vertexRadius);\n";
    fragmentShader +=
      "    float reflectionDotNadir = dot(r, normalize(positionWC));\n";
    // Flipping the X vector is a cheap way to get the inverse of czm_temeToPseudoFixed, since that's a rotation about Z.
    fragmentShader += "    r.x = -r.x;\n";
    fragmentShader += "    r = -normalize(czm_temeToPseudoFixed * r);\n";
    fragmentShader += "    r.x = -r.x;\n";

    fragmentShader += "    float inverseRoughness = 1.04 - roughness;\n";
    fragmentShader += "    inverseRoughness *= inverseRoughness;\n";
    fragmentShader +=
      "    vec3 sceneSkyBox = textureCube(czm_environmentMap, r).rgb * inverseRoughness;\n";

    fragmentShader += "    float atmosphereHeight = 0.05;\n";
    fragmentShader +=
      "    float blendRegionSize = 0.1 * ((1.0 - inverseRoughness) * 8.0 + 1.1 - horizonDotNadir);\n";
    fragmentShader += "    float blendRegionOffset = roughness * -1.0;\n";
    fragmentShader +=
      "    float farAboveHorizon = clamp(horizonDotNadir - blendRegionSize * 0.5 + blendRegionOffset, 1.0e-10 - blendRegionSize, 0.99999);\n";
    fragmentShader +=
      "    float aroundHorizon = clamp(horizonDotNadir + blendRegionSize * 0.5, 1.0e-10 - blendRegionSize, 0.99999);\n";
    fragmentShader +=
      "    float farBelowHorizon = clamp(horizonDotNadir + blendRegionSize * 1.5, 1.0e-10 - blendRegionSize, 0.99999);\n";
    fragmentShader +=
      "    float smoothstepHeight = smoothstep(0.0, atmosphereHeight, horizonDotNadir);\n";

    fragmentShader +=
      "    vec3 belowHorizonColor = mix(vec3(0.1, 0.15, 0.25), vec3(0.4, 0.7, 0.9), smoothstepHeight);\n";
    fragmentShader += "    vec3 nadirColor = belowHorizonColor * 0.5;\n";
    fragmentShader +=
      "    vec3 aboveHorizonColor = mix(vec3(0.9, 1.0, 1.2), belowHorizonColor, roughness * 0.5);\n";
    fragmentShader +=
      "    vec3 blueSkyColor = mix(vec3(0.18, 0.26, 0.48), aboveHorizonColor, reflectionDotNadir * inverseRoughness * 0.5 + 0.75);\n";
    fragmentShader +=
      "    vec3 zenithColor = mix(blueSkyColor, sceneSkyBox, smoothstepHeight);\n";

    fragmentShader += "    vec3 blueSkyDiffuseColor = vec3(0.7, 0.85, 0.9);\n";
    fragmentShader +=
      "    float diffuseIrradianceFromEarth = (1.0 - horizonDotNadir) * (reflectionDotNadir * 0.25 + 0.75) * smoothstepHeight;\n";
    fragmentShader +=
      "    float diffuseIrradianceFromSky = (1.0 - smoothstepHeight) * (1.0 - (reflectionDotNadir * 0.25 + 0.25));\n";
    fragmentShader +=
      "    vec3 diffuseIrradiance = blueSkyDiffuseColor * clamp(diffuseIrradianceFromEarth + diffuseIrradianceFromSky, 0.0, 1.0);\n";

    fragmentShader +=
      "    float notDistantRough = (1.0 - horizonDotNadir * roughness * 0.8);\n";
    fragmentShader +=
      "    vec3 specularIrradiance = mix(zenithColor, aboveHorizonColor, smoothstep(farAboveHorizon, aroundHorizon, reflectionDotNadir) * notDistantRough);\n";
    fragmentShader +=
      "    specularIrradiance = mix(specularIrradiance, belowHorizonColor, smoothstep(aroundHorizon, farBelowHorizon, reflectionDotNadir) * inverseRoughness);\n";
    fragmentShader +=
      "    specularIrradiance = mix(specularIrradiance, nadirColor, smoothstep(farBelowHorizon, 1.0, reflectionDotNadir) * inverseRoughness);\n";

    // Luminance model from page 40 of http://silviojemma.com/public/papers/lighting/spherical-harmonic-lighting.pdf
    fragmentShader += "#ifdef USE_SUN_LUMINANCE \n";
    // Angle between sun and zenith
    fragmentShader +=
      "    float LdotZenith = clamp(dot(normalize(czm_inverseViewRotation * l), normalize(positionWC * -1.0)), 0.001, 1.0);\n";
    fragmentShader += "    float S = acos(LdotZenith);\n";
    // Angle between zenith and current pixel
    fragmentShader +=
      "    float NdotZenith = clamp(dot(normalize(czm_inverseViewRotation * n), normalize(positionWC * -1.0)), 0.001, 1.0);\n";
    // Angle between sun and current pixel
    fragmentShader += "    float gamma = acos(NdotL);\n";
    fragmentShader +=
      "    float numerator = ((0.91 + 10.0 * exp(-3.0 * gamma) + 0.45 * pow(NdotL, 2.0)) * (1.0 - exp(-0.32 / NdotZenith)));\n";
    fragmentShader +=
      "    float denominator = (0.91 + 10.0 * exp(-3.0 * S) + 0.45 * pow(LdotZenith,2.0)) * (1.0 - exp(-0.32));\n";
    fragmentShader +=
      "    float luminance = gltf_luminanceAtZenith * (numerator / denominator);\n";
    fragmentShader += "#endif \n";

    fragmentShader +=
      "    vec2 brdfLut = texture2D(czm_brdfLut, vec2(NdotV, roughness)).rg;\n";
    fragmentShader +=
      "    vec3 IBLColor = (diffuseIrradiance * diffuseColor * gltf_iblFactor.x) + (specularIrradiance * SRGBtoLINEAR3(specularColor * brdfLut.x + brdfLut.y) * gltf_iblFactor.y);\n";

    fragmentShader +=
      "    float maximumComponent = max(max(lightColorHdr.x, lightColorHdr.y), lightColorHdr.z);\n";
    fragmentShader +=
      "    vec3 lightColor = lightColorHdr / max(maximumComponent, 1.0);\n";
    fragmentShader += "    IBLColor *= lightColor;\n";

    fragmentShader += "#ifdef USE_SUN_LUMINANCE \n";
    fragmentShader += "    color += IBLColor * luminance;\n";
    fragmentShader += "#else \n";
    fragmentShader += "    color += IBLColor; \n";
    fragmentShader += "#endif \n";

    // Environment maps were provided, use them for IBL
    fragmentShader += "#elif defined(DIFFUSE_IBL) || defined(SPECULAR_IBL) \n";
    fragmentShader +=
      "    const mat3 yUpToZUp = mat3(-1.0, 0.0, 0.0, 0.0, 0.0, -1.0, 0.0, 1.0, 0.0); \n";
    fragmentShader +=
      "    vec3 cubeDir = normalize(yUpToZUp * gltf_iblReferenceFrameMatrix * normalize(reflect(-v, n))); \n";

    fragmentShader += "#ifdef DIFFUSE_IBL \n";
    fragmentShader += "#ifdef CUSTOM_SPHERICAL_HARMONICS \n";
    fragmentShader +=
      "    vec3 diffuseIrradiance = czm_sphericalHarmonics(cubeDir, gltf_sphericalHarmonicCoefficients); \n";
    fragmentShader += "#else \n";
    fragmentShader +=
      "    vec3 diffuseIrradiance = czm_sphericalHarmonics(cubeDir, czm_sphericalHarmonicCoefficients); \n";
    fragmentShader += "#endif \n";
    fragmentShader += "#else \n";
    fragmentShader += "    vec3 diffuseIrradiance = vec3(0.0); \n";
    fragmentShader += "#endif \n";

    fragmentShader += "#ifdef SPECULAR_IBL \n";
    fragmentShader +=
      "    vec2 brdfLut = texture2D(czm_brdfLut, vec2(NdotV, roughness)).rg;\n";
    fragmentShader += "#ifdef CUSTOM_SPECULAR_IBL \n";
    fragmentShader +=
      "    vec3 specularIBL = czm_sampleOctahedralProjection(gltf_specularMap, gltf_specularMapSize, cubeDir,  roughness * gltf_maxSpecularLOD, gltf_maxSpecularLOD);\n";
    fragmentShader += "#else \n";
    fragmentShader +=
      "    vec3 specularIBL = czm_sampleOctahedralProjection(czm_specularEnvironmentMaps, czm_specularEnvironmentMapSize, cubeDir,  roughness * czm_specularEnvironmentMapsMaximumLOD, czm_specularEnvironmentMapsMaximumLOD);\n";
    fragmentShader += "#endif \n";
    fragmentShader += "    specularIBL *= F * brdfLut.x + brdfLut.y;\n";
    fragmentShader += "#else \n";
    fragmentShader += "    vec3 specularIBL = vec3(0.0); \n";
    fragmentShader += "#endif \n";

    fragmentShader +=
      "    color += diffuseIrradiance * diffuseColor + specularColor * specularIBL;\n";

    fragmentShader += "#endif \n";
  } else {
    fragmentShader += "    vec3 color = baseColor;\n";
  }

  // Ignore occlusion and emissive when unlit
  if (!isUnlit) {
    if (defined(generatedMaterialValues.u_occlusionTexture)) {
      fragmentShader += `    color *= texture2D(u_occlusionTexture, ${occlusionTexCoord}).r;\n`;
    }
    if (defined(generatedMaterialValues.u_emissiveTexture)) {
      fragmentShader += `    vec3 emissive = SRGBtoLINEAR3(texture2D(u_emissiveTexture, ${emissiveTexCoord}).rgb);\n`;
      if (defined(generatedMaterialValues.u_emissiveFactor)) {
        fragmentShader += "    emissive *= u_emissiveFactor;\n";
      }
      fragmentShader += "    color += emissive;\n";
    } else if (defined(generatedMaterialValues.u_emissiveFactor)) {
      fragmentShader += "    color += u_emissiveFactor;\n";
    }
  }

  if (!isUnlit) {
    fragmentShader += "    color = applyTonemapping(color);\n";
  }

  fragmentShader += "    color = LINEARtoSRGB(color);\n";

  if (hasOutline) {
    fragmentShader += "    float outlineness = max(\n";
    fragmentShader +=
      "        texture2D(u_outlineTexture, vec2(v_outlineCoordinates.x, 0.5)).r,\n";
    fragmentShader += "        max(\n";
    fragmentShader +=
      "          texture2D(u_outlineTexture, vec2(v_outlineCoordinates.y, 0.5)).r,\n";
    fragmentShader +=
      "          texture2D(u_outlineTexture, vec2(v_outlineCoordinates.z, 0.5)).r));\n";
    fragmentShader +=
      "    color = mix(color, vec3(0.0, 0.0, 0.0), outlineness);\n";
  }

  if (defined(alphaMode)) {
    if (alphaMode === "MASK") {
      fragmentShader += "    if (baseColorWithAlpha.a < u_alphaCutoff) {\n";
      fragmentShader += "        discard;\n";
      fragmentShader += "    }\n";
      fragmentShader += "    gl_FragColor = vec4(color, 1.0);\n";
    } else if (alphaMode === "BLEND") {
      fragmentShader +=
        "    gl_FragColor = vec4(color, baseColorWithAlpha.a);\n";
    } else {
      fragmentShader += "    gl_FragColor = vec4(color, 1.0);\n";
    }
  } else {
    fragmentShader += "    gl_FragColor = vec4(color, 1.0);\n";
  }

  fragmentShader += "}\n";

  // Add shaders
  const vertexShaderId = addToArray(shaders, {
    type: WebGLConstants.VERTEX_SHADER,
    extras: {
      _pipeline: {
        source: vertexShader,
        extension: ".glsl",
      },
    },
  });

  const fragmentShaderId = addToArray(shaders, {
    type: WebGLConstants.FRAGMENT_SHADER,
    extras: {
      _pipeline: {
        source: fragmentShader,
        extension: ".glsl",
      },
    },
  });

  // Add program
  const programId = addToArray(programs, {
    fragmentShader: fragmentShaderId,
    vertexShader: vertexShaderId,
  });

  const techniqueId = addToArray(techniques, {
    attributes: techniqueAttributes,
    program: programId,
    uniforms: techniqueUniforms,
  });

  return techniqueId;
}

function getPBRValueType(paramName) {
  if (paramName.indexOf("Offset") !== -1) {
    return WebGLConstants.FLOAT_VEC2;
  } else if (paramName.indexOf("Rotation") !== -1) {
    return WebGLConstants.FLOAT;
  } else if (paramName.indexOf("Scale") !== -1) {
    return WebGLConstants.FLOAT_VEC2;
  } else if (paramName.indexOf("Texture") !== -1) {
    return WebGLConstants.SAMPLER_2D;
  }

  switch (paramName) {
    case "u_baseColorFactor":
      return WebGLConstants.FLOAT_VEC4;
    case "u_metallicFactor":
      return WebGLConstants.FLOAT;
    case "u_roughnessFactor":
      return WebGLConstants.FLOAT;
    case "u_emissiveFactor":
      return WebGLConstants.FLOAT_VEC3;
    // Specular Glossiness Types
    case "u_diffuseFactor":
      return WebGLConstants.FLOAT_VEC4;
    case "u_specularFactor":
      return WebGLConstants.FLOAT_VEC3;
    case "u_glossinessFactor":
      return WebGLConstants.FLOAT;
  }
}
export default processPbrMaterials;
