import Check from "../Core/Check.js";
import clone from "../Core/clone.js";
import defined from "../Core/defined.js";
import defaultValue from "../Core/defaultValue.js";
import DeveloperError from "../Core/DeveloperError.js";
import ShaderDestination from "./ShaderDestination.js";
import ShaderProgram from "./ShaderProgram.js";
import ShaderSource from "./ShaderSource.js";

/**
 * An object that makes it easier to build the text of a {@link ShaderProgram}. This tracks GLSL code for both the vertex shader and the fragment shader.
 * <p>
 * For vertex shaders, the shader builder tracks a list of <code>#defines</code>,
 * a list of attributes, a list of uniforms, and a list of shader lines. It also
 * tracks the location of each attribute so the caller can easily build the {@link VertexArray}
 * </p>
 * <p>
 * For fragment shaders, the shader builder tracks a list of <code>#defines</code>,
 * a list of attributes, a list of uniforms, and a list of shader lines.
 * </p>
 *
 * @alias ShaderBuilder
 * @constructor
 *
 * @example
 * var shaderBuilder = new ShaderBuilder();
 * shaderBuilder.addDefine("SOLID_COLOR", undefined, ShaderDestination.FRAGMENT);
 * shaderBuilder.addUniform("vec3", "u_color", ShaderDestination.FRAGMENT);
 * shaderBuilder.addVarying("vec3", v_color");
 * // These locations can be used when creating the VertexArray
 * var positionLocation = shaderBuilder.addPositionAttribute("vec3", "a_position");
 * var colorLocation = shaderBuilder.addAttribute("vec3", "a_color");
 * shaderBuilder.addVertexLines([
 *  "void main()",
 *  "{",
 *  "    v_color = a_color;",
 *  "    gl_Position = vec4(a_position, 1.0);",
 *  "}"
 * ]);
 * shaderBuilder.addFragmentLines([
 *  "void main()",
 *  "{",
 *  "    #ifdef SOLID_COLOR",
 *  "    gl_FragColor = vec4(u_color, 1.0);",
 *  "    #else",
 *  "    gl_FragColor = vec4(v_color, 1.0);",
 *  "    #endif",
 *  "}"
 * ]);
 * var shaderProgram = shaderBuilder.build(context);
 *
 * @private
 */
export default function ShaderBuilder() {
  // Some WebGL implementations require attribute 0 to always
  // be active, so the position attribute is tracked separately
  this._positionAttributeLine = undefined;
  this._nextAttributeLocation = 1;
  this._attributeLocations = {};
  this._attributeLines = [];

  this._vertexShaderParts = {
    defineLines: [],
    uniformLines: [],
    shaderLines: [],
    varyingLines: [],
  };
  this._fragmentShaderParts = {
    defineLines: [],
    uniformLines: [],
    shaderLines: [],
    varyingLines: [],
  };
}

Object.defineProperties(ShaderBuilder.prototype, {
  /**
   * Get a dictionary of attribute names to the integer location in
   * the vertex shader.
   *
   * @memberof ShaderBuilder.prototype
   * @type {Object.<String, Number>}
   * @readonly
   * @private
   */
  attributeLocations: {
    get: function () {
      return this._attributeLocations;
    },
  },
});

/**
 * Add a <code>#define</code> macro to one or both of the shaders. These lines
 * will appear at the top of the final shader source.
 *
 * @param {String} identifier An identifier for the macro. Identifiers must use uppercase letters with underscores to be consistent with Cesium's style guide.
 * @param {String} [value] The value of the macro. If undefined, the define will not include a value. The value will be converted to GLSL code via <code>toString()</code>
 * @param {ShaderDestination} [destination=ShaderDestination.BOTH] Whether the define appears in the vertex shader, the fragment shader, or both.
 *
 * @example
 * // creates the line "#define ENABLE_LIGHTING" in both shaders
 * shaderBuilder.addDefine("ENABLE_LIGHTING");
 * // creates the line "#define PI 3.141592" in the fragment shader
 * shaderBuilder.addDefine("PI", 3.141593, ShaderDestination.FRAGMENT);
 */
