import combine from "../../Core/combine.js";
import defined from "../../Core/defined.js";
import oneTimeWarning from "../../Core/oneTimeWarning.js";
import ShaderDestination from "../../Renderer/ShaderDestination.js";
import Pass from "../../Renderer/Pass.js";
import CustomShaderStageVS from "../../Shaders/Model/CustomShaderStageVS.js";
import CustomShaderStageFS from "../../Shaders/Model/CustomShaderStageFS.js";
import CustomShaderMode from "./CustomShaderMode.js";
import FeatureIdPipelineStage from "./FeatureIdPipelineStage.js";
import MetadataPipelineStage from "./MetadataPipelineStage.js";
import ModelUtility from "./ModelUtility.js";
import CustomShaderTranslucencyMode from "./CustomShaderTranslucencyMode.js";

/**
 * The custom shader pipeline stage takes GLSL callbacks from the
 * {@link CustomShader} and inserts them into the overall shader code for the
 * {@link Model}. The input to the callback is a struct with many
 * properties that depend on the attributes of the primitive. This shader code
 * is automatically generated by this stage.
 *
 * @namespace CustomShaderPipelineStage
 *
 * @private
 */
const CustomShaderPipelineStage = {
  name: "CustomShaderPipelineStage", // Helps with debugging

  STRUCT_ID_ATTRIBUTES_VS: "AttributesVS",
  STRUCT_ID_ATTRIBUTES_FS: "AttributesFS",
  STRUCT_NAME_ATTRIBUTES: "Attributes",
  STRUCT_ID_VERTEX_INPUT: "VertexInput",
  STRUCT_NAME_VERTEX_INPUT: "VertexInput",
  STRUCT_ID_FRAGMENT_INPUT: "FragmentInput",
  STRUCT_NAME_FRAGMENT_INPUT: "FragmentInput",
  FUNCTION_ID_INITIALIZE_INPUT_STRUCT_VS: "initializeInputStructVS",
  FUNCTION_SIGNATURE_INITIALIZE_INPUT_STRUCT_VS:
    "void initializeInputStruct(out VertexInput vsInput, ProcessedAttributes attributes)",
  FUNCTION_ID_INITIALIZE_INPUT_STRUCT_FS: "initializeInputStructFS",
  FUNCTION_SIGNATURE_INITIALIZE_INPUT_STRUCT_FS:
    "void initializeInputStruct(out FragmentInput fsInput, ProcessedAttributes attributes)",

  // Expose method for testing.
  _oneTimeWarning: oneTimeWarning,
};

/**
 * Process a primitive. This modifies the following parts of the render
 * resources:
 * <ul>
 *   <li>Modifies the shader to include the custom shader code to the vertex and fragment shaders</li>
 *   <li>Modifies the shader to include automatically-generated structs that serve as input to the custom shader callbacks </li>
 *   <li>Modifies the shader to include any additional user-defined uniforms</li>
 *   <li>Modifies the shader to include any additional user-defined varyings</li>
 *   <li>Adds any user-defined uniforms to the uniform map</li>
 *   <li>If the user specified a lighting model, the settings are overridden in the render resources</li>
 * </ul>
 * <p>
 * This pipeline stage is designed to fail gracefully where possible. If the
 * primitive does not have the right attributes to satisfy the shader code,
 * defaults will be inferred (when reasonable to do so). If not, the custom
 * shader will be disabled.
 * <p>
 *
 * @param {PrimitiveRenderResources} renderResources The render resources for the primitive
 * @param {ModelComponents.Primitive} primitive The primitive to be rendered
 * @param {FrameState} frameState The frame state.
 * @private
 */
