import Cartesian3 from "./Cartesian3.js";
import Cartesian4 from "./Cartesian4.js";
import CullingVolume from "./CullingVolume.js";
import defaultValue from "./defaultValue.js";
import defined from "./defined.js";
import DeveloperError from "./DeveloperError.js";
import CesiumMath from "./Math.js";
import Matrix4 from "./Matrix4.js";

/**
 * The viewing frustum is defined by 6 planes.
 * Each plane is represented by a {@link Cartesian4} object, where the x, y, and z components
 * define the unit vector normal to the plane, and the w component is the distance of the
 * plane from the origin/camera position.
 *
 * @alias PerspectiveOffCenterFrustum
 * @constructor
 *
 * @param {Object} [options] An object with the following properties:
 * @param {Number} [options.left] The left clipping plane distance.
 * @param {Number} [options.right] The right clipping plane distance.
 * @param {Number} [options.top] The top clipping plane distance.
 * @param {Number} [options.bottom] The bottom clipping plane distance.
 * @param {Number} [options.near=1.0] The near clipping plane distance.
 * @param {Number} [options.far=500000000.0] The far clipping plane distance.
 *
 * @example
 * var frustum = new Cesium.PerspectiveOffCenterFrustum({
 *     left : -1.0,
 *     right : 1.0,
 *     top : 1.0,
 *     bottom : -1.0,
 *     near : 1.0,
 *     far : 100.0
 * });
 *
 * @see PerspectiveFrustum
 */
function PerspectiveOffCenterFrustum(options) {
  options = defaultValue(options, defaultValue.EMPTY_OBJECT);

  /**
   * Defines the left clipping plane.
   * @type {Number}
   * @default undefined
   */
  this.left = options.left;
  this._left = undefined;

  /**
   * Defines the right clipping plane.
   * @type {Number}
   * @default undefined
   */
  this.right = options.right;
  this._right = undefined;

  /**
   * Defines the top clipping plane.
   * @type {Number}
   * @default undefined
   */
  this.top = options.top;
  this._top = undefined;

  /**
   * Defines the bottom clipping plane.
   * @type {Number}
   * @default undefined
   */
  this.bottom = options.bottom;
  this._bottom = undefined;

  /**
   * The distance of the near plane.
   * @type {Number}
   * @default 1.0
   */
  this.near = defaultValue(options.near, 1.0);
  this._near = this.near;

  /**
   * The distance of the far plane.
   * @type {Number}
   * @default 500000000.0
   */
  this.far = defaultValue(options.far, 500000000.0);
  this._far = this.far;

  this._cullingVolume = new CullingVolume();
  this._perspectiveMatrix = new Matrix4();
  this._infinitePerspective = new Matrix4();
}

function update(frustum) {
  //>>includeStart('debug', pragmas.debug);
  if (
    !defined(frustum.right) ||
    !defined(frustum.left) ||
    !defined(frustum.top) ||
    !defined(frustum.bottom) ||
    !defined(frustum.near) ||
    !defined(frustum.far)
  ) {
    throw new DeveloperError(
      "right, left, top, bottom, near, or far parameters are not set."
    );
  }
  //>>includeEnd('debug');

  var t = frustum.top;
  var b = frustum.bottom;
  var r = frustum.right;
  var l = frustum.left;
  var n = frustum.near;
  var f = frustum.far;

  if (
    t !== frustum._top ||
    b !== frustum._bottom ||
    l !== frustum._left ||
    r !== frustum._right ||
    n !== frustum._near ||
    f !== frustum._far
  ) {
    //>>includeStart('debug', pragmas.debug);
    if (frustum.near <= 0 || frustum.near > frustum.far) {
      throw new DeveloperError(
        "near must be greater than zero and less than far."
      );
    }
    //>>includeEnd('debug');

    frustum._left = l;
    frustum._right = r;
    frustum._top = t;
    frustum._bottom = b;
    frustum._near = n;
    frustum._far = f;
    frustum._perspectiveMatrix = Matrix4.computePerspectiveOffCenter(
      l,
      r,
      b,
      t,
      n,
      f,
      frustum._perspectiveMatrix
    );
    frustum._infinitePerspective = Matrix4.computeInfinitePerspectiveOffCenter(
      l,
      r,
      b,
      t,
      n,
      frustum._infinitePerspective
    );
  }
}

