import { last, omit } from "@lincode/utils"
import { FolderApi, Pane } from "tweakpane"
import settings from "../../api/settings"
import mainCamera from "../../engine/mainCamera"
import { setOrbitControls } from "../../states/useOrbitControls"
import { useEffect, useLayoutEffect, useState } from "preact/hooks"
import register from "preact-custom-element"
import {
    useSelectionTarget,
    useCameraList,
    useMultipleSelectionTargets,
    useCameraStack,
    useNodeEditor
} from "../states"
import { Cancellable } from "@lincode/promiselikes"
import {
    getSecondaryCamera,
    setSecondaryCamera
} from "../../states/useSecondaryCamera"
import { nonEditorSettings } from "../../api/serializer/types"
import { setupDefaults, setupSchema } from "../../interface/ISetup"
import mainOrbitCamera from "../../engine/mainOrbitCamera"
import getComponentName from "../utils/getComponentName"
import addInputs, { setProgrammatic } from "./addInputs"
import getParams from "./getParams"
import splitObject from "./splitObject"
import { onTransformControls } from "../../events/onTransformControls"
import assignIn from "./assignIn"
import { emitSceneGraphNameChange } from "../../events/onSceneGraphNameChange"
import { dummyDefaults } from "../../interface/IDummy"
import useInit from "../utils/useInit"
import {
    decreaseEditorMounted,
    increaseEditorMounted
} from "../../states/useEditorMounted"
import useHotkeys from "./useHotkeys"

Object.assign(dummyDefaults, {
    stride: { x: 0, y: 0 }
})

