import { ContextLimits } from "../../Source/Cesium.js";
import { ShaderProgram } from "../../Source/Cesium.js";
import { ShaderSource } from "../../Source/Cesium.js";
import createContext from "../createContext.js";

describe(
  "Renderer/ShaderProgram",
  function () {
    var webglStub = !!window.webglStub;
    var context;
    var sp;

    var injectedTestFunctions = {
      czm_circularDependency1:
        "void czm_circularDependency1() { czm_circularDependency2(); }",
      czm_circularDependency2:
        "void czm_circularDependency2() { czm_circularDependency1(); }",
      czm_testFunction3:
        "void czm_testFunction3(vec4 color) { czm_testFunction2(color); }",
      czm_testFunction2:
        "void czm_testFunction2(vec4 color) { czm_testFunction1(color); }",
      czm_testFunction1:
        "void czm_testFunction1(vec4 color) { gl_FragColor = color; }",
      czm_testDiamondDependency1:
        "vec4 czm_testDiamondDependency1(vec4 color) { return czm_testAddAlpha(color); }",
      czm_testDiamondDependency2:
        "vec4 czm_testDiamondDependency2(vec4 color) { return czm_testAddAlpha(color); }",
      czm_testAddAlpha:
        "vec4 czm_testAddAlpha(vec4 color) { color.a = clamp(color.a + 0.1, 0.0, 1.0); return color; }",
      czm_testAddRed:
        "vec4 czm_testAddRed(vec4 color) { color.r = clamp(color.r + 0.1, 0.0, 1.0); return color; }",
      czm_testAddGreen:
        "vec4 czm_testAddGreen(vec4 color) { color.g = clamp(color.g + 0.1, 0.0, 1.0); return color; }",
      czm_testAddRedGreenAlpha:
        "vec4 czm_testAddRedGreenAlpha(vec4 color) { color = czm_testAddRed(color); color = czm_testAddGreen(color); color = czm_testAddAlpha(color); return color; }",
      czm_testFunction4:
        "void czm_testFunction4(vec4 color) { color = czm_testAddAlpha(color); color = czm_testAddRedGreenAlpha(color); czm_testFunction3(color); }",
      czm_testFunctionWithComment:
        "/**\n czm_circularDependency1()  \n*/\nvoid czm_testFunctionWithComment(vec4 color) { czm_testFunction1(color); }",
    };

    beforeAll(function () {
      context = createContext();

      for (var functionName in injectedTestFunctions) {
        if (injectedTestFunctions.hasOwnProperty(functionName)) {
          ShaderSource._czmBuiltinsAndUniforms[functionName] =
            injectedTestFunctions[functionName];
        }
      }
    });

    afterAll(function () {
      context.destroyForSpecs();

      for (var functionName in injectedTestFunctions) {
        if (injectedTestFunctions.hasOwnProperty(functionName)) {
          delete ShaderSource._czmBuiltinsAndUniforms[functionName];
        }
      }
    });

    afterEach(function () {
      sp = sp && sp.destroy();
    });

    it("has vertex and fragment shader source", function () {
      var vs = "void main() { gl_Position = vec4(1.0); }";
      var fs = "void main() { gl_FragColor = vec4(1.0); }";
      sp = ShaderProgram.fromCache({
        context: context,
        vertexShaderSource: vs,
        fragmentShaderSource: fs,
      });

      var expectedVSText = new ShaderSource({
        sources: [vs],
      }).createCombinedVertexShader(context);

      expect(sp._vertexShaderText).toEqual(expectedVSText);

      var expectedFSText = new ShaderSource({
        sources: [fs],
      }).createCombinedFragmentShader(context);

      expect(sp._fragmentShaderText).toEqual(expectedFSText);
    });

    it("has a position vertex attribute", function () {
      var vs =
        "attribute vec4 position; void main() { gl_Position = position; }";
      var fs = "void main() { gl_FragColor = vec4(1.0); }";
      sp = ShaderProgram.fromCache({
        context: context,
        vertexShaderSource: vs,
        fragmentShaderSource: fs,
      });

      if (webglStub) {
        return; // WebGL Stub does not return vertex attribute and uniforms in the shader
      }

      expect(sp.numberOfVertexAttributes).toEqual(1);
      expect(sp.vertexAttributes.position.name).toEqual("position");
    });

    it("sets attribute indices", function () {
      var vs =
        "attribute vec4 position;" +
        "attribute vec3 normal;" +
        "attribute float heat;" +
        "void main() { gl_Position = position + vec4(normal, 0.0) + vec4(heat); }";
      var fs = "void main() { gl_FragColor = vec4(1.0); }";

      var attributes = {
        position: 3,
        normal: 2,
        heat: 1,
      };

      sp = ShaderProgram.fromCache({
        context: context,
        vertexShaderSource: vs,
        fragmentShaderSource: fs,
        attributeLocations: attributes,
      });

      if (webglStub) {
        return; // WebGL Stub does not return vertex attribute and uniforms in the shader
      }

      expect(sp.numberOfVertexAttributes).toEqual(3);
      expect(sp.vertexAttributes.position.name).toEqual("position");
      expect(sp.vertexAttributes.position.index).toEqual(attributes.position);
      expect(sp.vertexAttributes.normal.name).toEqual("normal");
      expect(sp.vertexAttributes.normal.index).toEqual(attributes.normal);
      expect(sp.vertexAttributes.heat.name).toEqual("heat");
      expect(sp.vertexAttributes.heat.index).toEqual(attributes.heat);
    });

    it("has an automatic uniform", function () {
      var vs = "uniform vec4 u_vec4; void main() { gl_Position = u_vec4; }";
      var fs =
        "void main() { gl_FragColor = vec4((czm_viewport.x == 0.0) && (czm_viewport.y == 0.0) && (czm_viewport.z == 1.0) && (czm_viewport.w == 1.0)); }";
      sp = ShaderProgram.fromCache({
        context: context,
        vertexShaderSource: vs,
        fragmentShaderSource: fs,
      });

      if (webglStub) {
        return; // WebGL Stub does not return vertex attribute and uniforms in the shader
      }

      expect(sp.allUniforms.u_vec4.name).toEqual("u_vec4");
      expect(sp.allUniforms.czm_viewport.name).toEqual("czm_viewport");
    });

    it("has uniforms of every datatype", function () {
      var d = context;
      var vs =
        "uniform float u_float;" +
        "uniform vec2 u_vec2;" +
        "uniform vec3 u_vec3;" +
        "uniform vec4 u_vec4;" +
        "uniform int u_int;" +
        "uniform ivec2 u_ivec2;" +
        "uniform ivec3 u_ivec3;" +
        "uniform ivec4 u_ivec4;" +
        "uniform bool u_bool;" +
        "uniform bvec2 u_bvec2;" +
        "uniform bvec3 u_bvec3;" +
        "uniform bvec4 u_bvec4;" +
        "uniform mat2 u_mat2;" +
        "uniform mat3 u_mat3;" +
        "uniform mat4 u_mat4;" +
        "void main() { gl_Position = vec4(u_float) * vec4((u_mat2 * u_vec2), 0.0, 0.0) * vec4((u_mat3 * u_vec3), 0.0) * (u_mat4 * u_vec4) * vec4(u_int) * vec4(u_ivec2, 0.0, 0.0) * vec4(u_ivec3, 0.0) * vec4(u_ivec4) * vec4(u_bool) * vec4(u_bvec2, 0.0, 0.0) * vec4(u_bvec3, 0.0) * vec4(u_bvec4); }";
      var fs =
        "uniform sampler2D u_sampler2D;" +
        "uniform samplerCube u_samplerCube;" +
        "void main() { gl_FragColor = texture2D(u_sampler2D, vec2(0.0)) + textureCube(u_samplerCube, vec3(1.0)); }";
      sp = ShaderProgram.fromCache({
        context: d,
        vertexShaderSource: vs,
        fragmentShaderSource: fs,
      });

      if (webglStub) {
        return; // WebGL Stub does not return vertex attribute and uniforms in the shader
      }

      expect(sp.allUniforms.u_float.name).toEqual("u_float");
      expect(sp.allUniforms.u_vec2.name).toEqual("u_vec2");
      expect(sp.allUniforms.u_vec3.name).toEqual("u_vec3");
      expect(sp.allUniforms.u_vec4.name).toEqual("u_vec4");
      expect(sp.allUniforms.u_int.name).toEqual("u_int");
      expect(sp.allUniforms.u_ivec2.name).toEqual("u_ivec2");
      expect(sp.allUniforms.u_ivec3.name).toEqual("u_ivec3");
      expect(sp.allUniforms.u_ivec4.name).toEqual("u_ivec4");
      expect(sp.allUniforms.u_bool.name).toEqual("u_bool");
      expect(sp.allUniforms.u_bvec2.name).toEqual("u_bvec2");
      expect(sp.allUniforms.u_bvec3.name).toEqual("u_bvec3");
      expect(sp.allUniforms.u_bvec4.name).toEqual("u_bvec4");
      expect(sp.allUniforms.u_mat2.name).toEqual("u_mat2");
      expect(sp.allUniforms.u_mat3.name).toEqual("u_mat3");
      expect(sp.allUniforms.u_mat4.name).toEqual("u_mat4");
      expect(sp.allUniforms.u_sampler2D.name).toEqual("u_sampler2D");
      expect(sp.allUniforms.u_samplerCube.name).toEqual("u_samplerCube");
    });

    it("has a struct uniform", function () {
      var vs =
        "uniform struct { float f; vec4 v; } u_struct; void main() { gl_Position = u_struct.f * u_struct.v; }";
      var fs = "void main() { gl_FragColor = vec4(1.0); }";
      sp = ShaderProgram.fromCache({
        context: context,
        vertexShaderSource: vs,
        fragmentShaderSource: fs,
      });

      if (webglStub) {
        return; // WebGL Stub does not return vertex attribute and uniforms in the shader
      }

      expect(sp.allUniforms["u_struct.f"].name).toEqual("u_struct.f");
      expect(sp.allUniforms["u_struct.v"].name).toEqual("u_struct.v");
    });

    it("has uniform arrays of every datatype", function () {
      var d = context;
      var vs =
        "uniform float u_float[2];" +
        "uniform vec2 u_vec2[2];" +
        "uniform vec3 u_vec3[2];" +
        "uniform vec4 u_vec4[2];" +
        "uniform int u_int[2];" +
        "uniform ivec2 u_ivec2[2];" +
        "uniform ivec3 u_ivec3[2];" +
        "uniform ivec4 u_ivec4[2];" +
        "uniform bool u_bool[2];" +
        "uniform bvec2 u_bvec2[2];" +
        "uniform bvec3 u_bvec3[2];" +
        "uniform bvec4 u_bvec4[2];" +
        "uniform mat2 u_mat2[2];" +
        "uniform mat3 u_mat3[2];" +
        "uniform mat4 u_mat4[2];" +
        "void main() { gl_Position = vec4(u_float[0]) * vec4(u_float[1]) * vec4((u_mat2[0] * u_vec2[0]), 0.0, 0.0) * vec4((u_mat2[1] * u_vec2[1]), 0.0, 0.0) * vec4((u_mat3[0] * u_vec3[0]), 0.0) * vec4((u_mat3[1] * u_vec3[1]), 0.0) * (u_mat4[0] * u_vec4[0]) * (u_mat4[1] * u_vec4[1]) * vec4(u_int[0]) * vec4(u_int[1]) * vec4(u_ivec2[0], 0.0, 0.0) * vec4(u_ivec2[1], 0.0, 0.0) * vec4(u_ivec3[0], 0.0) * vec4(u_ivec3[1], 0.0) * vec4(u_ivec4[0]) * vec4(u_ivec4[1]) * vec4(u_bool[0]) * vec4(u_bool[1]) * vec4(u_bvec2[0], 0.0, 0.0) * vec4(u_bvec2[1], 0.0, 0.0) * vec4(u_bvec3[0], 0.0) * vec4(u_bvec3[1], 0.0) * vec4(u_bvec4[0]) * vec4(u_bvec4[1]); }";
      var fs =
        "uniform sampler2D u_sampler2D[2];" +
        "uniform samplerCube u_samplerCube[2];" +
        "void main() { gl_FragColor = texture2D(u_sampler2D[0], vec2(0.0)) + texture2D(u_sampler2D[1], vec2(0.0)) + textureCube(u_samplerCube[0], vec3(1.0)) + textureCube(u_samplerCube[1], vec3(1.0)); }";
      sp = ShaderProgram.fromCache({
        context: d,
        vertexShaderSource: vs,
        fragmentShaderSource: fs,
      });

      if (webglStub) {
        return; // WebGL Stub does not return vertex attribute and uniforms in the shader
      }

      expect(sp.allUniforms.u_float.name).toEqual("u_float");
      expect(sp.allUniforms.u_vec2.name).toEqual("u_vec2");
      expect(sp.allUniforms.u_vec3.name).toEqual("u_vec3");
      expect(sp.allUniforms.u_vec4.name).toEqual("u_vec4");
      expect(sp.allUniforms.u_int.name).toEqual("u_int");
      expect(sp.allUniforms.u_ivec2.name).toEqual("u_ivec2");
      expect(sp.allUniforms.u_ivec3.name).toEqual("u_ivec3");
      expect(sp.allUniforms.u_ivec4.name).toEqual("u_ivec4");
      expect(sp.allUniforms.u_bool.name).toEqual("u_bool");
      expect(sp.allUniforms.u_bvec2.name).toEqual("u_bvec2");
      expect(sp.allUniforms.u_bvec3.name).toEqual("u_bvec3");
      expect(sp.allUniforms.u_bvec4.name).toEqual("u_bvec4");
      expect(sp.allUniforms.u_mat2.name).toEqual("u_mat2");
      expect(sp.allUniforms.u_mat3.name).toEqual("u_mat3");
      expect(sp.allUniforms.u_mat4.name).toEqual("u_mat4");
      expect(sp.allUniforms.u_sampler2D.name).toEqual("u_sampler2D");
      expect(sp.allUniforms.u_samplerCube.name).toEqual("u_samplerCube");

      expect(sp.allUniforms.u_float.value.length).toEqual(2);
      expect(sp.allUniforms.u_vec2.value.length).toEqual(2);
      expect(sp.allUniforms.u_vec3.value.length).toEqual(2);
      expect(sp.allUniforms.u_vec4.value.length).toEqual(2);
      expect(sp.allUniforms.u_int.value.length).toEqual(2);
      expect(sp.allUniforms.u_ivec2.value.length).toEqual(2);
      expect(sp.allUniforms.u_ivec3.value.length).toEqual(2);
      expect(sp.allUniforms.u_ivec4.value.length).toEqual(2);
      expect(sp.allUniforms.u_bool.value.length).toEqual(2);
      expect(sp.allUniforms.u_bvec2.value.length).toEqual(2);
      expect(sp.allUniforms.u_bvec3.value.length).toEqual(2);
      expect(sp.allUniforms.u_bvec4.value.length).toEqual(2);
      expect(sp.allUniforms.u_mat2.value.length).toEqual(2);
      expect(sp.allUniforms.u_mat3.value.length).toEqual(2);
      expect(sp.allUniforms.u_mat4.value.length).toEqual(2);
      expect(sp.allUniforms.u_sampler2D.value.length).toEqual(2);
      expect(sp.allUniforms.u_samplerCube.value.length).toEqual(2);
    });

    it("has predefined constants", function () {
      var fs =
        "void main() { " +
        "  float f = ((czm_pi > 0.0) && \n" +
        "    (czm_oneOverPi > 0.0) && \n" +
        "    (czm_piOverTwo > 0.0) && \n" +
        "    (czm_piOverThree > 0.0) && \n" +
        "    (czm_piOverFour > 0.0) && \n" +
        "    (czm_piOverSix > 0.0) && \n" +
        "    (czm_threePiOver2 > 0.0) && \n" +
        "    (czm_twoPi > 0.0) && \n" +
        "    (czm_oneOverTwoPi > 0.0) && \n" +
        "    (czm_radiansPerDegree > 0.0) && \n" +
        "    (czm_degreesPerRadian > 0.0)) ? 1.0 : 0.0; \n" +
        "  gl_FragColor = vec4(f); \n" +
        "}";

      expect({
        context: context,
        fragmentShader: fs,
      }).contextToRender();
    });

    it("has built-in constant, structs, and functions", function () {
      var fs =
        "void main() { \n" +
        "  czm_materialInput materialInput; \n" +
        "  czm_material material = czm_getDefaultMaterial(materialInput); \n" +
        "  material.diffuse = vec3(1.0, 1.0, 1.0); \n" +
        "  material.alpha = 1.0; \n" +
        "  material.diffuse = czm_hue(material.diffuse, czm_twoPi); \n" +
        "  gl_FragColor = vec4(material.diffuse, material.alpha); \n" +
        "}";

      expect({
        context: context,
        fragmentShader: fs,
      }).contextToRender();
    });

    it("creates duplicate uniforms if precision of uniforms in vertex and fragment shader do not match", function () {
      var highpFloatSupported = ContextLimits.highpFloatSupported;
      ContextLimits._highpFloatSupported = false;
      var vs =
        "attribute vec4 position; uniform float u_value; varying float v_value; void main() { gl_PointSize = 1.0; v_value = u_value * czm_viewport.z; gl_Position = position; }";
      var fs =
        "uniform float u_value; varying float v_value; void main() { gl_FragColor = vec4(u_value * v_value * czm_viewport.z); }";
      var uniformMap = {
        u_value: function () {
          return 1.0;
        },
      };

      sp = ShaderProgram.fromCache({
        context: context,
        vertexShaderSource: vs,
        fragmentShaderSource: fs,
      });

      if (!webglStub) {
        // WebGL Stub does not return vertex attribute and uniforms in the shader
        expect(sp.allUniforms.u_value).toBeDefined();
        expect(sp.allUniforms.czm_mediump_u_value).toBeDefined();
      }

      expect({
        context: context,
        vertexShader: vs,
        fragmentShader: fs,
        uniformMap: uniformMap,
      }).notContextToRender([0, 0, 0, 0]);

      ContextLimits._highpFloatSupported = highpFloatSupported;
    });

    it("1 level function dependency", function () {
      var fs = "void main() { \n" + "  czm_testFunction1(vec4(1.0)); \n" + "}";

      expect({
        context: context,
        fragmentShader: fs,
      }).contextToRender();
    });

    it("2 level function dependency", function () {
      var fs = "void main() { \n" + "  czm_testFunction2(vec4(1.0)); \n" + "}";

      expect({
        context: context,
        fragmentShader: fs,
      }).contextToRender();
    });

    it("3 level function dependency", function () {
      var fs = "void main() { \n" + "  czm_testFunction3(vec4(1.0)); \n" + "}";

      expect({
        context: context,
        fragmentShader: fs,
      }).contextToRender();
    });

    it("diamond dependency", function () {
      var fs =
        "void main() { \n" +
        "  vec4 color = vec4(1.0, 1.0, 1.0, 0.8); \n" +
        "  color = czm_testDiamondDependency1(color); \n" +
        "  color = czm_testDiamondDependency2(color); \n" +
        "  gl_FragColor = color; \n" +
        "}";

      expect({
        context: context,
        fragmentShader: fs,
      }).contextToRender();
    });

    it("diamond plus 3 level function dependency", function () {
      var fs =
        "void main() { \n" +
        "  vec4 color = vec4(1.0, 1.0, 1.0, 0.8); \n" +
        "  color = czm_testDiamondDependency1(color); \n" +
        "  color = czm_testDiamondDependency2(color); \n" +
        "  czm_testFunction3(color); \n" +
        "}";

      expect({
        context: context,
        fragmentShader: fs,
      }).contextToRender();
    });

    it("big mess of function dependencies", function () {
      var fs =
        "void main() { \n" +
        "  vec4 color = vec4(0.9, 0.9, 1.0, 0.6); \n" +
        "  color = czm_testDiamondDependency1(color); \n" +
        "  color = czm_testDiamondDependency2(color); \n" +
        "  czm_testFunction4(color); \n" +
        "}";

      expect({
        context: context,
        fragmentShader: fs,
      }).contextToRender();
    });

    it("doc comment with reference to another function", function () {
      var fs =
        "void main() { \n" +
        "  vec4 color = vec4(1.0, 1.0, 1.0, 1.0); \n" +
        "  czm_testFunctionWithComment(color); \n" +
        "}";

      expect({
        context: context,
        fragmentShader: fs,
      }).contextToRender();
    });

    it("compiles with #version at the top", function () {
      var vs =
        "#version 100 \n" +
        "attribute vec4 position; void main() { gl_Position = position; }";
      var fs = "#version 100 \n" + "void main() { gl_FragColor = vec4(1.0); }";
      sp = ShaderProgram.fromCache({
        context: context,
        vertexShaderSource: vs,
        fragmentShaderSource: fs,
      });
    });

    it("compiles with #version after whitespace and comments", function () {
      var vs =
        "// comment before version directive. \n" +
        "#version 100 \n" +
        "attribute vec4 position; void main() { gl_Position = position; }";
      var fs =
        "\n" + "#version 100 \n" + "void main() { gl_FragColor = vec4(1.0); }";
      sp = ShaderProgram.fromCache({
        context: context,
        vertexShaderSource: vs,
        fragmentShaderSource: fs,
      });
    });

    it("fails vertex shader compile", function () {
      if (webglStub) {
        return; // WebGL Stub does not actually try to compile the shader
      }

      var vs = "does not compile.";
      var fs = "void main() { gl_FragColor = vec4(1.0); }";
      sp = ShaderProgram.fromCache({
        context: context,
        vertexShaderSource: vs,
        fragmentShaderSource: fs,
      });

      expect(function () {
        sp._bind();
      }).toThrowRuntimeError();
    });

    it("fails fragment shader compile", function () {
      if (webglStub) {
        return; // WebGL Stub does not actually try to compile the shader
      }

      var vs = "void main() { gl_Position = vec4(0.0); }";
      var fs = "does not compile.";
      sp = ShaderProgram.fromCache({
        context: context,
        vertexShaderSource: vs,
        fragmentShaderSource: fs,
      });

      expect(function () {
        sp._bind();
      }).toThrowRuntimeError();
    });

    it("fails to link", function () {
      if (webglStub) {
        return; // WebGL Stub does not actually try to compile and link the shader
      }

      var vs = "void nomain() { }";
      var fs = "void nomain() { }";
      sp = ShaderProgram.fromCache({
        context: context,
        vertexShaderSource: vs,
        fragmentShaderSource: fs,
      });

      expect(function () {
        sp._bind();
      }).toThrowRuntimeError();
    });

    it("fails with built-in function circular dependency", function () {
      var vs = "void main() { gl_Position = vec4(0.0); }";
      var fs =
        "void main() { czm_circularDependency1(); gl_FragColor = vec4(1.0); }";
      expect(function () {
        sp = ShaderProgram.fromCache({
          context: context,
          vertexShaderSource: vs,
          fragmentShaderSource: fs,
        });
        sp._bind();
      }).toThrowDeveloperError();
    });
  },
  "WebGL"
);
