/** biome-ignore-all lint/complexity/noBannedTypes: "Function" is used as a type in GodotJS, keeping it for consistency */
import { Callable, Control, Node, type NodePathMap, PackedScene, ResourceLoader, Signal } from "godot"
import ReactReconciler, { type HostConfig } from "react-reconciler"
import { hasProperty } from "./utils.js"


const DefaultEventPriority = 32

export const make = () => {
    let eventTime = 0
    let currentPriority = DefaultEventPriority

    return ReactReconciler<
        string,
        Record<string, unknown>,
        Node<NodePathMap>,
        Node<NodePathMap>,
        Node<NodePathMap>,
        Node<NodePathMap>,
        Node<NodePathMap>,
        Node<NodePathMap>,
        Node<NodePathMap>,
        unknown,
        unknown,
        unknown,
        unknown,
        unknown
    >({
        supportsMutation: true,

        createInstance(type, props) {
            let instance: Node
            if (type === "node") {
                if (!hasProperty(props, "class"))
                    throw new Error("Property 'class' required when using the 'node' intrinsic type")
                instance = new (props.class as any)()
            }
            else if (type === "scene") {
                if (!hasProperty(props, "path"))
                    throw new Error("Property 'path' required when using the 'scene' intrinsic type")
                if (typeof props.path !== "string")
                    throw new Error("Property 'path' is not a string")
                const res = ResourceLoader.load(props.path)
                if (!(res instanceof PackedScene))
                    throw new Error(`Resource at 'path' is not a PackedScene`)
                instance = res.instantiate()
            }
            else {
                // const className = snakeToPascal(camelToSnake(type))
                // if (!ClassDB.class_exists(className))
                //     throw new Error(`Class is invalid: '${className}' (declared as '${type}') is not a valid engine or GDExtension class`)
                // instance = ClassDB.instantiate(className)
                throw new Error(`Unsupported JSX type: '${ type }'`)
            }

            if (!(instance instanceof Node))
                throw new Error("Class is not a subclass of 'Node'")
            applyNextProps(instance, props)

            return instance
        },

        createTextInstance(_text) {
            throw new Error("createTextInstance not supported")
        },

        appendInitialChild(parent, child) {
            parent.add_child(child)
        },

        appendChild(parent, child) {
            parent.add_child(child)
        },

        appendChildToContainer(container, child) {
            container.add_child(child)
        },

        removeChild(parent, child) {
            parent.remove_child(child)
        },

        removeChildFromContainer(container, child) {
            container.remove_child(child)
        },

        insertBefore(_parent, child, before) {
            before.add_sibling(child)
        },

        finalizeInitialChildren() {
            return false
        },

        prepareForCommit() { return null },
        resetAfterCommit() {},

        getRootHostContext() {
            return {}
        },

        getChildHostContext(parentHostContext, _type, _rootContainer) {
            return parentHostContext
        },

        shouldSetTextContent() {
            return false
        },

        commitUpdate(instance, _type, _prevProps, nextProps, _internalHandle) {
            applyNextProps(instance, nextProps)
        },

        commitTextUpdate(_textInstance, _oldText, _newText) {
            throw new Error("commitTextUpdate not supported")
        },

        getPublicInstance(instance) {
            return instance
        },

        resolveUpdatePriority() {
            return DefaultEventPriority
        },

        resolveEventTimeStamp() {
            return ++eventTime
        },

        trackSchedulerEvent() {},

        resolveEventType() {
            return null
        },

        getCurrentUpdatePriority() {
            return currentPriority
        },

        setCurrentUpdatePriority(newPriority) {
            currentPriority = newPriority
        },

        clearContainer(container) {
            for (const child of container.get_children()) {
                container.remove_child(child)
            }
        },

        scheduleTimeout: setTimeout,
        cancelTimeout: clearTimeout,
        noTimeout: -1,

        // isPrimaryRenderer: true,
        // supportsPersistence: false,

        // preparePortalMount(_containerInfo) {
        //     throw new Error("Function not implemented.")
        // },
        // getInstanceFromNode(_node) {
        //     throw new Error("Function not implemented.")
        // },
        // beforeActiveInstanceBlur() {
        //     throw new Error("Function not implemented.")
        // },
        // afterActiveInstanceBlur() {
        //     throw new Error("Function not implemented.")
        // },
        // prepareScopeUpdate(_scopeInstance, _instance) {
        //     throw new Error("Function not implemented.")
        // },
        // getInstanceFromScope(_scopeInstance) {
        //     throw new Error("Function not implemented.")
        // },
        detachDeletedInstance(node) {
            node.queue_free()
        },

        // supportsHydration: false,
        // NotPendingTransition: undefined,
        // HostTransitionContext: undefined as any,

        // resetFormInstance(_form) {
        //     throw new Error("Function not implemented.")
        // },
        // requestPostPaintCallback(_callback: (time: number) => void) {
        //     throw new Error("Function not implemented.")
        // },
        // shouldAttemptEagerTransition: (): boolean => {
        //     throw new Error("Function not implemented.")
        // },
        // maySuspendCommit(_type, _props) {
        //     throw new Error("Function not implemented.")
        // },
        // preloadInstance(_type, _props) {
        //     throw new Error("Function not implemented.")
        // },
        // startSuspendingCommit() {
        //     throw new Error("Function not implemented.")
        // },
        // suspendInstance(_type, _props) {
        //     throw new Error("Function not implemented.")
        // },
        // waitForCommitToBeReady() {
        //     throw new Error("Function not implemented.")
        // },
    } as Partial<
        HostConfig<
            string,
            Record<string, unknown>,
            Node<NodePathMap>,
            Node<NodePathMap>,
            Node<NodePathMap>,
            Node<NodePathMap>,
            Node<NodePathMap>,
            Node<NodePathMap>,
            Node<NodePathMap>,
            unknown,
            unknown,
            unknown,
            unknown,
            unknown
        >
    > as any)
}

const applyNextProps = (instance: Node<NodePathMap>, nextProps: Record<string, unknown>) => {
    Object.keys(nextProps).forEach(name => {
        if (!hasProperty(instance, name)) return

        if (name === "name") {
            if (typeof nextProps[name] !== "string")
                throw new Error("Prop 'name' should be a string")
            instance.set_name(nextProps[name])
            return
        }

        if (instance[name] instanceof Signal) {
            if ((typeof nextProps[name] !== "function") || nextProps[name] === undefined)
                throw new Error(`Prop '${ name }' should be a function or undefined`)
            instance[`${ name }_event`] = nextProps[name]

            if (!isNodeSignalRegistered(instance, name)) {
                const callable = Callable.create(instance, function(this, ...args) {
                    if (this[`${ name }_event`])
                        (this[`${ name }_event`] as Function)(...args)
                })
                instance[`${ name }_callable`] = callable
                instance.connect(name, callable)
            }

            return
        }

        if (instance instanceof Control) {
            if (name === "anchors_preset") {
                if (typeof nextProps[name] !== "number")
                    throw new Error("Prop 'anchors_preset' should be a number")
                instance.set_anchors_preset(nextProps[name])
                return
            }
        }

        instance[name] = nextProps[name]
    })
}

type NodeWithSignalMetadata<A extends string> = Node<NodePathMap> & {
    [K in `${ A }_callable`]: Callable
} & {
    [K in `${ A }_event`]?: Function
}

const isNodeSignalRegistered = <A extends string>(
    u: Node<NodePathMap>,
    name: A,
): u is NodeWithSignalMetadata<A> => (
    hasProperty(u, `${ name }_callable`) &&
    u[`${ name }_callable`] instanceof Callable
)