Object.defineProperties(PerspectiveOffCenterFrustum.prototype, {
  /**
   * Gets the perspective projection matrix computed from the view frustum.
   * @memberof PerspectiveOffCenterFrustum.prototype
   * @type {Matrix4}
   * @readonly
   *
   * @see PerspectiveOffCenterFrustum#infiniteProjectionMatrix
   */
  projectionMatrix: {
    get: function () {
      update(this);
      return this._perspectiveMatrix;
    },
  },

  /**
   * Gets the perspective projection matrix computed from the view frustum with an infinite far plane.
   * @memberof PerspectiveOffCenterFrustum.prototype
   * @type {Matrix4}
   * @readonly
   *
   * @see PerspectiveOffCenterFrustum#projectionMatrix
   */
  infiniteProjectionMatrix: {
    get: function () {
      update(this);
      return this._infinitePerspective;
    },
  },
});

var getPlanesRight = new Cartesian3();
var getPlanesNearCenter = new Cartesian3();
var getPlanesFarCenter = new Cartesian3();
var getPlanesNormal = new Cartesian3();
/**
 * Creates a culling volume for this frustum.
 *
 * @param {Cartesian3} position The eye position.
 * @param {Cartesian3} direction The view direction.
 * @param {Cartesian3} up The up direction.
 * @returns {CullingVolume} A culling volume at the given position and orientation.
 *
 * @example
 * // Check if a bounding volume intersects the frustum.
 * var cullingVolume = frustum.computeCullingVolume(cameraPosition, cameraDirection, cameraUp);
 * var intersect = cullingVolume.computeVisibility(boundingVolume);
 */
PerspectiveOffCenterFrustum.prototype.computeCullingVolume = function (
  position,
  direction,
  up
) {
  //>>includeStart('debug', pragmas.debug);
  if (!defined(position)) {
    throw new DeveloperError("position is required.");
  }

  if (!defined(direction)) {
    throw new DeveloperError("direction is required.");
  }

  if (!defined(up)) {
    throw new DeveloperError("up is required.");
  }
  //>>includeEnd('debug');

  var planes = this._cullingVolume.planes;

  var t = this.top;
  var b = this.bottom;
  var r = this.right;
  var l = this.left;
  var n = this.near;
  var f = this.far;

  var right = Cartesian3.cross(direction, up, getPlanesRight);

  var nearCenter = getPlanesNearCenter;
  Cartesian3.multiplyByScalar(direction, n, nearCenter);
  Cartesian3.add(position, nearCenter, nearCenter);

  var farCenter = getPlanesFarCenter;
  Cartesian3.multiplyByScalar(direction, f, farCenter);
  Cartesian3.add(position, farCenter, farCenter);

  var normal = getPlanesNormal;

  //Left plane computation
  Cartesian3.multiplyByScalar(right, l, normal);
  Cartesian3.add(nearCenter, normal, normal);
  Cartesian3.subtract(normal, position, normal);
  Cartesian3.normalize(normal, normal);
  Cartesian3.cross(normal, up, normal);
  Cartesian3.normalize(normal, normal);

  var plane = planes[0];
  if (!defined(plane)) {
    plane = planes[0] = new Cartesian4();
  }
  plane.x = normal.x;
  plane.y = normal.y;
  plane.z = normal.z;
  plane.w = -Cartesian3.dot(normal, position);

  //Right plane computation
  Cartesian3.multiplyByScalar(right, r, normal);
  Cartesian3.add(nearCenter, normal, normal);
  Cartesian3.subtract(normal, position, normal);
  Cartesian3.cross(up, normal, normal);
  Cartesian3.normalize(normal, normal);

  plane = planes[1];
  if (!defined(plane)) {
    plane = planes[1] = new Cartesian4();
  }
  plane.x = normal.x;
  plane.y = normal.y;
  plane.z = normal.z;
  plane.w = -Cartesian3.dot(normal, position);

  //Bottom plane computation
  Cartesian3.multiplyByScalar(up, b, normal);
  Cartesian3.add(nearCenter, normal, normal);
  Cartesian3.subtract(normal, position, normal);
  Cartesian3.cross(right, normal, normal);
  Cartesian3.normalize(normal, normal);

  plane = planes[2];
  if (!defined(plane)) {
    plane = planes[2] = new Cartesian4();
  }
  plane.x = normal.x;
  plane.y = normal.y;
  plane.z = normal.z;
  plane.w = -Cartesian3.dot(normal, position);

  //Top plane computation
  Cartesian3.multiplyByScalar(up, t, normal);
  Cartesian3.add(nearCenter, normal, normal);
  Cartesian3.subtract(normal, position, normal);
  Cartesian3.cross(normal, right, normal);
  Cartesian3.normalize(normal, normal);

  plane = planes[3];
  if (!defined(plane)) {
    plane = planes[3] = new Cartesian4();
  }
  plane.x = normal.x;
  plane.y = normal.y;
  plane.z = normal.z;
  plane.w = -Cartesian3.dot(normal, position);

  //Near plane computation
  plane = planes[4];
  if (!defined(plane)) {
    plane = planes[4] = new Cartesian4();
  }
  plane.x = direction.x;
  plane.y = direction.y;
  plane.z = direction.z;
  plane.w = -Cartesian3.dot(direction, nearCenter);

  //Far plane computation
  Cartesian3.negate(direction, normal);

  plane = planes[5];
  if (!defined(plane)) {
    plane = planes[5] = new Cartesian4();
  }
  plane.x = normal.x;
  plane.y = normal.y;
  plane.z = normal.z;
  plane.w = -Cartesian3.dot(normal, farCenter);

  return this._cullingVolume;
};

