import './types';
import { getNodeMeta } from "./node-meta";

/**
 * This exists as a very basic example/test for JSX-to-DOM
 * @category VDOM
 * @param {VNode} vnode
 * @param {RenderOptions} [opts]
 * @param {Element} [opts.host] - The element to update to the specified VDOM
 * @param {VNode} [opts.old] - Old VNode representation of rendered host
 * @param {Document} [opts.document] - The document we're rendering to
 * @return {Element}
 */
export function render(vnode, opts = {}) {
    // 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 = {
        refs: [],
        queue: [{
            // Start item
            item: {
                document: opts.document || document,
                host: opts.host,
                old: opts.old,
                vnode: vnode
            },
            meta: getNodeMeta(vnode),
            parent: null,
            attached: true// Root item is treated as if it is attached
        }]
    };
    
    let newRoot = undefined;
    while (state.queue.length > 0) {
        /** @type {VRenderQueueItem} */
        let queueItem = state.queue.shift();
        let {
            /** @type {VRenderItem} */  item,
            /** @type {VNodeMeta} */    meta,
            /** @type {VRenderItem} */  previousNode,
            /** @type {boolean} */  attached
        } = queueItem;
        /** @type {VNodeRenderer} */
        let renderer = meta.renderer;
        if (!renderer) throw new Error("No renderer for vnode", item.vnode);
        
        // SVG handling.. (this could be improved)
        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 props specify a ref-function, queue it to be called at the end of the render
            if (item.vnode?.props?.ref) {
                state.refs.push([item.vnode.props.ref, item.host]);
            }
        }
        
        // Update the element
        renderer.update(item, meta);
        
        // Update children
        if (meta.hasChildren) {
            /** @type {FlattenedVNode[]} */
            let vChildren = flattenVNodeChildren(item.vnode);
            /** @type {FlattenedOldVNode[]} */
            let oldVChildren = flattenVNodeChildren(item?.old);
            /** @type {Map<*, FlattenedOldVNode>} */
            let keyedChildren = new Map();// Old child-items
            /** @type {NodeState[]} */
            let nodes = Array.from(item.host.childNodes).map(node=>({node, attached: true}));
            /** @type {FlattenedOldVNode[]} */
            let specialOldVChildren = []; // List of old-vnodes that are not node-type node and have no key
            
            // Iterate old children and link them to their corresponding elements
            let nodeI = 0, nodesLength = nodes.length;
            let oldVItemI = 0, oldVItemLength = oldVChildren.length;
            while(oldVItemI < oldVItemLength){
                /** @type {FlattenedOldVNode} */
                let oldVItem = oldVChildren[oldVItemI];
                /** @type {NodeState} */
                let node = nodes[nodeI];
                let nodeKey = oldVItem.vnode?.props?.key
                if(nodeKey !== undefined){
                    keyedChildren.set(nodeKey, oldVItem);
                }
                ++oldVItemI;
                if(oldVItem.meta.isNode && node){
                    oldVItem.node = node;
                    node.old = oldVItem;
                    ++nodeI;
                }else if(nodeKey === undefined){
                    specialOldVChildren.push(oldVItem);
                }
            }
            // Any items not part of the old-vnode spec should be left alone, and ignored in further processing
            nodesLength = nodeI; // TODO this feels dirty, but not having this here will screw up our examples, we have yet to add a proper test
            
            // Iterate new children, remove old nodes as needed and create new queue items
            /** @type {VRenderQueueItem[]} */
            let queueChildren = [];
            /** @type {VRenderItem} */
            let previousNodeItem = undefined;
            let vItemI = 0, vItemLength = vChildren.length;
            nodeI = 0;
            while(vItemI < vItemLength || nodeI < nodesLength){
                let vItem = vChildren[vItemI];
                let node = nodes[nodeI];
                
                if(vItem){
                    let nodeKey = vItem?.vnode?.props?.key;
                    /** @type {FlattenedOldVNode} */
                    let oldItem = undefined;
                    if(nodeKey!==undefined){
                        oldItem = keyedChildren.get(nodeKey);
                    }
                    
                    /** @type {NodeState} */
                    let matchedNode = undefined;
                    
                    // If this is a DOM-node-type match it to an existing node
                    if(vItem.meta.isNode) {
                        // Match new item to existing node when possible
                        matchedNode = oldItem?.node;
                        if (matchedNode) {
                            // DOM-node matched by key
                            if (matchedNode === node) {
                                // Item in order with old representation
                                ++nodeI;
                            } else {
                                // Out of order, detach
                                if (matchedNode.attached) {
                                    item.host.removeChild(matchedNode.node);
                                    matchedNode.attached = false;
                                }
                            }
                        } else if (node) {
                            // No match by key
                            // If old VNode associated, check if updatable
                            if (node.old
                                && node.old.vnode?.props?.key === undefined
                                && node.old.meta.normedType === vItem.meta.normedType
                            ) {
                                oldItem = node.old;
                                matchedNode = node;
                                ++nodeI;
                            } else {
                                // Not updatable, detach
                                if (node.attached) {
                                    item.host.removeChild(node.node);
                                    node.attached = false;
                                }
                            }
                        }
                    }else{
                        // If this is a special type and it was not matched to an old VNode by key, find it the first non-keyed
                        // old VNode with the same normedType (These are ShadowDOM or Host-types)
                        if(!oldItem){
                            let indexOf = specialOldVChildren.findIndex(old=>old.meta.normedType===vItem.meta.normedType);
                            if(indexOf>=0){
                                oldItem = specialOldVChildren[indexOf];
                                specialOldVChildren.splice(indexOf,1);
                            }
                        }
                    }
                    
                    // Track that the node is used by a new node (we don't use this anywhere?!)
                    if(matchedNode){
                        matchedNode.new = vItem;
                    }
                    
                    /** @type {VRenderQueueItem} */
                    let queueItem = {
                        item: {
                            vnode: vItem.vnode,
                            document: item.document,
                            host: matchedNode?.node,
                            old: oldItem?.vnode,
                            inSvg: item.inSvg,
                            parent: item,
                        },
                        attached: matchedNode?.attached,
                        previousNode: previousNodeItem,
                        meta: vItem.meta
                    }
                    queueChildren.push(queueItem);
                    if(vItem.meta.isNode){
                        previousNodeItem = queueItem.item;
                    }
                    
                    ++vItemI;
                }else if(node){
                    // Trailing node item
                    if(node.attached && !node.new) {
                        // If this node was not reused for a new vnode representation, and was not already detached
                        // for other reasons, remove it
                        item.host.removeChild(node.node);
                        node.attached = false;
                    }
                    ++nodeI;
                }
            }
            
            state.queue.unshift(...queueChildren);
            
            // Remove any special types (Host, ShadowDOM that were no longer present)
            for(let oldSpecialVnode of specialOldVChildren){
                oldSpecialVnode.meta.renderer.remove?.(oldSpecialVnode.vnode);
            }
        }
        
        if (!attached && meta.isNode) {
            if (previousNode) {
                // Subsequent child
                previousNode.host.after(item.host);
            } else if (item.parent) {
                // First child
                item.parent.host.prepend(item.host);
            }
        }
        if(!item.parent && !newRoot){
            newRoot = item.host;
        }
    }
    
    for (let [refCb, refItem] of state.refs) {
        refCb(refItem);
    }
    return newRoot;
}


