import { JSONMeta, Object3DJSON, Object3DJSONObject } from "../core/Object3D.js";
import { Vector2 } from "../math/Vector2.js";
import { Camera } from "./Camera.js";

export interface PerspectiveCameraJSONObject extends Object3DJSONObject {
    fov: number;
    zoom: number;

    near: number;
    far: number;
    focus: number;

    aspect: number;

    view?: {
        enabled: boolean;
        fullWidth: number;
        fullHeight: number;
        offsetX: number;
        offsetY: number;
        width: number;
        height: number;
    };

    filmGauge: number;
    filmOffset: number;
}

export interface PerspectiveCameraJSON extends Object3DJSON {
    object: PerspectiveCameraJSONObject;
}

/**
 * Camera that uses {@link https://en.wikipedia.org/wiki/Perspective_(graphical) | perspective projection}.
 * This projection mode is designed to mimic the way the human eye sees
 * @remarks
 * It is the most common projection mode used for rendering a 3D scene.
 * @example
 * ```typescript
 * const camera = new THREE.PerspectiveCamera(45, width / height, 1, 1000);
 * scene.add(camera);
 * ```
 * @see Example: {@link https://threejs.org/examples/#webgl_animation_skinning_blending | animation / skinning / blending }
 * @see Example: {@link https://threejs.org/examples/#webgl_animation_skinning_morph | animation / skinning / morph }
 * @see Example: {@link https://threejs.org/examples/#webgl_effects_stereo | effects / stereo }
 * @see Example: {@link https://threejs.org/examples/#webgl_interactive_cubes | interactive / cubes }
 * @see Example: {@link https://threejs.org/examples/#webgl_loader_collada_skinning | loader / collada / skinning }
 * @see {@link https://threejs.org/docs/index.html#api/en/cameras/PerspectiveCamera | Official Documentation}
 * @see {@link https://github.com/mrdoob/three.js/blob/master/src/cameras/PerspectiveCamera.js | Source}
 */
export class PerspectiveCamera extends Camera {
    /**
     * Creates a new {@link PerspectiveCamera}.
     * @remarks Together these define the camera's {@link https://en.wikipedia.org/wiki/Viewing_frustum | viewing frustum}.
     * @param fov Camera frustum vertical field of view. Default `50`.
     * @param aspect Camera frustum aspect ratio. Default `1`.
     * @param near Camera frustum near plane. Default `0.1`.
     * @param far Camera frustum far plane. Default `2000`.
     */
    constructor(fov?: number, aspect?: number, near?: number, far?: number);

    /**
     * Read-only flag to check if a given object is of type {@link Camera}.
     * @remarks This is a _constant_ value
     * @defaultValue `true`
     */
    readonly isPerspectiveCamera: true;

    /**
     * @override
     * @defaultValue `PerspectiveCamera`
     */
    override readonly type: string | "PerspectiveCamera";

    /**
     * Gets or sets the zoom factor of the camera.
     * @defaultValue `1`
     */
    zoom: number;

    /**
     * Camera frustum vertical field of view, from bottom to top of view, in degrees.
     * @remarks Expects a `Float`
     * @defaultValue `50`
     */
    fov: number;

    /**
     * Camera frustum aspect ratio, usually the canvas width / canvas height.
     * @remarks Expects a `Float`
     * @defaultValue `1`, _(square canvas)_.
     */
    aspect: number;

    /**
     * Camera frustum near plane.
     * @remarks The valid range is greater than `0` and less than the current value of the {@link far | .far} plane.
     * @remarks Note that, unlike for the {@link THREE.OrthographicCamera | OrthographicCamera}, `0` is **not** a valid value for a {@link PerspectiveCamera |PerspectiveCamera's}. near plane.
     * @defaultValue `0.1`
     * @remarks Expects a `Float`
     */
    near: number;

    /**
     * Camera frustum far plane.
     * @remarks Must be greater than the current value of {@link near | .near} plane.
     * @remarks Expects a `Float`
     * @defaultValue `2000`
     */
    far: number;

    /**
     * Object distance used for stereoscopy and depth-of-field effects.
     * @remarks This parameter does not influence the projection matrix unless a {@link THREE.StereoCamera | StereoCamera} is being used.
     * @remarks Expects a `Float`
     * @defaultValue `10`
     */
    focus: number;

    /**
     * Frustum window specification or null.
     * This is set using the {@link setViewOffset | .setViewOffset} method and cleared using {@link clearViewOffset | .clearViewOffset}.
     * @defaultValue `null`
     */
    view: null | {
        enabled: boolean;
        fullWidth: number;
        fullHeight: number;
        offsetX: number;
        offsetY: number;
        width: number;
        height: number;
    };