/**
 * Returns the pixel's width and height in meters.
 *
 * @param {Number} drawingBufferWidth The width of the drawing buffer.
 * @param {Number} drawingBufferHeight The height of the drawing buffer.
 * @param {Number} distance The distance to the near plane in meters.
 * @param {Number} pixelRatio The scaling factor from pixel space to coordinate space.
 * @param {Cartesian2} result The object onto which to store the result.
 * @returns {Cartesian2} The modified result parameter or a new instance of {@link Cartesian2} with the pixel's width and height in the x and y properties, respectively.
 *
 * @exception {DeveloperError} drawingBufferWidth must be greater than zero.
 * @exception {DeveloperError} drawingBufferHeight must be greater than zero.
 * @exception {DeveloperError} pixelRatio must be greater than zero.
 *
 * @example
 * // Example 1
 * // Get the width and height of a pixel.
 * var pixelSize = camera.frustum.getPixelDimensions(scene.drawingBufferWidth, scene.drawingBufferHeight, 1.0, scene.pixelRatio, new Cesium.Cartesian2());
 *
 * @example
 * // Example 2
 * // Get the width and height of a pixel if the near plane was set to 'distance'.
 * // For example, get the size of a pixel of an image on a billboard.
 * var position = camera.position;
 * var direction = camera.direction;
 * var toCenter = Cesium.Cartesian3.subtract(primitive.boundingVolume.center, position, new Cesium.Cartesian3());      // vector from camera to a primitive
 * var toCenterProj = Cesium.Cartesian3.multiplyByScalar(direction, Cesium.Cartesian3.dot(direction, toCenter), new Cesium.Cartesian3()); // project vector onto camera direction vector
 * var distance = Cesium.Cartesian3.magnitude(toCenterProj);
 * var pixelSize = camera.frustum.getPixelDimensions(scene.drawingBufferWidth, scene.drawingBufferHeight, distance, scene.pixelRatio, new Cesium.Cartesian2());
 */