CustomShaderPipelineStage.process = function (
  renderResources,
  primitive,
  frameState,
) {
  const { shaderBuilder, model, alphaOptions } = renderResources;
  const { customShader } = model;

  // Check the lighting model and translucent options first, as sometimes
  // these are used even if there is no vertex or fragment shader text.
  const { lightingModel, translucencyMode } = customShader;

  // if present, the lighting model overrides the material's lighting model.
  if (defined(lightingModel)) {
    renderResources.lightingOptions.lightingModel = lightingModel;
  }

  if (translucencyMode === CustomShaderTranslucencyMode.TRANSLUCENT) {
    alphaOptions.pass = Pass.TRANSLUCENT;
  } else if (translucencyMode === CustomShaderTranslucencyMode.OPAQUE) {
    // Use the default opqaue pass (either OPAQUE or 3D_TILES), regardless of whether
    // the material pipeline stage used translucent. The default is configured
    // in AlphaPipelineStage
    alphaOptions.pass = undefined;
  }
  // For CustomShaderTranslucencyMode.INHERIT, do not modify alphaOptions.pass

  // Generate lines of code for the shader, but don't add them to the shader
  // yet.
  const generatedCode = generateShaderLines(customShader, primitive);

  // In some corner cases, the primitive may not be compatible with the
  // shader. In this case, skip the custom shader.
  if (!generatedCode.customShaderEnabled) {
    return;
  }
  addLinesToShader(shaderBuilder, customShader, generatedCode);

  // the input to the fragment shader may include a low-precision ECEF position
  if (generatedCode.shouldComputePositionWC) {
    shaderBuilder.addDefine(
      "COMPUTE_POSITION_WC_CUSTOM_SHADER",
      undefined,
      ShaderDestination.BOTH,
    );
  }

  if (defined(customShader.vertexShaderText)) {
    shaderBuilder.addDefine(
      "HAS_CUSTOM_VERTEX_SHADER",
      undefined,
      ShaderDestination.VERTEX,
    );
  }

  if (defined(customShader.fragmentShaderText)) {
    shaderBuilder.addDefine(
      "HAS_CUSTOM_FRAGMENT_SHADER",
      undefined,
      ShaderDestination.FRAGMENT,
    );

    // add defines like CUSTOM_SHADER_MODIFY_MATERIAL
    const shaderModeDefine = CustomShaderMode.getDefineName(customShader.mode);
    shaderBuilder.addDefine(
      shaderModeDefine,
      undefined,
      ShaderDestination.FRAGMENT,
    );
  }

  const uniforms = customShader.uniforms;
  for (const uniformName in uniforms) {
    if (uniforms.hasOwnProperty(uniformName)) {
      const uniform = uniforms[uniformName];
      shaderBuilder.addUniform(uniform.type, uniformName);
    }
  }

  const varyings = customShader.varyings;
  for (const varyingName in varyings) {
    if (varyings.hasOwnProperty(varyingName)) {
      const varyingType = varyings[varyingName];
      shaderBuilder.addVarying(varyingType, varyingName);
    }
  }

  renderResources.uniformMap = combine(
    renderResources.uniformMap,
    customShader.uniformMap,
  );
};

/**
 * @private
 * @param {ModelComponents.Attribute[]} attributes
 * @returns {Object<string, ModelComponents.Attribute>}
 */
function getAttributesByName(attributes) {
  const names = {};
  for (let i = 0; i < attributes.length; i++) {
    const attributeInfo = ModelUtility.getAttributeInfo(attributes[i]);
    names[attributeInfo.variableName] = attributeInfo;
  }
  return names;
}

// GLSL types of standard attribute types when uniquely defined
const attributeTypeLUT = {
  position: "vec3",
  normal: "vec3",
  tangent: "vec3",
  bitangent: "vec3",
  texCoord: "vec2",
  color: "vec4",
  joints: "ivec4",
  weights: "vec4",
};

// Corresponding attribute values
const attributeDefaultValueLUT = {
  position: "vec3(0.0)",
  normal: "vec3(0.0, 0.0, 1.0)",
  tangent: "vec3(1.0, 0.0, 0.0)",
  bitangent: "vec3(0.0, 1.0, 0.0)",
  texCoord: "vec2(0.0)",
  color: "vec4(1.0)",
  joints: "ivec4(0)",
  weights: "vec4(0.0)",
};

