import Frozen from "../Core/Frozen.js";
import defined from "../Core/defined.js";
import DeveloperError from "../Core/DeveloperError.js";
import CzmBuiltins from "../Shaders/Builtin/CzmBuiltins.js";
import AutomaticUniforms from "./AutomaticUniforms.js";
import demodernizeShader from "./demodernizeShader.js";

function removeComments(source) {
  // remove inline comments
  source = source.replace(/\/\/.*/g, "");
  // remove multiline comment block
  return source.replace(/\/\*\*[\s\S]*?\*\//gm, function (match) {
    // preserve the number of lines in the comment block so the line numbers will be correct when debugging shaders
    const numberOfLines = match.match(/\n/gm).length;
    let replacement = "";
    for (let lineNumber = 0; lineNumber < numberOfLines; ++lineNumber) {
      replacement += "\n";
    }
    return replacement;
  });
}

function getDependencyNode(name, glslSource, nodes) {
  let dependencyNode;

  // check if already loaded
  for (let i = 0; i < nodes.length; ++i) {
    if (nodes[i].name === name) {
      dependencyNode = nodes[i];
    }
  }

  if (!defined(dependencyNode)) {
    // strip doc comments so we don't accidentally try to determine a dependency for something found
    // in a comment
    glslSource = removeComments(glslSource);

    // create new node
    dependencyNode = {
      name: name,
      glslSource: glslSource,
      dependsOn: [],
      requiredBy: [],
      evaluated: false,
    };
    nodes.push(dependencyNode);
  }

  return dependencyNode;
}

function generateDependencies(currentNode, dependencyNodes) {
  if (currentNode.evaluated) {
    return;
  }

  currentNode.evaluated = true;

  // identify all dependencies that are referenced from this glsl source code
  let czmMatches = currentNode.glslSource.match(/\bczm_[a-zA-Z0-9_]*/g);
  if (defined(czmMatches) && czmMatches !== null) {
    // remove duplicates
    czmMatches = czmMatches.filter(function (elem, pos) {
      return czmMatches.indexOf(elem) === pos;
    });

    czmMatches.forEach(function (element) {
      if (
        element !== currentNode.name &&
        ShaderSource._czmBuiltinsAndUniforms.hasOwnProperty(element)
      ) {
        const referencedNode = getDependencyNode(
          element,
          ShaderSource._czmBuiltinsAndUniforms[element],
          dependencyNodes,
        );
        currentNode.dependsOn.push(referencedNode);
        referencedNode.requiredBy.push(currentNode);

        // recursive call to find any dependencies of the new node
        generateDependencies(referencedNode, dependencyNodes);
      }
    });
  }
}

function sortDependencies(dependencyNodes) {
  const nodesWithoutIncomingEdges = [];
  const allNodes = [];

  while (dependencyNodes.length > 0) {
    const node = dependencyNodes.pop();
    allNodes.push(node);

    if (node.requiredBy.length === 0) {
      nodesWithoutIncomingEdges.push(node);
    }
  }

  while (nodesWithoutIncomingEdges.length > 0) {
    const currentNode = nodesWithoutIncomingEdges.shift();

    dependencyNodes.push(currentNode);

    for (let i = 0; i < currentNode.dependsOn.length; ++i) {
      // remove the edge from the graph
      const referencedNode = currentNode.dependsOn[i];
      const index = referencedNode.requiredBy.indexOf(currentNode);
      referencedNode.requiredBy.splice(index, 1);

      // if referenced node has no more incoming edges, add to list
      if (referencedNode.requiredBy.length === 0) {
        nodesWithoutIncomingEdges.push(referencedNode);
      }
    }
  }

  // if there are any nodes left with incoming edges, then there was a circular dependency somewhere in the graph
  const badNodes = [];
  for (let j = 0; j < allNodes.length; ++j) {
    if (allNodes[j].requiredBy.length !== 0) {
      badNodes.push(allNodes[j]);
    }
  }

  //>>includeStart('debug', pragmas.debug);
  if (badNodes.length !== 0) {
    let message =
      "A circular dependency was found in the following built-in functions/structs/constants: \n";
    for (let k = 0; k < badNodes.length; ++k) {
      message = `${message + badNodes[k].name}\n`;
    }
    throw new DeveloperError(message);
  }
  //>>includeEnd('debug');
}

