import { Matrix, Ray, TargetCamera, Vector3, Camera, Scalar, Epsilon, Axis, Frustum } from '../loader/babylonjs-import';
import { intersectsPlaneAtToRef } from '../math';
import { getCanvasCenterCoordinatesToRef } from './Inputs/input-utils';
import { PivotPlane } from './PivotPlane';
import { CoordinateAxesRenderer } from './CoordinateAxesRenderer';
import { PredefinedCanvasPosition, PickOptionType } from '../loader/Selectables';
import { TwinfinityDefaultCameraStateSnapshot, TwinfinityTargetCameraStateSnapshot } from './TwinfinityCameraStateSnapshot';
import { PivotTargetCamera } from './PivotTargetCamera';
import { Extensions } from '../Extensions';
import { ShortFrustumProjectionMatrixState } from '../loader/ShortFrustumProjectionMatrixState';
import { PickResultEmpty, PickResultType } from '../loader/PickResult';
const cameraDirectionVector = {
    top: new Vector3(0, -1, 0),
    bottom: new Vector3(0, 1, 0),
    front: new Vector3(0, 0, -1),
    back: new Vector3(0, 0, 1),
    left: new Vector3(1, 0, 0),
    right: new Vector3(-1, 0, 0),
    default: new Vector3(1, -1, 1)
};
class TwinfinityCameraPivotImplementation {
    _camera;
    static _tmpPickingRay = Ray.Zero();
    static _tmpCurrentOnPivotPlane = Vector3.Zero();
    static _tmpMouseInWorld = Vector3.Zero();
    static _pivotPickOptions = {
        type: PickOptionType.Canvas,
        position: { x: 0, y: 0 },
        isAABBIntersectionEnabled: true,
        isCenterIntersectionFallbackEnabled: true,
        isGeometryIntersectionEnabled: true,
        isNearbyRenderedObjectsIntersectionFallbackEnabled: true
    };
    _pivotPlane = PivotPlane.Empty();
    _pivotPointRenderer;
    target = Vector3.Zero();
    isFrozen = false;
    constructor(_camera) {
        this._camera = _camera;
        // TODO Need to send in correct pivot point
        this._pivotPointRenderer = new CoordinateAxesRenderer(_camera.getScene(), () => this.target);
        const camera = this._camera;
        // Keep pivotplane and pivotpoint up to date with changes to camera viewmatrix (rotation, translation etc is included in this)
        let isUpdating = false;
        const cameraViewMatrixObserver = camera.onViewMatrixChangedObservable.add((camera, eventData) => {
            if (!isUpdating) {
                isUpdating = true; // avoid infinite recursion
                this._pivotPlane.update(this._camera);
            }
        });
        this._camera.onDispose = () => camera.onViewMatrixChangedObservable.remove(cameraViewMatrixObserver);
    }
    get signedDistanceToCamera() {
        return this._pivotPlane.signedDistanceTo(this._camera.globalPosition);
    }
    lookFrom(direction) {
        // TODO We must go from ortho to perspective abd then back to make this work
        const directionVector = cameraDirectionVector[direction] ?? cameraDirectionVector.default;
        const currentPosition = this._camera.globalPosition;
        const currentTarget = this._camera.target;
        const distance = Vector3.Distance(currentPosition, currentTarget);
        const newTargetDist = directionVector.scale(distance);
        this._camera.target = currentPosition.add(newTargetDist);
        this._camera.getViewMatrix(true);
    }
    zoomToExtent(aabb, direction) {
        const delta = aabb.maximum.subtract(aabb.minimum);
        const isAtleast2D = [delta.x > 0, delta.y > 0, delta.z > 0].filter(Boolean).length > 1;
        if (delta.x < 0 || delta.y < 0 || delta.z < 0 || !isAtleast2D) {
            return; // Do nothing because aabb is invalid
        }
        const min = aabb.minimum;
        const max = aabb.maximum;
        const center = aabb.boundingSphere.centerWorld.clone();
        const k = 2; //(direction ?? 'default') === 'default' ? 3.3 : 2;
        const size = Vector3.Distance(min, max) / k;
        center.y -= size / 6;
        const currentMode = this._camera.mode;
        this._camera.mode = Camera.PERSPECTIVE_CAMERA;
        let cameraForwardVector;
        if (direction === undefined) {
            cameraForwardVector = this._camera.getDirection(Axis.Z);
        }
        else {
            cameraForwardVector = cameraDirectionVector[direction ?? 'default'].negate().normalize();
        }
        // calculate half angle of field ov view
        const angle = this._camera.fov * 0.5; // (this._camera.fov * (180 / Math.PI)) / 2;
        const targetDistance = size / Math.tan(angle);
        const newCameraPosition = cameraForwardVector.scale(targetDistance);
        this._camera.position.copyFrom(center.add(newCameraPosition));
        this._camera.setTarget(center);
        this._camera.getViewMatrix(true); // Force recompute of word matrix etc
        this._camera.twinfinity.pivot.update({ target: center });
        this._camera.twinfinity.setOrtho({ distance: targetDistance });
        this._camera.getViewMatrix(true);
        this._camera.mode = currentMode;
        this._camera.getViewMatrix(true);
        // Pivotpoint is always initially at center
        // NOTE Is this needed anymore?
        //this._camera.getScene().render(true);
    }
    /** `true` if camera is currently in front of pivot plane */
    get isInFront() {
        this._pivotPlane.update(this._camera);
        return this._pivotPlane.isInFrontOfCamera(this._camera);
    }
    get normal() {
        return this._pivotPlane.normal;
    }
    update(o) {
        // TODO Should we use screenCoordinate?
        // Create a zoom vector. Hmm this becomes a bit odd. We already have a zoom vector. We get that when we do the
        // pickoperation. (x,y coordinate gives us it). Its actually not the result of the pick operation that should
        // determine the zoom vector!!! We should only use the zoom vector to calculate the DISTANCE!!! (By intersecting it with the pivotplane)
        if (o === undefined || this.isFrozen) {
            this._pivotPlane.update(this._camera);
            return;
        }
        if ('target' in o) {
            this.target.copyFrom(o.target);
            this._pivotPlane.update(this._camera, this.target);
            // TODO How do we calculate the zoomray here? I guess the easiest way is
            // to intersect with the plane? Hmm no!!
            // What is the zoomray here. We need to calculate a ray that goes from
            // center where camera is looking
        }
        else {
            // indata was a screen coordinate. Use it to pick something in the camera's view.
            const scene = this._camera.getScene();
            const pO = TwinfinityCameraPivotImplementation._pivotPickOptions;
            pO.position = o.canvasCoordinate;
            const pickResult = scene.twinfinity.viewer?.selectables.pick(pO) ?? PickResultEmpty.instance;
            if (pickResult.type === PickResultType.Empty) {
                // We have not direct position to use. Ensure that pivotplane is
                // rotated towards us. If it still is behind use we create
                // a new plane 2 meters in front of us. In both cases we perfrom a intersection on it to
                // get the coordinates of the target
                this._pivotPlane.update(this._camera);
                if (!this._pivotPlane.isInFrontOfCamera(this._camera)) {
                    this._pivotPlane.putInFrontOfCamera(this._camera);
                }
                this.intersectWithScreenCoordinateToRef(this.target, o.canvasCoordinate);
                return;
            }
            const { hitInfo } = pickResult;
            if (hitInfo.length > 0) {
                this.target.copyFrom(hitInfo[0].position);
                this._pivotPlane.update(this._camera, this.target);
                // We cannot be sure that the hitinfo actually represents a world coordinate below
                // the specified screenCoordinate (because the hit may actually be for a nearby object
                // due to the options (pO) we specified. Hence we need to reacalculate the pivot target
                // by intersecting the new pivotplane.
                this.intersectWithScreenCoordinateToRef(this.target, pO.position);
                // And then we update the plane again to use (possibly) new target.
                this._pivotPlane.update(this._camera, this.target);
                return;
            }
        }
    }
    zoom(deltaToUse, screenCoordinate) {
        // Move camera along zoom vector. When come up and are behinde pivot plane we will simply move at default (slow speed)
        const direction = Math.sign(deltaToUse);
        deltaToUse = Math.abs(deltaToUse);
        if (deltaToUse < this._camera.speed * Epsilon) {
            return;
        }
        if (this._camera.mode === Camera.ORTHOGRAPHIC_CAMERA) {
            //this.camera.getViewMatrix(true);
            //this.camera.computeWorldMatrix();
            const halfSizeX = Math.abs(this._camera.orthoRight - this._camera.orthoLeft);
            //const sizeXMinusOne = halfSizeX - 1;
            const speed = this._camera.twinfinity.computeOrthographicZoomSpeed(halfSizeX);
            let distanceToMove = halfSizeX * deltaToUse * speed;
            const minDistance = 0.01;
            distanceToMove =
                direction * Scalar.Clamp(distanceToMove, minDistance, Math.max(minDistance, halfSizeX / 2));
            const mouseInWorld = TwinfinityCameraPivotImplementation._tmpMouseInWorld;
            this.unprojectCurrentMouseCoordToRefToRef(screenCoordinate, mouseInWorld);
            this.zoomOrtho(distanceToMove, mouseInWorld);
        }
        else {
            const zoomRay = this._camera.twinfinity.createPickingRayToRef(TwinfinityCameraPivotImplementation._tmpPickingRay, screenCoordinate);
            let distanceToZoomTarget = zoomRay.intersectsPlane(this._pivotPlane);
            const minDistance = this._camera.minZ / 10.0;
            if (!distanceToZoomTarget || distanceToZoomTarget <= 0) {
                distanceToZoomTarget = minDistance;
            }
            const distanceToPivotPointAndBeyond = distanceToZoomTarget + 1;
            const speed = this._camera.twinfinity.computeOrthographicZoomSpeed(distanceToZoomTarget); //.computeLocalSpeed(distanceToPivotPoint, 0.5);
            let distanceToMove = distanceToPivotPointAndBeyond * deltaToUse * speed;
            const maxDistance = Math.max(minDistance, distanceToPivotPointAndBeyond / 2);
            distanceToMove = Scalar.Clamp(distanceToMove, minDistance, maxDistance);
            const dir = zoomRay.direction.scaleInPlace(distanceToMove);
            if (direction === -1) {
                dir.negateInPlace();
            }
            this._camera.position.addInPlace(dir); // new pos. Calculate change along old camera.y and camera.x
        }
    }
    pan(currentScreenCoordinate) {
        // Update plane to ensure that it is correct before we pan
        this._pivotPlane.update(this._camera);
        // TODO ensure that this.target is still on the plane!!! Or project target on plane
        const currentOnPivotPlane = TwinfinityCameraPivotImplementation._tmpCurrentOnPivotPlane;
        if (this.intersectWithScreenCoordinateToRef(currentOnPivotPlane, currentScreenCoordinate) > -1) {
            this._camera.position.subtractInPlace(currentOnPivotPlane.subtractInPlace(this.target));
            return true;
        }
        return false;
    }
    rotate(pitch, yaw) {
        // Calculate plane and rotate and the update plane?
        this._camera.cameraRotation.x += pitch;
        this._camera.cameraRotation.y += yaw;
    }
    get isVisible() {
        // Turn on pivotpoint visualization
        return this._pivotPointRenderer.isEnabled;
    }
    set isVisible(visible) {
        // Turn off pivotpoint visualization
        if (visible !== this.isVisible) {
            this._pivotPointRenderer.isEnabled = visible;
        }
    }
    intersectWithScreenCoordinateToRef(dst, o) {
        // Intersection pos in dst. Returns distance. If distance < 0 then there was no intersection
        // if no screenPos then ray is shot through center of canvas.
        this._camera.twinfinity.createPickingRayToRef(TwinfinityCameraPivotImplementation._tmpPickingRay, o);
        return intersectsPlaneAtToRef(TwinfinityCameraPivotImplementation._tmpPickingRay, this._pivotPlane, dst);
    }
    unprojectCurrentMouseCoordToRefToRef(screenCoordinate, dst) {
        // const cameraViewport = this._camera.viewport;
        const engine = this._camera.getEngine();
        Vector3.UnprojectFloatsToRef(screenCoordinate.x / engine.getHardwareScalingLevel(), screenCoordinate.y / engine.getHardwareScalingLevel(), 0, engine.getRenderWidth(), engine.getRenderHeight(), this._camera.getWorldMatrix(), this._camera.getViewMatrix(), this._camera.getProjectionMatrix(), dst);
    }
    zoomOrtho(deltaInMeters, mouseInWorld) {
        const camera = this._camera;
        if (camera) {
            const oldHorizontalDirection = Math.sign(camera.orthoLeft - camera.orthoRight);
            const oldVerticalDirection = Math.sign(camera.orthoTop - camera.orthoBottom);
            const sizeX = Math.abs((camera.orthoLeft ?? 0) - (camera.orthoRight ?? 0));
            const sizeY = Math.abs((camera.orthoTop ?? 0) - (camera.orthoBottom ?? 0));
            const aspectRatio = sizeY / sizeX;
            let fromCoord = camera.orthoLeft - mouseInWorld.x;
            let ratio = fromCoord / sizeX;
            const newOrthoLeft = camera.orthoLeft - ratio * deltaInMeters;
            fromCoord = camera.orthoRight - mouseInWorld.x;
            ratio = fromCoord / sizeX;
            const newOrthoRight = camera.orthoRight - ratio * deltaInMeters;
            fromCoord = camera.orthoTop - mouseInWorld.y;
            ratio = fromCoord / sizeY;
            const newOrthoTop = camera.orthoTop - ratio * aspectRatio * deltaInMeters;
            fromCoord = camera.orthoBottom - mouseInWorld.y;
            ratio = fromCoord / sizeY;
            const newOrthoBottom = camera.orthoBottom - ratio * aspectRatio * deltaInMeters;
            const newHorizontalDirection = Math.sign(newOrthoLeft - newOrthoRight);
            const newVerticalDirection = Math.sign(newOrthoTop - newOrthoBottom);
            if (newHorizontalDirection !== oldHorizontalDirection || newVerticalDirection !== oldVerticalDirection) {
                return;
            }
            camera.orthoBottom = newOrthoBottom;
            camera.orthoTop = newOrthoTop;
            camera.orthoLeft = newOrthoLeft;
            camera.orthoRight = newOrthoRight;
        }
    }
}
class TwinfinityCameraExtensionsImplementation {
    _camera;
    static _tmpIdentityMatrix = Matrix.Identity();
    static _tmpCenterPos = { x: 0, y: 0 };
    static _maxLocalCameraSpeed = 0.4;
    _projectionMatrixChangeCount = 0;
    _viewMatrixChangeCount = 0;
    _shortFrustumProjectionMatrixState;
    constructor(_camera) {
        this._camera = _camera;
        _camera.onProjectionMatrixChangedObservable.add(() => this._projectionMatrixChangeCount++);
        _camera.onViewMatrixChangedObservable.add(() => this._viewMatrixChangeCount++);
        this._shortFrustumProjectionMatrixState = new ShortFrustumProjectionMatrixState(_camera);
    }
    get viewer() {
        return this._camera.getScene().twinfinity.viewer;
    }
    get shortFrustum() {
        return this._shortFrustumProjectionMatrixState;
    }
    getFrustumToRef(dst) {
        Frustum.GetPlanesToRef(this._camera.getTransformationMatrix(), dst);
        return dst;
    }
    stateSnapshot() {
        let state;
        if (this._camera instanceof TargetCamera) {
            state = new TwinfinityTargetCameraStateSnapshot(this._camera);
        }
        else if (this._camera instanceof Camera) {
            state = new TwinfinityDefaultCameraStateSnapshot(this._camera);
        }
        else {
            throw new Error(`No supported.`);
        }
        state.refresh();
        return state;
    }
    pick(o) {
        return this._camera.getScene().twinfinity.viewer?.selectables.pick(o) ?? PickResultEmpty.instance;
    }
    setOrtho(o) {
        const engine = this._camera.getEngine();
        const aspectRatio = engine.getRenderWidth() / engine.getRenderHeight();
        if ('distance' in o) {
            const s = o.distance * Math.tan(this._camera.fov * 0.5);
            this._camera.orthoTop = s;
            this._camera.orthoBottom = -s;
            this._camera.orthoLeft = -s * aspectRatio;
            this._camera.orthoRight = s * aspectRatio;
        }
        else {
            const h = o.height * 0.5;
            this._camera.orthoTop = h;
            this._camera.orthoBottom = -h;
            this._camera.orthoLeft = -h * aspectRatio;
            this._camera.orthoRight = h * aspectRatio;
        }
    }
    get hasMovedInCurrentFrame() {
        return this._projectionMatrixChangeCount > 0 || this._viewMatrixChangeCount > 0;
    }
    clearHasMovedInCurrentFrame() {
        this._projectionMatrixChangeCount = 0;
        this._viewMatrixChangeCount = 0;
    }
    get perspectiveDistanceFromOrtho() {
        return (Math.abs(this._camera.orthoTop - this._camera.orthoBottom) * 0.5) / Math.tan(this._camera.fov * 0.5);
    }
    getOrthoFrustumSizeToRef(dst) {
        const halfWidth = this._camera.getEngine().getRenderWidth() / 2.0;
        const halfHeight = this._camera.getEngine().getRenderHeight() / 2.0;
        dst.x = Math.abs((this._camera.orthoRight ?? halfWidth) - (this._camera.orthoLeft ?? -halfWidth));
        dst.y = Math.abs((this._camera.orthoTop ?? halfHeight) - (this._camera.orthoBottom ?? -halfHeight));
        return dst;
    }
    computeOrthographicZoomSpeed(distance) {
        const engine = this._camera.getEngine();
        const cameraSpeed = this._camera instanceof TargetCamera ? this._camera.speed : 1;
        let speed = cameraSpeed;
        if (this._camera.mode === Camera.ORTHOGRAPHIC_CAMERA) {
            speed = cameraSpeed + Math.log10(Math.max(1, distance));
        }
        let ret = speed * Math.sqrt(engine.getDeltaTime() / (engine.getFps() * 100.0));
        ret = Math.min(TwinfinityCameraExtensionsImplementation._maxLocalCameraSpeed, ret);
        return ret;
    }
    createPickingRayToRef(dst, canvasPosition) {
        const scene = this._camera.getScene();
        let canvasPos;
        if (canvasPosition === PredefinedCanvasPosition.Center) {
            // Calculate middle screen coordinate if screenpos is missing.
            // That is where the camera is looking.
            canvasPos = getCanvasCenterCoordinatesToRef(scene, TwinfinityCameraExtensionsImplementation._tmpCenterPos);
        }
        else if (canvasPosition === PredefinedCanvasPosition.Mouse) {
            TwinfinityCameraExtensionsImplementation._tmpCenterPos.x = scene.pointerX;
            TwinfinityCameraExtensionsImplementation._tmpCenterPos.y = scene.pointerY;
            canvasPos = TwinfinityCameraExtensionsImplementation._tmpCenterPos;
        }
        else {
            canvasPos = canvasPosition;
        }
        scene.createPickingRayToRef(canvasPos.x, canvasPos.y, TwinfinityCameraExtensionsImplementation._tmpIdentityMatrix, dst, this._camera);
        return dst;
    }
}
class TwinfinityTargetCameraExtensionsImplementation extends TwinfinityCameraExtensionsImplementation {
    pivot;
    isFreeLook = false;
    constructor(camera) {
        super(camera);
        this.pivot = new TwinfinityCameraPivotImplementation(camera);
    }
}
Object.defineProperty(Camera.prototype, Extensions.TWINFINITY_PROPERTY, {
    enumerable: false,
    configurable: true,
    get: function () {
        const ret = new TwinfinityCameraExtensionsImplementation(this);
        Object.defineProperty(this, Extensions.TWINFINITY_PROPERTY, {
            value: ret
        });
        return ret;
    }
});
Object.defineProperty(TargetCamera.prototype, Extensions.TWINFINITY_PROPERTY, {
    enumerable: false,
    configurable: true,
    get: function () {
        const ret = new TwinfinityTargetCameraExtensionsImplementation(this);
        Object.defineProperty(this, Extensions.TWINFINITY_PROPERTY, {
            value: ret
        });
        return ret;
    }
});
Object.defineProperty(TargetCamera.prototype, 'mode', {
    enumerable: true,
    configurable: true,
    get: function () {
        const self = this;
        return self._twinfinityMode ?? Camera.PERSPECTIVE_CAMERA;
    },
    set: function (m) {
        const self = this;
        if (self._twinfinityMode === undefined && m === 0) {
            self._twinfiinityMode = 0; // This is during camera initialization (constructor).
            return;
        }
        if (self._twinfinityMode !== m) {
            this.twinfinity.pivot.update();
            if (m === Camera.PERSPECTIVE_CAMERA) {
                // this.minZ /= 10;
                // this.maxZ /= 10;
                const cameraForwardIntersectionWithPivotPlane = Vector3.Zero();
                this.twinfinity.pivot.intersectWithScreenCoordinateToRef(cameraForwardIntersectionWithPivotPlane, PredefinedCanvasPosition.Center);
                // When switching from ortho to perspective means that we have to calculate a new camera position
                // First calculate the distance in front of the pivot plane that the camera should be positioned at
                const perspectiveDistance = this.twinfinity.perspectiveDistanceFromOrtho;
                // position camera at perspectiveDistance from pivotplane along plane normal positioned at pivot point
                // intersection.
                cameraForwardIntersectionWithPivotPlane.addToRef(this.twinfinity.pivot.normal.scale(perspectiveDistance), this.position);
            }
            else {
                // Set size of orthographic frustum based on the distance to the pivotplane.
                this.twinfinity.setOrtho({ distance: this.twinfinity.pivot.signedDistanceToCamera });
            }
            // NEVER change order here. If we change _mode before then
            // get mode() will return the new mode in calculations above. Ie instead
            // of doing calculations in orthographic view when going from orthographic -> perspective
            // we would perform calculations in perspective view instead (which would be completely wrong).
            self._twinfinityMode = m;
            // After new _mode is sat, notify Observers that mode has change.
            // The get mode() will return the new mode!
            if (this instanceof PivotTargetCamera) {
                this.onMode.notifyObservers(this);
            }
        }
    }
});
//# sourceMappingURL=CameraExtensions.js.map