function inferAttributeDefaults(attributeName) {
  // remove trailing set indices. E.g. "texCoord_0" -> "texCoord"
  let trimmed = attributeName.replace(/_[0-9]+$/, "");
  // also remove the MC/EC since they will have the same default value
  trimmed = trimmed.replace(/(MC|EC)$/, "");

  const glslType = attributeTypeLUT[trimmed];
  const value = attributeDefaultValueLUT[trimmed];

  // - _CUSTOM_ATTRIBUTE has an unknown type.
  if (!defined(glslType)) {
    return undefined;
  }

  return {
    attributeField: [glslType, attributeName],
    value: value,
  };
}

/**
 * @private
 * @param {CustomShader} customShader
 * @param {Object<string, ModelComponents.Attribute>} attributesByName
 * @returns {object}
 */
function generateVertexShaderLines(customShader, attributesByName) {
  if (!defined(customShader.vertexShaderText)) {
    return { enabled: false };
  }

  const primitiveAttributes = customShader.usedVariablesVertex.attributeSet;
  const addToShader = getPrimitiveAttributesUsedInShader(
    attributesByName,
    primitiveAttributes,
    false,
  );
  const needsDefault = getAttributesNeedingDefaults(
    attributesByName,
    primitiveAttributes,
    false,
  );

  let vertexInitialization;
  const attributeFields = [];
  const initializationLines = [];
  for (const variableName in addToShader) {
    if (!addToShader.hasOwnProperty(variableName)) {
      continue;
    }
    const attributeInfo = addToShader[variableName];
    const attributeField = [attributeInfo.glslType, variableName];
    attributeFields.push(attributeField);

    // Initializing attribute structs are just a matter of copying the
    // attribute or varying: E.g.:
    // "    vsInput.attributes.position = a_position;"
    vertexInitialization = `vsInput.attributes.${variableName} = attributes.${variableName};`;
    initializationLines.push(vertexInitialization);
  }

  for (let i = 0; i < needsDefault.length; i++) {
    const variableName = needsDefault[i];
    const attributeDefaults = inferAttributeDefaults(variableName);
    if (!defined(attributeDefaults)) {
      CustomShaderPipelineStage._oneTimeWarning(
        "CustomShaderPipelineStage.incompatiblePrimitiveVS",
        `Primitive is missing attribute ${variableName}, disabling custom vertex shader`,
      );
      // This primitive isn't compatible with the shader. Return early
      // to skip the vertex shader
      return { enabled: false };
    }

    attributeFields.push(attributeDefaults.attributeField);
    vertexInitialization = `vsInput.attributes.${variableName} = ${attributeDefaults.value};`;
    initializationLines.push(vertexInitialization);
  }

  return {
    enabled: true,
    attributeFields: attributeFields,
    initializationLines: initializationLines,
  };
}

function generatePositionBuiltins(customShader) {
  const attributeFields = [];
  const initializationLines = [];
  const usedVariables = customShader.usedVariablesFragment.attributeSet;

  // Model space position is the same position as in the glTF accessor,
  // this is already added to the shader with other attributes.

  // World coordinates in ECEF coordinates. Note that this is
  // low precision (32-bit floats) on the GPU.
  if (usedVariables.hasOwnProperty("positionWC")) {
    attributeFields.push(["vec3", "positionWC"]);
    initializationLines.push(
      "fsInput.attributes.positionWC = attributes.positionWC;",
    );
  }

  // position in eye coordinates
  if (usedVariables.hasOwnProperty("positionEC")) {
    attributeFields.push(["vec3", "positionEC"]);
    initializationLines.push(
      "fsInput.attributes.positionEC = attributes.positionEC;",
    );
  }

  return {
    attributeFields: attributeFields,
    initializationLines: initializationLines,
  };
}

/**
 * @private
 * @param {CustomShader} customShader
 * @param {Object<string, ModelComponents.Attribute>} attributesByName
 * @returns {object}
 */
