import { Cartesian3 } from "../../Source/Cesium.js";
import { Cartographic } from "../../Source/Cesium.js";
import { Ellipsoid } from "../../Source/Cesium.js";
import { GeographicProjection } from "../../../Source/Cesium.js";
import { Globe } from "../../../Source/Cesium.js";
import { Math as CesiumMath } from "../../Source/Cesium.js";
import { OrthographicOffCenterFrustum } from "../../Source/Cesium.js";
import { CameraFlightPath } from "../../Source/Cesium.js";
import { SceneMode } from "../../Source/Cesium.js";
import createScene from "../createScene.js";

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

    beforeEach(function () {
      scene = createScene();
    });

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

    function createOrthographicFrustum() {
      var current = scene.camera.frustum;
      var f = new OrthographicOffCenterFrustum();
      f.near = current.near;
      f.far = current.far;

      var tanTheta = Math.tan(0.5 * current.fovy);
      f.top = f.near * tanTheta;
      f.bottom = -f.top;
      f.right = current.aspectRatio * f.top;
      f.left = -f.right;

      return f;
    }

    it("create animation throws without a scene", function () {
      expect(function () {
        CameraFlightPath.createTween(undefined, {
          destination: new Cartesian3(1e9, 1e9, 1e9),
        });
      }).toThrowDeveloperError();
    });

    it("create animation throws without a destination", function () {
      expect(function () {
        CameraFlightPath.createTween(scene, {});
      }).toThrowDeveloperError();
    });

    it("creates an animation", function () {
      var destination = new Cartesian3(1e9, 1e9, 1e9);
      var duration = 5.0;
      var complete = function () {};
      var cancel = function () {};

      var flight = CameraFlightPath.createTween(scene, {
        destination: destination,
        duration: duration,
        complete: complete,
        cancel: cancel,
      });

      expect(flight.duration).toEqual(duration);
      expect(typeof flight.complete).toEqual("function");
      expect(typeof flight.cancel).toEqual("function");
      expect(typeof flight.update).toEqual("function");
      expect(flight.startObject).toBeDefined();
      expect(flight.stopObject).toBeDefined();
      expect(flight.easingFunction).toBeDefined();
    });

    it("creates an animation in 3d", function () {
      var camera = scene.camera;

      var startPosition = Cartesian3.clone(camera.position);
      var startHeading = camera.heading;
      var startPitch = camera.pitch;
      var startRoll = camera.roll;

      var endPosition = Cartesian3.negate(startPosition, new Cartesian3());
      var endHeading = CesiumMath.toRadians(20.0);
      var endPitch = CesiumMath.toRadians(-45.0);
      var endRoll = CesiumMath.TWO_PI;

      var duration = 5.0;
      var flight = CameraFlightPath.createTween(scene, {
        destination: endPosition,
        heading: endHeading,
        pitch: endPitch,
        roll: endRoll,
        duration: duration,
      });

      flight.update({ time: 0.0 });
      expect(camera.position).toEqualEpsilon(
        startPosition,
        CesiumMath.EPSILON12
      );
      expect(camera.heading).toEqualEpsilon(startHeading, CesiumMath.EPSILON12);
      expect(camera.pitch).toEqualEpsilon(startPitch, CesiumMath.EPSILON12);
      expect(camera.roll).toEqualEpsilon(startRoll, CesiumMath.EPSILON12);

      flight.update({ time: duration });
      expect(camera.position).toEqualEpsilon(endPosition, CesiumMath.EPSILON12);
      expect(camera.heading).toEqualEpsilon(endHeading, CesiumMath.EPSILON12);
      expect(camera.pitch).toEqualEpsilon(endPitch, CesiumMath.EPSILON12);
      expect(camera.roll).toEqualEpsilon(endRoll, CesiumMath.EPSILON12);
    });

    it("creates an animation in 3d using custom ellipsoid", function () {
      var ellipsoid = new Ellipsoid(1737400, 1737400, 1737400);
      var mapProjection = new GeographicProjection(ellipsoid);
      scene = createScene({
        mapProjection: mapProjection,
      });
      scene.globe = new Globe(ellipsoid);

      var camera = scene.camera;

      var startPosition = Cartesian3.clone(camera.position);
      var endPosition = Cartesian3.fromDegrees(0.0, 0.0, 100.0, ellipsoid);

      var duration = 1.0;
      var flight = CameraFlightPath.createTween(scene, {
        destination: endPosition,
        duration: duration,
      });

      flight.update({ time: 0.0 });
      expect(camera.position).toEqualEpsilon(
        startPosition,
        CesiumMath.EPSILON12
      );

      flight.update({ time: duration });
      expect(camera.position).toEqualEpsilon(endPosition, CesiumMath.EPSILON7);
    });

    it("creates an animation in Columbus view", function () {
      scene._mode = SceneMode.COLUMBUS_VIEW;
      var camera = scene.camera;

      camera.position = new Cartesian3(0.0, 0.0, 1000.0);
      camera.direction = Cartesian3.negate(Cartesian3.UNIT_Z, new Cartesian3());
      camera.up = Cartesian3.clone(Cartesian3.UNIT_Y);
      camera.right = Cartesian3.cross(
        camera.direction,
        camera.up,
        new Cartesian3()
      );

      var startPosition = Cartesian3.clone(camera.position);

      var projection = scene.mapProjection;
      var destination = Cartesian3.add(
        startPosition,
        new Cartesian3(-6e5 * Math.PI, 6e5 * CesiumMath.PI_OVER_FOUR, 100.0),
        new Cartesian3()
      );
      var endPosition = projection.ellipsoid.cartographicToCartesian(
        projection.unproject(destination)
      );

      var duration = 5.0;
      var flight = CameraFlightPath.createTween(scene, {
        destination: endPosition,
        duration: duration,
      });

      flight.update({ time: 0.0 });
      expect(camera.position).toEqualEpsilon(
        startPosition,
        CesiumMath.EPSILON12
      );

      flight.update({ time: duration });
      expect(camera.position).toEqualEpsilon(destination, CesiumMath.EPSILON4);
    });

    it("creates an animation in 2D", function () {
      scene._mode = SceneMode.SCENE2D;
      var camera = scene.camera;

      camera.position = new Cartesian3(0.0, 0.0, 1000.0);
      camera.direction = Cartesian3.negate(Cartesian3.UNIT_Z, new Cartesian3());
      camera.up = Cartesian3.clone(Cartesian3.UNIT_Y);
      camera.right = Cartesian3.cross(
        camera.direction,
        camera.up,
        new Cartesian3()
      );
      camera.frustum = createOrthographicFrustum();

      var startHeight = camera.frustum.right - camera.frustum.left;
      var startPosition = Cartesian3.clone(camera.position);

      var projection = scene.mapProjection;
      var destination = Cartesian3.add(
        startPosition,
        new Cartesian3(-6e6 * Math.PI, 6e6 * CesiumMath.PI_OVER_FOUR, 100.0),
        new Cartesian3()
      );
      var endPosition = projection.ellipsoid.cartographicToCartesian(
        projection.unproject(destination)
      );

      var duration = 5.0;
      var flight = CameraFlightPath.createTween(scene, {
        destination: endPosition,
        duration: duration,
      });

      flight.update({ time: 0.0 });
      expect(camera.position).toEqualEpsilon(
        startPosition,
        CesiumMath.EPSILON12
      );
      expect(camera.frustum.right - camera.frustum.left).toEqualEpsilon(
        startHeight,
        CesiumMath.EPSILON7
      );

      flight.update({ time: duration });
      expect(camera.position.x).toEqualEpsilon(
        destination.x,
        CesiumMath.EPSILON7
      );
      expect(camera.position.y).toEqualEpsilon(
        destination.y,
        CesiumMath.EPSILON7
      );
      expect(camera.position.z).toEqualEpsilon(
        startPosition.z,
        CesiumMath.EPSILON7
      );
      expect(camera.frustum.right - camera.frustum.left).toEqualEpsilon(
        destination.z,
        CesiumMath.EPSILON7
      );
    });

    it("creates a path where the start and end points only differ in height", function () {
      var camera = scene.camera;
      var start = Cartesian3.clone(camera.position);
      var mag = Cartesian3.magnitude(start);
      var end = Cartesian3.multiplyByScalar(
        Cartesian3.normalize(start, new Cartesian3()),
        mag - 1000000.0,
        new Cartesian3()
      );

      var duration = 3.0;
      var flight = CameraFlightPath.createTween(scene, {
        destination: end,
        duration: duration,
      });

      flight.update({ time: 0.0 });
      expect(camera.position).toEqualEpsilon(start, CesiumMath.EPSILON12);

      flight.update({ time: duration });
      expect(camera.position).toEqualEpsilon(end, CesiumMath.EPSILON12);
    });

    it("does not create a path to the same point", function () {
      var camera = scene.camera;
      camera.position = new Cartesian3(7000000.0, 0.0, 0.0);

      var startPosition = Cartesian3.clone(camera.position);
      var startHeading = camera.heading;
      var startPitch = camera.pitch;
      var startRoll = camera.roll;

      var duration = 3.0;
      var flight = CameraFlightPath.createTween(scene, {
        destination: startPosition,
        heading: startHeading,
        pitch: startPitch,
        roll: startRoll,
        duration: duration,
      });

      expect(flight.duration).toEqual(0);
      expect(camera.position).toEqual(startPosition);
      expect(camera.heading).toEqual(startHeading);
      expect(camera.pitch).toEqual(startPitch);
      expect(camera.roll).toEqual(startRoll);
    });

    it("creates an animation with 0 duration", function () {
      var destination = new Cartesian3(1e9, 1e9, 1e9);
      var duration = 0.0;
      var complete = function () {
        return true;
      };

      var flight = CameraFlightPath.createTween(scene, {
        destination: destination,
        duration: duration,
        complete: complete,
      });

      expect(flight.duration).toEqual(duration);
      expect(flight.complete).not.toEqual(complete);
      expect(flight.update).toBeUndefined();
      expect(scene.camera.position).not.toEqual(destination);
      flight.complete();
      expect(scene.camera.position).toEqualEpsilon(
        destination,
        CesiumMath.EPSILON14
      );
    });

    it("duration is 0 when destination is the same as camera position in 2D", function () {
      scene._mode = SceneMode.SCENE2D;
      var camera = scene.camera;

      camera.position = new Cartesian3(0.0, 0.0, 1000.0);
      camera.direction = Cartesian3.negate(Cartesian3.UNIT_Z, new Cartesian3());
      camera.up = Cartesian3.clone(Cartesian3.UNIT_Y);
      camera.right = Cartesian3.cross(
        camera.direction,
        camera.up,
        new Cartesian3()
      );
      camera.frustum = createOrthographicFrustum();
      camera.update(scene.mode);
      var frustum = camera.frustum;
      var destination = Cartesian3.clone(camera.position);
      destination.z = Math.max(
        frustum.right - frustum.left,
        frustum.top - frustum.bottom
      );

      var projection = scene.mapProjection;
      var endPosition = projection.ellipsoid.cartographicToCartesian(
        projection.unproject(destination)
      );

      var flight = CameraFlightPath.createTween(scene, {
        destination: endPosition,
      });

      expect(flight.duration).toEqual(0.0);
    });

    it("duration is 0 when destination is the same as camera position in 3D", function () {
      scene._mode = SceneMode.SCENE3D;
      var camera = scene.camera;

      camera.position = new Cartesian3(0.0, 0.0, 1000.0);
      camera.setView({
        orientation: {
          heading: 0,
          pitch: -CesiumMath.PI_OVER_TWO,
          roll: 0,
        },
      });
      camera.frustum = createOrthographicFrustum();

      var flight = CameraFlightPath.createTween(scene, {
        destination: camera.position,
      });

      expect(flight.duration).toEqual(0.0);
    });

    it("duration is 0 when destination is the same as camera position in CV", function () {
      scene._mode = SceneMode.COLUMBUS_VIEW;
      var camera = scene.camera;

      camera.position = new Cartesian3(0.0, 0.0, 1000.0);
      camera.setView({
        orientation: {
          heading: 0,
          pitch: -CesiumMath.PI_OVER_TWO,
          roll: 0,
        },
      });

      var projection = scene.mapProjection;
      var endPosition = projection.ellipsoid.cartographicToCartesian(
        projection.unproject(camera.position)
      );

      var flight = CameraFlightPath.createTween(scene, {
        destination: endPosition,
      });

      expect(flight.duration).toEqual(0.0);
    });

    it("creates an animation in 2D 0 duration", function () {
      scene._mode = SceneMode.SCENE2D;
      var camera = scene.camera;

      camera.position = new Cartesian3(0.0, 0.0, 1000.0);
      camera.direction = Cartesian3.negate(Cartesian3.UNIT_Z, new Cartesian3());
      camera.up = Cartesian3.clone(Cartesian3.UNIT_Y);
      camera.right = Cartesian3.cross(
        camera.direction,
        camera.up,
        new Cartesian3()
      );
      camera.frustum = createOrthographicFrustum();

      camera.update(scene.mode);

      var startPosition = Cartesian3.clone(camera.position);

      var projection = scene.mapProjection;
      var destination = Cartesian3.add(
        startPosition,
        new Cartesian3(-6e5 * Math.PI, 6e5 * CesiumMath.PI_OVER_FOUR, 100.0),
        new Cartesian3()
      );
      var endPosition = projection.ellipsoid.cartographicToCartesian(
        projection.unproject(destination)
      );

      var flight = CameraFlightPath.createTween(scene, {
        destination: endPosition,
        duration: 0.0,
      });

      expect(typeof flight.complete).toEqual("function");
      flight.complete();
      expect(camera.position.x).toEqualEpsilon(
        destination.x,
        CesiumMath.EPSILON7
      );
      expect(camera.position.y).toEqualEpsilon(
        destination.y,
        CesiumMath.EPSILON7
      );
      expect(camera.frustum.right - camera.frustum.left).toEqualEpsilon(
        destination.z,
        CesiumMath.EPSILON7
      );
    });

    it("creates an animation in Columbus view 0 duration", function () {
      scene._mode = SceneMode.COLUMBUS_VIEW;
      var camera = scene.camera;

      camera.position = new Cartesian3(0.0, 0.0, 1000.0);
      camera.direction = Cartesian3.negate(Cartesian3.UNIT_Z, new Cartesian3());
      camera.up = Cartesian3.clone(Cartesian3.UNIT_Y);
      camera.right = Cartesian3.cross(
        camera.direction,
        camera.up,
        new Cartesian3()
      );

      var startPosition = Cartesian3.clone(camera.position);

      var projection = scene.mapProjection;
      var destination = Cartesian3.add(
        startPosition,
        new Cartesian3(-6e6 * Math.PI, 6e6 * CesiumMath.PI_OVER_FOUR, 100.0),
        new Cartesian3()
      );
      var endPosition = projection.ellipsoid.cartographicToCartesian(
        projection.unproject(destination)
      );

      var flight = CameraFlightPath.createTween(scene, {
        destination: endPosition,
        duration: 0.0,
      });

      expect(typeof flight.complete).toEqual("function");
      flight.complete();
      expect(camera.position).toEqualEpsilon(destination, CesiumMath.EPSILON8);
    });

    it("creates an animation in 3d 0 duration", function () {
      var camera = scene.camera;

      var startPosition = Cartesian3.clone(camera.position);
      var endPosition = Cartesian3.negate(startPosition, new Cartesian3());

      var flight = CameraFlightPath.createTween(scene, {
        destination: endPosition,
        duration: 0.0,
      });

      expect(typeof flight.complete).toEqual("function");
      flight.complete();
      expect(camera.position).toEqualEpsilon(endPosition, CesiumMath.EPSILON12);
    });

    it("creates animation to hit flyOverLongitude", function () {
      var camera = scene.camera;
      var projection = scene.mapProjection;
      var position = new Cartographic();

      camera.position = Cartesian3.fromDegrees(10.0, 45.0, 1000.0);

      var endPosition = Cartesian3.fromDegrees(20.0, 45.0, 1000.0);

      var overLonFlight = CameraFlightPath.createTween(scene, {
        destination: endPosition,
        duration: 1.0,
        flyOverLongitude: CesiumMath.toRadians(0.0),
      });

      var directFlight = CameraFlightPath.createTween(scene, {
        destination: endPosition,
        duration: 1.0,
      });

      expect(typeof overLonFlight.update).toEqual("function");
      expect(typeof directFlight.update).toEqual("function");

      overLonFlight.update({ time: 0.3 });
      projection.ellipsoid.cartesianToCartographic(camera.position, position);
      var lon = CesiumMath.toDegrees(position.longitude);

      expect(lon).toBeLessThan(10.0);

      directFlight.update({ time: 0.3 });
      projection.ellipsoid.cartesianToCartographic(camera.position, position);
      lon = CesiumMath.toDegrees(position.longitude);

      expect(lon).toBeGreaterThan(10.0);
      expect(lon).toBeLessThan(20.0);
    });

    it("uses flyOverLongitudeWeight", function () {
      var camera = scene.camera;
      var projection = scene.mapProjection;
      var position = new Cartographic();

      camera.position = Cartesian3.fromDegrees(10.0, 45.0, 1000.0);

      var endPosition = Cartesian3.fromDegrees(50.0, 45.0, 1000.0);

      var overLonFlightSmallWeight = CameraFlightPath.createTween(scene, {
        destination: endPosition,
        duration: 1.0,
        flyOverLongitude: CesiumMath.toRadians(0.0),
        flyOverLongitudeWeight: 2,
      });

      var overLonFlightBigWeight = CameraFlightPath.createTween(scene, {
        destination: endPosition,
        duration: 1.0,
        flyOverLongitude: CesiumMath.toRadians(0.0),
        flyOverLongitudeWeight: 20,
      });

      overLonFlightBigWeight.update({ time: 0.3 });
      projection.ellipsoid.cartesianToCartographic(camera.position, position);
      var lon = CesiumMath.toDegrees(position.longitude);

      expect(lon).toBeLessThan(10.0);

      overLonFlightSmallWeight.update({ time: 0.3 });
      projection.ellipsoid.cartesianToCartographic(camera.position, position);
      lon = CesiumMath.toDegrees(position.longitude);

      expect(lon).toBeGreaterThan(10.0);
      expect(lon).toBeLessThan(50.0);
    });

    it("adjust pitch if camera flyes higher than pitchAdjustHeight", function () {
      var camera = scene.camera;
      var duration = 5.0;

      camera.setView({
        destination: Cartesian3.fromDegrees(-20.0, 0.0, 1000.0),
        orientation: {
          heading: CesiumMath.toRadians(0.0),
          pitch: CesiumMath.toRadians(-15.0),
          roll: 0.0,
        },
      });

      var startPitch = camera.pitch;
      var endPitch = CesiumMath.toRadians(-45.0);

      var flight = CameraFlightPath.createTween(scene, {
        destination: Cartesian3.fromDegrees(60.0, 0.0, 2000.0),
        pitch: endPitch,
        duration: duration,
        pitchAdjustHeight: 2000,
      });

      flight.update({ time: 0.0 });
      expect(camera.pitch).toEqualEpsilon(startPitch, CesiumMath.EPSILON6);

      flight.update({ time: duration });
      expect(camera.pitch).toEqualEpsilon(endPitch, CesiumMath.EPSILON6);

      flight.update({ time: duration / 2.0 });
      expect(camera.pitch).toEqualEpsilon(
        -CesiumMath.PI_OVER_TWO,
        CesiumMath.EPSILON4
      );
    });

    it("animation with flyOverLongitude is smooth over two pi", function () {
      var camera = scene.camera;
      var duration = 100.0;
      var projection = scene.mapProjection;
      var position = new Cartographic();

      var startLonDegrees = 10.0;
      var endLonDegrees = 20.0;

      camera.position = Cartesian3.fromDegrees(startLonDegrees, 45.0, 1000.0);
      var endPosition = Cartesian3.fromDegrees(endLonDegrees, 45.0, 1000.0);

      var outsideTwoPiFlight = CameraFlightPath.createTween(scene, {
        destination: endPosition,
        duration: duration,
        flyOverLongitude: CesiumMath.toRadians(0.0),
      });

      var prevLon = startLonDegrees;
      var crossedDateChangesLine = 0;
      for (var t = 1; t < duration; t++) {
        outsideTwoPiFlight.update({ time: t });
        projection.ellipsoid.cartesianToCartographic(camera.position, position);
        var lon = CesiumMath.toDegrees(position.longitude);
        var d = lon - prevLon;
        if (d > 0) {
          expect(prevLon).toBeLessThan(-90);
          crossedDateChangesLine++;
          d -= 360;
        }
        prevLon = lon;
        expect(d).toBeLessThan(0);
      }

      expect(crossedDateChangesLine).toEqual(1);
    });

    it("animation with flyOverLongitude is smooth", function () {
      var camera = scene.camera;
      var duration = 100.0;
      var projection = scene.mapProjection;
      var position = new Cartographic();

      var startLonDegrees = -100.0;
      var endLonDegrees = 100.0;

      camera.position = Cartesian3.fromDegrees(startLonDegrees, 45.0, 1000.0);
      var endPosition = Cartesian3.fromDegrees(endLonDegrees, 45.0, 1000.0);

      var flight = CameraFlightPath.createTween(scene, {
        destination: endPosition,
        duration: duration,
        flyOverLongitude: CesiumMath.toRadians(0.0),
      });

      var prevLon = startLonDegrees;
      for (var t = 1; t < duration; t++) {
        flight.update({ time: t });
        projection.ellipsoid.cartesianToCartographic(camera.position, position);
        var lon = CesiumMath.toDegrees(position.longitude);
        var d = lon - prevLon;
        prevLon = lon;
        expect(d).toBeGreaterThan(0);
      }
    });

    it("does not go above the maximum height", function () {
      var camera = scene.camera;

      var startPosition = Cartesian3.fromDegrees(0.0, 0.0, 1000.0);
      var endPosition = Cartesian3.fromDegrees(10.0, 0.0, 1000.0);
      var duration = 5.0;

      camera.setView({
        destination: startPosition,
      });

      var flight = CameraFlightPath.createTween(scene, {
        destination: endPosition,
        duration: duration,
      });

      var maximumHeight = Number.NEGATIVE_INFINITY;
      var i;
      for (i = 0; i <= duration; ++i) {
        flight.update({ time: i });
        maximumHeight = Math.max(
          maximumHeight,
          camera.positionCartographic.height
        );
      }

      maximumHeight *= 0.5;

      camera.setView({
        destination: startPosition,
      });

      flight = CameraFlightPath.createTween(scene, {
        destination: endPosition,
        duration: duration,
        maximumHeight: maximumHeight,
      });

      for (i = 0; i <= duration; ++i) {
        flight.update({ time: i });
        expect(camera.positionCartographic.height).toBeLessThan(maximumHeight);
      }
    });
  },
  "WebGL"
);