import { boundingClientRectCache } from '../BoundingClientRectCache';
import { Vector3, Nullable, TargetCamera, Camera, Matrix } from '../loader/babylonjs-import';
import { MutableShallow } from '../loader/bim-format-types';

/**
 * Is the snapshot of a camera state. The snapshot can be used to check if the camera state
 * (rotation, position etc.) has changed since the snapshot was created {@link checkIsChanged}. The snapshot
 * can also be updated {@link refresh} so it matches the {@link Camera} from which it was created.
 */
export interface TwinfinityCameraStateSnapshot {
    /**
     * {@link Camera} to which the state snapshot is linked.
     */
    readonly camera: Camera;

    /**
     * `true` if camera has changed state since last time {@link refresh} was called, otherwise `false`
     */
    readonly isChanged: boolean;

    /**
     * Timestamp when last camera change check was true.
     */
    readonly lastChangeTimestamp: number;

    /**
     * Timestamp when camera state was last checked.
     */
    readonly lastCheckTimestamp: number;

    /**
     * Resets the camera state so {@link isChanged} is set to `false` (ie we refresh the snapshot so it is equal to
     * the state of {@link camera}).
     * @returns `true` if {@link isChanged} was `true` when call was made. Otherwise `false`
     */
    refresh(): boolean;

    /**
     * `true` if camera has changed state since last time {@link refresh} or {@link checkIsChanged} was called, otherwise `false`
     * @param checkOnlyDimensions A flag indicating if to check only if the camera dimensions changed, if set to true only width/height of the camera is checked, otherwise translation and rotation of the camera is checked too
     */
    checkIsChanged(checkOnlyDimensions?: boolean): boolean;
}

export class TwinfinityDefaultCameraStateSnapshot implements TwinfinityCameraStateSnapshot {
    private readonly _position = Vector3.Zero();

    private _minZ = 0;
    private _maxZ = 0;
    private _mode = 0;
    private _fov = 0;
    private _orthoBottom: Nullable<number> = null;
    private _orthoTop: Nullable<number> = null;
    private _orthoLeft: Nullable<number> = null;
    private _orthoRight: Nullable<number> = null;
    private _canvasClientRect: Omit<MutableShallow<DOMRect>, 'x' | 'y' | 'toJSON'> = {
        bottom: 0,
        height: 0,
        left: 0,
        right: 0,
        top: 0,
        width: 0
    };
    private _viewMatrix = new Matrix();
    private _projectionMatrix = new Matrix();
    private _engineHardwareScaling: number;
    private _lastChangeTimestamp = -1;
    private _lastCheckTimestamp = -1;

    public constructor(public readonly camera: Camera) {}

    /**
     * Checks if the camera has changed since the last time _copyCameraStateToSelf(this.camera) was called. If checkOnlyDimensions is true,
     * only the camera dimensions are checked for differences. If checkOnlyDimensions is false, camera dimensions, scale, rotation and translation is checked.
     * @param checkOnlyDimensions Specifies whether to only check camera dimensions (width, height, stuff like that) or to check camera dimensions in addition to everything else
     * @returns A boolean which is true if the current camera state is different from when _copyCameraStateToSelf(this.camera) was last called
     */
    public checkIsChanged(checkOnlyDimensions = false): boolean {
        const engine = this.camera.getEngine();
        const clientRect = boundingClientRectCache.getOrAdd(engine.getRenderingCanvas()!);
        const clientHardwareScaling = engine.getHardwareScalingLevel();

        const isDimensionsChanged =
            this._canvasClientRect.bottom !== clientRect.bottom ||
            this._canvasClientRect.height !== clientRect.height ||
            this._canvasClientRect.left !== clientRect.left ||
            this._canvasClientRect.right !== clientRect.right ||
            this._canvasClientRect.top !== clientRect.top ||
            this._canvasClientRect.width !== clientRect.width ||
            this.camera.mode !== this._mode ||
            this._engineHardwareScaling !== clientHardwareScaling;

        return (
            (isDimensionsChanged && checkOnlyDimensions) ||
            isDimensionsChanged ||
            !this._viewMatrix.equals(this.camera.getViewMatrix()) ||
            !this._projectionMatrix.equals(this.camera.getProjectionMatrix()) ||
            this.camera.minZ !== this._minZ ||
            this.camera.maxZ !== this._maxZ ||
            this.camera.fov !== this._fov ||
            this.camera.orthoBottom !== this._orthoBottom ||
            this.camera.orthoTop !== this._orthoTop ||
            this.camera.orthoLeft !== this._orthoLeft ||
            this.camera.orthoRight !== this._orthoRight ||
            !this.camera.position.equalsWithEpsilon(this._position)
        );
    }

    public get isChanged(): boolean {
        return this.checkIsChanged(false);
    }

    /**
     * @returns timestamp in miliseconds of when the last refresh that returned true was done.
     */
    public get lastChangeTimestamp(): number {
        return this._lastChangeTimestamp;
    }

    /**
     * @returns timestamp in miliseconds of when the last refresh was done.
     */
    public get lastCheckTimestamp(): number {
        return this._lastCheckTimestamp;
    }

    public refresh(): boolean {
        const ret = this.isChanged;

        this._lastCheckTimestamp = performance.now();
        if (ret) {
            this._lastChangeTimestamp = this._lastCheckTimestamp;
        }

        this._copyCameraStateToSelf(this.camera);
        return ret;
    }

    protected _copyCameraStateToSelf(camera: Camera): void {
        this._viewMatrix.copyFrom(camera.getViewMatrix());
        this._projectionMatrix.copyFrom(camera.getProjectionMatrix());

        const engine = this.camera.getEngine();
        const clientRect = boundingClientRectCache.getOrAdd(engine.getRenderingCanvas()!);
        if (clientRect) {
            this._canvasClientRect.bottom = clientRect.bottom;
            this._canvasClientRect.height = clientRect.height;
            this._canvasClientRect.left = clientRect.left;
            this._canvasClientRect.right = clientRect.right;
            this._canvasClientRect.top = clientRect.top;
            this._canvasClientRect.width = clientRect.width;
        } else {
            for (const p of Object.keys(this._canvasClientRect)) {
                (this._canvasClientRect as any)[p] = 0;
            }
        }

        this._position.copyFrom(camera.position);
        this._minZ = camera.minZ;
        this._maxZ = camera.maxZ;
        this._mode = camera.mode;
        this._fov = camera.fov;
        this._orthoBottom = camera.orthoBottom;
        this._orthoTop = camera.orthoTop;
        this._orthoLeft = camera.orthoLeft;
        this._orthoRight = camera.orthoRight;
        this._engineHardwareScaling = camera.getEngine().getHardwareScalingLevel();
    }
}

export class TwinfinityTargetCameraStateSnapshot extends TwinfinityDefaultCameraStateSnapshot {
    readonly _rotation = Vector3.Zero();
    readonly _target = Vector3.Zero();

    public constructor(private readonly _targetCamera: TargetCamera) {
        super(_targetCamera);
    }

    public get isChanged(): boolean {
        return (
            super.isChanged ||
            !this._rotation.equalsWithEpsilon(this._targetCamera.rotation) ||
            !this._target.equalsWithEpsilon(this._targetCamera.target)
        );
    }

    protected _copyCameraStateToSelf(state: TargetCamera): void {
        super._copyCameraStateToSelf(state);
        this._rotation.copyFrom(state.rotation);
        this._target.copyFrom(state.target);
    }
}