function generateFragmentShaderLines(customShader, attributesByName) {
  if (!defined(customShader.fragmentShaderText)) {
    return { enabled: false };
  }

  const primitiveAttributes = customShader.usedVariablesFragment.attributeSet;
  const addToShader = getPrimitiveAttributesUsedInShader(
    attributesByName,
    primitiveAttributes,
    true,
  );
  const needsDefault = getAttributesNeedingDefaults(
    attributesByName,
    primitiveAttributes,
    true,
  );

  let fragmentInitialization;
  const attributeFields = [];
  const initializationLines = [];
  for (const variableName in addToShader) {
    if (!addToShader.hasOwnProperty(variableName)) {
      continue;
    }
    const attributeInfo = addToShader[variableName];

    const attributeField = [attributeInfo.glslType, variableName];
    attributeFields.push(attributeField);

    // Initializing attribute structs are just a matter of copying the
    // value from the processed attributes
    // "    fsInput.attributes.positionMC = attributes.positionMC;"
    fragmentInitialization = `fsInput.attributes.${variableName} = attributes.${variableName};`;
    initializationLines.push(fragmentInitialization);
  }

  for (let i = 0; i < needsDefault.length; i++) {
    const variableName = needsDefault[i];
    const attributeDefaults = inferAttributeDefaults(variableName);
    if (!defined(attributeDefaults)) {
      CustomShaderPipelineStage._oneTimeWarning(
        "CustomShaderPipelineStage.incompatiblePrimitiveFS",
        `Primitive is missing attribute ${variableName}, disabling custom fragment shader.`,
      );

      // This primitive isn't compatible with the shader. Return early
      // so the fragment shader is skipped
      return { enabled: false };
    }

    attributeFields.push(attributeDefaults.attributeField);
    fragmentInitialization = `fsInput.attributes.${variableName} = ${attributeDefaults.value};`;
    initializationLines.push(fragmentInitialization);
  }

  // Built-ins for positions in various coordinate systems.
  const positionBuiltins = generatePositionBuiltins(customShader);

  return {
    enabled: true,
    attributeFields: attributeFields.concat(positionBuiltins.attributeFields),
    initializationLines:
      positionBuiltins.initializationLines.concat(initializationLines),
  };
}

// These attributes are derived from positionMC, and are handled separately
// from other attributes
const builtinAttributes = {
  positionWC: true,
  positionEC: true,
};

/**
 * Get the primitive attributes that are referenced in the shader
 *
 * @private
 * @param {Object<string, ModelComponents.Attribute>} primitiveAttributes set of all the primitive's attributes
 * @param {Object<string, ModelComponents.Attribute>} shaderAttributeSet set of all attributes used in the shader
 * @param {boolean} isFragmentShader
 * @returns {Object<string, ModelComponents.Attribute>} A dictionary of the primitive attributes used in the shader
 */
function getPrimitiveAttributesUsedInShader(
  primitiveAttributes,
  shaderAttributeSet,
  isFragmentShader,
) {
  const addToShader = {};
  for (const attributeName in primitiveAttributes) {
    if (!primitiveAttributes.hasOwnProperty(attributeName)) {
      continue;
    }
    const attribute = primitiveAttributes[attributeName];

    // normals and tangents are in model coordinates in the attributes but
    // in eye coordinates in the fragment shader.
    let renamed = attributeName;
    if (isFragmentShader && attributeName === "normalMC") {
      renamed = "normalEC";
    } else if (isFragmentShader && attributeName === "tangentMC") {
      renamed = "tangentEC";
      attribute.glslType = "vec3";
    }

    if (shaderAttributeSet.hasOwnProperty(renamed)) {
      addToShader[renamed] = attribute;
    }
  }
  return addToShader;
}

/**
 * Get the attributes that will need to have default values defined.
 * Attributes referenced in the shader which are not already defined
 * for the primitive and are not built-in will need default values.
 *
 * @private
 * @param {Object<string, ModelComponents.Attribute>} primitiveAttributes set of all the primitive's attributes
 * @param {Object<string, ModelComponents.Attribute>} shaderAttributeSet set of all attributes used in the shader
 * @param {boolean} isFragmentShader
 * @returns {string[]} The names of the attributes needing defaults
 */
