import { CameraHelper, PerspectiveCamera, Quaternion } from "three"
import ObjectManager from "../ObjectManager"
import { debounceInstance, last } from "@lincode/utils"
import { scaleDown } from "../../../engine/constants"
import {
    ray,
    euler,
    quaternion,
    quaternion_,
    halfPi
} from "../../utils/reusables"
import pillShape from "../PhysicsObjectManager/cannon/shapes/pillShape"
import ICameraBase, { MouseControl } from "../../../interface/ICameraBase"
import { deg2Rad, Point3d } from "@lincode/math"
import { MIN_POLAR_ANGLE, MAX_POLAR_ANGLE } from "../../../globals"
import { Reactive } from "@lincode/reactivity"
import MeshItem from "../MeshItem"
import { Cancellable } from "@lincode/promiselikes"
import mainCamera from "../../../engine/mainCamera"
import scene from "../../../engine/scene"
import {
    onSelectionTarget,
    emitSelectionTarget
} from "../../../events/onSelectionTarget"
import { bokehDefault } from "../../../states/useBokeh"
import { bokehApertureDefault } from "../../../states/useBokehAperture"
import { bokehFocusDefault } from "../../../states/useBokehFocus"
import { bokehMaxBlurDefault } from "../../../states/useBokehMaxBlur"
import { setBokehRefresh } from "../../../states/useBokehRefresh"
import { pushCameraList, pullCameraList } from "../../../states/useCameraList"
import { getCameraRendered } from "../../../states/useCameraRendered"
import {
    pullCameraStack,
    getCameraStack,
    pushCameraStack
} from "../../../states/useCameraStack"
import makeCameraSprite from "../utils/makeCameraSprite"
import getWorldPosition from "../../utils/getWorldPosition"
import getWorldQuaternion from "../../utils/getWorldQuaternion"
import getWorldDirection from "../../utils/getWorldDirection"

