import { BoundingSphere } from "../../Source/Cesium.js";
import { Cartesian3 } from "../../Source/Cesium.js";
import { defaultValue } from "../../Source/Cesium.js";
import { HeadingPitchRange } from "../../Source/Cesium.js";
import { HeadingPitchRoll } from "../../Source/Cesium.js";
import { JulianDate } from "../../Source/Cesium.js";
import { Matrix4 } from "../../Source/Cesium.js";
import { Axis } from "../../Source/Cesium.js";
import { PrimitiveType } from "../../Source/Cesium.js";
import { Resource } from "../../Source/Cesium.js";
import { Transforms } from "../../Source/Cesium.js";
import { Model } from "../../Source/Cesium.js";
import { ModelInstanceCollection } from "../../Source/Cesium.js";
import { ShadowMode } from "../../Source/Cesium.js";
import createScene from "../createScene.js";
import pollToPromise from "../pollToPromise.js";
import { when } from "../../Source/Cesium.js";

describe(
  "Scene/ModelInstanceCollection",
  function () {
    var boxUrl = "./Data/Models/Box/CesiumBoxTest.gltf";
    var cesiumAirUrl = "./Data/Models/CesiumAir/Cesium_Air.gltf";
    var riggedFigureUrl =
      "./Data/Models/rigged-figure-test/rigged-figure-test.gltf";
    var movingBoxUrl = "./Data/Models/moving-box/moving-box.gltf";

    var scene;
    var boxRadius;

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

      return loadModel(boxUrl).then(function (model) {
        boxRadius = model.boundingSphere.radius;
        scene.primitives.remove(model);
      });
    });

    beforeEach(function () {
      scene.morphTo3D(0.0);
    });

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

    afterEach(function () {
      scene.primitives.removeAll();
    });

    function loadModel(url) {
      var model = scene.primitives.add(
        Model.fromGltf({
          url: url,
        })
      );

      return pollToPromise(function () {
        // Render scene to progressively load the model
        scene.renderForSpecs();
        return model.ready;
      }).then(function () {
        return model;
      });
    }

    function loadCollection(options) {
      var collection = scene.primitives.add(
        new ModelInstanceCollection(options)
      );

      return pollToPromise(function () {
        // Render scene to progressively load the model
        scene.renderForSpecs();
        return collection.ready;
      }).then(function () {
        zoomTo(collection, 0);
        return collection;
      });
    }

    function createInstances(count, heightOffset) {
      heightOffset = defaultValue(heightOffset, 0.0);

      var spacing = 20.0;
      var centerLongitude = -123.0744619;
      var centerLatitude = 44.0503706;
      var height = 5000.0 + heightOffset;

      var instances = [];
      for (var i = 0; i < count; ++i) {
        var instanceHeight = height + spacing * i;
        var position = Cartesian3.fromDegrees(
          centerLongitude,
          centerLatitude,
          instanceHeight
        );
        var heading = Math.PI / 2.0;
        var pitch = 0.0;
        var roll = 0.0;
        var hpr = new HeadingPitchRoll(heading, pitch, roll);
        var modelMatrix = Transforms.headingPitchRollToFixedFrame(
          position,
          hpr
        );
        instances.push({
          modelMatrix: modelMatrix,
        });
      }

      return instances;
    }

    function getBoundingSphere(instances, modelRadius) {
      var length = instances.length;
      var points = new Array(length);
      for (var i = 0; i < length; ++i) {
        var translation = new Cartesian3();
        Matrix4.getTranslation(instances[i].modelMatrix, translation);
        points[i] = translation;
      }
      var boundingSphere = new BoundingSphere();
      BoundingSphere.fromPoints(points, boundingSphere);
      boundingSphere.radius += modelRadius;
      return boundingSphere;
    }

    var centerScratch = new Cartesian3();

    function zoomTo(collection, instance) {
      var center = Matrix4.getTranslation(
        collection._instances[instance].modelMatrix,
        centerScratch
      );
      var camera = scene.camera;
      camera.lookAt(center, new HeadingPitchRange(0.0, 0.0, 10.0));
    }

    function expectRender(collection, expectColor, time) {
      expectColor = defaultValue(expectColor, true);

      collection.show = false;
      expect(scene).toRender([0, 0, 0, 255]);
      collection.show = true;

      // Verify each instance
      var length = collection.length;
      for (var i = 0; i < length; ++i) {
        zoomTo(collection, i);
        if (expectColor) {
          expect({
            scene: scene,
            time: time,
          }).notToRender([0, 0, 0, 255]);
        } else {
          expect({
            scene: scene,
            time: time,
          }).toRender([0, 0, 0, 255]);
        }
      }
    }

    function verifyPickedInstance(collection, instanceId) {
      return function (result) {
        expect(result.primitive).toBe(collection);
        expect(result.modelMatrix).toBeDefined();
        expect(result.instanceId).toBe(instanceId);
        expect(result.model).toBe(collection._model);
      };
    }

    function expectPick(collection) {
      collection.show = false;
      expect(scene).notToPick();
      collection.show = true;

      // Verify each instance
      var length = collection.length;
      for (var i = 0; i < length; ++i) {
        zoomTo(collection, i);
        expect(scene).toPickAndCall(verifyPickedInstance(collection, i));
      }
    }

    it("throws if neither options.gltf nor options.url are provided", function () {
      expect(function () {
        return new ModelInstanceCollection();
      }).toThrowDeveloperError();
    });

    it("throws when both options.gltf and options.url are provided", function () {
      return loadModel(boxUrl).then(function (model) {
        expect(function () {
          return new ModelInstanceCollection({
            url: boxUrl,
            gltf: model.gltf,
          });
        }).toThrowDeveloperError();
      });
    });

    it("sets properties", function () {
      return loadCollection({
        url: boxUrl,
        instances: createInstances(4),
      }).then(function (collection) {
        expect(collection.ready).toEqual(true);
        expect(collection.show).toEqual(true);
        expect(collection.allowPicking).toEqual(true);
        expect(collection.length).toEqual(4);
        expect(collection.debugShowBoundingVolume).toEqual(false);
        expect(collection.debugWireframe).toEqual(false);
        expect(collection._dynamic).toEqual(false);
        expect(collection._cull).toEqual(true);
        expect(collection._model).toBeDefined();
        expect(collection._model.ready).toEqual(true);

        if (collection._instancingSupported) {
          expect(collection._model.cacheKey).toEqual(boxUrl + "#instanced");
        }
      });
    });

    it("renders from url", function () {
      return loadCollection({
        url: boxUrl,
        instances: createInstances(4),
      }).then(function (collection) {
        expectRender(collection);
      });
    });

    it("renders from Resource", function () {
      var resource = new Resource({
        url: boxUrl,
      });

      return loadCollection({
        url: resource,
        instances: createInstances(4),
      }).then(function (collection) {
        expectRender(collection);
      });
    });

    it("renders from gltf", function () {
      return loadCollection({
        url: boxUrl,
        instances: createInstances(4),
      }).then(function (collection) {
        expectRender(collection);
      });
    });

    it("resolves readyPromise", function () {
      var collection = scene.primitives.add(
        new ModelInstanceCollection({
          url: boxUrl,
          instances: createInstances(4),
        })
      );

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

    it("rejects readyPromise on error", function () {
      // Expect promise to be rejected in Model, then in ModelInstanceCollection.
      var collection = scene.primitives.add(
        new ModelInstanceCollection({
          url: "invalid.gltf",
          instances: createInstances(4),
        })
      );

      collection.update(scene.frameState);

      return collection.readyPromise
        .then(function (collection) {
          fail("should not resolve");
        })
        .otherwise(function (error) {
          expect(collection.ready).toEqual(false);
        });
    });

    it("renders one instance", function () {
      return loadCollection({
        url: boxUrl,
        instances: createInstances(1),
      }).then(function (collection) {
        expectRender(collection);
      });
    });

    it("renders zero instances", function () {
      var collection = scene.primitives.add(
        new ModelInstanceCollection({
          url: boxUrl,
          instances: createInstances(0),
        })
      );

      // Collection never reaches the ready state due to returning early
      for (var i = 0; i < 10; ++i) {
        expectRender(collection, false);
        expect(collection.ready).toBe(false);
      }
    });

    it("renders 100 instances", function () {
      return loadCollection({
        url: boxUrl,
        instances: createInstances(100),
      }).then(function (collection) {
        expectRender(collection);
      });
    });

    it("renders cesiumAir", function () {
      return loadCollection({
        url: cesiumAirUrl,
        instances: createInstances(4),
      }).then(function (collection) {
        expectRender(collection);
      });
    });

    it("renders rigged figure", function () {
      return loadCollection({
        url: riggedFigureUrl,
        instances: createInstances(4),
      }).then(function (collection) {
        var instances = collection._instances;
        // Rotate instances to account for empty space between legs of rigged model.
        for (var i = 0; i < instances.length; ++i) {
          instances[i].modelMatrix = Matrix4.multiply(
            instances[i].modelMatrix,
            Axis.Y_UP_TO_X_UP,
            new Matrix4()
          );
        }

        expectRender(collection);
      });
    });

    it("renders when instancing is disabled", function () {
      // Disable extension
      var instancedArrays = scene.context._instancedArrays;
      scene.context._instancedArrays = undefined;

      return loadCollection({
        url: boxUrl,
        instances: createInstances(4),
      }).then(function (collection) {
        expectRender(collection);
        // Re-enable extension
        scene.context._instancedArrays = instancedArrays;
      });
    });

    it("renders when dynamic is true", function () {
      return loadCollection({
        url: boxUrl,
        instances: createInstances(4),
        dynamic: true,
      }).then(function (collection) {
        expectRender(collection);
      });
    });

    it("verify bounding volume", function () {
      var instances = createInstances(4);
      return loadCollection({
        url: boxUrl,
        instances: instances,
      }).then(function (collection) {
        var boundingSphere = getBoundingSphere(instances, boxRadius);
        expect(collection._boundingSphere.center).toEqual(
          boundingSphere.center
        );
        expect(collection._boundingSphere.radius).toEqual(
          boundingSphere.radius
        );
      });
    });

    it("renders bounding volume", function () {
      return loadCollection({
        url: boxUrl,
        instances: createInstances(4),
      }).then(function (collection) {
        collection.debugShowBoundingVolume = true;
        expectRender(collection);
      });
    });

    it("renders in wireframe", function () {
      return loadCollection({
        url: boxUrl,
        instances: createInstances(4),
      }).then(function (collection) {
        collection.debugWireframe = true;
        scene.renderForSpecs();
        expect(collection._drawCommands[0].primitiveType).toEqual(
          PrimitiveType.LINES
        );
      });
    });

    it("renders with animations", function () {
      // Test that all instances are being animated.
      // The moving box is in view on frame 1 and out of view by frame 5.
      return loadCollection({
        url: movingBoxUrl,
        instances: createInstances(4),
      }).then(function (collection) {
        collection.activeAnimations.addAll();

        // Render when animation is in view
        var time = JulianDate.now();
        expectRender(collection, true, time);

        // Render when animation is out of view
        time = JulianDate.addSeconds(time, 0.1, new JulianDate());
        expectRender(collection, false, time);
      });
    });

    it("renders with animations when instancing is disabled", function () {
      // Instance transforms are updated differently when instancing is disabled

      // Disable extension
      var instancedArrays = scene.context._instancedArrays;
      scene.context._instancedArrays = undefined;

      return loadCollection({
        url: movingBoxUrl,
        instances: createInstances(4),
      }).then(function (collection) {
        collection.activeAnimations.addAll();

        // Render when animation is in view
        var time = JulianDate.now();
        expectRender(collection, true, time);

        // Render when animation is out of view
        time = JulianDate.addSeconds(time, 0.1, new JulianDate());
        expectRender(collection, false, time);

        // Re-enable extension
        scene.context._instancedArrays = instancedArrays;
      });
    });

    it("renders two model instance collections that use the same cache key", function () {
      var collections = [];
      var promises = [];

      promises.push(
        loadCollection({
          url: boxUrl,
          instances: createInstances(2),
        }).then(function (collection) {
          collections.push(collection);
        })
      );

      promises.push(
        loadCollection({
          url: boxUrl,
          instances: createInstances(2, 1000.0),
        }).then(function (collection) {
          collections.push(collection);
        })
      );

      return when.all(promises).then(function () {
        var resourcesFirst = collections[0]._model._rendererResources;
        var resourcesSecond = collections[1]._model._rendererResources;
        var name;

        expect(collections[0]._model.cacheKey).toEqual(
          collections[1]._model.cacheKey
        );
        zoomTo(collections[0], 0);
        expectRender(collections[0]);
        zoomTo(collections[1], 0);
        expectRender(collections[1]);

        // Check that buffers are equal
        for (name in resourcesFirst.buffers) {
          if (resourcesFirst.buffers.hasOwnProperty(name)) {
            expect(resourcesFirst.buffers[name]).toEqual(
              resourcesSecond.buffers[name]
            );
          }
        }

        // Check that programs are equal
        for (name in resourcesFirst.programs) {
          if (resourcesFirst.programs.hasOwnProperty(name)) {
            expect(resourcesFirst.programs[name]).toEqual(
              resourcesSecond.programs[name]
            );
          }
        }

        if (collections[0]._instancingSupported) {
          // Check that vertex arrays are different, since each collection has a unique vertex buffer for instanced attributes.
          for (name in resourcesFirst.vertexArrays) {
            if (resourcesFirst.vertexArrays.hasOwnProperty(name)) {
              expect(resourcesFirst.vertexArrays[name]).not.toEqual(
                resourcesSecond.vertexArrays[name]
              );
            }
          }
        }
      });
    });

    it("culls when out of view and cull is true", function () {
      return loadCollection({
        url: boxUrl,
        instances: createInstances(4),
        cull: true,
      }).then(function (collection) {
        scene.renderForSpecs();
        expect(scene.frustumCommandsList.length).not.toEqual(0);
        scene.camera.lookAt(
          new Cartesian3(100000.0, 0.0, 0.0),
          new HeadingPitchRange(0.0, 0.0, 10.0)
        );
        scene.renderForSpecs();
        expect(scene.frustumCommandsList.length).toEqual(0);
      });
    });

    it("does not cull when out of view and cull is false", function () {
      return loadCollection({
        url: boxUrl,
        instances: createInstances(4),
        cull: false,
      }).then(function (collection) {
        scene.renderForSpecs();
        expect(scene.frustumCommandsList.length).not.toEqual(0);
        scene.camera.lookAt(
          new Cartesian3(100000.0, 0.0, 0.0),
          new HeadingPitchRange(0.0, 0.0, 10.0)
        );
        scene.renderForSpecs();
        expect(scene.frustumCommandsList.length).not.toEqual(0);
      });
    });

    it("shadows", function () {
      return loadCollection({
        url: boxUrl,
        instances: createInstances(4),
      }).then(function (collection) {
        scene.renderForSpecs();
        expect(collection._shadows).toBe(ShadowMode.ENABLED);
        var drawCommand = collection._drawCommands[0];
        expect(drawCommand.castShadows).toBe(true);
        expect(drawCommand.receiveShadows).toBe(true);
        collection.shadows = ShadowMode.DISABLED;
        scene.renderForSpecs();

        // Expect commands to have been recreated
        drawCommand = collection._drawCommands[0];
        expect(drawCommand.castShadows).toBe(false);
        expect(drawCommand.receiveShadows).toBe(false);
      });
    });

    it("picks", function () {
      return loadCollection({
        url: boxUrl,
        instances: createInstances(4),
      }).then(function (collection) {
        expectPick(collection);
      });
    });

    it("picks when instancing is disabled", function () {
      // Disable extension
      var instancedArrays = scene.context._instancedArrays;
      scene.context._instancedArrays = undefined;

      return loadCollection({
        url: boxUrl,
        instances: createInstances(4),
      }).then(function (collection) {
        expectPick(collection);
        // Re-enable extension
        scene.context._instancedArrays = instancedArrays;
      });
    });

    it("moves instance", function () {
      return loadCollection({
        url: boxUrl,
        instances: createInstances(4),
      }).then(function (collection) {
        expect(scene).toPickAndCall(function (result) {
          var originalMatrix = result.modelMatrix;
          result.modelMatrix = Matrix4.IDENTITY;
          expect(scene).notToPick();
          result.modelMatrix = originalMatrix;
          expect(scene).toPickPrimitive(collection);
        });
      });
    });

    it("moves instance when instancing is disabled", function () {
      // Disable extension
      var instancedArrays = scene.context._instancedArrays;
      scene.context._instancedArrays = undefined;

      return loadCollection({
        url: boxUrl,
        instances: createInstances(4),
      }).then(function (collection) {
        expect(scene).toPickAndCall(function (result) {
          var originalMatrix = result.modelMatrix;
          var originalRadius = collection._boundingSphere.radius;
          result.modelMatrix = Matrix4.IDENTITY;
          expect(scene).notToPick();
          expect(collection._boundingSphere.radius).toBeGreaterThan(
            originalRadius
          );
          result.modelMatrix = originalMatrix;
          expect(scene).toPickPrimitive(collection);
        });
        // Re-enable extension
        scene.context._instancedArrays = instancedArrays;
      });
    });

    it("renders in 2D", function () {
      return loadCollection({
        url: boxUrl,
        instances: createInstances(4),
      }).then(function (collection) {
        expectRender(collection);
        scene.morphTo2D(0.0);
        expectRender(collection);
      });
    });

    it("renders in 2D when instancing is disabled", function () {
      // Disable extension
      var instancedArrays = scene.context._instancedArrays;
      scene.context._instancedArrays = undefined;

      return loadCollection({
        url: boxUrl,
        instances: createInstances(4),
      }).then(function (collection) {
        expectRender(collection);
        scene.morphTo2D(0.0);
        expectRender(collection);

        // Re-enable extension
        scene.context._instancedArrays = instancedArrays;
      });
    });

    it("renders in CV", function () {
      return loadCollection({
        url: boxUrl,
        instances: createInstances(4),
      }).then(function (collection) {
        expectRender(collection);
        scene.morphToColumbusView(0.0);
        expectRender(collection);
      });
    });

    it("renders in CV when instancing is disabled", function () {
      // Disable extension
      var instancedArrays = scene.context._instancedArrays;
      scene.context._instancedArrays = undefined;

      return loadCollection({
        url: boxUrl,
        instances: createInstances(4),
      }).then(function (collection) {
        expectRender(collection);
        scene.morphToColumbusView(0.0);
        expectRender(collection);

        // Re-enable extension
        scene.context._instancedArrays = instancedArrays;
      });
    });

    it("does not render during morph", function () {
      return loadCollection({
        url: boxUrl,
        instances: createInstances(4),
        cull: false,
      }).then(function () {
        var commandList = scene.frameState.commandList;
        scene.renderForSpecs();
        expect(commandList.length).toBeGreaterThan(0);
        scene.morphToColumbusView(1.0);
        scene.renderForSpecs();
        expect(commandList.length).toBe(0);
      });
    });

    it("destroys", function () {
      return loadCollection({
        url: boxUrl,
        instances: createInstances(4),
      }).then(function (collection) {
        expect(collection.isDestroyed()).toEqual(false);
        scene.primitives.remove(collection);
        expect(collection.isDestroyed()).toEqual(true);
      });
    });
  },
  "WebGL"
);
