import { ComponentDatatype } from "../../Source/Cesium.js";
import { Geometry } from "../../Source/Cesium.js";
import { GeometryAttribute } from "../../Source/Cesium.js";
import { GeometryPipeline } from "../../Source/Cesium.js";
import { IndexDatatype } from "../../Source/Cesium.js";
import { PrimitiveType } from "../../Source/Cesium.js";
import { BufferUsage } from "../../Source/Cesium.js";
import { ClearCommand } 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/VertexArrayFactory",
  function () {
    var context;
    var va;
    var sp;

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

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

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

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

    it("creates with no optional arguments", function () {
      va = VertexArray.fromGeometry({
        context: context,
      });
      expect(va.numberOfAttributes).toEqual(0);
      expect(va.indexBuffer).not.toBeDefined();
    });

    it("creates with no geometry", function () {
      va = VertexArray.fromGeometry({
        context: context,
        interleave: true,
      });
      expect(va.numberOfAttributes).toEqual(0);
      expect(va.indexBuffer).not.toBeDefined();
    });

    it("creates a single-attribute vertex (non-interleaved)", function () {
      var geometry = new Geometry({
        attributes: {
          position: new GeometryAttribute({
            componentDatatype: ComponentDatatype.FLOAT,
            componentsPerAttribute: 3,
            values: [0.0, 0.0, 0.0, 1.0, 1.0, 1.0],
          }),
        },
        primitiveType: PrimitiveType.POINTS,
      });

      var va = VertexArray.fromGeometry({
        context: context,
        geometry: geometry,
        attributeLocations: GeometryPipeline.createAttributeLocations(geometry),
      });

      expect(va.numberOfAttributes).toEqual(1);
      expect(va.indexBuffer).not.toBeDefined();

      var position = geometry.attributes.position;
      expect(va.getAttribute(0).index).toEqual(0);
      expect(va.getAttribute(0).componentDatatype).toEqual(
        position.componentDatatype
      );
      expect(va.getAttribute(0).componentsPerAttribute).toEqual(
        position.componentsPerAttribute
      );
      expect(va.getAttribute(0).offsetInBytes).toEqual(0);
      expect(va.getAttribute(0).strideInBytes).toEqual(0); // Tightly packed

      expect(va.getAttribute(0).vertexBuffer.usage).toEqual(
        BufferUsage.DYNAMIC_DRAW
      ); // Default
    });

    it("creates a single-attribute vertex (interleaved)", function () {
      var geometry = new Geometry({
        attributes: {
          position: new GeometryAttribute({
            componentDatatype: ComponentDatatype.FLOAT,
            componentsPerAttribute: 3,
            values: [0.0, 0.0, 0.0, 1.0, 1.0, 1.0],
          }),
        },
        primitiveType: PrimitiveType.POINTS,
      });

      var va = VertexArray.fromGeometry({
        context: context,
        geometry: geometry,
        attributeLocations: GeometryPipeline.createAttributeLocations(geometry),
        interleave: true,
        bufferUsage: BufferUsage.STATIC_DRAW,
      });

      expect(va.numberOfAttributes).toEqual(1);
      expect(va.indexBuffer).not.toBeDefined();

      var position = geometry.attributes.position;
      expect(va.getAttribute(0).index).toEqual(0);
      expect(va.getAttribute(0).componentDatatype).toEqual(
        position.componentDatatype
      );
      expect(va.getAttribute(0).componentsPerAttribute).toEqual(
        position.componentsPerAttribute
      );
      expect(va.getAttribute(0).offsetInBytes).toEqual(0);
      expect(va.getAttribute(0).strideInBytes).toEqual(
        ComponentDatatype.getSizeInBytes(position.componentDatatype) *
          position.componentsPerAttribute
      );

      expect(va.getAttribute(0).vertexBuffer.usage).toEqual(
        BufferUsage.STATIC_DRAW
      );
    });

    it("creates a homogeneous multiple-attribute vertex (non-interleaved)", function () {
      var geometry = new Geometry({
        attributes: {
          customPosition: new GeometryAttribute({
            componentDatatype: ComponentDatatype.FLOAT,
            componentsPerAttribute: 3,
            values: [0.0, 0.0, 0.0, 2.0, 2.0, 2.0],
          }),
          customNormal: new GeometryAttribute({
            componentDatatype: ComponentDatatype.FLOAT,
            componentsPerAttribute: 3,
            values: [1.0, 1.0, 1.0, 3.0, 3.0, 3.0],
          }),
        },
        primitiveType: PrimitiveType.POINTS,
      });

      var va = VertexArray.fromGeometry({
        context: context,
        geometry: geometry,
        attributeLocations: GeometryPipeline.createAttributeLocations(geometry),
      });

      expect(va.numberOfAttributes).toEqual(2);
      expect(va.indexBuffer).not.toBeDefined();

      var position = geometry.attributes.customPosition;
      expect(va.getAttribute(0).index).toEqual(0);
      expect(va.getAttribute(0).componentDatatype).toEqual(
        position.componentDatatype
      );
      expect(va.getAttribute(0).componentsPerAttribute).toEqual(
        position.componentsPerAttribute
      );
      expect(va.getAttribute(0).offsetInBytes).toEqual(0);
      expect(va.getAttribute(0).strideInBytes).toEqual(0); // Tightly packed

      var normal = geometry.attributes.customNormal;
      expect(va.getAttribute(1).index).toEqual(1);
      expect(va.getAttribute(1).componentDatatype).toEqual(
        normal.componentDatatype
      );
      expect(va.getAttribute(1).componentsPerAttribute).toEqual(
        normal.componentsPerAttribute
      );
      expect(va.getAttribute(1).offsetInBytes).toEqual(0);
      expect(va.getAttribute(1).strideInBytes).toEqual(0); // Tightly packed

      expect(va.getAttribute(0).vertexBuffer).not.toBe(
        va.getAttribute(1).vertexBuffer
      );
    });

    it("creates a homogeneous multiple-attribute vertex (interleaved)", function () {
      var geometry = new Geometry({
        attributes: {
          customPosition: new GeometryAttribute({
            componentDatatype: ComponentDatatype.FLOAT,
            componentsPerAttribute: 3,
            values: [0.0, 0.0, 0.0, 2.0, 2.0, 2.0],
          }),
          customNormal: new GeometryAttribute({
            componentDatatype: ComponentDatatype.FLOAT,
            componentsPerAttribute: 3,
            values: [1.0, 1.0, 1.0, 3.0, 3.0, 3.0],
          }),
        },
        primitiveType: PrimitiveType.POINTS,
      });

      var va = VertexArray.fromGeometry({
        context: context,
        geometry: geometry,
        attributeLocations: GeometryPipeline.createAttributeLocations(geometry),
        interleave: true,
      });

      expect(va.numberOfAttributes).toEqual(2);
      expect(va.indexBuffer).not.toBeDefined();

      var position = geometry.attributes.customPosition;
      var normal = geometry.attributes.customNormal;
      var expectedStride =
        ComponentDatatype.getSizeInBytes(position.componentDatatype) *
          position.componentsPerAttribute +
        ComponentDatatype.getSizeInBytes(normal.componentDatatype) *
          normal.componentsPerAttribute;

      expect(va.getAttribute(0).index).toEqual(0);
      expect(va.getAttribute(0).componentDatatype).toEqual(
        position.componentDatatype
      );
      expect(va.getAttribute(0).componentsPerAttribute).toEqual(
        position.componentsPerAttribute
      );
      expect(va.getAttribute(0).offsetInBytes).toEqual(0);
      expect(va.getAttribute(0).strideInBytes).toEqual(expectedStride);

      expect(va.getAttribute(1).index).toEqual(1);
      expect(va.getAttribute(1).componentDatatype).toEqual(
        normal.componentDatatype
      );
      expect(va.getAttribute(1).componentsPerAttribute).toEqual(
        normal.componentsPerAttribute
      );
      expect(va.getAttribute(1).offsetInBytes).toEqual(
        ComponentDatatype.getSizeInBytes(position.componentDatatype) *
          position.componentsPerAttribute
      );
      expect(va.getAttribute(1).strideInBytes).toEqual(expectedStride);

      expect(va.getAttribute(0).vertexBuffer).toBe(
        va.getAttribute(1).vertexBuffer
      );
    });

    it("creates a heterogeneous multiple-attribute vertex (interleaved)", function () {
      var geometry = new Geometry({
        attributes: {
          position: new GeometryAttribute({
            componentDatatype: ComponentDatatype.FLOAT,
            componentsPerAttribute: 3,
            values: [0.0, 0.0, 0.0, 2.0, 2.0, 2.0],
          }),
          colors: new GeometryAttribute({
            componentDatatype: ComponentDatatype.UNSIGNED_BYTE,
            componentsPerAttribute: 4,
            values: [1, 1, 1, 1, 2, 2, 2, 2],
          }),
        },
        primitiveType: PrimitiveType.POINTS,
      });

      var va = VertexArray.fromGeometry({
        context: context,
        geometry: geometry,
        attributeLocations: GeometryPipeline.createAttributeLocations(geometry),
        interleave: true,
      });

      expect(va.numberOfAttributes).toEqual(2);
      expect(va.indexBuffer).not.toBeDefined();

      var position = geometry.attributes.position;
      var colors = geometry.attributes.colors;
      var expectedStride =
        ComponentDatatype.getSizeInBytes(position.componentDatatype) *
          position.componentsPerAttribute +
        ComponentDatatype.getSizeInBytes(colors.componentDatatype) *
          colors.componentsPerAttribute;

      expect(va.getAttribute(0).index).toEqual(0);
      expect(va.getAttribute(0).componentDatatype).toEqual(
        position.componentDatatype
      );
      expect(va.getAttribute(0).componentsPerAttribute).toEqual(
        position.componentsPerAttribute
      );
      expect(va.getAttribute(0).offsetInBytes).toEqual(0);
      expect(va.getAttribute(0).strideInBytes).toEqual(expectedStride);

      expect(va.getAttribute(1).index).toEqual(1);
      expect(va.getAttribute(1).componentDatatype).toEqual(
        colors.componentDatatype
      );
      expect(va.getAttribute(1).componentsPerAttribute).toEqual(
        colors.componentsPerAttribute
      );
      expect(va.getAttribute(1).offsetInBytes).toEqual(
        ComponentDatatype.getSizeInBytes(position.componentDatatype) *
          position.componentsPerAttribute
      );
      expect(va.getAttribute(1).strideInBytes).toEqual(expectedStride);

      expect(va.getAttribute(0).vertexBuffer).toBe(
        va.getAttribute(1).vertexBuffer
      );
    });

    it("sorts interleaved attributes from large to small components", function () {
      var geometry = new Geometry({
        attributes: {
          bytes: new GeometryAttribute({
            componentDatatype: ComponentDatatype.BYTE,
            componentsPerAttribute: 1,
            values: [0],
          }),
          shorts: new GeometryAttribute({
            componentDatatype: ComponentDatatype.SHORT,
            componentsPerAttribute: 1,
            values: [1],
          }),
          floats: new GeometryAttribute({
            componentDatatype: ComponentDatatype.FLOAT,
            componentsPerAttribute: 1,
            values: [2],
          }),
        },
        primitiveType: PrimitiveType.POINTS,
      });

      var attributeLocations = GeometryPipeline.createAttributeLocations(
        geometry
      );
      var va = VertexArray.fromGeometry({
        context: context,
        geometry: geometry,
        attributeLocations: attributeLocations,
        interleave: true,
      });

      expect(va.numberOfAttributes).toEqual(3);

      var vertexBuffer = va.getAttribute(0).vertexBuffer;
      expect(vertexBuffer).toBe(va.getAttribute(1).vertexBuffer);
      expect(vertexBuffer).toBe(va.getAttribute(2).vertexBuffer);
      expect(vertexBuffer.sizeInBytes).toEqual(8); // Includes 1 byte per-vertex padding

      // Validate via rendering
      var vs =
        "attribute float bytes; " +
        "attribute float shorts; " +
        "attribute float floats; " +
        "varying vec4 fsColor; " +
        "void main() { " +
        "  gl_PointSize = 1.0; " +
        "  gl_Position = vec4(0.0, 0.0, 0.0, 1.0); " +
        "  fsColor = vec4((bytes == 0.0) && (shorts == 1.0) && (floats == 2.0)); " +
        "}";
      var fs =
        "varying vec4 fsColor; " +
        "void main() { " +
        "  gl_FragColor = fsColor; " +
        "}";

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

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

      var command = new DrawCommand({
        primitiveType: PrimitiveType.POINTS,
        shaderProgram: sp,
        vertexArray: va,
      });
      command.execute(context);
      expect(context).toReadPixels([255, 255, 255, 255]);
    });

    it("sorts interleaved attributes from large to small components (2)", function () {
      var geometry = new Geometry({
        attributes: {
          color: new GeometryAttribute({
            componentDatatype: ComponentDatatype.UNSIGNED_BYTE,
            componentsPerAttribute: 4,
            normalize: true,
            values: [255, 0, 0, 255, 0, 255, 0, 255],
          }),
          position: new GeometryAttribute({
            componentDatatype: ComponentDatatype.FLOAT,
            componentsPerAttribute: 3,
            values: [0.0, 0.0, 0.0, 0.0, 0.0, 0.0],
          }),
        },
        primitiveType: PrimitiveType.POINTS,
      });

      var attributeLocations = GeometryPipeline.createAttributeLocations(
        geometry
      );
      var va = VertexArray.fromGeometry({
        context: context,
        geometry: geometry,
        attributeLocations: attributeLocations,
        interleave: true,
      });

      expect(va.getAttribute(0).vertexBuffer.sizeInBytes).toEqual(32); // No per-vertex padding needed

      // Validate via rendering
      var vs =
        "attribute vec3 position; " +
        "attribute vec4 color; " +
        "varying vec4 fsColor; " +
        "void main() { " +
        "  gl_PointSize = 1.0; " +
        "  gl_Position = vec4(position, 1.0); " +
        "  fsColor = color; " +
        "}";
      var fs =
        "varying vec4 fsColor; " +
        "void main() { " +
        "  gl_FragColor = fsColor; " +
        "}";
      sp = ShaderProgram.fromCache({
        context: context,
        vertexShaderSource: vs,
        fragmentShaderSource: fs,
        attributeLocations: attributeLocations,
      });

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

      var command = new DrawCommand({
        primitiveType: PrimitiveType.POINTS,
        shaderProgram: sp,
        vertexArray: va,
        offset: 0,
        count: 1,
      });
      command.execute(context);
      expect(context).toReadPixels([255, 0, 0, 255]);

      command = new DrawCommand({
        primitiveType: PrimitiveType.POINTS,
        shaderProgram: sp,
        vertexArray: va,
        offset: 1,
        count: 1,
      });
      command.execute(context);
      expect(context).toReadPixels([0, 255, 0, 255]);
    });

    it("sorts interleaved attributes from large to small components (3)", function () {
      var geometry = new Geometry({
        attributes: {
          unsignedByteAttribute: new GeometryAttribute({
            componentDatatype: ComponentDatatype.UNSIGNED_BYTE,
            componentsPerAttribute: 2,
            values: [1, 2],
          }),
          unsignedShortAttribute: new GeometryAttribute({
            componentDatatype: ComponentDatatype.UNSIGNED_SHORT,
            componentsPerAttribute: 1,
            values: [3],
          }),
          byteAttribute: new GeometryAttribute({
            componentDatatype: ComponentDatatype.BYTE,
            componentsPerAttribute: 1,
            values: [4],
          }),
          shortAttribute: new GeometryAttribute({
            componentDatatype: ComponentDatatype.SHORT,
            componentsPerAttribute: 1,
            values: [5],
          }),
        },
        primitiveType: PrimitiveType.POINTS,
      });

      var attributeLocations = GeometryPipeline.createAttributeLocations(
        geometry
      );
      var va = VertexArray.fromGeometry({
        context: context,
        geometry: geometry,
        attributeLocations: attributeLocations,
        interleave: true,
      });

      expect(va.numberOfAttributes).toEqual(4);
      expect(va.getAttribute(0).vertexBuffer.sizeInBytes).toEqual(8); // Includes 1 byte per-vertex padding

      // Validate via rendering
      var vs =
        "attribute vec2 unsignedByteAttribute; " +
        "attribute float unsignedShortAttribute; " +
        "attribute float byteAttribute; " +
        "attribute float shortAttribute; " +
        "varying vec4 fsColor; " +
        "void main() { " +
        "  gl_PointSize = 1.0; " +
        "  gl_Position = vec4(0.0, 0.0, 0.0, 1.0); " +
        "  fsColor = vec4((unsignedByteAttribute.x == 1.0) && (unsignedByteAttribute.y == 2.0) && (unsignedShortAttribute == 3.0) && (byteAttribute == 4.0) && (shortAttribute == 5.0)); " +
        "}";
      var fs =
        "varying vec4 fsColor; " +
        "void main() { " +
        "  gl_FragColor = fsColor; " +
        "}";
      sp = ShaderProgram.fromCache({
        context: context,
        vertexShaderSource: vs,
        fragmentShaderSource: fs,
        attributeLocations: attributeLocations,
      });

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

      var command = new DrawCommand({
        primitiveType: PrimitiveType.POINTS,
        shaderProgram: sp,
        vertexArray: va,
      });
      command.execute(context);
      expect(context).toReadPixels([255, 255, 255, 255]);
    });

    it("creates a custom interleaved vertex", function () {
      var geometry = new Geometry({
        attributes: {
          position: new GeometryAttribute({
            componentDatatype: ComponentDatatype.FLOAT,
            componentsPerAttribute: 3,
            values: [0.0, 0.0, 0.0, 0.0, 0.0, 0.0],
          }),
          color: new GeometryAttribute({
            componentDatatype: ComponentDatatype.UNSIGNED_BYTE,
            componentsPerAttribute: 3,
            normalize: true,
            values: [255, 0, 0, 0, 255, 0],
          }),
          normal: new GeometryAttribute({
            componentDatatype: ComponentDatatype.FLOAT,
            componentsPerAttribute: 3,
            values: [1.0, 0.0, 0.0, 0.0, 1.0, 0.0],
          }),
          temperature: new GeometryAttribute({
            componentDatatype: ComponentDatatype.UNSIGNED_SHORT,
            componentsPerAttribute: 1,
            values: [75, 100],
          }),
        },
        primitiveType: PrimitiveType.POINTS,
      });

      var attributeLocations = GeometryPipeline.createAttributeLocations(
        geometry
      );
      var va = VertexArray.fromGeometry({
        context: context,
        geometry: geometry,
        attributeLocations: attributeLocations,
        interleave: true,
      });

      expect(va.getAttribute(0).vertexBuffer.sizeInBytes).toEqual(2 * 32); // Includes 3 byte per-vertex padding

      // Validate via rendering
      var vs =
        "attribute vec3 position; " +
        "attribute vec3 color; " +
        "attribute vec3 normal; " +
        "attribute float temperature; " +
        "varying vec4 fsColor; " +
        "void main() { " +
        "  gl_PointSize = 1.0; " +
        "  gl_Position = vec4(position, 1.0); " +
        "  if ((normal == vec3(1.0, 0.0, 0.0)) && (temperature == 75.0)) { " +
        "    fsColor = vec4(color, 1.0); " +
        "  } " +
        "  else {" +
        "    fsColor = vec4(1.0); " +
        "  }" +
        "}";
      var fs =
        "varying vec4 fsColor; " +
        "void main() { " +
        "  gl_FragColor = fsColor; " +
        "}";
      sp = ShaderProgram.fromCache({
        context: context,
        vertexShaderSource: vs,
        fragmentShaderSource: fs,
        attributeLocations: attributeLocations,
      });

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

      var command = new DrawCommand({
        primitiveType: PrimitiveType.POINTS,
        shaderProgram: sp,
        vertexArray: va,
        offset: 0,
        count: 1,
      });
      command.execute(context);
      expect(context).toReadPixels([255, 0, 0, 255]);

      var vs2 =
        "attribute vec3 position; " +
        "attribute vec3 color; " +
        "attribute vec3 normal; " +
        "attribute float temperature; " +
        "varying vec4 fsColor; " +
        "void main() { " +
        "  gl_PointSize = 1.0; " +
        "  gl_Position = vec4(position, 1.0); " +
        "  if ((normal == vec3(0.0, 1.0, 0.0)) && (temperature == 100.0)) { " +
        "    fsColor = vec4(color, 1.0); " +
        "  } " +
        "  else {" +
        "    fsColor = vec4(1.0); " +
        "  }" +
        "}";
      sp = sp.destroy();
      sp = ShaderProgram.fromCache({
        context: context,
        vertexShaderSource: vs2,
        fragmentShaderSource: fs,
        attributeLocations: attributeLocations,
      });

      command = new DrawCommand({
        primitiveType: PrimitiveType.POINTS,
        shaderProgram: sp,
        vertexArray: va,
        offset: 1,
        count: 1,
      });
      command.execute(context);
      expect(context).toReadPixels([0, 255, 0, 255]);
    });

    it("creates an index buffer", function () {
      var geometry = new Geometry({
        attributes: {},
        indices: [0],
        primitiveType: PrimitiveType.POINTS,
      });

      var va = VertexArray.fromGeometry({
        context: context,
        geometry: geometry,
      });

      expect(va.numberOfAttributes).toEqual(0);
      expect(va.indexBuffer).toBeDefined();
      expect(va.indexBuffer.usage).toEqual(BufferUsage.DYNAMIC_DRAW); // Default
      expect(va.indexBuffer.indexDatatype).toEqual(
        IndexDatatype.UNSIGNED_SHORT
      );
      expect(va.indexBuffer.numberOfIndices).toEqual(geometry.indices.length);
    });

    it("throws with different number of interleaved attributes", function () {
      var geometry = new Geometry({
        attributes: {
          position: new GeometryAttribute({
            componentDatatype: ComponentDatatype.FLOAT,
            componentsPerAttribute: 1,
            values: [0.0],
          }),
          normal: new GeometryAttribute({
            componentDatatype: ComponentDatatype.FLOAT,
            componentsPerAttribute: 1,
            values: [1.0, 2.0],
          }),
        },
        primitiveType: PrimitiveType.POINTS,
      });

      expect(function () {
        return VertexArray.fromGeometry({
          context: context,
          geometry: geometry,
          interleave: true,
        });
      }).toThrowRuntimeError();
    });

    it("throws with duplicate indices", function () {
      var geometry = new Geometry({
        attributes: {
          position: new GeometryAttribute({
            componentDatatype: ComponentDatatype.FLOAT,
            componentsPerAttribute: 1,
            values: [0.0],
          }),
          normal: new GeometryAttribute({
            componentDatatype: ComponentDatatype.FLOAT,
            componentsPerAttribute: 1,
            values: [1.0],
          }),
        },
        primitiveType: PrimitiveType.POINTS,
      });

      expect(function () {
        return VertexArray.fromGeometry({
          context: context,
          geometry: geometry,
          attributeLocations: {
            position: 0,
            normal: 0,
          },
        });
      }).toThrowDeveloperError();
    });
  },
  "WebGL"
);
