import './types';
import {
    HostNodeRenderer, Host,
    ShadowNodeRenderer, ShadowDOM,
    PrimitiveRenderer, Primitive,
    NodeTreeRenderer, NativeRenderer
} from "./renderers";

export function getNodeMeta(vnode) {
    if (vnode === undefined || vnode === null) return undefined; // Indicate it shouldn't render
    if (vnode instanceof Element) return { renderer: NativeRenderer, normedType: Element };
    let type = vnode?.type;
    if (!type) return { renderer: PrimitiveRenderer, normedType: Primitive };
    else if (type === Host) return { renderer: HostNodeRenderer, normedType: Host };
    else if (type === ShadowDOM) return { renderer: ShadowNodeRenderer, normedType: ShadowDOM };
    else return { renderer: NodeTreeRenderer, normedType: window.customElements?.get(type) ?? type };
}

/**
 * @typedef {Object} RenderOptions
 * @category VDOM
 * @property {Element} [host] - The element to update to the specified VDOM
 * @property {VNode} [old] - Old VNode representation of rendered host
 * @property {Document} [document] - The document we're rendering to
 * @property {Element} [parent] - The parent element (TODO not sure what this will do when specified; Insert it as child element of the parent where?)
 */

/**
 * This exists as a very basic example/test for JSX-to-DOM
 * @category VDOM
 * @param {VNode} vnode
 * @param {RenderOptions} opts
 * @return {Element}
 */