const Editor = () => {
    const elRef = useInit()
    useHotkeys()

    useLayoutEffect(() => {
        window.onbeforeunload = confirmExit
        function confirmExit() {
            return "Are you sure you want to close the current page?"
        }

        mainOrbitCamera.active = true
        setOrbitControls(true)
        settings.gridHelper = true
        increaseEditorMounted()

        return () => {
            setOrbitControls(false)
            settings.gridHelper = false
            decreaseEditorMounted()
        }
    }, [])

    const [cameraStack] = useCameraStack()
    const camera = last(cameraStack)!

    const [selectionTarget] = useSelectionTarget()
    const [multipleSelectionTargets] = useMultipleSelectionTargets()
    const [cameraList] = useCameraList()

    const [pane, setPane] = useState<Pane>()
    const [cameraFolder, setCameraFolder] = useState<FolderApi>()

    useLayoutEffect(() => {
        if (!pane || !cameraFolder) return

        const mainCameraName = "editor camera"

        const options = cameraList.reduce<Record<string, any>>(
            (acc, cam, i) => {
                acc[
                    i === 0
                        ? mainCameraName
                        : getComponentName(cam.userData.manager)
                ] = i
                return acc
            },
            {}
        )
        const cameraInput = pane.addInput(
            { camera: cameraList.indexOf(camera) },
            "camera",
            { options }
        )
        cameraFolder.add(cameraInput)
        cameraInput.on("change", ({ value }) => {
            cameraList[value].userData.manager.active = true
        })

        const secondaryOptions: any = {
            none: 0,
            ...omit(options, mainCameraName)
        }
        const secondaryCameraInput = pane.addInput(
            {
                "secondary camera": cameraList.indexOf(
                    getSecondaryCamera() ?? mainCamera
                )
            },
            "secondary camera",
            { options: secondaryOptions }
        )
        cameraFolder.add(secondaryCameraInput)
        secondaryCameraInput.on("change", ({ value }) =>
            setSecondaryCamera(value === 0 ? undefined : cameraList[value])
        )

        return () => {
            cameraInput.dispose()
            secondaryCameraInput.dispose()
        }
    }, [pane, cameraFolder, cameraList, camera])

    useEffect(() => {
        const el = elRef.current
        if (!el) return

        const pane = new Pane({ container: el })
        setPane(pane)
        setCameraFolder(pane.addFolder({ title: "camera" }))

        if (!selectionTarget) {
            const [editorParams, editorRest] = splitObject(
                omit(
                    getParams(setupSchema, setupDefaults, settings),
                    nonEditorSettings
                ),
                ["gridHelper", "gridHelperSize"]
            )
            addInputs(pane, "editor", settings, setupDefaults, editorParams)

            const [rendererParams, rendererRest] = splitObject(editorRest, [
                "antiAlias",
                "pixelRatio",
                "fps",
                "logarithmicDepth",
                "pbr"
            ])
            addInputs(pane, "renderer", settings, setupDefaults, rendererParams)

            const [sceneParams, sceneRest] = splitObject(rendererRest, [
                "exposure",
                "defaultLight",
                "shadowDistance",
                "shadowResolution",
                "shadowBias",
                "skybox",
                "texture",
                "color"
            ])
            addInputs(
                pane,
                "lighting & environment",
                settings,
                setupDefaults,
                sceneParams
            )

            const [effectsParams, effectsRest] = splitObject(sceneRest, [
                "ambientOcclusion",
                "bloom",
                "bloomStrength",
                "bloomRadius",
                "bloomThreshold",
                "bokeh",
                "bokehAperture",
                "bokehFocus",
                "bokehMaxBlur",
                "lensDistortion",
                "lensIor",
                "lensBand",
                "motionBlur",
                "motionBlurStrength"
            ])
            addInputs(pane, "effects", settings, setupDefaults, effectsParams)

            const [outlineParams, outlineRest] = splitObject(effectsRest, [
                "outlineColor",
                "outlineHiddenColor",
                "outlinePattern",
                "outlinePulse",
                "outlineStrength",
                "outlineThickness"
            ])
            addInputs(
                pane,
                "outline effect",
                settings,
                setupDefaults,
                outlineParams
            )

            const [physicsParams, physicsRest] = splitObject(outlineRest, [
                "gravity",
                "repulsion",
                "centripetal"
            ])
            addInputs(pane, "physics", settings, setupDefaults, physicsParams)

            Object.keys(physicsRest).length &&
                addInputs(
                    pane,
                    "settings",
                    settings,
                    setupDefaults,
                    physicsRest
                )

            return () => {
                pane.dispose()
            }
        }

        const target = selectionTarget as any
        const handle = new Cancellable()

        if (!multipleSelectionTargets.length) {
            const { schema, defaults, componentName } = target.constructor

            const [generalParams, generalRest] = splitObject(
                omit(getParams(schema, defaults, target), [
                    "rotation",
                    "innerRotation",
                    "frustumCulled",
                    "minAzimuthAngle",
                    "maxAzimuthAngle"
                ]),
                ["name", "id", "physics", "gravity"]
            )
            if (generalParams) {
                const { name: nameInput } = addInputs(
                    pane,
                    "general",
                    target,
                    defaults,
                    generalParams
                )
                nameInput?.on("change", () => emitSceneGraphNameChange())
            }

            const [transformParams0, transformRest] = splitObject(generalRest, [
                "x",
                "y",
                "z",
                "rotationX",
                "rotationY",
                "rotationZ",
                "scale",
                "scaleX",
                "scaleY",
                "scaleZ",
                "innerX",
                "innerY",
                "innerZ",
                "innerRotationX",
                "innerRotationY",
                "innerRotationZ",
                "width",
                "height",
                "depth"
            ])
            if (transformParams0) {
                const [innerTransformParams, transformParams] = splitObject(
                    transformParams0,
                    [
                        "innerX",
                        "innerY",
                        "innerZ",
                        "innerRotationX",
                        "innerRotationY",
                        "innerRotationZ",
                        "width",
                        "height",
                        "depth"
                    ]
                )
                addInputs(pane, "transform", target, defaults, transformParams)
                innerTransformParams &&
                    addInputs(
                        pane,
                        "inner transform",
                        target,
                        defaults,
                        innerTransformParams
                    )

                handle.watch(
                    onTransformControls(() => {
                        setProgrammatic()
                        assignIn(transformParams, target, [
                            "x",
                            "y",
                            "z",
                            "rotationX",
                            "rotationY",
                            "rotationZ",
                            "scaleX",
                            "scaleY",
                            "scaleZ"
                        ])
                        pane.refresh()
                    })
                )
            }

            const [displayParams, displayRest] = splitObject(transformRest, [
                "visible",
                "innerVisible",
                "castShadow",
                "receiveShadow"
            ])
            displayParams &&
                addInputs(pane, "display", target, defaults, displayParams)

            const [effectsParams, effectsRest] = splitObject(displayRest, [
                "bloom",
                "outline"
            ])
            effectsParams &&
                addInputs(pane, "effects", target, defaults, effectsParams)

            const [animationParams, animationRest] = splitObject(effectsRest, [
                "animation",
                "animationPaused",
                "animationRepeat"
            ])
            animationParams &&
                addInputs(pane, "animation", target, defaults, animationParams)

            const [adjustMaterialParams, adjustMaterialRest] = splitObject(
                animationRest,
                [
                    "metalnessFactor",
                    "roughnessFactor",
                    "opacityFactor",
                    "envFactor",
                    "adjustColor",
                    "reflection",
                    "illumination",
                    "toon"
                ]
            )
            adjustMaterialParams &&
                addInputs(
                    pane,
                    "adjust material",
                    target,
                    defaults,
                    adjustMaterialParams
                )

            const [materialParams, materialRest] = splitObject(
                adjustMaterialRest,
                [
                    "fog",
                    "opacity",
                    "color",
                    "texture",
                    "textureRepeat",
                    "textureFlipY",
                    "textureRotation",
                    "videoTexture",
                    "wireframe"
                ]
            )
            materialParams &&
                addInputs(pane, "material", target, defaults, materialParams)

            const [pbrMaterialParams, pbrMaterialRest] = splitObject(
                materialRest,
                [
                    "metalnessMap",
                    "metalness",
                    "roughnessMap",
                    "roughness",
                    "normalMap",
                    "normalScale",
                    "normalMapType",
                    "bumpMap",
                    "bumpScale",
                    "displacementMap",
                    "displacementScale",
                    "displacementBias",
                    "aoMap",
                    "aoMapIntensity",
                    "lightMap",
                    "lightMapIntensity",
                    "emissiveMap",
                    "emissiveIntensity",
                    "emissiveColor",
                    "emissive",
                    "envMap",
                    "alphaMap"
                ]
            )
            pbrMaterialParams &&
                addInputs(
                    pane,
                    "pbr material",
                    target,
                    defaults,
                    pbrMaterialParams
                )

            if (componentName === "dummy") {
                pbrMaterialRest.stride = {
                    x: target.strideRight,
                    y: -target.strideForward
                }
                const { stride: strideInput } = addInputs(
                    pane,
                    componentName,
                    target,
                    defaults,
                    pbrMaterialRest
                )
                strideInput.on("change", ({ value }) => {
                    Object.assign(pbrMaterialRest, {
                        strideForward: -value.y,
                        strideRight: value.x
                    })
                    pane.refresh()
                })
            } else if (Object.keys(pbrMaterialRest).length)
                addInputs(
                    pane,
                    componentName,
                    target,
                    defaults,
                    pbrMaterialRest
                )
        }

        return () => {
            handle.cancel()
            pane.dispose()
        }
    }, [selectionTarget, multipleSelectionTargets])

    return (
        <div
            ref={elRef}
            className="lingo3d-ui lingo3d-bg"
            style={{ width: 300, height: "100%", overflowY: "scroll" }}
        />
    )
}

const EditorParent = () => {
    const [nodeEditor] = useNodeEditor()

    if (nodeEditor) return null

    return <Editor />
}
export default EditorParent

register(EditorParent, "lingo3d-editor")
