import { TwinfinityViewer } from './loader/twinfinity-viewer';
import { Vector3, BoundingInfo } from './loader/babylonjs-import';
import { CameraMode, CameraOptions, PivotMode, Projection } from './camera/ViewerCamera';
import { Bound } from './Types';
import { BimIfcObject } from './loader/bim-ifc-object';
import { TwinfinityCameraBehavior } from './camera/TwinfinityCameraBehavior';
import { CameraDirection } from './camera/CameraExtensions';
import { BimCoreApi } from './BimCoreApi';

/**
 * This interface defines a Camera look at, where two 3D points define positions for eye to look from and target to look towards.
 */
export interface CameraLookAt {
    eye: Vector3;
    target: Vector3;
}

/**
 * CameraProjection can have one of two strings.
 * * 'perspective' = normal geometry scale by distance.
 * * 'orthographic' = isometric view projection where distance to camera does not affect object scale.
 */
export type CameraProjection = 'perspective' | 'orthographic';

export class BimCamera {
    /**
     * @internal NOTE: Internal API. Subject to change. Use of these APIs in production applications is not supported.
     */
    public constructor(
        private readonly _viewer: TwinfinityViewer,
        private readonly _api: BimCoreApi
    ) {}

    /**
     * Sets camera projection.
     * @param projection The camera projection to set.
     */
    public setProjection(projection: CameraProjection): void {
        this._viewer.camera.projection =
            projection === 'perspective' ? Projection.Perspective : Projection.Orthographic;
    }

    /**
     * Sets camera position at eye and rotation to look towards target.
     * @param eye Point to look from.
     * @param target Point to look towards.
     */
    public lookAt(eye: Vector3, target: Vector3): void {
        this._viewer.camera.position = eye;
        this._viewer.camera.target = target;
    }

    /**
     * Sets camera rotation to look towards the specified direction and zooms to extent of visible objects.
     * @param direction The direction to look from (top = -Z, bottom = +Z, front = +Y, back = -Y, left = +X, right = -X).
     */
    public lookFrom(direction: CameraDirection): void {
        this._viewer.camera.activeCamera.twinfinity.pivot.lookFrom(direction);
    }

    /**
     * Zooms camera to the extent of the specified bounding box.
     * @param extent Bounding box in format [minX, minY, minZ, maxX, maxY, maxZ].
     * @param direction [Optional]. If set, the camera is positioned according to the direction during zoom. Otherwise, the current direction is kept.
     */
    public zoomToExtent(extent: Bound, direction?: CameraDirection): void {
        this._viewer.camera.zoomToExtent(
            new BoundingInfo(new Vector3(...extent.slice(0, 3)), new Vector3(...extent.slice(3, 6))),
            direction
        );
    }

    /**
     * Resets the camera to the default view. Camera is positioned to view entire building
     * @param direction Optional. Direction camera should look at building from. Default is a angled top position.
     */
    public defaultView(direction?: CameraDirection): void {
        if (direction === undefined) {
            direction = this._viewer.camera.topDownMode ? 'top' : 'default';
        }
        this._viewer.camera.zoomToExtent(this._api.ifc.regionBoundingInfo, direction);
    }

    /**
     * Returns camera forward vector.
     */
    public forwardVector(): Vector3 {
        return this._viewer.camera.forwardVector;
    }

    /**
     * @deprecated Use {@link cameraOptions}  instead
     * Locks camera to prevent user interaction.
     */
    public lock(): void {
        this.setLock(true);
    }

    /**
     * @deprecated Use Use {@link cameraOptions} instead
     * Unlocks camera to allow for user interaction.
     */
    public unlock(): void {
        this.setLock(false);
    }

    /**
     * @deprecated Use {@link cameraOptions} instead
     * Sets whether camera should prevent user interaction or not.
     * @param locked Whether camera should be locked or not.
     */
    public setLock(locked: boolean): void {
        this.cameraOptions.isPanEnabled = !locked;
        this.cameraOptions.isRotationEnabled = !locked;
        this.cameraOptions.isZoomEnabled = !locked;
        this.cameraOptions.isKeyboardMoveEnabled = !locked;
    }

    /**
     * @deprecated Use the mode property instead
     * Sets camera mode.
     * @param mode The camera mode to set.
     */
    public setMode(mode: CameraMode): void {
        this.mode = mode;
    }

    /**
     * Sets the camera mode (pivot, fps or all). See {@link CameraMode} for more information
     */
    public set mode(mode: CameraMode) {
        this._viewer.camera.mode = mode;
    }

    /**
     * Gets the camera mode (pivot, fps or all). See {@link CameraMode} for more information
     */
    public get mode(): CameraMode {
        return this._viewer.camera.mode;
    }

    /**
     * Sets the pivot point to use a (mouse, touch) cursor-based pivot point (Auto).
     *
     * @param mode Should be PivotMode.Auto
     */
    public setPivot(mode: PivotMode.Auto): void;
    /**
     * Sets the pivot point to a fixed point.
     * @param mode Should be PivotMode.Fixed
     * @param pivot The object or point that the camera should pivot around.
     */
    public setPivot(mode: PivotMode.Fixed, pivot: Vector3 | BimIfcObject): void;
    public setPivot(mode: PivotMode, pivot?: Vector3 | BimIfcObject): void {
        if (pivot instanceof BimIfcObject) {
            pivot = pivot.boundingInfo().boundingBox.center;
        }

        this._viewer.camera.setPivot(mode, pivot);
    }

    /**
     * Returns the current camera position and target as a string
     */
    public getViewAsString(): string {
        const p = this._viewer.camera.position.asArray();
        const t = this._viewer.camera.target.asArray();

        return [...p, ...t].map((x) => x.toFixed(2)).join(',');
    }

    /**
     * Sets the camera's position and target from given string
     * @param viewString A string generated by {@link getViewAsString}
     */
    public setViewFromString(viewString: string): void {
        const coordinates = viewString.split(',').map((s) => parseFloat(s));

        if (coordinates.length !== 6) {
            throw new Error('Malformed view string');
        }

        this._viewer.camera.position = new Vector3(...coordinates.slice(0, 3));
        this._viewer.camera.target = new Vector3(...coordinates.slice(3, 6));
    }

    /**
     * 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 {
        return this._viewer.camera.attachBehavior(behavior);
    }

    /**
     * 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 this._viewer.camera.detachBehavior(behavior);
    }

    /**
     * Gets or sets options for camera pivot control.
     */
    public get cameraOptions(): CameraOptions {
        return this._viewer.camera.options;
    }

    /**
     * @deprecated Same as {@link cameraOptions}.
     */
    public get pivotOptions(): CameraOptions {
        return this.cameraOptions;
    }
}