PerspectiveOffCenterFrustum.prototype.getPixelDimensions = function (
  drawingBufferWidth,
  drawingBufferHeight,
  distance,
  pixelRatio,
  result
) {
  update(this);

  //>>includeStart('debug', pragmas.debug);
  if (!defined(drawingBufferWidth) || !defined(drawingBufferHeight)) {
    throw new DeveloperError(
      "Both drawingBufferWidth and drawingBufferHeight are required."
    );
  }
  if (drawingBufferWidth <= 0) {
    throw new DeveloperError("drawingBufferWidth must be greater than zero.");
  }
  if (drawingBufferHeight <= 0) {
    throw new DeveloperError("drawingBufferHeight must be greater than zero.");
  }
  if (!defined(distance)) {
    throw new DeveloperError("distance is required.");
  }
  if (!defined(pixelRatio)) {
    throw new DeveloperError("pixelRatio is required");
  }
  if (pixelRatio <= 0) {
    throw new DeveloperError("pixelRatio must be greater than zero.");
  }
  if (!defined(result)) {
    throw new DeveloperError("A result object is required.");
  }
  //>>includeEnd('debug');

  var inverseNear = 1.0 / this.near;
  var tanTheta = this.top * inverseNear;
  var pixelHeight =
    (2.0 * pixelRatio * distance * tanTheta) / drawingBufferHeight;
  tanTheta = this.right * inverseNear;
  var pixelWidth =
    (2.0 * pixelRatio * distance * tanTheta) / drawingBufferWidth;

  result.x = pixelWidth;
  result.y = pixelHeight;
  return result;
};

/**
 * Returns a duplicate of a PerspectiveOffCenterFrustum instance.
 *
 * @param {PerspectiveOffCenterFrustum} [result] The object onto which to store the result.
 * @returns {PerspectiveOffCenterFrustum} The modified result parameter or a new PerspectiveFrustum instance if one was not provided.
 */
PerspectiveOffCenterFrustum.prototype.clone = function (result) {
  if (!defined(result)) {
    result = new PerspectiveOffCenterFrustum();
  }

  result.right = this.right;
  result.left = this.left;
  result.top = this.top;
  result.bottom = this.bottom;
  result.near = this.near;
  result.far = this.far;

  // force update of clone to compute matrices
  result._left = undefined;
  result._right = undefined;
  result._top = undefined;
  result._bottom = undefined;
  result._near = undefined;
  result._far = undefined;

  return result;
};

/**
 * Compares the provided PerspectiveOffCenterFrustum componentwise and returns
 * <code>true</code> if they are equal, <code>false</code> otherwise.
 *
 * @param {PerspectiveOffCenterFrustum} [other] The right hand side PerspectiveOffCenterFrustum.
 * @returns {Boolean} <code>true</code> if they are equal, <code>false</code> otherwise.
 */
PerspectiveOffCenterFrustum.prototype.equals = function (other) {
  return (
    defined(other) &&
    other instanceof PerspectiveOffCenterFrustum &&
    this.right === other.right &&
    this.left === other.left &&
    this.top === other.top &&
    this.bottom === other.bottom &&
    this.near === other.near &&
    this.far === other.far
  );
};

/**
 * Compares the provided PerspectiveOffCenterFrustum componentwise and returns
 * <code>true</code> if they pass an absolute or relative tolerance test,
 * <code>false</code> otherwise.
 *
 * @param {PerspectiveOffCenterFrustum} other The right hand side PerspectiveOffCenterFrustum.
 * @param {Number} relativeEpsilon The relative epsilon tolerance to use for equality testing.
 * @param {Number} [absoluteEpsilon=relativeEpsilon] The absolute epsilon tolerance to use for equality testing.
 * @returns {Boolean} <code>true</code> if this and other are within the provided epsilon, <code>false</code> otherwise.
 */
PerspectiveOffCenterFrustum.prototype.equalsEpsilon = function (
  other,
  relativeEpsilon,
  absoluteEpsilon
) {
  return (
    other === this ||
    (defined(other) &&
      other instanceof PerspectiveOffCenterFrustum &&
      CesiumMath.equalsEpsilon(
        this.right,
        other.right,
        relativeEpsilon,
        absoluteEpsilon
      ) &&
      CesiumMath.equalsEpsilon(
        this.left,
        other.left,
        relativeEpsilon,
        absoluteEpsilon
      ) &&
      CesiumMath.equalsEpsilon(
        this.top,
        other.top,
        relativeEpsilon,
        absoluteEpsilon
      ) &&
      CesiumMath.equalsEpsilon(
        this.bottom,
        other.bottom,
        relativeEpsilon,
        absoluteEpsilon
      ) &&
      CesiumMath.equalsEpsilon(
        this.near,
        other.near,
        relativeEpsilon,
        absoluteEpsilon
      ) &&
      CesiumMath.equalsEpsilon(
        this.far,
        other.far,
        relativeEpsilon,
        absoluteEpsilon
      ))
  );
};
export default PerspectiveOffCenterFrustum;