function getAttributesNeedingDefaults(
  primitiveAttributes,
  shaderAttributeSet,
  isFragmentShader,
) {
  const needDefaults = [];
  for (const attributeName in shaderAttributeSet) {
    if (!shaderAttributeSet.hasOwnProperty(attributeName)) {
      continue;
    }
    if (builtinAttributes.hasOwnProperty(attributeName)) {
      // Builtins are handled separately from attributes, so skip them here
      continue;
    }

    // normals and tangents are in model coordinates in the attributes but
    // in eye coordinates in the fragment shader.
    let renamed = attributeName;
    if (isFragmentShader && attributeName === "normalEC") {
      renamed = "normalMC";
    } else if (isFragmentShader && attributeName === "tangentEC") {
      renamed = "tangentMC";
    }

    if (!primitiveAttributes.hasOwnProperty(renamed)) {
      needDefaults.push(attributeName);
    }
  }
  return needDefaults;
}

/**
 * @private
 * @param {CustomShader} customShader
 * @param {ModelComponents.Primitive} primitive
 * @returns {object}
 */
function generateShaderLines(customShader, primitive) {
  // Attempt to generate vertex and fragment shader lines before adding any
  // code to the shader.
  const attributesByName = getAttributesByName(primitive.attributes);
  const vertexLines = generateVertexShaderLines(customShader, attributesByName);
  const fragmentLines = generateFragmentShaderLines(
    customShader,
    attributesByName,
  );

  // positionWC must be computed in the vertex shader
  // for use in the fragmentShader. However, this can be skipped if:
  // - positionWC isn't used in the fragment shader
  // - or the fragment shader is disabled
  const attributeSetFS = customShader.usedVariablesFragment.attributeSet;
  const shouldComputePositionWC =
    attributeSetFS.hasOwnProperty("positionWC") && fragmentLines.enabled;

  // Return any generated shader code along with some flags to indicate which
  // defines should be added.
  return {
    vertexLines: vertexLines,
    fragmentLines: fragmentLines,
    customShaderEnabled: vertexLines.enabled || fragmentLines.enabled,
    shouldComputePositionWC: shouldComputePositionWC,
  };
}

function addVertexLinesToShader(shaderBuilder, vertexLines) {
  let structId = CustomShaderPipelineStage.STRUCT_ID_ATTRIBUTES_VS;
  shaderBuilder.addStruct(
    structId,
    CustomShaderPipelineStage.STRUCT_NAME_ATTRIBUTES,
    ShaderDestination.VERTEX,
  );

  const { attributeFields, initializationLines } = vertexLines;
  for (let i = 0; i < attributeFields.length; i++) {
    const [glslType, variableName] = attributeFields[i];
    shaderBuilder.addStructField(structId, glslType, variableName);
  }

  // This could be hard-coded, but the symmetry with other structs makes unit
  // tests more convenient
  structId = CustomShaderPipelineStage.STRUCT_ID_VERTEX_INPUT;
  shaderBuilder.addStruct(
    structId,
    CustomShaderPipelineStage.STRUCT_NAME_VERTEX_INPUT,
    ShaderDestination.VERTEX,
  );
  shaderBuilder.addStructField(
    structId,
    CustomShaderPipelineStage.STRUCT_NAME_ATTRIBUTES,
    "attributes",
  );
  // Add FeatureIds struct from the Feature ID stage
  shaderBuilder.addStructField(
    structId,
    FeatureIdPipelineStage.STRUCT_NAME_FEATURE_IDS,
    "featureIds",
  );
  // Add Metadata struct from the metadata stage
  shaderBuilder.addStructField(
    structId,
    MetadataPipelineStage.STRUCT_NAME_METADATA,
    "metadata",
  );
  // Add MetadataClass struct from the metadata stage
  shaderBuilder.addStructField(
    structId,
    MetadataPipelineStage.STRUCT_NAME_METADATA_CLASS,
    "metadataClass",
  );
  // Add MetadataStatistics struct from the metadata stage
  shaderBuilder.addStructField(
    structId,
    MetadataPipelineStage.STRUCT_NAME_METADATA_STATISTICS,
    "metadataStatistics",
  );

  const functionId =
    CustomShaderPipelineStage.FUNCTION_ID_INITIALIZE_INPUT_STRUCT_VS;
  shaderBuilder.addFunction(
    functionId,
    CustomShaderPipelineStage.FUNCTION_SIGNATURE_INITIALIZE_INPUT_STRUCT_VS,
    ShaderDestination.VERTEX,
  );

  shaderBuilder.addFunctionLines(functionId, initializationLines);
}