export function render(vnode, opts = {}) {
    // TODO this code could use restructuring when opts.host and vnode.type are incompatible (non updatable type), the host element should be replaced
    // with a newly created element, like it does with all child-elements..
    // General flow of this code is to process the hierarchy using a queue (so no recursion is used)
    // on each node of the hierarchy a renderer is determined which is compared to the renderer of the previous version of this vnode-hierarchy
    // to determine if these nodes can be updated (e.g updating a div, or textnode) and if they behave as a child-node (e.g. shows up in childNodes)
    // or are some other special type of node (like Host or ShadowDOM)
    /**
     *
     * @type {VRenderState}
     */
    let state = {
        keyedElements: new Map(),
        refs: [],
        queue: [{
            // Start item
            item: {
                document: opts.document || document,
                host: opts.host,
                parent: opts.parent,
                old: opts.old,
                vnode: vnode
            },
            meta: getNodeMeta(vnode)
        }]
    };
    
    let newRoot = undefined;
    while (state.queue.length > 0) {
        let { item, meta, previous } = state.queue.splice(0, 1)[ 0 ];
        let renderer = meta.renderer;
        if (!renderer) throw new Error("No renderer for vnode", item.vnode);
        
        // SVG handling..
        if (!item.inSvg && item.vnode?.type === 'svg') item.inSvg = true;
        else if (item.inSvg && item.vnode?.type === 'foreignObject') item.inSvg = false;
        
        // Create the element if no matching existing element was set
        let newlyCreated = false;
        if (!item.host) {
            item.host = renderer.create(item, meta);
            newlyCreated = true;
            
            if (item.vnode?.props?.ref) {// If props specify a ref-function, queue it to be called at the end of the render
                state.refs.push([item.vnode.props.ref, item.host]);
            }
        }
        
        // Update the element
        renderer.update(item, meta);
        
        // Update children
        if (meta.normedType !== Element && (item.vnode?.children || item.old?.children)) {
            let childTypes = new Set();
            
            // Flatten and organize new vNode-children (this could be a separate function, or implemented using a helper function (because mucht of the code is similar between old/new vnodes)
            /**
             * @type {  Object.<VNodeType, Array.<VRenderQueueItem>> }
             */
            let vChildren = {};
            let queue = (item.vnode?.children || []).slice();
            while (queue.length > 0) {
                let next = queue.splice(0, 1)[ 0 ];
                if (next instanceof Array) queue.splice(0, 0, ...next);
                else {
                    let meta = getNodeMeta(next);
                    if (meta && meta.renderer) {
                        // Only items with a renderer are tracked (any other are undefined or null and shoulnd't be rendered at all)
                        let childType = meta.normedType;
                        if (!meta.renderer.remove) childType = 'node'; // Treat anything that doesnt have a special remove-function as ChildNode-type (e.g. it shows up in Element.childNodes)
                        childTypes.add(childType);// Track that children of this type exist and should be iterated later
                        vChildren[ childType ] = vChildren[ childType ] || []; // Make sure the array exists
                        vChildren[ childType ].push({
                            item: {
                                ...item,
                                old: undefined,
                                vnode: next,
                                host: undefined,
                                parent: item.host
                            },
                            meta: meta
                        });
                    }
                }
            }
            
            // Flatten and organize old-children
            /**
             * @type {  Object.<VNodeType, Array.<VOldQueueItem>> }
             */
            let oldVChildren = {};
            let curElement = item.host.firstChild;
            queue = (item.old?.children || []).slice();
            while (queue.length > 0) {
                let next = queue.splice(0, 1)[ 0 ];
                if (next instanceof Array) queue.splice(0, 0, ...next);
                else {
                    let meta = getNodeMeta(next);
                    if (meta && meta.renderer) {
                        // Only items with a renderer are tracked (any other are undefined or null and shoulnd't be rendered at all)
                        let childType = meta.normedType;
                        let childElement;
                        if (!meta.renderer.remove) {
                            childType = 'node';// Treat anything that doesnt have a special remove-function as ChildNode-type (e.g. it shows up in Element.childNodes)
                            if (curElement) {
                                childElement = curElement;
                                curElement = curElement.nextSibling;
                            }
                        }
                        childTypes.add(childType);// Track that children of this type exist and should be iterated later
                        oldVChildren[ childType ] = oldVChildren[ childType ] || []; // Make sure the array exists
                        let oldItem = {
                            vnode: next,
                            element: childElement,
                            meta: meta
                        };
                        oldVChildren[ childType ].push(oldItem);
                        if (next.props?.key) {
                            state.keyedElements.set(next.key, oldItem);
                        }
                    }
                }
            }
            
            
            let sortedChildTypes = Array.from(childTypes).sort((a, b) => a === 'node' ? 1 : -1); // Always do ChildNode-types last
            let queuedItems = [];
            /**@type {VRenderQueueItem}*/ let previous = null;
            for (let childType of sortedChildTypes) {
                let newChildren = vChildren[ childType ];
                let oldChildren = oldVChildren[ childType ];
                
                while (newChildren && newChildren.length) {
                    let child = newChildren.splice(0, 1)[ 0 ];
                    
                    // Key handling
                    let childKey = child.item.vnode.props?.key;
                    /**@type {VOldQueueItem}*/ let oldChild;
                    if (childKey) {
                        oldChild = state.keyedElements.get(childKey);
                        if (oldChild) {
                            if (oldChildren && oldChildren[ 0 ] === oldChild) {
                                // Old keyed child already in the right place (just clear it from the queue);
                                oldChildren.splice(0, 1);
                            } else {
                                // Old keyed child not already in the right place
                                let indexOfKeyed = oldChildren.indexOf(oldChild);
                                if (indexOfKeyed) {
                                    oldChildren.splice(indexOfKeyed, 1);
                                    item.host.removeChild(oldChild.element);
                                }
                                if (previous) {
                                    previous.item.host.after(oldChild.element);
                                } else {
                                    item.parent.prepend(oldChild.element);
                                }
                            }
                        }
                    }
                    if (!oldChild) oldChild = oldChildren && oldChildren.splice(0, 1)[ 0 ];
                    
                    child.previous = previous;
                    if (oldChild && child.meta.normedType === oldChild.meta.normedType && childKey === oldChild.vnode.props?.key
                        && (child.meta.normedType !== Element || child.item.vnode === oldChild.vnode)) {
                        // Update old-child
                        child.item.host = oldChild.element;
                        child.item.old = oldChild.vnode;
                        queuedItems.push(child);
                    } else {
                        // New child
                        if (oldChild) {
                            if (oldChild.meta.renderer.remove)
                                oldChild.meta.renderer.remove({ ...item, parent: item.host, host: oldChild.element });
                            else
                                item.host.removeChild(oldChild.element);
                        }
                        queuedItems.push(child);
                    }
                    if (!child.meta.renderer.remove) {
                        // If child is a node-type item track it as the previous (so we can insert next node-type items after it as intended)
                        previous = child.item;
                    }
                }
                while (oldChildren && oldChildren.length) {
                    let oldChild = oldChildren.splice(0, 1)[ 0 ];
                    if (oldChild.meta.renderer.remove)
                        oldChild.meta.renderer.remove({ ...item, parent: item.host, host: oldChild.element });
                    else
                        item.host.removeChild(oldChild.element);
                }
            }
            
            state.queue.splice(0, 0, ...queuedItems);
        }
        
        if (newlyCreated) {
            if (!meta.renderer.remove) {
                if (item.parent) {
                    if (!previous) {
                        // First child
                        item.parent.prepend(item.host);
                    } else {
                        // Subsequent child
                        previous.host.after(item.host);
                    }
                }
            }
            if (!item.parent) newRoot = item.host;
        }
    }
    
    for (let [refCb, refItem] of state.refs) {
        refCb(refItem);
    }
    return newRoot;
}
