import { Vector3, BoundingInfo, FxaaPostProcess, Axis } from '../loader/babylonjs-import';
import { PivotTargetCamera } from './PivotTargetCamera';

import { TwinfinityViewer } from '../loader/twinfinity-viewer';
import { TwinfinityCameraBehavior } from './TwinfinityCameraBehavior';

import './CameraExtensions';
import '../loader/SceneExtensions';
import { CameraDirection } from './CameraExtensions';
import { PickOption } from '../loader/Selectables';
import { PickResult } from '../loader/PickResult';

export type Viewer = TwinfinityViewer;
export type AABB = BoundingInfo;

/** Camera projection modes */
export enum Projection {
    Perspective,
    Orthographic
}

/**
 * CameraMode can have one of three values
 * * 'fps' = first person camera, mouse control the rotation of the camera pivoting around itself. Move around with keyboard keys W, A, S and D.
 * * 'pivot' = Left mouse button intersects BIM model for finding a pivot point. This point is then used for rotation calculation camera about pivot.
 * * 'all' = 'pivot' by default, 'fps' when holding the right mouse button.
 *
 * Middle mouse button pans the camera view and scroll wheel controls zoom.
 */
export type CameraMode = 'fps' | 'pivot' | 'all';

export enum PivotMode {
    Auto,
    Fixed
}

/** Contains options for toggling camera functionality */
export interface CameraOptions {
    /** If `true` then camera can be moved along its axes using a keyboard. Default is by arrow keys or WASD keys. */
    isKeyboardMoveEnabled: boolean;

    /** If `true` then camera can be panned. */
    isPanEnabled: boolean;

    /** If `true` then camera can be zoomed */
    isZoomEnabled: boolean;

    /** If `true` then camera can be rotated */
    isRotationEnabled: boolean;

    /** Determines mode of camera.  */
    mode: CameraMode;
}

export class ViewerCamera {
    public readonly activeCamera: PivotTargetCamera;

    private _projection: Projection;
    public static readonly cameraMinZ = 0.01;
    private _pivotCameraPostProcess: FxaaPostProcess;

    public constructor(private viewer: Viewer) {
        this._projection = Projection.Perspective;

        this.activeCamera = new PivotTargetCamera('camera', new Vector3(-1, 1, -1), viewer.scene);
        this.activeCamera.mode = this._projection;
        this.activeCamera.minZ = ViewerCamera.cameraMinZ; // 10 cm is enough
        this.activeCamera.maxZ = 10000; // TODO Should be adjusted so we only see world bounding box
        this.activeCamera.inertia = 0.6;
        this.activeCamera.attachControl();

        this.disposeAndReEnableFXAA();
    }

    /**
     * This method is to make sure that the FXAA filter is always the last postprocess to be applied. ATM Babylon has no real way of rearranging post processes
     */
    private disposeAndReEnableFXAA(): void {
        if (this._pivotCameraPostProcess) {
            this._pivotCameraPostProcess.dispose();
        }

        this._pivotCameraPostProcess = new FxaaPostProcess('fxaaPivotCamera', 1.0, this.activeCamera);
        this._pivotCameraPostProcess.samples = 4;
    }

    public get maxZ(): number {
        return this.activeCamera.maxZ;
    }

    public set maxZ(n: number) {
        this.activeCamera.maxZ = n;
    }

    public get minZ(): number {
        return this.activeCamera.minZ;
    }

    public set minZ(n: number) {
        this.activeCamera.minZ = n;
    }

    /**
     * Attaches a behavior to the camera.
     * @param behavior The behavior to be attached to the camera.
     * @returns true if the behavior was successfully attached to the camera, otherwise false.
     */
    public attachBehavior(behavior: TwinfinityCameraBehavior): boolean {
        const attachment = behavior.attach(this.activeCamera);

        if (attachment) {
            this.disposeAndReEnableFXAA();
        }

        return attachment;
    }

    /**
     * Detaches a behavior from the camera.
     * @param behavior The behavior to be detached from the camera.
     * @returns true if the behavior was successfully detached to the camera, otherwise false.
     */
    public detachBehavior(behavior: TwinfinityCameraBehavior): boolean {
        return behavior.detach(this.activeCamera);
    }

    public setPivot(mode: PivotMode, pivot?: Vector3): void {
        if (mode === PivotMode.Auto || !pivot) {
            this.activeCamera.twinfinity.pivot.isFrozen = false;
            this.activeCamera.twinfinity.pivot.update();
        } else {
            this.activeCamera.twinfinity.pivot.update({ target: pivot });
            this.activeCamera.twinfinity.pivot.isFrozen = true;
        }
    }

    public clearPivot(): void {
        this.setPivot(PivotMode.Auto);
    }

    public get position(): Vector3 {
        return this.activeCamera.position;
    }
    public set position(position: Vector3) {
        this.activeCamera.position.copyFrom(position);
        this.viewer.scene.render(true);
    }

    public get target(): Vector3 {
        return this.activeCamera.getTarget();
    }
    public set target(target: Vector3) {
        this.activeCamera.setTarget(target);
    }

    public get forwardVector(): Vector3 {
        return this.activeCamera.getDirection(Axis.Z);
    }

    public zoomToExtent(aabb: BoundingInfo, direction?: CameraDirection): void {
        this.activeCamera.twinfinity.pivot.zoomToExtent(aabb, direction);
    }

    public pick(o: PickOption): PickResult {
        return this.activeCamera.twinfinity.pick(o);
    }

    public get projection(): Projection {
        return this.activeCamera.mode;
    }

    public set projection(projection: Projection) {
        this.activeCamera.mode = projection;
        // force camera to render with new camera projection
        this.viewer.scene.render(true);
    }

    public get mode(): CameraMode {
        return this.options.mode;
    }

    public set mode(v: CameraMode) {
        this.options.mode = v;
    }

    public get topDownMode(): boolean {
        return this.activeCamera.topDownMode;
    }

    public set topDownMode(topDownMode: boolean) {
        this.activeCamera.topDownMode = topDownMode;
        this.viewer.scene.render(true);
    }

    public get options(): CameraOptions {
        return this.activeCamera.inputs;
    }
}
