import Cartesian3 from "../Core/Cartesian3.js";
import Check from "../Core/Check.js";
import defined from "../Core/defined.js";
import Ellipsoid from "../Core/Ellipsoid.js";
import HeadingPitchRange from "../Core/HeadingPitchRange.js";
import JulianDate from "../Core/JulianDate.js";
import CesiumMath from "../Core/Math.js";
import Matrix3 from "../Core/Matrix3.js";
import Matrix4 from "../Core/Matrix4.js";
import Quaternion from "../Core/Quaternion.js";
import TrackingReferenceFrame from "../Core/TrackingReferenceFrame.js";
import Transforms from "../Core/Transforms.js";
import SceneMode from "../Scene/SceneMode.js";
import VelocityVectorProperty from "./VelocityVectorProperty.js";

const updateTransformMatrix3Scratch1 = new Matrix3();
const updateTransformMatrix3Scratch2 = new Matrix3();
const updateTransformMatrix3Scratch3 = new Matrix3();
const updateTransformMatrix4Scratch = new Matrix4();
const updateTransformCartesian3Scratch1 = new Cartesian3();
const updateTransformCartesian3Scratch2 = new Cartesian3();
const updateTransformCartesian3Scratch3 = new Cartesian3();
const updateTransformCartesian3Scratch4 = new Cartesian3();
const updateTransformCartesian3Scratch5 = new Cartesian3();
const updateTransformCartesian3Scratch6 = new Cartesian3();
const updateTransformOrientationScratch = new Quaternion();
const velocityScratch = new Cartesian3();
const rotationScratch = new Matrix3();
const deltaTime = new JulianDate();
const northUpAxisFactor = 1.25; // times ellipsoid's maximum radius

