import { ApproximateTerrainHeights } from "../../Source/Cesium.js";
import { arraySlice } from "../../Source/Cesium.js";
import { Color } from "../../Source/Cesium.js";
import { ColorGeometryInstanceAttribute } from "../../Source/Cesium.js";
import { destroyObject } from "../../Source/Cesium.js";
import { DistanceDisplayConditionGeometryInstanceAttribute } from "../../Source/Cesium.js";
import { Ellipsoid } from "../../Source/Cesium.js";
import { GeometryInstance } from "../../Source/Cesium.js";
import { HeadingPitchRange } from "../../Source/Cesium.js";
import { Math as CesiumMath } from "../../Source/Cesium.js";
import { PolygonGeometry } from "../../Source/Cesium.js";
import { Rectangle } from "../../Source/Cesium.js";
import { RectangleGeometry } from "../../Source/Cesium.js";
import { ShowGeometryInstanceAttribute } from "../../Source/Cesium.js";
import { Pass } from "../../Source/Cesium.js";
import { RenderState } from "../../Source/Cesium.js";
import { ClassificationType } from "../../Source/Cesium.js";
import { EllipsoidSurfaceAppearance } from "../../Source/Cesium.js";
import { GroundPrimitive } from "../../Source/Cesium.js";
import { InvertClassification } from "../../Source/Cesium.js";
import { Material } from "../../Source/Cesium.js";
import { PerInstanceColorAppearance } from "../../Source/Cesium.js";
import { Primitive } from "../../Source/Cesium.js";
import { StencilConstants } from "../../Source/Cesium.js";
import createCanvas from "../createCanvas.js";
import createScene from "../createScene.js";
import pollToPromise from "../pollToPromise.js";