    /**
     * Film size used for the larger axis.
     * This parameter does not influence the projection matrix unless {@link filmOffset | .filmOffset} is set to a nonzero value.
     * @remarks Expects a `Float`
     * @defaultValue `35`, _millimeters_.
     */
    filmGauge: number;

    /**
     * Horizontal off-center offset in the same unit as {@link filmGauge | .filmGauge}.
     * @remarks Expects a `Float`
     * @defaultValue `0`
     */
    filmOffset: number;

    /**
     * Returns the focal length of the current {@link .fov | fov} in respect to {@link filmGauge | .filmGauge}.
     */
    getFocalLength(): number;

    /**
     * Sets the FOV by focal length in respect to the current {@link filmGauge | .filmGauge}.
     * @remarks By default, the focal length is specified for a `35mm` (full frame) camera.
     * @param focalLength Expects a `Float`
     */
    setFocalLength(focalLength: number): void;

    /**
     * Returns the current vertical field of view angle in degrees considering {@link zoom | .zoom}.
     */
    getEffectiveFOV(): number;

    /**
     * Returns the width of the image on the film
     * @remarks
     * If {@link aspect | .aspect}. is greater than or equal to one (landscape format), the result equals {@link filmGauge | .filmGauge}.
     */
    getFilmWidth(): number;

    /**
     * Returns the height of the image on the film
     * @remarks
     * If {@link aspect | .aspect}. is less than or equal to one (portrait format), the result equals {@link filmGauge | .filmGauge}.
     */
    getFilmHeight(): number;

    /**
     * Computes the 2D bounds of the camera's viewable rectangle at a given distance along the viewing direction.
     * Sets minTarget and maxTarget to the coordinates of the lower-left and upper-right corners of the view rectangle.
     */
    getViewBounds(distance: number, minTarget: Vector2, maxTarget: Vector2): void;

    /**
     * Computes the width and height of the camera's viewable rectangle at a given distance along the viewing direction.
     * Copies the result into the target Vector2, where x is width and y is height.
     */
    getViewSize(distance: number, target: Vector2): Vector2;

    /**
     * Sets an offset in a larger frustum.
     * @remarks
     * This is useful for multi-window or multi-monitor/multi-machine setups.
     *
     * For example, if you have 3x2 monitors and each monitor is _1920x1080_ and
     * the monitors are in grid like this
     * ```
     * ┌───┬───┬───┐
     * │ A │ B │ C │
     * ├───┼───┼───┤
     * │ D │ E │ F │
     * └───┴───┴───┘
     * ```
     * then for each monitor you would call it like this
     * ```typescript
     *   const w = 1920;
     *   const h = 1080;
     *   const fullWidth = w * 3;
     *   const fullHeight = h * 2;
     *
     *   // Monitor - A
     *   camera.setViewOffset( fullWidth, fullHeight, w * 0, h * 0, w, h );
     *   // Monitor - B
     *   camera.setViewOffset( fullWidth, fullHeight, w * 1, h * 0, w, h );
     *   // Monitor - C
     *   camera.setViewOffset( fullWidth, fullHeight, w * 2, h * 0, w, h );
     *   // Monitor - D
     *   camera.setViewOffset( fullWidth, fullHeight, w * 0, h * 1, w, h );
     *   // Monitor - E
     *   camera.setViewOffset( fullWidth, fullHeight, w * 1, h * 1, w, h );
     *   // Monitor - F
     *   camera.setViewOffset( fullWidth, fullHeight, w * 2, h * 1, w, h );
     * ```
     * Note there is no reason monitors have to be the same size or in a grid.
     * @param fullWidth Full width of multiview setup Expects a `Float`.
     * @param fullHeight Full height of multiview setup Expects a `Float`.
     * @param x Horizontal offset of subcamera Expects a `Float`.
     * @param y Vertical offset of subcamera Expects a `Float`.
     * @param width Width of subcamera Expects a `Float`.
     * @param height Height of subcamera Expects a `Float`.
     */
    setViewOffset(fullWidth: number, fullHeight: number, x: number, y: number, width: number, height: number): void;

    /**
     * Removes any offset set by the {@link setViewOffset | .setViewOffset} method.
     */
    clearViewOffset(): void;

    /**
     * Updates the camera projection matrix
     * @remarks Must be called after any change of parameters.
     */
    updateProjectionMatrix(): void;

    /**
     * @deprecated Use {@link PerspectiveCamera.setFocalLength | .setFocalLength()} and {@link PerspectiveCamera.filmGauge | .filmGauge} instead.
     */
    setLens(focalLength: number, frameHeight?: number): void;

    toJSON(meta?: JSONMeta): PerspectiveCameraJSON;
}