ShaderBuilder.prototype.addDefine = function (identifier, value, destination) {
  //>>includeStart('debug', pragmas.debug);
  Check.typeOf.string("identifier", identifier);
  //>>includeEnd('debug');

  destination = defaultValue(destination, ShaderDestination.BOTH);

  // The ShaderSource created in build() will add the #define part
  var line = identifier;
  if (defined(value)) {
    line += " " + value.toString();
  }

  if (ShaderDestination.includesVertexShader(destination)) {
    this._vertexShaderParts.defineLines.push(line);
  }

  if (ShaderDestination.includesFragmentShader(destination)) {
    this._fragmentShaderParts.defineLines.push(line);
  }
};

/**
 * Add a uniform declaration to one or both of the shaders. These lines
 * will appear grouped near the top of the final shader source.
 *
 * @param {String} type The GLSL type of the uniform.
 * @param {String} identifier An identifier for the uniform. Identifiers must begin with <code>u_</code> to be consistent with Cesium's style guide.
 * @param {ShaderDestination} [destination=ShaderDestination.BOTH] Whether the uniform appears in the vertex shader, the fragment shader, or both.
 *
 * @example
 * // creates the line "uniform vec3 u_resolution;"
 * shaderBuilder.addUniform("vec3", "u_resolution", ShaderDestination.FRAGMENT);
 * // creates the line "uniform float u_time;" in both shaders
 * shaderBuilder.addDefine("float", "u_time", ShaderDestination.BOTH);
 */
ShaderBuilder.prototype.addUniform = function (type, identifier, destination) {
  //>>includeStart('debug', pragmas.debug);
  Check.typeOf.string("type", type);
  Check.typeOf.string("identifier", identifier);
  //>>includeEnd('debug');

  destination = defaultValue(destination, ShaderDestination.BOTH);
  var line = "uniform " + type + " " + identifier + ";";

  if (ShaderDestination.includesVertexShader(destination)) {
    this._vertexShaderParts.uniformLines.push(line);
  }

  if (ShaderDestination.includesFragmentShader(destination)) {
    this._fragmentShaderParts.uniformLines.push(line);
  }
};

/**
 * Add a position attribute declaration to the vertex shader. These lines
 * will appear grouped near the top of the final shader source.
 * <p>
 * Some WebGL implementations require attribute 0 to be enabled, so this is
 * reserved for the position attribute. For all other attributes, see
 * {@link ShaderBuilder#addAttribute}
 * </p>
 *
 * @param {String} type The GLSL type of the attribute
 * @param {String} identifier An identifier for the attribute. Identifiers must begin with <code>a_</code> to be consistent with Cesium's style guide.
 * @return {Number} The integer location of the attribute. This location can be used when creating attributes for a {@link VertexArray}. This will always be 0.
 *
 * @example
 * // creates the line "attribute vec3 a_position;"
 * shaderBuilder.setPositionAttribute("vec3", "a_position");
 */
ShaderBuilder.prototype.setPositionAttribute = function (type, identifier) {
  //>>includeStart('debug', pragmas.debug);
  Check.typeOf.string("type", type);
  Check.typeOf.string("identifier", identifier);

  if (defined(this._positionAttributeLine)) {
    throw new DeveloperError(
      "setPositionAttribute() must be called exactly once for the attribute used for gl_Position. For other attributes, use addAttribute()"
    );
  }
  //>>includeEnd('debug');

  this._positionAttributeLine = "attribute " + type + " " + identifier + ";";

  // Some WebGL implementations require attribute 0 to always be active, so
  // this builder assumes the position will always go in location 0
  this._attributeLocations[identifier] = 0;
  return 0;
};

/**
 * Add an attribute declaration to the vertex shader. These lines
 * will appear grouped near the top of the final shader source.
 * <p>
 * Some WebGL implementations require attribute 0 to be enabled, so this is
 * reserved for the position attribute. See {@link ShaderBuilder#setPositionAttribute}
 * </p>
 *
 * @param {String} type The GLSL type of the attribute
 * @param {String} identifier An identifier for the attribute. Identifiers must begin with <code>a_</code> to be consistent with Cesium's style guide.
 * @return {Number} The integer location of the attribute. This location can be used when creating attributes for a {@link VertexArray}
 *
 * @example
 * // creates the line "attribute vec2 a_texCoord0;" in the vertex shader
 * shaderBuilder.addAttribute("vec2", "a_texCoord0");
 */
ShaderBuilder.prototype.addAttribute = function (type, identifier) {
  //>>includeStart('debug', pragmas.debug);
  Check.typeOf.string("type", type);
  Check.typeOf.string("identifier", identifier);
  //>>includeEnd('debug');

  var line = "attribute " + type + " " + identifier + ";";
  this._attributeLines.push(line);

  var location = this._nextAttributeLocation;
  this._attributeLocations[identifier] = location;
  this._nextAttributeLocation++;
  return location;
};