function getBuiltinsAndAutomaticUniforms(shaderSource) {
  // generate a dependency graph for builtin functions
  const dependencyNodes = [];
  const root = getDependencyNode("main", shaderSource, dependencyNodes);
  generateDependencies(root, dependencyNodes);
  sortDependencies(dependencyNodes);

  // Concatenate the source code for the function dependencies.
  // Iterate in reverse so that dependent items are declared before they are used.
  let builtinsSource = "";
  for (let i = dependencyNodes.length - 1; i >= 0; --i) {
    builtinsSource = `${builtinsSource + dependencyNodes[i].glslSource}\n`;
  }

  return builtinsSource.replace(root.glslSource, "");
}

function combineShader(shaderSource, isFragmentShader, context) {
  // Combine shader sources, generally for pseudo-polymorphism, e.g., czm_getMaterial.
  let combinedSources = "";
  const sources = shaderSource.sources;
  if (defined(sources)) {
    for (let i = 0; i < sources.length; ++i) {
      // #line needs to be on its own line.
      combinedSources += `\n#line 0\n${sources[i]}`;
    }
  }

  combinedSources = removeComments(combinedSources);

  // Extract existing shader version from sources
  let version;
  combinedSources = combinedSources.replace(
    /#version\s+(.*?)\n/gm,
    function (match, group1) {
      //>>includeStart('debug', pragmas.debug);
      if (defined(version) && version !== group1) {
        throw new DeveloperError(
          `inconsistent versions found: ${version} and ${group1}`,
        );
      }
      //>>includeEnd('debug');

      // Extract #version to put at the top
      version = group1;

      // Replace original #version directive with a new line so the line numbers
      // are not off by one.  There can be only one #version directive
      // and it must appear at the top of the source, only preceded by
      // whitespace and comments.
      return "\n";
    },
  );

  // Extract shader extensions from sources
  const extensions = [];
  combinedSources = combinedSources.replace(
    /#extension.*\n/gm,
    function (match) {
      // Extract extension to put at the top
      extensions.push(match);

      // Replace original #extension directive with a new line so the line numbers
      // are not off by one.
      return "\n";
    },
  );

  // Remove precision qualifier
  combinedSources = combinedSources.replace(
    /precision\s(lowp|mediump|highp)\s(float|int);/,
    "",
  );

  // Replace main() for picked if desired.
  const pickColorQualifier = shaderSource.pickColorQualifier;
  if (defined(pickColorQualifier)) {
    combinedSources = ShaderSource.createPickFragmentShaderSource(
      combinedSources,
      pickColorQualifier,
    );
  }

  // combine into single string
  let result = "";

  const extensionsLength = extensions.length;
  for (let i = 0; i < extensionsLength; i++) {
    result += extensions[i];
  }

  if (isFragmentShader) {
    // If high precision isn't supported, replace occurrences of highp with mediump.
    // The highp keyword is not always available on older mobile devices.
    // See https://developer.mozilla.org/en-US/docs/Web/API/WebGL_API/WebGL_best_practices#In_WebGL_1_highp_float_support_is_optional_in_fragment_shaders
    result += `
#ifdef GL_FRAGMENT_PRECISION_HIGH
    precision highp float;
    precision highp int;
#else
    precision mediump float;
    precision mediump int;
    #define highp mediump
#endif
`;
  }

  if (context.webgl2) {
    result += `precision highp sampler3D;\n\n`;
  }

  // Prepend #defines for uber-shaders
  const defines = shaderSource.defines;
  if (defined(defines)) {
    for (let i = 0, length = defines.length; i < length; ++i) {
      const define = defines[i];
      if (define.length !== 0) {
        result += `#define ${define}\n`;
      }
    }
  }

  // Define a constant for the OES_texture_float_linear extension since WebGL does not.
  if (context.textureFloatLinear) {
    result += "#define OES_texture_float_linear\n\n";
  }

  // Define a constant for the OES_texture_float extension since WebGL does not.
  if (context.floatingPointTexture) {
    result += "#define OES_texture_float\n\n";
  }

  // append built-ins
  let builtinSources = "";
  if (shaderSource.includeBuiltIns) {
    builtinSources = getBuiltinsAndAutomaticUniforms(combinedSources);
  }

  // reset line number
  result += "\n#line 0\n";

  // append actual source
  const combinedShader = builtinSources + combinedSources;
  if (
    context.webgl2 &&
    isFragmentShader &&
    !/layout\s*\(location\s*=\s*0\)\s*out\s+vec4\s+out_FragColor;/g.test(
      combinedShader,
    ) &&
    !/czm_out_FragColor/g.test(combinedShader) &&
    /out_FragColor/g.test(combinedShader)
  ) {
    result += "layout(location = 0) out vec4 out_FragColor;\n\n";
  }

  result += builtinSources;
  result += combinedSources;

  // modernize the source
  if (!context.webgl2) {
    result = demodernizeShader(result, isFragmentShader);
  } else {
    result = `#version 300 es\n${result}`;
  }

  return result;
}