/**
 * Intermediate type used to process child-items
 * @typedef {object} FlattenedVNode
 * @property {VNode} vnode
 * @property {VNodeMeta} meta
 */
/**
 * @typedef {object} NodeState
 * @property {ChildNode} node
 * @property {boolean} attached
 * @property {FlattenedOldVNode} old
 * @property {FlattenedVNode} new
 */
/**
 * Intermediate type used to process old child-items
 * @typedef {FlattenedVNode} FlattenedOldVNode
 * @property {NodeState} node - The DOM-node
 */

/**
 *
 * @param {VNode} vnode
 * @returns {FlattenedVNode[]}
 */
function flattenVNodeChildren(vnode){
    let children = [];
    let queue = (vnode?.children || []).slice();
    while (queue.length > 0) {
        let next = queue.shift();
        if (next instanceof Array) queue.splice(0, 0, ...next);
        else {
            let meta = getNodeMeta(next);
            if(meta === 'function'){
                let resolved = next.type(next.props, next.children);
                if(resolved instanceof Array){
                    queue.splice(0,0, ...resolved)
                }else{
                    queue.splice(0,0,resolved);
                }
            } else if (meta) {
                children.push({ vnode: next, meta });
            }else{
                // null or undefined child, ignored
            }
        }
    }
    return children;
}