/**
 * Add a varying declaration to both the vertex and fragment shaders.
 *
 * @param {String} type The GLSL type of the varying
 * @param {String} identifier An identifier for the varying. Identifiers must begin with <code>v_</code> to be consistent with Cesium's style guide.
 *
 * @example
 * // creates the line "varying vec3 v_color;" in both shaders
 * shaderBuilder.addVarying("vec3", "v_color");
 */
ShaderBuilder.prototype.addVarying = function (type, identifier) {
  //>>includeStart('debug', pragmas.debug);
  Check.typeOf.string("type", type);
  Check.typeOf.string("identifier", identifier);
  //>>includeEnd('debug');

  var line = "varying " + type + " " + identifier + ";";
  this._vertexShaderParts.varyingLines.push(line);
  this._fragmentShaderParts.varyingLines.push(line);
};

/**
 * Appends lines of GLSL code to the vertex shader
 *
 * @param {String[]} lines The lines to add to the end of the vertex shader source
 * @example
 * shaderBuilder.addVertexLines([
 *  "void main()",
 *  "{",
 *  "    v_color = a_color;",
 *  "    gl_Position = vec4(a_position, 1.0);",
 *  "}"
 * ]);
 */
ShaderBuilder.prototype.addVertexLines = function (lines) {
  //>>includeStart('debug', pragmas.debug);
  Check.typeOf.object("lines", lines);
  //>>includeEnd('debug');

  Array.prototype.push.apply(this._vertexShaderParts.shaderLines, lines);
};

/**
 * Appends lines of GLSL code to the fragment shader
 *
 * @param {String[]} lines The lines to add to the end of the fragment shader source
 * @example
 * shaderBuilder.addFragmentLines([
 *  "void main()",
 *  "{",
 *  "    #ifdef SOLID_COLOR",
 *  "    gl_FragColor = vec4(u_color, 1.0);",
 *  "    #else",
 *  "    gl_FragColor = vec4(v_color, 1.0);",
 *  "    #endif",
 *  "}"
 * ]);
 */
ShaderBuilder.prototype.addFragmentLines = function (lines) {
  //>>includeStart('debug', pragmas.debug);
  Check.typeOf.object("lines", lines);
  //>>includeEnd('debug');
  Array.prototype.push.apply(this._fragmentShaderParts.shaderLines, lines);
};

/**
 * Builds the {@link ShaderProgram} from the pieces added by the other methods.
 * Call this one time at the end of modifying the shader through the other
 * methods in this class.
 *
 * @param {Context} context The context to use for creating the shader.
 * @return {ShaderProgram} A shader program to use for rendering.
 *
 * @example
 * var shaderProgram = shaderBuilder.buildShaderProgram(context);
 */
ShaderBuilder.prototype.buildShaderProgram = function (context) {
  //>>includeStart('debug', pragmas.debug);
  Check.typeOf.object("context", context);
  //>>includeEnd('debug');

  var positionAttribute = defined(this._positionAttributeLine)
    ? [this._positionAttributeLine]
    : [];

  // Lines are joined here so the ShaderSource
  // generates a single #line 0 directive
  var vertexLines = positionAttribute
    .concat(
      this._attributeLines,
      this._vertexShaderParts.uniformLines,
      this._vertexShaderParts.varyingLines,
      this._vertexShaderParts.shaderLines
    )
    .join("\n");
  var vertexShaderSource = new ShaderSource({
    defines: this._vertexShaderParts.defineLines,
    sources: [vertexLines],
  });

  var fragmentLines = this._fragmentShaderParts.uniformLines
    .concat(
      this._fragmentShaderParts.varyingLines,
      this._fragmentShaderParts.shaderLines
    )
    .join("\n");
  var fragmentShaderSource = new ShaderSource({
    defines: this._fragmentShaderParts.defineLines,
    sources: [fragmentLines],
  });

  return ShaderProgram.fromCache({
    context: context,
    vertexShaderSource: vertexShaderSource,
    fragmentShaderSource: fragmentShaderSource,
    attributeLocations: this._attributeLocations,
  });
};

ShaderBuilder.prototype.clone = function () {
  return clone(this, true);
};