export default abstract class CameraBase<T extends PerspectiveCamera>
    extends ObjectManager
    implements ICameraBase
{
    protected override _physicsShape = pillShape

    protected midObject3d = this.outerObject3d

    public constructor(protected camera: T) {
        super()
        this.object3d.add(camera)
        camera.userData.manager = this

        pushCameraList(camera)
        this.then(() => {
            pullCameraStack(camera)
            pullCameraList(camera)
        })

        this.createEffect(() => {
            if (
                getCameraRendered() !== mainCamera ||
                getCameraRendered() === camera
            )
                return

            const helper = new CameraHelper(camera)
            scene.add(helper)

            const sprite = makeCameraSprite()
            helper.add(sprite.outerObject3d)

            const handle = onSelectionTarget(({ target }) => {
                target === sprite && emitSelectionTarget(this as any)
            })
            return () => {
                helper.dispose()
                scene.remove(helper)

                sprite.dispose()
                handle.cancel()
            }
        }, [getCameraRendered])
    }

    public override lookAt(target: MeshItem | Point3d): void
    public override lookAt(x: number, y: number | undefined, z: number): void
    public override lookAt(a0: any, a1?: any, a2?: any) {
        super.lookAt(a0, a1, a2)
        const angle = euler.setFromQuaternion(this.outerObject3d.quaternion)
        angle.x += Math.PI
        angle.z += Math.PI
        this.outerObject3d.setRotationFromEuler(angle)
    }

    public get fov() {
        return this.camera.fov
    }
    public set fov(val) {
        this.camera.fov = val
        this.camera.updateProjectionMatrix?.()
    }

    public get zoom() {
        return this.camera.zoom
    }
    public set zoom(val) {
        this.camera.zoom = val
        this.camera.updateProjectionMatrix?.()
    }

    public get near() {
        return this.camera.near
    }
    public set near(val) {
        this.camera.near = val
        this.camera.updateProjectionMatrix?.()
    }

    public get far() {
        return this.camera.far
    }
    public set far(val) {
        this.camera.far = val
        this.camera.updateProjectionMatrix?.()
    }

    public get active() {
        return last(getCameraStack()) === this.camera
    }
    public set active(val) {
        pullCameraStack(this.camera)
        val && pushCameraStack(this.camera)
    }

    public get transition() {
        return this.camera.userData.transition as boolean | number | undefined
    }
    public set transition(val) {
        this.camera.userData.transition = val
    }

    public get bokeh() {
        return this.camera.userData.bokeh ?? bokehDefault
    }
    public set bokeh(val) {
        this.camera.userData.bokeh = val
        setBokehRefresh({})
    }

    public get bokehFocus() {
        return this.camera.userData.bokehFocus ?? bokehFocusDefault
    }
    public set bokehFocus(val) {
        this.camera.userData.bokehFocus = val
        setBokehRefresh({})
    }

    public get bokehMaxBlur() {
        return this.camera.userData.bokehMaxBlur ?? bokehMaxBlurDefault
    }
    public set bokehMaxBlur(val) {
        this.camera.userData.bokehMaxBlur = val
        setBokehRefresh({})
    }

    public get bokehAperture() {
        return this.camera.userData.bokehAperture ?? bokehApertureDefault
    }
    public set bokehAperture(val) {
        this.camera.userData.bokehAperture = val
        setBokehRefresh({})
    }

    protected override getRay() {
        return ray.set(
            getWorldPosition(this.camera),
            getWorldDirection(this.camera)
        )
    }

    public override append(object: MeshItem) {
        this._append(object)
        this.camera.add(object.outerObject3d)
    }

    public override attach(object: MeshItem) {
        this._append(object)
        this.camera.attach(object.outerObject3d)
    }

    public override get width() {
        return super.width
    }
    public override set width(val) {
        const num = val * scaleDown
        this.object3d.scale.x = num
        this.camera.scale.x = 1 / num
    }

    public override get height() {
        return super.height
    }
    public override set height(val) {
        const num = val * scaleDown
        this.object3d.scale.y = num
        this.camera.scale.y = 1 / num
    }

    public override get depth() {
        return super.depth
    }
    public override set depth(val) {
        const num = val * scaleDown
        this.object3d.scale.z = num
        this.camera.scale.z = 1 / num
    }

    protected orbitMode?: boolean

    private _gyrate(movementX: number, movementY: number, inner?: boolean) {
        const manager = inner ? this.object3d : this.midObject3d
        euler.setFromQuaternion(manager.quaternion)

        euler.y -= movementX * 0.002
        euler.y = Math.max(
            halfPi - this._maxAzimuthAngle * deg2Rad,
            Math.min(halfPi - this._minAzimuthAngle * deg2Rad, euler.y)
        )

        euler.x -= movementY * 0.002
        euler.x = Math.max(
            halfPi - this._maxPolarAngle * deg2Rad,
            Math.min(halfPi - this._minPolarAngle * deg2Rad, euler.x)
        )

        manager.setRotationFromEuler(euler)
        !inner && this.rotationUpdate?.updateXYZ()
    }

    private gyrateHandle?: Cancellable
    public gyrate(movementX: number, movementY: number, noDamping?: boolean) {
        if (this.enableDamping) {
            movementX *= 0.5
            movementY *= 0.5
        }
        if (this.orbitMode) this._gyrate(movementX, movementY)
        else {
            this._gyrate(movementX, 0)
            this._gyrate(0, movementY, true)
        }
        if (!this.enableDamping || noDamping || !(movementX || movementY))
            return

        this.gyrateHandle?.cancel()

        let factor = 1
        const handle = (this.gyrateHandle = this.beforeRender(() => {
            factor *= 0.95
            this._gyrate(movementX * factor, movementY * factor)
            factor <= 0.001 && handle.cancel()
        }))
    }

    private static updateAngle = debounceInstance(
        (target: CameraBase<PerspectiveCamera>) => target.gyrate(0, 0),
        0,
        "trailing"
    )
    protected updateAngle() {
        CameraBase.updateAngle(this, this)
    }

    private _minPolarAngle = MIN_POLAR_ANGLE
    public get minPolarAngle() {
        return this._minPolarAngle
    }
    public set minPolarAngle(val) {
        this._minPolarAngle = val
        this.updateAngle()
    }

    private _maxPolarAngle = MAX_POLAR_ANGLE
    public get maxPolarAngle() {
        return this._maxPolarAngle
    }
    public set maxPolarAngle(val) {
        this._maxPolarAngle = val
        this.updateAngle()
    }

    private _minAzimuthAngle = -Infinity
    public get minAzimuthAngle() {
        return this._minAzimuthAngle
    }
    public set minAzimuthAngle(val) {
        this._minAzimuthAngle = val
        this.updateAngle()
    }

    private _maxAzimuthAngle = Infinity
    public get maxAzimuthAngle() {
        return this._maxAzimuthAngle
    }
    public set maxAzimuthAngle(val) {
        this._maxAzimuthAngle = val
        this.updateAngle()
    }

    public setPolarAngle(angle: number) {
        const { _minPolarAngle, _maxPolarAngle } = this
        this.minPolarAngle = this.maxPolarAngle = angle
        this.queueMicrotask(() => {
            this.minPolarAngle = _minPolarAngle
            this.maxPolarAngle = _maxPolarAngle
        })
    }

    public setAzimuthAngle(angle: number) {
        const { _minAzimuthAngle, _maxAzimuthAngle } = this
        this.minAzimuthAngle = this.maxAzimuthAngle = angle
        this.queueMicrotask(() => {
            this.minAzimuthAngle = _minAzimuthAngle
            this.maxAzimuthAngle = _maxAzimuthAngle
        })
    }

    private _polarAngle?: number
    public get polarAngle() {
        return this._polarAngle
    }
    public set polarAngle(val) {
        this._polarAngle = val
        val && this.setPolarAngle(val)
    }

    private _azimuthAngle?: number
    public get azimuthAngle() {
        return this._azimuthAngle
    }
    public set azimuthAngle(val) {
        this._azimuthAngle = val
        val && this.setAzimuthAngle(val)
    }

    public enableDamping = false

    protected mouseControlState = new Reactive<MouseControl>(false)
    private mouseControlInit?: boolean
    public get mouseControl() {
        return this.mouseControlState.get()
    }
    public set mouseControl(val) {
        this.mouseControlState.set(val)

        if (!val || this.mouseControlInit) return
        this.mouseControlInit = true

        import("./enableMouseControl").then((module) =>
            module.default.call(this)
        )
    }

    private _gyroControl?: boolean
    public get gyroControl() {
        return !!this._gyroControl
    }
    public set gyroControl(val) {
        this._gyroControl = val

        const deviceEuler = euler
        const deviceQuaternion = quaternion
        const screenTransform = quaternion_
        const worldTransform = new Quaternion(
            -Math.sqrt(0.5),
            0,
            0,
            Math.sqrt(0.5)
        )

        const quat = getWorldQuaternion(this.object3d)
        const orient = 0

        const cb = (e: DeviceOrientationEvent) => {
            this.object3d.quaternion.copy(quat)
            deviceEuler.set(
                (e.beta ?? 0) * deg2Rad,
                (e.alpha ?? 0) * deg2Rad,
                -(e.gamma ?? 0) * deg2Rad,
                "YXZ"
            )

            this.object3d.quaternion.multiply(
                deviceQuaternion.setFromEuler(deviceEuler)
            )

            const minusHalfAngle = -orient * 0.5
            screenTransform.set(
                0,
                Math.sin(minusHalfAngle),
                0,
                Math.cos(minusHalfAngle)
            )

            this.object3d.quaternion.multiply(screenTransform)
            this.object3d.quaternion.multiply(worldTransform)
        }
        val && window.addEventListener("deviceorientation", cb)
        this.cancelHandle(
            "gyroControl",
            val &&
                (() =>
                    new Cancellable(() =>
                        window.removeEventListener("deviceorientation", cb)
                    ))
        )
    }
}