function addFragmentLinesToShader(shaderBuilder, fragmentLines) {
  let structId = CustomShaderPipelineStage.STRUCT_ID_ATTRIBUTES_FS;
  shaderBuilder.addStruct(
    structId,
    CustomShaderPipelineStage.STRUCT_NAME_ATTRIBUTES,
    ShaderDestination.FRAGMENT,
  );

  const { attributeFields, initializationLines } = fragmentLines;
  for (let i = 0; i < attributeFields.length; i++) {
    const [glslType, variableName] = attributeFields[i];
    shaderBuilder.addStructField(structId, glslType, variableName);
  }

  structId = CustomShaderPipelineStage.STRUCT_ID_FRAGMENT_INPUT;
  shaderBuilder.addStruct(
    structId,
    CustomShaderPipelineStage.STRUCT_NAME_FRAGMENT_INPUT,
    ShaderDestination.FRAGMENT,
  );
  shaderBuilder.addStructField(
    structId,
    CustomShaderPipelineStage.STRUCT_NAME_ATTRIBUTES,
    "attributes",
  );
  // Add FeatureIds struct from the Feature ID stage
  shaderBuilder.addStructField(
    structId,
    FeatureIdPipelineStage.STRUCT_NAME_FEATURE_IDS,
    "featureIds",
  );
  // Add Metadata struct from the metadata stage
  shaderBuilder.addStructField(
    structId,
    MetadataPipelineStage.STRUCT_NAME_METADATA,
    "metadata",
  );
  // Add MetadataClass struct from the metadata stage
  shaderBuilder.addStructField(
    structId,
    MetadataPipelineStage.STRUCT_NAME_METADATA_CLASS,
    "metadataClass",
  );
  // Add MetadataStatistics struct from the metadata stage
  shaderBuilder.addStructField(
    structId,
    MetadataPipelineStage.STRUCT_NAME_METADATA_STATISTICS,
    "metadataStatistics",
  );

  const functionId =
    CustomShaderPipelineStage.FUNCTION_ID_INITIALIZE_INPUT_STRUCT_FS;
  shaderBuilder.addFunction(
    functionId,
    CustomShaderPipelineStage.FUNCTION_SIGNATURE_INITIALIZE_INPUT_STRUCT_FS,
    ShaderDestination.FRAGMENT,
  );

  shaderBuilder.addFunctionLines(functionId, initializationLines);
}

const scratchShaderLines = [];

function addLinesToShader(shaderBuilder, customShader, generatedCode) {
  const { vertexLines, fragmentLines } = generatedCode;
  const shaderLines = scratchShaderLines;

  if (vertexLines.enabled) {
    addVertexLinesToShader(shaderBuilder, vertexLines);

    shaderLines.length = 0;
    shaderLines.push(
      "#line 0",
      customShader.vertexShaderText,
      CustomShaderStageVS,
    );

    shaderBuilder.addVertexLines(shaderLines);
  }

  if (fragmentLines.enabled) {
    addFragmentLinesToShader(shaderBuilder, fragmentLines);

    shaderLines.length = 0;
    shaderLines.push(
      "#line 0",
      customShader.fragmentShaderText,
      CustomShaderStageFS,
    );

    shaderBuilder.addFragmentLines(shaderLines);
  }
}

export default CustomShaderPipelineStage;