function updateTransform(
  that,
  camera,
  updateLookAt,
  saveCamera,
  positionProperty,
  velocityProperty,
  orientationProperty,
  trackingReferenceFrame,
  time,
  ellipsoid,
) {
  const mode = that.scene.mode;
  let cartesian = positionProperty.getValue(time, that._lastCartesian);
  if (defined(cartesian)) {
    let hasBasis = false;
    let invertVelocity = false;
    let xBasis;
    let yBasis;
    let zBasis;

    if (mode === SceneMode.SCENE3D) {
      // The time delta was determined based on how fast satellites move compared to vehicles near the surface.
      // Slower moving vehicles will most likely default to east-north-up, while faster ones will be VVLH.
      JulianDate.addSeconds(time, 0.001, deltaTime);
      let deltaCartesian = positionProperty.getValue(
        deltaTime,
        updateTransformCartesian3Scratch1,
      );

      // If no valid position at (time + 0.001), sample at (time - 0.001) and invert the vector
      if (!defined(deltaCartesian)) {
        JulianDate.addSeconds(time, -0.001, deltaTime);
        deltaCartesian = positionProperty.getValue(
          deltaTime,
          updateTransformCartesian3Scratch1,
        );
        invertVelocity = true;
      }

      if (defined(deltaCartesian)) {
        let toInertial = Transforms.computeFixedToIcrfMatrix(
          time,
          updateTransformMatrix3Scratch1,
        );
        let toInertialDelta = Transforms.computeFixedToIcrfMatrix(
          deltaTime,
          updateTransformMatrix3Scratch2,
        );
        let toFixed;

        if (!defined(toInertial) || !defined(toInertialDelta)) {
          toFixed = Transforms.computeTemeToPseudoFixedMatrix(
            time,
            updateTransformMatrix3Scratch3,
          );
          toInertial = Matrix3.transpose(
            toFixed,
            updateTransformMatrix3Scratch1,
          );
          toInertialDelta = Transforms.computeTemeToPseudoFixedMatrix(
            deltaTime,
            updateTransformMatrix3Scratch2,
          );
          Matrix3.transpose(toInertialDelta, toInertialDelta);
        } else {
          toFixed = Matrix3.transpose(
            toInertial,
            updateTransformMatrix3Scratch3,
          );
        }

        const inertialCartesian = Matrix3.multiplyByVector(
          toInertial,
          cartesian,
          updateTransformCartesian3Scratch5,
        );
        const inertialDeltaCartesian = Matrix3.multiplyByVector(
          toInertialDelta,
          deltaCartesian,
          updateTransformCartesian3Scratch6,
        );

        Cartesian3.subtract(
          inertialCartesian,
          inertialDeltaCartesian,
          updateTransformCartesian3Scratch4,
        );
        const inertialVelocity =
          Cartesian3.magnitude(updateTransformCartesian3Scratch4) * 1000.0; // meters/sec

        const mu = CesiumMath.GRAVITATIONALPARAMETER; // m^3 / sec^2
        const semiMajorAxis =
          -mu /
          (inertialVelocity * inertialVelocity -
            (2 * mu) / Cartesian3.magnitude(inertialCartesian));

        if (
          semiMajorAxis < 0 ||
          semiMajorAxis > northUpAxisFactor * ellipsoid.maximumRadius
        ) {
          // North-up viewing from deep space.

          // X along the nadir
          xBasis = updateTransformCartesian3Scratch2;
          Cartesian3.normalize(cartesian, xBasis);
          Cartesian3.negate(xBasis, xBasis);

          // Z is North
          zBasis = Cartesian3.clone(
            Cartesian3.UNIT_Z,
            updateTransformCartesian3Scratch3,
          );

          // Y is along the cross of z and x (right handed basis / in the direction of motion)
          yBasis = Cartesian3.cross(
            zBasis,
            xBasis,
            updateTransformCartesian3Scratch1,
          );
          if (Cartesian3.magnitude(yBasis) > CesiumMath.EPSILON7) {
            Cartesian3.normalize(xBasis, xBasis);
            Cartesian3.normalize(yBasis, yBasis);

            zBasis = Cartesian3.cross(
              xBasis,
              yBasis,
              updateTransformCartesian3Scratch3,
            );
            Cartesian3.normalize(zBasis, zBasis);

            hasBasis = true;
          }
        } else if (
          !Cartesian3.equalsEpsilon(
            cartesian,
            deltaCartesian,
            CesiumMath.EPSILON7,
          )
        ) {
          // Approximation of VVLH (Vehicle Velocity Local Horizontal) with the Z-axis flipped.

          // Z along the position
          zBasis = updateTransformCartesian3Scratch2;
          Cartesian3.normalize(inertialCartesian, zBasis);
          Cartesian3.normalize(inertialDeltaCartesian, inertialDeltaCartesian);

          // Y is along the angular momentum vector (e.g. "orbit normal")
          yBasis = Cartesian3.cross(
            zBasis,
            inertialDeltaCartesian,
            updateTransformCartesian3Scratch3,
          );

          if (invertVelocity) {
            yBasis = Cartesian3.multiplyByScalar(yBasis, -1, yBasis);
          }

          if (
            !Cartesian3.equalsEpsilon(
              yBasis,
              Cartesian3.ZERO,
              CesiumMath.EPSILON7,
            )
          ) {
            // X is along the cross of y and z (right handed basis / in the direction of motion)
            xBasis = Cartesian3.cross(
              yBasis,
              zBasis,
              updateTransformCartesian3Scratch1,
            );

            Matrix3.multiplyByVector(toFixed, xBasis, xBasis);
            Matrix3.multiplyByVector(toFixed, yBasis, yBasis);
            Matrix3.multiplyByVector(toFixed, zBasis, zBasis);

            Cartesian3.normalize(xBasis, xBasis);
            Cartesian3.normalize(yBasis, yBasis);
            Cartesian3.normalize(zBasis, zBasis);

            hasBasis = true;
          }
        }
      }
    }

    if (defined(that.boundingSphere)) {
      cartesian = that.boundingSphere.center;
    }

    let position;
    let direction;
    let up;

    if (saveCamera) {
      position = Cartesian3.clone(
        camera.position,
        updateTransformCartesian3Scratch4,
      );
      direction = Cartesian3.clone(
        camera.direction,
        updateTransformCartesian3Scratch5,
      );
      up = Cartesian3.clone(camera.up, updateTransformCartesian3Scratch6);
    }

    const transform = updateTransformMatrix4Scratch;

    let orientation;
    if (defined(orientationProperty)) {
      orientation = orientationProperty.getValue(
        time,
        updateTransformOrientationScratch,
      );
    }

    const velocity = velocityProperty.getValue(time, velocityScratch);

    if (
      trackingReferenceFrame === TrackingReferenceFrame.INERTIAL &&
      defined(orientation)
    ) {
      Matrix4.fromTranslationQuaternionRotationScale(
        cartesian,
        orientation,
        Cartesian3.ONE,
        transform,
      );
    } else if (
      trackingReferenceFrame === TrackingReferenceFrame.VELOCITY &&
      defined(velocity)
    ) {
      const rotation = Transforms.rotationMatrixFromPositionVelocity(
        cartesian,
        velocity,
        ellipsoid,
        rotationScratch,
      );
      Matrix4.fromRotationTranslation(rotation, cartesian, transform);
    } else if (
      trackingReferenceFrame === TrackingReferenceFrame.ENU ||
      !hasBasis
    ) {
      Transforms.eastNorthUpToFixedFrame(cartesian, ellipsoid, transform);
    } else {
      transform[0] = xBasis.x;
      transform[1] = xBasis.y;
      transform[2] = xBasis.z;
      transform[3] = 0.0;
      transform[4] = yBasis.x;
      transform[5] = yBasis.y;
      transform[6] = yBasis.z;
      transform[7] = 0.0;
      transform[8] = zBasis.x;
      transform[9] = zBasis.y;
      transform[10] = zBasis.z;
      transform[11] = 0.0;
      transform[12] = cartesian.x;
      transform[13] = cartesian.y;
      transform[14] = cartesian.z;
      transform[15] = 0.0;
    }

    camera._setTransform(transform);

    if (saveCamera) {
      Cartesian3.clone(position, camera.position);
      Cartesian3.clone(direction, camera.direction);
      Cartesian3.clone(up, camera.up);
      Cartesian3.cross(direction, up, camera.right);
    }
  }

  if (updateLookAt) {
    const offset =
      mode === SceneMode.SCENE2D ||
      Cartesian3.equals(that._offset3D, Cartesian3.ZERO)
        ? undefined
        : that._offset3D;
    camera.lookAtTransform(camera.transform, offset);
  }
}

