// The loader parses a DOM node and detects which resource (script, style, image, ...) needs to be loaded from it

import type {
    LoadResult,
    LoadNodeCallback,
    LoadReference,
    NodeMapping,
    AttributeReference, BodyReference, LoadedReference
} from '../types/index.d.ts';
import {parseFragment as parseHtmlFragment, serialize as serializeHtml, DefaultTreeAdapterMap} from "parse5";

import {KnownMappings, defaultMapping} from "./loader-mappings.ts";

/**
 * Makes a unique but human-readable name from a path within a HTML file.
 * i.e html.body.script0
 * @param node
 */
export function makeHtmlPath(node: DefaultTreeAdapterMap['childNode']){
    const path = [];
    let cur = node;
    while(cur?.parentNode){
        const parent = cur.parentNode;
        const asElement = (<DefaultTreeAdapterMap['element']>cur);
        const similarChildNodes = parent.childNodes?.filter(x=>(<DefaultTreeAdapterMap['element']>x).nodeName == cur.nodeName) || [];
        const pathName = `${asElement.tagName}${similarChildNodes.length>1? similarChildNodes.indexOf(cur): ''}`;
        path.unshift(pathName);
        cur = (<DefaultTreeAdapterMap['childNode']>cur.parentNode);
        if((<DefaultTreeAdapterMap['element']>cur).tagName==='html'
            && (!cur.parentNode || cur.parentNode?.nodeName === '#document')
            && (!cur.parentNode || cur.parentNode?.childNodes.length===1)
            ){
            break; // Break early, don't include 'html0' if we can prevent it
        }
    }
    return path.join('.');
}

/**
 * // TODO check if this works cross platform (windows)
 * @param sourceId
 * @param node
 * @param ext
 */
export function makeInlineId(sourceId: string, node: DefaultTreeAdapterMap['childNode'], ext = '.js'){
    return [sourceId, [makeHtmlPath(node), 'js'].join('.')].join('.');
}

/**
 * Creates a loader function that maps node types and attributes to load operations.
 *
 * @param mappings - An array of NodeMapping objects specifying how to map and load different nodes.
 * @returns A LoadNodeCallback function that can be used to load nodes based on the mappings.
 */
export function makeLoader(mappings: NodeMapping[] = defaultMapping){
    const fn : LoadNodeCallback = async function ({node, sourceId}, load){
        for(const mapping of mappings){

            // Test the mapping for a match
            if (mapping.tagName && mapping.tagName !== node.tagName) continue; // No match, skip
            if (mapping.match){
                if(typeof(mapping.match) === 'function'){
                    if(!mapping.match(node)) continue;
                }else{
                    if(mapping.match.body && !(node.childNodes?.length>0)) continue; // No match, skip
                    if(mapping.match.attr) {
                        for (const [attrName, attrMatch] of Object.entries(mapping.match.attr)) {
                            if(!node.attrs.find(attr=>{
                                if(attr.name !== attrName) return false;
                                if(typeof(attrMatch) === 'string') return attrMatch === attr.value;
                                if(attrMatch instanceof RegExp) return !!(attrMatch.exec(attr.value));
                                if(typeof(attrMatch) === 'function') return attrMatch(attr.value);
                            })) continue; // No match, skip
                        }
                    }
                }
            }

            // If we've gotten this far its a valid mapping. (either inline or a src/href attribute)
            if((<AttributeReference>mapping).attr){
                // Mapped on attribute, resolve its src or href (or whatever was returned)
                const attr = node.attrs.find(attr=>attr.name === (<AttributeReference>mapping).attr);
                if(!attr) continue ;// No match, skip
                const placeholder = await load({
                    id: attr.value,
                    type: mapping.loadType||'default', // Use the default export unless explicitely mapped differently
                });
                attr.value = placeholder;
            }else if((<BodyReference>mapping).body){
                // Mapped as body, use the contents of the DOM element
                const body = serializeHtml(node); // unlike what you' might expect, this doesn't serialize the <script>-tag itself, only its contents. Which is what we want.
                if(!body) continue; // Empty body, skip
                const placeholder = await load({
                    source: body,
                    type: mapping.loadType||'chunk'
                });
                node.childNodes = parseHtmlFragment(placeholder).childNodes;
                return false;
            }
        }
    }
    return fn;
}