/**
 * An object containing various inputs that will be combined to form a final GLSL shader string.
 *
 * @param {object} [options] Object with the following properties:
 * @param {string[]} [options.sources] An array of strings to combine containing GLSL code for the shader.
 * @param {string[]} [options.defines] An array of strings containing GLSL identifiers to <code>#define</code>.
 * @param {string} [options.pickColorQualifier] The GLSL qualifier, <code>uniform</code> or <code>in</code>, for the input <code>czm_pickColor</code>.  When defined, a pick fragment shader is generated.
 * @param {boolean} [options.includeBuiltIns=true] If true, referenced built-in functions will be included with the combined shader.  Set to false if this shader will become a source in another shader, to avoid duplicating functions.
 *
 * @exception {DeveloperError} options.pickColorQualifier must be 'uniform' or 'in'.
 *
 * @example
 * // 1. Prepend #defines to a shader
 * const source = new Cesium.ShaderSource({
 *   defines : ['WHITE'],
 *   sources : ['void main() { \n#ifdef WHITE\n out_FragColor = vec4(1.0); \n#else\n out_FragColor = vec4(0.0); \n#endif\n }']
 * });
 *
 * // 2. Modify a fragment shader for picking
 * const source2 = new Cesium.ShaderSource({
 *   sources : ['void main() { out_FragColor = vec4(1.0); }'],
 *   pickColorQualifier : 'uniform'
 * });
 *
 * @private
 */
function ShaderSource(options) {
  options = options ?? Frozen.EMPTY_OBJECT;
  const pickColorQualifier = options.pickColorQualifier;

  //>>includeStart('debug', pragmas.debug);
  if (
    defined(pickColorQualifier) &&
    pickColorQualifier !== "uniform" &&
    pickColorQualifier !== "in"
  ) {
    throw new DeveloperError(
      "options.pickColorQualifier must be 'uniform' or 'in'.",
    );
  }
  //>>includeEnd('debug');

  this.defines = defined(options.defines) ? options.defines.slice(0) : [];
  this.sources = defined(options.sources) ? options.sources.slice(0) : [];
  this.pickColorQualifier = pickColorQualifier;
  this.includeBuiltIns = options.includeBuiltIns ?? true;
}

ShaderSource.prototype.clone = function () {
  return new ShaderSource({
    sources: this.sources,
    defines: this.defines,
    pickColorQualifier: this.pickColorQualifier,
    includeBuiltIns: this.includeBuiltIns,
  });
};

ShaderSource.replaceMain = function (source, renamedMain) {
  renamedMain = `void ${renamedMain}()`;
  return source.replace(/void\s+main\s*\(\s*(?:void)?\s*\)/g, renamedMain);
};

/**
 * Since {@link ShaderSource#createCombinedVertexShader} and
 * {@link ShaderSource#createCombinedFragmentShader} are both expensive to
 * compute, create a simpler string key for lookups in the {@link ShaderCache}.
 *
 * @returns {string} A key for identifying this shader
 *
 * @private
 */
ShaderSource.prototype.getCacheKey = function () {
  // Sort defines to make the key comparison deterministic
  const sortedDefines = this.defines.slice().sort();
  const definesKey = sortedDefines.join(",");
  const pickKey = this.pickColorQualifier;
  const builtinsKey = this.includeBuiltIns;
  const sourcesKey = this.sources.join("\n");

  return `${definesKey}:${pickKey}:${builtinsKey}:${sourcesKey}`;
};