/**
 * A utility object for tracking an entity with the camera.
 * @alias EntityView
 * @constructor
 *
 * @param {Entity} entity The entity to track with the camera.
 * @param {Scene} scene The scene to use.
 * @param {Ellipsoid} [ellipsoid=Ellipsoid.default] The ellipsoid to use for orienting the camera.
 */
function EntityView(entity, scene, ellipsoid) {
  //>>includeStart('debug', pragmas.debug);
  Check.defined("entity", entity);
  Check.defined("scene", scene);
  //>>includeEnd('debug');

  /**
   * The entity to track with the camera.
   * @type {Entity}
   */
  this.entity = entity;

  /**
   * The scene in which to track the object.
   * @type {Scene}
   */
  this.scene = scene;

  /**
   * The ellipsoid to use for orienting the camera.
   * @type {Ellipsoid}
   */
  this.ellipsoid = ellipsoid ?? Ellipsoid.default;

  /**
   * The bounding sphere of the object.
   * @type {BoundingSphere}
   */
  this.boundingSphere = undefined;

  // Shadow copies of the objects so we can detect changes.
  this._lastEntity = undefined;
  this._mode = undefined;

  this._lastCartesian = new Cartesian3();
  this._defaultOffset3D = undefined;

  this._velocityProperty = new VelocityVectorProperty(entity.position, true);

  this._offset3D = new Cartesian3();
}

// STATIC properties defined here, not per-instance.
Object.defineProperties(EntityView, {
  /**
   * Gets or sets a camera offset that will be used to
   * initialize subsequent EntityViews.
   * @memberof EntityView
   * @type {Cartesian3}
   */
  defaultOffset3D: {
    get: function () {
      return this._defaultOffset3D;
    },
    set: function (vector) {
      this._defaultOffset3D = Cartesian3.clone(vector, new Cartesian3());
    },
  },
});

// Initialize the static property.
EntityView.defaultOffset3D = new Cartesian3(-14000, 3500, 3500);

const scratchHeadingPitchRange = new HeadingPitchRange();
const scratchCartesian = new Cartesian3();

/**
 * Should be called each animation frame to update the camera
 * to the latest settings.
 * @param {JulianDate} time The current animation time.
 * @param {BoundingSphere} [boundingSphere] bounding sphere of the object.
 */
EntityView.prototype.update = function (time, boundingSphere) {
  //>>includeStart('debug', pragmas.debug);
  Check.defined("time", time);
  //>>includeEnd('debug');

  const scene = this.scene;
  const ellipsoid = this.ellipsoid;
  const sceneMode = scene.mode;
  if (sceneMode === SceneMode.MORPHING) {
    return;
  }

  const entity = this.entity;
  const trackingReferenceFrame = entity.trackingReferenceFrame;
  const positionProperty = entity.position;
  if (!defined(positionProperty)) {
    return;
  }
  const velocityProperty = this._velocityProperty;
  const orientationProperty = entity.orientation;
  const objectChanged = entity !== this._lastEntity;
  const sceneModeChanged = sceneMode !== this._mode;

  const camera = scene.camera;

  let updateLookAt = objectChanged || sceneModeChanged;
  let saveCamera = true;

  if (objectChanged) {
    const viewFromProperty = entity.viewFrom;
    const hasViewFrom = defined(viewFromProperty);

    if (!hasViewFrom && defined(boundingSphere)) {
      // The default HPR is not ideal for high altitude objects so
      // we scale the pitch as we get further from the earth for a more
      // downward view.
      scratchHeadingPitchRange.pitch = -CesiumMath.PI_OVER_FOUR;
      scratchHeadingPitchRange.range = 0;
      const position = positionProperty.getValue(time, scratchCartesian);
      if (defined(position)) {
        const factor =
          2 -
          1 /
            Math.max(
              1,
              Cartesian3.magnitude(position) / ellipsoid.maximumRadius,
            );
        scratchHeadingPitchRange.pitch *= factor;
      }

      camera.viewBoundingSphere(boundingSphere, scratchHeadingPitchRange);
      this.boundingSphere = boundingSphere;
      updateLookAt = false;
      saveCamera = false;
    } else if (
      !hasViewFrom ||
      !defined(viewFromProperty.getValue(time, this._offset3D))
    ) {
      Cartesian3.clone(EntityView._defaultOffset3D, this._offset3D);
    }
  } else if (!sceneModeChanged && this._mode !== SceneMode.SCENE2D) {
    Cartesian3.clone(camera.position, this._offset3D);
  }

  this._lastEntity = entity;
  this._mode = sceneMode;

  updateTransform(
    this,
    camera,
    updateLookAt,
    saveCamera,
    positionProperty,
    velocityProperty,
    orientationProperty,
    trackingReferenceFrame,
    time,
    ellipsoid,
  );
};
export default EntityView;