describe(
  "Scene/GroundPrimitive",
  function () {
    var scene;
    var context;

    var ellipsoid;
    var rectangle;

    var depthColor;
    var rectColor;

    var rectangleInstance;
    var primitive;
    var globePrimitive;
    var tilesetPrimitive;
    var reusableGlobePrimitive;
    var reusableTilesetPrimitive;

    function createPrimitive(rectangle, pass) {
      var renderState;
      if (pass === Pass.CESIUM_3D_TILE) {
        renderState = RenderState.fromCache({
          stencilTest: StencilConstants.setCesium3DTileBit(),
          stencilMask: StencilConstants.CESIUM_3D_TILE_MASK,
          depthTest: {
            enabled: true,
          },
        });
      }
      var depthColorAttribute = ColorGeometryInstanceAttribute.fromColor(
        new Color(0.0, 0.0, 1.0, 1.0)
      );
      depthColor = depthColorAttribute.value;
      return new Primitive({
        geometryInstances: new GeometryInstance({
          geometry: new RectangleGeometry({
            ellipsoid: Ellipsoid.WGS84,
            rectangle: rectangle,
          }),
          id: "depth rectangle",
          attributes: {
            color: depthColorAttribute,
          },
        }),
        appearance: new PerInstanceColorAppearance({
          translucent: false,
          flat: true,
          renderState: renderState,
        }),
        asynchronous: false,
      });
    }

    function MockPrimitive(primitive, pass) {
      this._primitive = primitive;
      this._pass = pass;
      this.show = true;
    }

    MockPrimitive.prototype.update = function (frameState) {
      if (!this.show) {
        return;
      }

      var commandList = frameState.commandList;
      var startLength = commandList.length;
      this._primitive.update(frameState);

      for (var i = startLength; i < commandList.length; ++i) {
        var command = commandList[i];
        command.pass = this._pass;
      }
    };

    MockPrimitive.prototype.isDestroyed = function () {
      return false;
    };

    MockPrimitive.prototype.destroy = function () {
      return destroyObject(this);
    };

    beforeAll(function () {
      scene = createScene();

      scene.postProcessStages.fxaa.enabled = false;

      context = scene.context;

      ellipsoid = Ellipsoid.WGS84;

      var bigRectangle = Rectangle.fromDegrees(
        -180 + CesiumMath.EPSILON4,
        -90 + CesiumMath.EPSILON4,
        180 - CesiumMath.EPSILON4,
        90 - CesiumMath.EPSILON4
      );
      reusableGlobePrimitive = createPrimitive(bigRectangle, Pass.GLOBE);
      reusableTilesetPrimitive = createPrimitive(
        bigRectangle,
        Pass.CESIUM_3D_TILE
      );

      return GroundPrimitive.initializeTerrainHeights();
    });

    afterAll(function () {
      reusableGlobePrimitive.destroy();
      reusableTilesetPrimitive.destroy();
      scene.destroyForSpecs();
      // Leave ground primitive uninitialized
      ApproximateTerrainHeights._initPromise = undefined;
      ApproximateTerrainHeights._terrainHeights = undefined;
    });

    beforeEach(function () {
      scene.morphTo3D(0);

      rectangle = Rectangle.fromDegrees(-80.0, 20.0, -70.0, 30.0);

      // wrap rectangle primitive so it gets executed during the globe pass and 3D Tiles pass to lay down depth
      globePrimitive = new MockPrimitive(reusableGlobePrimitive, Pass.GLOBE);
      tilesetPrimitive = new MockPrimitive(
        reusableTilesetPrimitive,
        Pass.CESIUM_3D_TILE
      );

      var rectColorAttribute = ColorGeometryInstanceAttribute.fromColor(
        new Color(1.0, 1.0, 0.0, 1.0)
      );
      rectColor = rectColorAttribute.value;
      rectangleInstance = new GeometryInstance({
        geometry: new RectangleGeometry({
          ellipsoid: ellipsoid,
          rectangle: rectangle,
        }),
        id: "rectangle",
        attributes: {
          color: rectColorAttribute,
        },
      });
    });

    afterEach(function () {
      scene.primitives.removeAll();
      scene.groundPrimitives.removeAll();
      primitive = primitive && !primitive.isDestroyed() && primitive.destroy();
      globePrimitive =
        globePrimitive &&
        !globePrimitive.isDestroyed() &&
        globePrimitive.destroy();
      tilesetPrimitive =
        tilesetPrimitive &&
        !tilesetPrimitive.isDestroyed() &&
        tilesetPrimitive.destroy();
    });

    it("default constructs", function () {
      primitive = new GroundPrimitive();
      expect(primitive.geometryInstances).not.toBeDefined();
      expect(primitive.show).toEqual(true);
      expect(primitive.vertexCacheOptimize).toEqual(false);
      expect(primitive.interleave).toEqual(false);
      expect(primitive.compressVertices).toEqual(true);
      expect(primitive.releaseGeometryInstances).toEqual(true);
      expect(primitive.allowPicking).toEqual(true);
      expect(primitive.asynchronous).toEqual(true);
      expect(primitive.debugShowBoundingVolume).toEqual(false);
      expect(primitive.debugShowShadowVolume).toEqual(false);
    });

    it("constructs with options", function () {
      var geometryInstances = [];

      primitive = new GroundPrimitive({
        geometryInstances: geometryInstances,
        show: false,
        vertexCacheOptimize: true,
        interleave: true,
        compressVertices: false,
        releaseGeometryInstances: false,
        allowPicking: false,
        asynchronous: false,
        debugShowBoundingVolume: true,
        debugShowShadowVolume: true,
      });

      expect(primitive.geometryInstances).toEqual(geometryInstances);
      expect(primitive.show).toEqual(false);
      expect(primitive.vertexCacheOptimize).toEqual(true);
      expect(primitive.interleave).toEqual(true);
      expect(primitive.compressVertices).toEqual(false);
      expect(primitive.releaseGeometryInstances).toEqual(false);
      expect(primitive.allowPicking).toEqual(false);
      expect(primitive.asynchronous).toEqual(false);
      expect(primitive.debugShowBoundingVolume).toEqual(true);
      expect(primitive.debugShowShadowVolume).toEqual(true);
    });

    it("releases geometry instances when releaseGeometryInstances is true", function () {
      if (!GroundPrimitive.isSupported(scene)) {
        return;
      }

      primitive = new GroundPrimitive({
        geometryInstances: rectangleInstance,
        releaseGeometryInstances: true,
        asynchronous: false,
      });

      expect(primitive.geometryInstances).toBeDefined();
      scene.groundPrimitives.add(primitive);
      scene.renderForSpecs();
      expect(primitive.geometryInstances).not.toBeDefined();
    });

    it("does not release geometry instances when releaseGeometryInstances is false", function () {
      if (!GroundPrimitive.isSupported(scene)) {
        return;
      }

      primitive = new GroundPrimitive({
        geometryInstances: rectangleInstance,
        releaseGeometryInstances: false,
        asynchronous: false,
      });

      expect(primitive.geometryInstances).toBeDefined();
      scene.groundPrimitives.add(primitive);
      scene.renderForSpecs();
      expect(primitive.geometryInstances).toBeDefined();
    });

    it("adds afterRender promise to frame state", function () {
      if (!GroundPrimitive.isSupported(scene)) {
        return;
      }

      primitive = new GroundPrimitive({
        geometryInstances: rectangleInstance,
        releaseGeometryInstances: false,
        asynchronous: false,
      });

      scene.groundPrimitives.add(primitive);
      scene.renderForSpecs();

      return primitive.readyPromise.then(function (param) {
        expect(param.ready).toBe(true);
      });
    });

    it("does not render when geometryInstances is undefined", function () {
      if (!GroundPrimitive.isSupported(scene)) {
        return;
      }

      primitive = new GroundPrimitive({
        geometryInstances: undefined,
        appearance: new PerInstanceColorAppearance(),
        asynchronous: false,
      });

      scene.groundPrimitives.add(primitive);
      scene.renderForSpecs();
      expect(scene.frameState.commandList.length).toEqual(0);
    });

    it("does not render when show is false", function () {
      if (!GroundPrimitive.isSupported(scene)) {
        return;
      }

      primitive = new GroundPrimitive({
        geometryInstances: rectangleInstance,
        asynchronous: false,
      });

      scene.groundPrimitives.add(primitive);
      scene.renderForSpecs();
      expect(scene.frameState.commandList.length).toBeGreaterThan(0);

      primitive.show = false;
      scene.renderForSpecs();
      expect(scene.frameState.commandList.length).toEqual(0);
    });

    it("becomes ready when show is false", function () {
      if (!GroundPrimitive.isSupported(scene)) {
        return;
      }

      primitive = scene.groundPrimitives.add(
        new GroundPrimitive({
          geometryInstances: rectangleInstance,
          asynchronous: false,
          show: false,
        })
      );

      var ready = false;
      primitive.readyPromise.then(function () {
        ready = true;
      });

      return pollToPromise(function () {
        scene.renderForSpecs();
        return ready;
      }).then(function () {
        expect(ready).toEqual(true);
      });
    });

    it("does not render other than for the color or pick pass", function () {
      if (!GroundPrimitive.isSupported(scene)) {
        return;
      }

      primitive = new GroundPrimitive({
        geometryInstances: rectangleInstance,
        asynchronous: false,
      });

      var frameState = scene.frameState;
      frameState.passes.render = false;
      frameState.passes.pick = false;

      primitive.update(frameState);
      expect(frameState.commandList.length).toEqual(0);
    });

    function expectRender(scene, color) {
      expect(scene).toRenderAndCall(function (rgba) {
        expect(arraySlice(rgba, 0, 4)).toEqual(color);
      });
    }

    function expectRenderBlank(scene) {
      expect(scene).toRenderAndCall(function (rgba) {
        expect(arraySlice(rgba)).not.toEqual([0, 0, 0, 255]);
        expect(rgba[0]).toEqual(0);
      });
    }

    function verifyGroundPrimitiveRender(primitive, color) {
      scene.camera.setView({ destination: rectangle });

      scene.primitives.add(globePrimitive);
      scene.primitives.add(tilesetPrimitive);

      expectRenderBlank(scene);

      scene.groundPrimitives.add(primitive);

      primitive.classificationType = ClassificationType.BOTH;
      globePrimitive.show = false;
      tilesetPrimitive.show = true;
      expectRender(scene, color);
      globePrimitive.show = true;
      tilesetPrimitive.show = false;
      expectRender(scene, color);

      primitive.classificationType = ClassificationType.CESIUM_3D_TILE;
      globePrimitive.show = false;
      tilesetPrimitive.show = true;
      expectRender(scene, color);
      globePrimitive.show = true;
      tilesetPrimitive.show = false;
      expectRenderBlank(scene);

      primitive.classificationType = ClassificationType.TERRAIN;
      globePrimitive.show = false;
      tilesetPrimitive.show = true;
      expectRenderBlank(scene);
      globePrimitive.show = true;
      tilesetPrimitive.show = false;
      expectRender(scene, color);

      globePrimitive.show = true;
      tilesetPrimitive.show = true;
    }

    it("renders in 3D", function () {
      if (!GroundPrimitive.isSupported(scene)) {
        return;
      }

      primitive = new GroundPrimitive({
        geometryInstances: rectangleInstance,
        asynchronous: false,
      });

      verifyGroundPrimitiveRender(primitive, rectColor);
    });

    it("renders GroundPrimitives with spherical texture coordinates across the IDL in 3D", function () {
      if (!GroundPrimitive.isSupported(scene)) {
        return;
      }

      var rectColorAttribute = ColorGeometryInstanceAttribute.fromColor(
        new Color(1.0, 1.0, 0.0, 1.0)
      );
      var bigIdlRectangle = Rectangle.fromDegrees(176.0, 30.0, -176.0, 34.0);
      var bigIdlRectangleInstance = new GeometryInstance({
        geometry: new RectangleGeometry({
          ellipsoid: ellipsoid,
          rectangle: bigIdlRectangle,
        }),
        id: "rectangle",
        attributes: {
          color: rectColorAttribute,
        },
      });

      primitive = new GroundPrimitive({
        geometryInstances: bigIdlRectangleInstance,
        asynchronous: false,
      });

      scene.camera.setView({ destination: bigIdlRectangle });

      scene.primitives.add(globePrimitive);
      expect(scene).toRenderAndCall(function (rgba) {
        expect(rgba).not.toEqual([0, 0, 0, 255]);
        expect(rgba[0]).toEqual(0);
      });

      scene.groundPrimitives.add(primitive);
      expect(scene).toRender(rectColor);
    });

    it("renders GroundPrimitives with planar texture coordinates across the IDL in 3D", function () {
      if (!GroundPrimitive.isSupported(scene)) {
        return;
      }

      var rectColorAttribute = ColorGeometryInstanceAttribute.fromColor(
        new Color(1.0, 1.0, 0.0, 1.0)
      );
      var smallIdlRectangle = Rectangle.fromDegrees(179.6, 30.0, -179.6, 30.9);
      var smallIdlRectangleInstance = new GeometryInstance({
        geometry: new RectangleGeometry({
          ellipsoid: ellipsoid,
          rectangle: smallIdlRectangle,
        }),
        id: "rectangle",
        attributes: {
          color: rectColorAttribute,
        },
      });

      primitive = new GroundPrimitive({
        geometryInstances: smallIdlRectangleInstance,
        asynchronous: false,
      });

      scene.camera.setView({ destination: smallIdlRectangle });

      scene.primitives.add(globePrimitive);
      expect(scene).toRenderAndCall(function (rgba) {
        expect(rgba).not.toEqual([0, 0, 0, 255]);
        expect(rgba[0]).toEqual(0);
      });

      scene.groundPrimitives.add(primitive);
      expect(scene).toRender(rectColor);
    });

    it("renders in Columbus view when scene3DOnly is false", function () {
      if (!GroundPrimitive.isSupported(scene)) {
        return;
      }

      primitive = new GroundPrimitive({
        geometryInstances: rectangleInstance,
        asynchronous: false,
      });

      scene.morphToColumbusView(0);
      verifyGroundPrimitiveRender(primitive, rectColor);
    });

    it("renders in 2D when scene3DOnly is false", function () {
      if (!GroundPrimitive.isSupported(scene)) {
        return;
      }

      primitive = new GroundPrimitive({
        geometryInstances: rectangleInstance,
        asynchronous: false,
      });

      scene.morphTo2D(0);
      verifyGroundPrimitiveRender(primitive, rectColor);
    });

    describe("larger scene", function () {
      // Screen space techniques may produce unexpected results with 1x1 canvasses
      var largeScene;
      var largeSceneReusableGlobePrimitive;
      var largeSceneReusableTilesetPrimitive;
      beforeAll(function () {
        largeScene = createScene({
          canvas: createCanvas(2, 2),
        });

        var bigRectangle = Rectangle.fromDegrees(
          -180 + CesiumMath.EPSILON4,
          -90 + CesiumMath.EPSILON4,
          180 - CesiumMath.EPSILON4,
          90 - CesiumMath.EPSILON4
        );
        largeSceneReusableGlobePrimitive = createPrimitive(
          bigRectangle,
          Pass.GLOBE
        );
        largeSceneReusableTilesetPrimitive = createPrimitive(
          bigRectangle,
          Pass.CESIUM_3D_TILE
        );
      });
      afterAll(function () {
        largeSceneReusableGlobePrimitive.destroy();
        largeSceneReusableTilesetPrimitive.destroy();
        largeScene.destroyForSpecs();
      });
      afterEach(function () {
        largeScene.primitives.removeAll();
        largeScene.groundPrimitives.removeAll();
      });

      function verifyLargerScene(groundPrimitive, expectedColor, destination) {
        largeScene.renderForSpecs();

        largeScene.postProcessStages.fxaa.enabled = false;
        largeScene.camera.setView({ destination: destination });

        var largeSceneGlobePrimitive = new MockPrimitive(
          largeSceneReusableGlobePrimitive,
          Pass.GLOBE
        );
        var largeSceneTilesetPrimitive = new MockPrimitive(
          largeSceneReusableTilesetPrimitive,
          Pass.CESIUM_3D_TILE
        );

        largeScene.primitives.add(largeSceneGlobePrimitive);
        largeScene.primitives.add(largeSceneTilesetPrimitive);

        expectRenderBlank(largeScene);

        largeScene.groundPrimitives.add(groundPrimitive);

        groundPrimitive.classificationType = ClassificationType.BOTH;
        largeSceneGlobePrimitive.show = false;
        largeSceneTilesetPrimitive.show = true;
        expectRender(largeScene, expectedColor);
        globePrimitive.show = true;
        tilesetPrimitive.show = false;
        expectRender(largeScene, expectedColor);

        groundPrimitive.classificationType = ClassificationType.CESIUM_3D_TILE;
        largeSceneGlobePrimitive.show = false;
        largeSceneTilesetPrimitive.show = true;
        expectRender(largeScene, expectedColor);
        globePrimitive.show = true;
        largeSceneTilesetPrimitive.show = false;
        expectRenderBlank(largeScene);

        groundPrimitive.classificationType = ClassificationType.TERRAIN;
        largeSceneGlobePrimitive.show = false;
        largeSceneTilesetPrimitive.show = true;
        expectRenderBlank(largeScene);
        largeSceneGlobePrimitive.show = true;
        largeSceneTilesetPrimitive.show = false;
        expectRender(largeScene, expectedColor);

        largeSceneGlobePrimitive.show = true;
        largeSceneTilesetPrimitive.show = true;
      }

      it("renders batched instances", function () {
        if (!GroundPrimitive.isSupported(scene)) {
          return;
        }

        var rectColorAttribute = ColorGeometryInstanceAttribute.fromColor(
          new Color(0.0, 1.0, 1.0, 1.0)
        );
        var rectangleInstance1 = new GeometryInstance({
          geometry: new RectangleGeometry({
            ellipsoid: ellipsoid,
            rectangle: new Rectangle(
              rectangle.west,
              rectangle.south,
              rectangle.east,
              (rectangle.north + rectangle.south) * 0.5
            ),
          }),
          id: "rectangle1",
          attributes: {
            color: rectColorAttribute,
          },
        });
        var rectangleInstance2 = new GeometryInstance({
          geometry: new RectangleGeometry({
            ellipsoid: ellipsoid,
            rectangle: new Rectangle(
              rectangle.west,
              (rectangle.north + rectangle.south) * 0.5,
              rectangle.east,
              rectangle.north
            ),
          }),
          id: "rectangle2",
          attributes: {
            color: rectColorAttribute,
          },
        });

        var batchedPrimitive = new GroundPrimitive({
          geometryInstances: [rectangleInstance1, rectangleInstance2],
          asynchronous: false,
        });

        verifyLargerScene(batchedPrimitive, [0, 255, 255, 255], rectangle);
      });

      it("renders small GeometryInstances with texture", function () {
        if (
          !GroundPrimitive.isSupported(scene) ||
          !GroundPrimitive.supportsMaterials(scene)
        ) {
          return;
        }

        var whiteImageMaterial = Material.fromType(Material.DiffuseMapType);
        whiteImageMaterial.uniforms.image = "./Data/Images/White.png";

        var radians = CesiumMath.toRadians(0.1);
        var west = rectangle.west;
        var south = rectangle.south;
        var smallRectangle = new Rectangle(
          west,
          south,
          west + radians,
          south + radians
        );
        var smallRectanglePrimitive = new GroundPrimitive({
          geometryInstances: new GeometryInstance({
            geometry: new RectangleGeometry({
              ellipsoid: ellipsoid,
              rectangle: smallRectangle,
            }),
            id: "smallRectangle",
          }),
          appearance: new EllipsoidSurfaceAppearance({
            aboveGround: false,
            flat: true,
            material: whiteImageMaterial,
          }),
          asynchronous: false,
        });

        verifyLargerScene(
          smallRectanglePrimitive,
          [255, 255, 255, 255],
          smallRectangle
        );
      });

      it("renders large GeometryInstances with texture", function () {
        if (
          !GroundPrimitive.isSupported(scene) ||
          !GroundPrimitive.supportsMaterials(scene)
        ) {
          return;
        }

        var whiteImageMaterial = Material.fromType(Material.DiffuseMapType);
        whiteImageMaterial.uniforms.image = "./Data/Images/White.png";

        var radians = CesiumMath.toRadians(1.1);
        var west = rectangle.west;
        var south = rectangle.south;
        var largeRectangle = new Rectangle(
          west,
          south,
          west + radians,
          south + radians
        );
        var largeRectanglePrimitive = new GroundPrimitive({
          geometryInstances: new GeometryInstance({
            geometry: new RectangleGeometry({
              ellipsoid: ellipsoid,
              rectangle: largeRectangle,
            }),
          }),
          id: "largeRectangle",
          appearance: new EllipsoidSurfaceAppearance({
            aboveGround: false,
            flat: true,
            material: whiteImageMaterial,
          }),
          asynchronous: false,
        });

        verifyLargerScene(
          largeRectanglePrimitive,
          [255, 255, 255, 255],
          largeRectangle
        );
      });

      it("renders GeometryInstances with texture across the IDL", function () {
        if (
          !GroundPrimitive.isSupported(scene) ||
          !GroundPrimitive.supportsMaterials(scene)
        ) {
          return;
        }

        var whiteImageMaterial = Material.fromType(Material.DiffuseMapType);
        whiteImageMaterial.uniforms.image = "./Data/Images/White.png";

        var largeRectangle = Rectangle.fromDegrees(179.0, 30.0, -179.0, 31.0);
        var largeRectanglePrimitive = new GroundPrimitive({
          geometryInstances: new GeometryInstance({
            geometry: new RectangleGeometry({
              ellipsoid: ellipsoid,
              rectangle: largeRectangle,
            }),
          }),
          id: "largeRectangle",
          appearance: new EllipsoidSurfaceAppearance({
            aboveGround: false,
            flat: true,
            material: whiteImageMaterial,
          }),
          asynchronous: false,
        });

        verifyLargerScene(
          largeRectanglePrimitive,
          [255, 255, 255, 255],
          largeRectangle
        );
      });
    });

    it("renders with invert classification and an opaque color", function () {
      if (!GroundPrimitive.isSupported(scene)) {
        return;
      }

      scene.invertClassification = true;
      scene.invertClassificationColor = new Color(0.25, 0.25, 0.25, 1.0);

      rectangleInstance.attributes.show = new ShowGeometryInstanceAttribute(
        true
      );

      primitive = new GroundPrimitive({
        geometryInstances: rectangleInstance,
        asynchronous: false,
      });

      scene.camera.setView({ destination: rectangle });

      var invertedColor = new Array(4);
      invertedColor[0] = Color.floatToByte(
        Color.byteToFloat(depthColor[0]) * scene.invertClassificationColor.red
      );
      invertedColor[1] = Color.floatToByte(
        Color.byteToFloat(depthColor[1]) * scene.invertClassificationColor.green
      );
      invertedColor[2] = Color.floatToByte(
        Color.byteToFloat(depthColor[2]) * scene.invertClassificationColor.blue
      );
      invertedColor[3] = 255;

      scene.primitives.add(tilesetPrimitive);
      expect(scene).toRender(invertedColor);

      scene.groundPrimitives.add(primitive);
      expect(scene).toRender(rectColor);

      primitive.getGeometryInstanceAttributes("rectangle").show = [0];
      expect(scene).toRender(depthColor);

      scene.invertClassification = false;
    });

    it("renders with invert classification and a translucent color", function () {
      if (!GroundPrimitive.isSupported(scene)) {
        return;
      }

      if (!InvertClassification.isTranslucencySupported(scene.context)) {
        return;
      }

      scene.invertClassification = true;
      scene.invertClassificationColor = new Color(0.25, 0.25, 0.25, 0.25);

      rectangleInstance.attributes.show = new ShowGeometryInstanceAttribute(
        true
      );

      primitive = new GroundPrimitive({
        geometryInstances: rectangleInstance,
        asynchronous: false,
      });

      scene.camera.setView({ destination: rectangle });

      var invertedColor = new Array(4);
      invertedColor[0] = Color.floatToByte(
        Color.byteToFloat(depthColor[0]) *
          scene.invertClassificationColor.red *
          scene.invertClassificationColor.alpha
      );
      invertedColor[1] = Color.floatToByte(
        Color.byteToFloat(depthColor[1]) *
          scene.invertClassificationColor.green *
          scene.invertClassificationColor.alpha
      );
      invertedColor[2] = Color.floatToByte(
        Color.byteToFloat(depthColor[2]) *
          scene.invertClassificationColor.blue *
          scene.invertClassificationColor.alpha
      );
      invertedColor[3] = 255;

      scene.primitives.add(tilesetPrimitive);
      expect(scene).toRender(invertedColor);

      scene.groundPrimitives.add(primitive);
      expect(scene).toRender(rectColor);

      primitive.getGeometryInstanceAttributes("rectangle").show = [0];
      expect(scene).toRender(depthColor);

      scene.invertClassification = false;
    });

    it("renders bounding volume with debugShowBoundingVolume", function () {
      if (!GroundPrimitive.isSupported(scene)) {
        return;
      }

      primitive = new GroundPrimitive({
        geometryInstances: rectangleInstance,
        asynchronous: false,
        debugShowBoundingVolume: true,
      });

      scene.groundPrimitives.add(primitive);
      scene.camera.setView({ destination: rectangle });
      expect(scene).toRenderAndCall(function (rgba) {
        expect(rgba[1]).toBeGreaterThanOrEqualTo(0);
        expect(rgba[1]).toBeGreaterThanOrEqualTo(0);
        expect(rgba[2]).toBeGreaterThanOrEqualTo(0);
        expect(rgba[3]).toEqual(255);
      });
    });

    it("renders shadow volume with debugShowShadowVolume", function () {
      if (!GroundPrimitive.isSupported(scene)) {
        return;
      }

      primitive = new GroundPrimitive({
        geometryInstances: rectangleInstance,
        asynchronous: false,
        debugShowShadowVolume: true,
      });

      scene.groundPrimitives.add(primitive);
      scene.camera.setView({ destination: rectangle });
      expect(scene).toRenderAndCall(function (rgba) {
        expect(rgba[1]).toBeGreaterThanOrEqualTo(0);
        expect(rgba[1]).toBeGreaterThanOrEqualTo(0);
        expect(rgba[2]).toBeGreaterThanOrEqualTo(0);
        expect(rgba[3]).toEqual(255);
      });
    });

    it("get per instance attributes", function () {
      if (!GroundPrimitive.isSupported(scene)) {
        return;
      }

      primitive = new GroundPrimitive({
        geometryInstances: rectangleInstance,
        asynchronous: false,
      });

      verifyGroundPrimitiveRender(primitive, rectColor);

      var attributes = primitive.getGeometryInstanceAttributes("rectangle");
      expect(attributes.color).toBeDefined();
    });

    it("modify color instance attribute", function () {
      if (!GroundPrimitive.isSupported(scene)) {
        return;
      }

      primitive = new GroundPrimitive({
        geometryInstances: rectangleInstance,
        asynchronous: false,
      });

      verifyGroundPrimitiveRender(primitive, rectColor);

      scene.primitives.destroyPrimitives = false;
      scene.primitives.removeAll();
      scene.primitives.destroyPrimitives = true;

      scene.groundPrimitives.destroyPrimitives = false;
      scene.groundPrimitives.removeAll();
      scene.groundPrimitives.destroyPrimitives = true;

      var newColor = [255, 255, 255, 255];
      var attributes = primitive.getGeometryInstanceAttributes("rectangle");
      expect(attributes.color).toBeDefined();
      attributes.color = newColor;

      verifyGroundPrimitiveRender(primitive, newColor);
    });

    it("modify show instance attribute", function () {
      if (!GroundPrimitive.isSupported(scene)) {
        return;
      }

      rectangleInstance.attributes.show = new ShowGeometryInstanceAttribute(
        true
      );

      primitive = new GroundPrimitive({
        geometryInstances: rectangleInstance,
        asynchronous: false,
      });

      verifyGroundPrimitiveRender(primitive, rectColor);

      scene.primitives.destroyPrimitives = false;
      scene.primitives.removeAll();
      scene.primitives.destroyPrimitives = true;

      scene.groundPrimitives.destroyPrimitives = false;
      scene.groundPrimitives.removeAll();
      scene.groundPrimitives.destroyPrimitives = true;

      var attributes = primitive.getGeometryInstanceAttributes("rectangle");
      expect(attributes.show).toBeDefined();
      attributes.show = [0];

      verifyGroundPrimitiveRender(primitive, depthColor);
    });

    it("renders with distance display condition per instance attribute", function () {
      if (!context.floatingPointTexture) {
        return;
      }

      if (!GroundPrimitive.isSupported(scene)) {
        return;
      }

      var near = 10000.0;
      var far = 1000000.0;
      var rect = Rectangle.fromDegrees(-1.0, -1.0, 1.0, 1.0);

      var rectColorAttribute = ColorGeometryInstanceAttribute.fromColor(
        new Color(1.0, 1.0, 0.0, 1.0)
      );
      var rectInstance = new GeometryInstance({
        geometry: new RectangleGeometry({
          ellipsoid: ellipsoid,
          rectangle: rectangle,
        }),
        id: "rect",
        attributes: {
          color: rectColorAttribute,
          distanceDisplayCondition: new DistanceDisplayConditionGeometryInstanceAttribute(
            near,
            far
          ),
        },
      });

      primitive = new GroundPrimitive({
        geometryInstances: rectInstance,
        asynchronous: false,
      });

      scene.primitives.add(globePrimitive);
      scene.groundPrimitives.add(primitive);
      scene.camera.setView({ destination: rect });
      scene.renderForSpecs();

      var boundingSphere = primitive.getGeometryInstanceAttributes("rect")
        .boundingSphere;
      var center = boundingSphere.center;
      var radius = boundingSphere.radius;

      scene.camera.lookAt(
        center,
        new HeadingPitchRange(0.0, -CesiumMath.PI_OVER_TWO, radius)
      );
      expect(scene).toRender([0, 0, 255, 255]);

      scene.camera.lookAt(
        center,
        new HeadingPitchRange(0.0, -CesiumMath.PI_OVER_TWO, radius + near + 1.0)
      );
      expect(scene).notToRender([0, 0, 255, 255]);

      scene.camera.lookAt(
        center,
        new HeadingPitchRange(0.0, -CesiumMath.PI_OVER_TWO, radius + far + 1.0)
      );
      expect(scene).toRender([0, 0, 255, 255]);
    });

    it("get bounding sphere from per instance attribute", function () {
      if (!GroundPrimitive.isSupported(scene)) {
        return;
      }

      primitive = new GroundPrimitive({
        geometryInstances: rectangleInstance,
        asynchronous: false,
      });

      verifyGroundPrimitiveRender(primitive, rectColor);

      var attributes = primitive.getGeometryInstanceAttributes("rectangle");
      expect(attributes.boundingSphere).toBeDefined();
    });

    it("getGeometryInstanceAttributes returns same object each time", function () {
      if (!GroundPrimitive.isSupported(scene)) {
        return;
      }

      primitive = new GroundPrimitive({
        geometryInstances: rectangleInstance,
        asynchronous: false,
      });

      verifyGroundPrimitiveRender(primitive, rectColor);

      var attributes = primitive.getGeometryInstanceAttributes("rectangle");
      var attributes2 = primitive.getGeometryInstanceAttributes("rectangle");
      expect(attributes).toBe(attributes2);
    });

    it("picking in 3D", function () {
      if (!GroundPrimitive.isSupported(scene)) {
        return;
      }

      primitive = new GroundPrimitive({
        geometryInstances: rectangleInstance,
        asynchronous: false,
      });

      verifyGroundPrimitiveRender(primitive, rectColor);

      expect(scene).toPickAndCall(function (result) {
        expect(result.id).toEqual("rectangle");
      });
    });

    it("picking in 2D", function () {
      if (!GroundPrimitive.isSupported(scene)) {
        return;
      }

      primitive = new GroundPrimitive({
        geometryInstances: rectangleInstance,
        asynchronous: false,
      });

      scene.morphTo2D(0);
      verifyGroundPrimitiveRender(primitive, rectColor);

      expect(scene).toPickAndCall(function (result) {
        expect(result.id).toEqual("rectangle");
      });
    });

    it("picking in Columbus View", function () {
      if (!GroundPrimitive.isSupported(scene)) {
        return;
      }

      primitive = new GroundPrimitive({
        geometryInstances: rectangleInstance,
        asynchronous: false,
      });

      scene.morphToColumbusView(0);
      verifyGroundPrimitiveRender(primitive, rectColor);

      expect(scene).toPickAndCall(function (result) {
        expect(result.id).toEqual("rectangle");
      });
    });

    it("picking without depth texture", function () {
      if (!GroundPrimitive.isSupported(scene)) {
        return;
      }

      spyOn(GroundPrimitive, "_supportsMaterials").and.callFake(function () {
        return false;
      });

      primitive = new GroundPrimitive({
        geometryInstances: rectangleInstance,
        asynchronous: false,
      });

      verifyGroundPrimitiveRender(primitive, rectColor);

      expect(scene).toPickAndCall(function (result) {
        expect(result.id).toEqual("rectangle");
      });
    });

    it("does not pick when allowPicking is false", function () {
      if (!GroundPrimitive.isSupported(scene)) {
        return;
      }

      primitive = new GroundPrimitive({
        geometryInstances: rectangleInstance,
        allowPicking: false,
        asynchronous: false,
      });

      verifyGroundPrimitiveRender(primitive, rectColor);

      expect(scene).notToPick();
    });

    it("internally invalid asynchronous geometry resolves promise and sets ready", function () {
      if (!GroundPrimitive.isSupported(scene)) {
        return;
      }

      primitive = new GroundPrimitive({
        geometryInstances: new GeometryInstance({
          geometry: PolygonGeometry.fromPositions({
            positions: [],
          }),
          attributes: {
            color: ColorGeometryInstanceAttribute.fromColor(Color.RED),
          },
        }),
        compressVertices: false,
      });

      scene.groundPrimitives.add(primitive);

      return pollToPromise(function () {
        scene.renderForSpecs();
        return primitive.ready;
      }).then(function () {
        return primitive.readyPromise.then(function (arg) {
          expect(arg).toBe(primitive);
          expect(primitive.ready).toBe(true);
        });
      });
    });

    it("internally invalid synchronous geometry resolves promise and sets ready", function () {
      if (!GroundPrimitive.isSupported(scene)) {
        return;
      }

      primitive = new GroundPrimitive({
        geometryInstances: new GeometryInstance({
          geometry: PolygonGeometry.fromPositions({
            positions: [],
          }),
          attributes: {
            color: ColorGeometryInstanceAttribute.fromColor(Color.RED),
          },
        }),
        asynchronous: false,
        compressVertices: false,
      });

      scene.groundPrimitives.add(primitive);

      return pollToPromise(function () {
        scene.renderForSpecs();
        return primitive.ready;
      }).then(function () {
        return primitive.readyPromise.then(function (arg) {
          expect(arg).toBe(primitive);
          expect(primitive.ready).toBe(true);
        });
      });
    });

    it("update throws when batched instance colors are different and materials on GroundPrimitives are not supported", function () {
      if (!GroundPrimitive.isSupported(scene)) {
        return;
      }
      spyOn(GroundPrimitive, "_supportsMaterials").and.callFake(function () {
        return false;
      });

      var rectColorAttribute = ColorGeometryInstanceAttribute.fromColor(
        new Color(0.0, 1.0, 1.0, 1.0)
      );
      var rectangleInstance1 = new GeometryInstance({
        geometry: new RectangleGeometry({
          ellipsoid: ellipsoid,
          rectangle: new Rectangle(
            rectangle.west,
            rectangle.south,
            rectangle.east,
            (rectangle.north + rectangle.south) * 0.5
          ),
        }),
        id: "rectangle1",
        attributes: {
          color: rectColorAttribute,
        },
      });
      rectColorAttribute = ColorGeometryInstanceAttribute.fromColor(
        new Color(1.0, 1.0, 0.0, 1.0)
      );
      var rectangleInstance2 = new GeometryInstance({
        geometry: new RectangleGeometry({
          ellipsoid: ellipsoid,
          rectangle: new Rectangle(
            rectangle.west,
            (rectangle.north + rectangle.south) * 0.5,
            rectangle.east,
            rectangle.north
          ),
        }),
        id: "rectangle2",
        attributes: {
          color: rectColorAttribute,
        },
      });

      primitive = new GroundPrimitive({
        geometryInstances: [rectangleInstance1, rectangleInstance2],
        asynchronous: false,
      });

      expect(function () {
        verifyGroundPrimitiveRender(primitive, rectColorAttribute.value);
      }).toThrowDeveloperError();
    });

    it("update throws when one batched instance color is undefined", function () {
      if (!GroundPrimitive.isSupported(scene)) {
        return;
      }

      var rectColorAttribute = ColorGeometryInstanceAttribute.fromColor(
        new Color(0.0, 1.0, 1.0, 1.0)
      );
      var rectangleInstance1 = new GeometryInstance({
        geometry: new RectangleGeometry({
          ellipsoid: ellipsoid,
          rectangle: new Rectangle(
            rectangle.west,
            rectangle.south,
            rectangle.east,
            (rectangle.north + rectangle.south) * 0.5
          ),
        }),
        id: "rectangle1",
        attributes: {
          color: rectColorAttribute,
        },
      });
      var rectangleInstance2 = new GeometryInstance({
        geometry: new RectangleGeometry({
          ellipsoid: ellipsoid,
          rectangle: new Rectangle(
            rectangle.west,
            (rectangle.north + rectangle.south) * 0.5,
            rectangle.east,
            rectangle.north
          ),
        }),
        id: "rectangle2",
      });

      primitive = new GroundPrimitive({
        geometryInstances: [rectangleInstance1, rectangleInstance2],
        asynchronous: false,
      });

      expect(function () {
        verifyGroundPrimitiveRender(primitive, rectColorAttribute.value);
      }).toThrowDeveloperError();
    });

    it("setting per instance attribute throws when value is undefined", function () {
      if (!GroundPrimitive.isSupported(scene)) {
        return;
      }

      primitive = new GroundPrimitive({
        geometryInstances: rectangleInstance,
        asynchronous: false,
      });

      verifyGroundPrimitiveRender(primitive, rectColor);

      var attributes = primitive.getGeometryInstanceAttributes("rectangle");

      expect(function () {
        attributes.color = undefined;
      }).toThrowDeveloperError();
    });

    it("can disable picking when asynchronous", function () {
      if (!GroundPrimitive.isSupported(scene)) {
        return;
      }

      primitive = new GroundPrimitive({
        geometryInstances: rectangleInstance,
        asynchronous: true,
        allowPicking: false,
      });

      scene.groundPrimitives.add(primitive);

      return pollToPromise(function () {
        scene.renderForSpecs();
        return primitive.ready;
      }).then(function () {
        var attributes = primitive.getGeometryInstanceAttributes("rectangle");
        expect(function () {
          attributes.color = undefined;
        }).toThrowDeveloperError();
      });
    });

    it("getGeometryInstanceAttributes throws without id", function () {
      if (!GroundPrimitive.isSupported(scene)) {
        return;
      }

      primitive = new GroundPrimitive({
        geometryInstances: rectangleInstance,
        asynchronous: false,
      });

      verifyGroundPrimitiveRender(primitive, rectColor);

      expect(function () {
        primitive.getGeometryInstanceAttributes();
      }).toThrowDeveloperError();
    });

    it("getGeometryInstanceAttributes throws if update was not called", function () {
      primitive = new GroundPrimitive({
        geometryInstances: rectangleInstance,
        asynchronous: false,
      });

      expect(function () {
        primitive.getGeometryInstanceAttributes("rectangle");
      }).toThrowDeveloperError();
    });

    it("getGeometryInstanceAttributes returns undefined if id does not exist", function () {
      if (!GroundPrimitive.isSupported(scene)) {
        return;
      }

      primitive = new GroundPrimitive({
        geometryInstances: rectangleInstance,
        asynchronous: false,
      });

      verifyGroundPrimitiveRender(primitive, rectColor);

      expect(
        primitive.getGeometryInstanceAttributes("unknown")
      ).not.toBeDefined();
    });

    it("isDestroyed", function () {
      primitive = new GroundPrimitive();
      expect(primitive.isDestroyed()).toEqual(false);
      primitive.destroy();
      expect(primitive.isDestroyed()).toEqual(true);
    });

    it("renders when using asynchronous pipeline", function () {
      if (!GroundPrimitive.isSupported(scene)) {
        return;
      }

      primitive = new GroundPrimitive({
        geometryInstances: rectangleInstance,
      });

      scene.groundPrimitives.add(primitive);

      return pollToPromise(function () {
        scene.renderForSpecs();
        return primitive.ready;
      }).then(function () {
        // verifyGroundPrimitiveRender adds the primitive, so remove it to avoid being added twice.
        scene.groundPrimitives.destroyPrimitives = false;
        scene.groundPrimitives.removeAll();
        scene.groundPrimitives.destroyPrimitives = true;

        verifyGroundPrimitiveRender(primitive, rectColor);
      });
    });

    it("destroy before asynchronous pipeline is complete", function () {
      if (!GroundPrimitive.isSupported(scene)) {
        return;
      }

      primitive = new GroundPrimitive({
        geometryInstances: rectangleInstance,
      });

      scene.groundPrimitives.add(primitive);

      scene.renderForSpecs();
      primitive.destroy();
      expect(primitive.isDestroyed()).toEqual(true);

      // The primitive has already been destroyed, so remove it from the scene so it doesn't get destroyed again.
      scene.groundPrimitives.destroyPrimitives = false;
      scene.groundPrimitives.removeAll();
      scene.groundPrimitives.destroyPrimitives = true;
    });

    it("creating a synchronous primitive throws if initializeTerrainHeights wasn't called", function () {
      // Make it seem like initializeTerrainHeights was never called
      var initPromise = ApproximateTerrainHeights._initPromise;
      var terrainHeights = ApproximateTerrainHeights._terrainHeights;
      ApproximateTerrainHeights._initPromise = undefined;
      ApproximateTerrainHeights._terrainHeights = undefined;

      primitive = new GroundPrimitive({
        geometryInstances: rectangleInstance,
        asynchronous: false,
      });

      scene.groundPrimitives.add(primitive);

      if (GroundPrimitive.isSupported(scene)) {
        expect(function () {
          scene.renderForSpecs();
        }).toThrowDeveloperError();
      }

      // Set back to initialized state
      ApproximateTerrainHeights._initPromise = initPromise;
      ApproximateTerrainHeights._terrainHeights = terrainHeights;
    });
  },
  "WebGL"
);