/**
 * Create a single string containing the full, combined vertex shader with all dependencies and defines.
 *
 * @param {Context} context The current rendering context
 *
 * @returns {string} The combined shader string.
 */
ShaderSource.prototype.createCombinedVertexShader = function (context) {
  return combineShader(this, false, context);
};

/**
 * Create a single string containing the full, combined fragment shader with all dependencies and defines.
 *
 * @param {Context} context The current rendering context
 *
 * @returns {string} The combined shader string.
 */
ShaderSource.prototype.createCombinedFragmentShader = function (context) {
  return combineShader(this, true, context);
};

/**
 * For ShaderProgram testing
 * @private
 */
ShaderSource._czmBuiltinsAndUniforms = {};

// combine automatic uniforms and Cesium built-ins
for (const builtinName in CzmBuiltins) {
  if (CzmBuiltins.hasOwnProperty(builtinName)) {
    ShaderSource._czmBuiltinsAndUniforms[builtinName] =
      CzmBuiltins[builtinName];
  }
}
for (const uniformName in AutomaticUniforms) {
  if (AutomaticUniforms.hasOwnProperty(uniformName)) {
    const uniform = AutomaticUniforms[uniformName];
    if (typeof uniform.getDeclaration === "function") {
      ShaderSource._czmBuiltinsAndUniforms[uniformName] =
        uniform.getDeclaration(uniformName);
    }
  }
}

ShaderSource.createPickVertexShaderSource = function (vertexShaderSource) {
  const renamedVS = ShaderSource.replaceMain(
    vertexShaderSource,
    "czm_old_main",
  );
  const pickMain =
    "in vec4 pickColor; \n" +
    "out vec4 czm_pickColor; \n" +
    "void main() \n" +
    "{ \n" +
    "    czm_old_main(); \n" +
    "    czm_pickColor = pickColor; \n" +
    "}";

  return `${renamedVS}\n${pickMain}`;
};

ShaderSource.createPickFragmentShaderSource = function (
  fragmentShaderSource,
  pickColorQualifier,
) {
  const renamedFS = ShaderSource.replaceMain(
    fragmentShaderSource,
    "czm_old_main",
  );
  const pickMain =
    `${pickColorQualifier} vec4 czm_pickColor; \n` +
    `void main() \n` +
    `{ \n` +
    `    czm_old_main(); \n` +
    `    if (out_FragColor.a == 0.0) { \n` +
    `       discard; \n` +
    `    } \n` +
    `    out_FragColor = czm_pickColor; \n` +
    `}`;

  return `${renamedFS}\n${pickMain}`;
};

function containsDefine(shaderSource, define) {
  const defines = shaderSource.defines;
  const definesLength = defines.length;
  for (let i = 0; i < definesLength; ++i) {
    if (defines[i] === define) {
      return true;
    }
  }
  return false;
}

function containsString(shaderSource, string) {
  const sources = shaderSource.sources;
  const sourcesLength = sources.length;
  for (let i = 0; i < sourcesLength; ++i) {
    if (sources[i].indexOf(string) !== -1) {
      return true;
    }
  }
  return false;
}

function findFirstString(shaderSource, strings) {
  const stringsLength = strings.length;
  for (let i = 0; i < stringsLength; ++i) {
    const string = strings[i];
    if (containsString(shaderSource, string)) {
      return string;
    }
  }
  return undefined;
}

const normalVaryingNames = ["v_normalEC", "v_normal"];

ShaderSource.findNormalVarying = function (shaderSource) {
  // Fix for Model: the shader text always has the word v_normalEC
  // wrapped in an #ifdef so instead of looking for v_normalEC look for the define
  if (containsString(shaderSource, "#ifdef HAS_NORMALS")) {
    if (containsDefine(shaderSource, "HAS_NORMALS")) {
      return "v_normalEC";
    }
    return undefined;
  }

  return findFirstString(shaderSource, normalVaryingNames);
};

const positionVaryingNames = ["v_positionEC"];

ShaderSource.findPositionVarying = function (shaderSource) {
  return findFirstString(shaderSource, positionVaryingNames);
};
export default ShaderSource;
