import { ComponentDatatype } from "../../Source/Cesium.js";
import { PrimitiveType } from "../../Source/Cesium.js";
import { Buffer } from "../../Source/Cesium.js";
import { BufferUsage } from "../../Source/Cesium.js";
import { DrawCommand } from "../../Source/Cesium.js";
import { ShaderProgram } from "../../Source/Cesium.js";
import { VertexArray } from "../../Source/Cesium.js";
import createContext from "../createContext.js";

describe(
  "Renderer/VertexArray",
  function () {
    var context;

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

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

    it("binds", function () {
      var positionBuffer = Buffer.createVertexBuffer({
        context: context,
        sizeInBytes: 3,
        usage: BufferUsage.STATIC_DRAW,
      });

      var attributes = [
        {
          index: 0,
          enabled: true,
          vertexBuffer: positionBuffer,
          componentsPerAttribute: 3,
          componentDatatype: ComponentDatatype.FLOAT,
          normalize: false,
          offsetInBytes: 0,
          strideInBytes: 0,
          instanceDivisor: 0,
          // tightly packed
        },
      ];

      var va = new VertexArray({
        context: context,
        attributes: attributes,
      });
      va._bind();
      va._unBind();
      va = va.destroy();
    });

    it("binds with default values", function () {
      var positionBuffer = Buffer.createVertexBuffer({
        context: context,
        sizeInBytes: 3,
        usage: BufferUsage.STATIC_DRAW,
      });

      var attributes = [
        {
          vertexBuffer: positionBuffer,
          componentsPerAttribute: 3,
        },
      ];

      var va = new VertexArray({
        context: context,
        attributes: attributes,
      });

      expect(va.numberOfAttributes).toEqual(1);
      expect(va.getAttribute(0).index).toEqual(0);
      expect(va.getAttribute(0).enabled).toEqual(true);
      expect(va.getAttribute(0).vertexBuffer).toEqual(positionBuffer);
      expect(va.getAttribute(0).componentsPerAttribute).toEqual(3);
      expect(va.getAttribute(0).componentDatatype).toEqual(
        ComponentDatatype.FLOAT
      );
      expect(va.getAttribute(0).normalize).toEqual(false);
      expect(va.getAttribute(0).offsetInBytes).toEqual(0);
      expect(va.getAttribute(0).strideInBytes).toEqual(0);
      expect(va.getAttribute(0).instanceDivisor).toEqual(0);

      va._bind();
      va._unBind();
      va = va.destroy();
    });

    it("binds with multiple buffers", function () {
      var attributeSize = 3 * Float32Array.BYTES_PER_ELEMENT;
      var positionBuffer = Buffer.createVertexBuffer({
        context: context,
        sizeInBytes: attributeSize,
        usage: BufferUsage.STATIC_DRAW,
      });
      var normalBuffer = Buffer.createVertexBuffer({
        context: context,
        sizeInBytes: attributeSize,
        usage: BufferUsage.STATIC_DRAW,
      });

      var attributes = [
        {
          index: 0,
          vertexBuffer: positionBuffer,
          componentsPerAttribute: 3,
          componentDatatype: ComponentDatatype.FLOAT,
        },
        {
          index: 1,
          vertexBuffer: normalBuffer,
          componentsPerAttribute: 3,
          componentDatatype: ComponentDatatype.FLOAT,
        },
      ];

      var va = new VertexArray({
        context: context,
        attributes: attributes,
      });

      expect(va.numberOfAttributes).toEqual(2);
      va._bind();
      va._unBind();
      va = va.destroy();
    });

    it("binds with interleaved buffer", function () {
      var attributeSize = 3 * Float32Array.BYTES_PER_ELEMENT;
      var buffer = Buffer.createVertexBuffer({
        context: context,
        sizeInBytes: attributeSize,
        usage: BufferUsage.STATIC_DRAW,
      });

      var attributes = [
        {
          vertexBuffer: buffer,
          componentsPerAttribute: 3,
          componentDatatype: ComponentDatatype.FLOAT,
          offsetInBytes: 0,
          strideInBytes: 2 * attributeSize,
        },
        {
          vertexBuffer: buffer,
          componentsPerAttribute: 3,
          componentDatatype: ComponentDatatype.FLOAT,
          normalize: true,
          offsetInBytes: attributeSize,
          strideInBytes: 2 * attributeSize,
        },
      ];

      var va = new VertexArray({
        context: context,
        attributes: attributes,
      });

      expect(va.numberOfAttributes).toEqual(2);
      va._bind();
      va._unBind();
      va = va.destroy();
    });

    it("adds attributes", function () {
      var positionBuffer = Buffer.createVertexBuffer({
        context: context,
        sizeInBytes: 3,
        usage: BufferUsage.STATIC_DRAW,
      });

      var va = new VertexArray({
        context: context,
        attributes: [
          {
            vertexBuffer: positionBuffer,
            componentsPerAttribute: 3,
          },
        ],
      });

      expect(va.numberOfAttributes).toEqual(1);
      expect(va.getAttribute(0).index).toEqual(0);
      expect(va.getAttribute(0).enabled).toEqual(true);
      expect(va.getAttribute(0).vertexBuffer).toEqual(positionBuffer);
      expect(va.getAttribute(0).componentsPerAttribute).toEqual(3);
      expect(va.getAttribute(0).componentDatatype).toEqual(
        ComponentDatatype.FLOAT
      );
      expect(va.getAttribute(0).normalize).toEqual(false);
      expect(va.getAttribute(0).offsetInBytes).toEqual(0);
      expect(va.getAttribute(0).strideInBytes).toEqual(0);
      expect(va.getAttribute(0).instanceDivisor).toEqual(0);

      va = va.destroy();
    });

    it("modifies attributes", function () {
      var buffer = Buffer.createVertexBuffer({
        context: context,
        sizeInBytes: 6,
        usage: BufferUsage.STATIC_DRAW,
      });

      var attributes = [
        {
          vertexBuffer: buffer,
          componentsPerAttribute: 3,
        },
      ];

      var va = new VertexArray({
        context: context,
        attributes: attributes,
      });

      expect(va.numberOfAttributes).toEqual(1);
      expect(va.getAttribute(0).enabled).toEqual(true);

      va.getAttribute(0).enabled = false;
      expect(va.getAttribute(0).enabled).toEqual(false);

      va._bind();
      va._unBind();
      va = va.destroy();
    });

    // The following specs test draw calls that pull from a constant attribute.
    // Due to what I believe is a range checking bug in Firefox (Section 6.4 of
    // the WebGL spec), an attribute backed by a buffer must also be bound,
    // otherwise drawArrays unjustly reports an INVALID_OPERATION, hence the
    // firefoxWorkaround attribute below.  In practice, we will always have
    // an attribute backed by a buffer anyway.

    it("renders with a one-component constant value", function () {
      var vs =
        "attribute float firefoxWorkaround;" +
        "attribute float attr;" +
        "varying vec4 v_color;" +
        "void main() { " +
        "  v_color = vec4(attr == 0.5) + vec4(firefoxWorkaround);" +
        "  gl_PointSize = 1.0;" +
        "  gl_Position = vec4(0.0, 0.0, 0.0, 1.0);" +
        "}";
      var fs =
        "varying vec4 v_color;" + "void main() { gl_FragColor = v_color; }";
      var sp = ShaderProgram.fromCache({
        context: context,
        vertexShaderSource: vs,
        fragmentShaderSource: fs,
        attributeLocations: {
          firefoxWorkaround: 0,
          attr: 1,
        },
      });

      var va = new VertexArray({
        context: context,
        attributes: [
          {
            vertexBuffer: Buffer.createVertexBuffer({
              context: context,
              sizeInBytes: Float32Array.BYTES_PER_ELEMENT,
              usage: BufferUsage.STATIC_DRAW,
            }),
            componentsPerAttribute: 1,
          },
          {
            value: [0.5],
          },
        ],
      });

      var command = new DrawCommand({
        primitiveType: PrimitiveType.POINTS,
        shaderProgram: sp,
        vertexArray: va,
        count: 1,
      });
      command.execute(context);

      expect(context).toReadPixels([255, 255, 255, 255]);

      sp = sp.destroy();
      va = va.destroy();
    });

    it("renders with a two-component constant value", function () {
      var vs =
        "attribute float firefoxWorkaround;" +
        "attribute vec2 attr;" +
        "varying vec4 v_color;" +
        "void main() { " +
        "  v_color = vec4(attr == vec2(0.25, 0.75)) + vec4(firefoxWorkaround);" +
        "  gl_PointSize = 1.0;" +
        "  gl_Position = vec4(0.0, 0.0, 0.0, 1.0);" +
        "}";
      var fs =
        "varying vec4 v_color;" + "void main() { gl_FragColor = v_color; }";
      var sp = ShaderProgram.fromCache({
        context: context,
        vertexShaderSource: vs,
        fragmentShaderSource: fs,
        attributeLocations: {
          firefoxWorkaround: 0,
          attr: 1,
        },
      });

      var va = new VertexArray({
        context: context,
        attributes: [
          {
            vertexBuffer: Buffer.createVertexBuffer({
              context: context,
              sizeInBytes: Float32Array.BYTES_PER_ELEMENT,
              usage: BufferUsage.STATIC_DRAW,
            }),
            componentsPerAttribute: 1,
          },
          {
            value: [0.25, 0.75],
          },
        ],
      });

      var command = new DrawCommand({
        primitiveType: PrimitiveType.POINTS,
        shaderProgram: sp,
        vertexArray: va,
        count: 1,
      });
      command.execute(context);

      expect(context).toReadPixels([255, 255, 255, 255]);

      sp = sp.destroy();
      va = va.destroy();
    });

    it("renders with a three-component constant value", function () {
      var vs =
        "attribute float firefoxWorkaround;" +
        "attribute vec3 attr;" +
        "varying vec4 v_color;" +
        "void main() { " +
        "  v_color = vec4(attr == vec3(0.25, 0.5, 0.75)) + vec4(firefoxWorkaround);" +
        "  gl_PointSize = 1.0;" +
        "  gl_Position = vec4(0.0, 0.0, 0.0, 1.0);" +
        "}";
      var fs =
        "varying vec4 v_color;" + "void main() { gl_FragColor = v_color; }";
      var sp = ShaderProgram.fromCache({
        context: context,
        vertexShaderSource: vs,
        fragmentShaderSource: fs,
        attributeLocations: {
          firefoxWorkaround: 0,
          attr: 1,
        },
      });

      var va = new VertexArray({
        context: context,
        attributes: [
          {
            vertexBuffer: Buffer.createVertexBuffer({
              context: context,
              sizeInBytes: Float32Array.BYTES_PER_ELEMENT,
              usage: BufferUsage.STATIC_DRAW,
            }),
            componentsPerAttribute: 1,
          },
          {
            value: [0.25, 0.5, 0.75],
          },
        ],
      });

      var command = new DrawCommand({
        primitiveType: PrimitiveType.POINTS,
        shaderProgram: sp,
        vertexArray: va,
        count: 1,
      });
      command.execute(context);

      expect(context).toReadPixels([255, 255, 255, 255]);

      sp = sp.destroy();
      va = va.destroy();
    });

    it("renders with a four-component constant value", function () {
      var vs =
        "attribute float firefoxWorkaround;" +
        "attribute vec4 attr;" +
        "varying vec4 v_color;" +
        "void main() { " +
        "  v_color = vec4(attr == vec4(0.2, 0.4, 0.6, 0.8)) + vec4(firefoxWorkaround);" +
        "  gl_PointSize = 1.0;" +
        "  gl_Position = vec4(0.0, 0.0, 0.0, 1.0);" +
        "}";
      var fs =
        "varying vec4 v_color;" + "void main() { gl_FragColor = v_color; }";
      var sp = ShaderProgram.fromCache({
        context: context,
        vertexShaderSource: vs,
        fragmentShaderSource: fs,
        attributeLocations: {
          firefoxWorkaround: 0,
          attr: 1,
        },
      });

      var va = new VertexArray({
        context: context,
        attributes: [
          {
            vertexBuffer: Buffer.createVertexBuffer({
              context: context,
              sizeInBytes: Float32Array.BYTES_PER_ELEMENT,
              usage: BufferUsage.STATIC_DRAW,
            }),
            componentsPerAttribute: 1,
          },
          {
            value: [0.2, 0.4, 0.6, 0.8],
          },
        ],
      });

      var command = new DrawCommand({
        primitiveType: PrimitiveType.POINTS,
        shaderProgram: sp,
        vertexArray: va,
        count: 1,
      });
      command.execute(context);

      expect(context).toReadPixels([255, 255, 255, 255]);

      sp = sp.destroy();
      va = va.destroy();
    });

    it("renders two vertex arrays with constant values", function () {
      var vs =
        "attribute float firefoxWorkaround;" +
        "attribute vec4 attr;" +
        "varying vec4 v_color;" +
        "void main() { " +
        "  v_color = attr + vec4(firefoxWorkaround);" +
        "  gl_PointSize = 1.0;" +
        "  gl_Position = vec4(0.0, 0.0, 0.0, 1.0);" +
        "}";

      var fs =
        "varying vec4 v_color;" + "void main() { gl_FragColor = v_color; }";

      var sp = ShaderProgram.fromCache({
        context: context,
        vertexShaderSource: vs,
        fragmentShaderSource: fs,
        attributeLocations: {
          firefoxWorkaround: 0,
          attr: 1,
        },
      });

      var vertexBuffer = Buffer.createVertexBuffer({
        context: context,
        sizeInBytes: Float32Array.BYTES_PER_ELEMENT,
        usage: BufferUsage.STATIC_DRAW,
      });

      var vaRed = new VertexArray({
        context: context,
        attributes: [
          {
            vertexBuffer: vertexBuffer,
            componentsPerAttribute: 1,
          },
          {
            value: [1, 0, 0, 1],
          },
        ],
      });

      var vaGreen = new VertexArray({
        context: context,
        attributes: [
          {
            vertexBuffer: vertexBuffer,
            componentsPerAttribute: 1,
          },
          {
            value: [0, 1, 0, 1],
          },
        ],
      });

      var commandRed = new DrawCommand({
        primitiveType: PrimitiveType.POINTS,
        shaderProgram: sp,
        vertexArray: vaRed,
        count: 1,
      });

      var commandGreen = new DrawCommand({
        primitiveType: PrimitiveType.POINTS,
        shaderProgram: sp,
        vertexArray: vaGreen,
        count: 1,
      });

      commandRed.execute(context);
      expect(context).toReadPixels([255, 0, 0, 255]);

      commandGreen.execute(context);
      expect(context).toReadPixels([0, 255, 0, 255]);

      sp = sp.destroy();
      vaRed = vaRed.destroy();
      vaGreen = vaGreen.destroy();
    });

    it("destroys", function () {
      var va = new VertexArray({
        context: context,
        attributes: [
          {
            vertexBuffer: Buffer.createVertexBuffer({
              context: context,
              sizeInBytes: new Float32Array([0, 0, 0, 1]).byteLength,
              usage: BufferUsage.STATIC_DRAW,
            }),
            componentsPerAttribute: 4,
          },
        ],
      });

      expect(va.isDestroyed()).toEqual(false);
      va.destroy();
      expect(va.isDestroyed()).toEqual(true);
    });

    it("fails to create (missing vertexBuffer and value)", function () {
      var attributes = [
        {
          componentsPerAttribute: 3,
        },
      ];

      expect(function () {
        return new VertexArray({
          context: context,
          attributes: attributes,
        });
      }).toThrowDeveloperError();
    });

    it("fails to create (provides both vertexBuffer and value)", function () {
      var buffer = Buffer.createVertexBuffer({
        context: context,
        sizeInBytes: 3,
        usage: BufferUsage.STATIC_DRAW,
      });

      var attributes = [
        {
          vertexBuffer: buffer,
          value: [1, 2, 3],
          componentsPerAttribute: 3,
        },
      ];

      expect(function () {
        return new VertexArray({
          context: context,
          attributes: attributes,
        });
      }).toThrowDeveloperError();
    });

    it("fails to create with duplicate indices", function () {
      var buffer = Buffer.createVertexBuffer({
        context: context,
        sizeInBytes: 1,
        usage: BufferUsage.STATIC_DRAW,
      });

      var attributes = [
        {
          index: 1,
          vertexBuffer: buffer,
          componentsPerAttribute: 1,
        },
        {
          index: 1,
          vertexBuffer: buffer,
          componentsPerAttribute: 1,
        },
      ];

      expect(function () {
        return new VertexArray({
          context: context,
          attributes: attributes,
        });
      }).toThrowDeveloperError();
    });

    it("fails to create (componentsPerAttribute missing)", function () {
      var buffer = Buffer.createVertexBuffer({
        context: context,
        sizeInBytes: 3,
        usage: BufferUsage.STATIC_DRAW,
      });

      var attributes = [
        {
          vertexBuffer: buffer,
        },
      ];

      expect(function () {
        return new VertexArray({
          context: context,
          attributes: attributes,
        });
      }).toThrowDeveloperError();
    });

    it("fails to create (componentsPerAttribute < 1)", function () {
      var attributes = [
        {
          componentsPerAttribute: 0,
        },
      ];

      expect(function () {
        return new VertexArray({
          context: context,
          attributes: attributes,
        });
      }).toThrowDeveloperError();
    });

    it("fails to create (componentsPerAttribute > 4)", function () {
      var attributes = [
        {
          componentsPerAttribute: 5,
        },
      ];

      expect(function () {
        return new VertexArray({
          context: context,
          attributes: attributes,
        });
      }).toThrowDeveloperError();
    });

    it("fails to create (value.length < 1)", function () {
      var attributes = [
        {
          value: [],
        },
      ];

      expect(function () {
        return new VertexArray({
          context: context,
          attributes: attributes,
        });
      }).toThrowDeveloperError();
    });

    it("fails to create (value.length > 4)", function () {
      var attributes = [
        {
          value: [1.0, 2.0, 3.0, 4.0, 5.0],
        },
      ];

      expect(function () {
        return new VertexArray({
          context: context,
          attributes: attributes,
        });
      }).toThrowDeveloperError();
    });

    it("fails to create (componentDatatype)", function () {
      var buffer = Buffer.createVertexBuffer({
        context: context,
        sizeInBytes: 3,
        usage: BufferUsage.STATIC_DRAW,
      });

      var attributes = [
        {
          vertexBuffer: buffer,
          componentsPerAttribute: 3,
          componentDatatype: "invalid component datatype",
        },
      ];

      expect(function () {
        return new VertexArray({
          context: context,
          attributes: attributes,
        });
      }).toThrowDeveloperError();
    });

    it("fails to create (strideInBytes)", function () {
      var buffer = Buffer.createVertexBuffer({
        context: context,
        sizeInBytes: 3,
        usage: BufferUsage.STATIC_DRAW,
      });

      var attributes = [
        {
          vertexBuffer: buffer,
          componentsPerAttribute: 3,
          strideInBytes: 256,
        },
      ];

      expect(function () {
        return new VertexArray({
          context: context,
          attributes: attributes,
        });
      }).toThrowDeveloperError();
    });

    it("fails to get attribute", function () {
      var attributes = [
        {
          value: [0.0, 0.0, 0.0],
          componentsPerAttribute: 3,
        },
      ];

      var va = new VertexArray({
        context: context,
        attributes: attributes,
      });

      expect(function () {
        return va.getAttribute();
      }).toThrowDeveloperError();
    });

    it("fails to destroy", function () {
      var attributes = [
        {
          value: [0.0, 0.0, 0.0],
          componentsPerAttribute: 3,
        },
      ];

      var va = new VertexArray({
        context: context,
        attributes: attributes,
      });

      va.destroy();

      expect(function () {
        va.destroy();
      }).toThrowDeveloperError();
    });

    it("throws when there is no context", function () {
      expect(function () {
        return new VertexArray();
      }).toThrowDeveloperError();
    });

    it("throws if instanceDivisor is less than zero", function () {
      var buffer = Buffer.createVertexBuffer({
        context: context,
        sizeInBytes: 3,
        usage: BufferUsage.STATIC_DRAW,
      });

      var attributes = [
        {
          vertexBuffer: buffer,
          componentsPerAttribute: 3,
          instanceDivisor: -1,
        },
      ];

      expect(function () {
        return new VertexArray({
          context: context,
          attributes: attributes,
        });
      }).toThrowDeveloperError();
    });

    // Direct3D 9 requires vertex attribute zero to not be instanced. While ANGLE can work around this, it is best
    // to follow this convention. This test also guarantees that not all vertex attributes are instanced.
    it("throws if vertex attribute zero is instanced", function () {
      var buffer = Buffer.createVertexBuffer({
        context: context,
        sizeInBytes: 3,
        usage: BufferUsage.STATIC_DRAW,
      });

      var attributes = [
        {
          index: 0,
          vertexBuffer: buffer,
          componentsPerAttribute: 3,
          instanceDivisor: 1,
        },
        {
          index: 1,
          vertexBuffer: buffer,
          componentsPerAttribute: 3,
        },
      ];

      expect(function () {
        return new VertexArray({
          context: context,
          attributes: attributes,
        });
      }).toThrowDeveloperError();
    });

    it("throws if an attribute has an instanceDivisor and is not backed by a buffer", function () {
      var buffer = Buffer.createVertexBuffer({
        context: context,
        sizeInBytes: 3,
        usage: BufferUsage.STATIC_DRAW,
      });

      var attributes = [
        {
          index: 0,
          vertexBuffer: buffer,
          componentsPerAttribute: 3,
        },
        {
          index: 1,
          value: [0.0, 0.0, 1.0],
          componentsPerAttribute: 3,
          instanceDivisor: 1,
        },
      ];

      expect(function () {
        return new VertexArray({
          context: context,
          attributes: attributes,
        });
      }).toThrowDeveloperError();
    });

    it("throws when instanceDivisor is greater than zero and the instanced arrays extension is not supported.", function () {
      if (!context.instancedArrays) {
        var buffer = Buffer.createVertexBuffer({
          context: context,
          sizeInBytes: 3,
          usage: BufferUsage.STATIC_DRAW,
        });

        var attributes = [
          {
            index: 0,
            vertexBuffer: buffer,
            componentsPerAttribute: 3,
          },
          {
            index: 1,
            vertexBuffer: buffer,
            componentsPerAttribute: 3,
            instanceDivisor: 1,
          },
        ];

        expect(function () {
          return new VertexArray({
            context: context,
            attributes: attributes,
          });
        }).toThrowDeveloperError();
      }
    });
  },
  "WebGL"
);
