import {traverse, Node} from '../../DOMUtils.js'
import {closingIfMarker, eachClosingMarker, eachStartMarkerRegex, elseMarker, ifStartMarkerRegex} from './markers.js'

/**
 * @typedef TextPlaceToFill
 * @property { {expression: string, replacedString:string}[] } expressions
 * @property {() => void} fill
 */


/**
 * @param {string} str
 * @param {Compartment} compartment 
 * @returns {TextPlaceToFill | undefined}
 */
function findPlacesToFillInString(str, compartment) {
    const matches = str.matchAll(/\{([^{#\/]+?)\}/g)

    /** @type {TextPlaceToFill['expressions']} */
    const expressions = []

    /** @type {(string | ((data:any) => void))[]} */
    const parts = []
    let remaining = str;

    for(const match of matches) {
        //console.log('match', match)
        const [matched, group1] = match

        const replacedString = matched
        const expression = group1.trim()
        expressions.push({expression, replacedString})

        const [fixedPart, newRemaining] = remaining.split(replacedString, 2)

        if(fixedPart.length >= 1)
            parts.push(fixedPart)

        parts.push(() => compartment.evaluate(expression))

        remaining = newRemaining
    }

    if(remaining.length >= 1)
        parts.push(remaining)

    //console.log('parts', parts)


    if(remaining === str) {
        // no match found
        return undefined
    }
    else {
        return {
            expressions,
            fill: (data) => {
                return parts.map(p => {
                    if(typeof p === 'string')
                        return p
                    else
                        return p(data)
                })
                    .join('')
            }
        }
    }


}


/**
 * Content between blockStartNode and blockEndNode is extracted to a documentFragment
 * The original document is modified because nodes are removed from it to be part of the returned documentFragment
 * 
 * startChild and endChild are ancestors of, respectively, blockStartNode and blockEndNode
 * and startChild.parentNode === endChild.parentNode
 * 
 * @precondition blockStartNode needs to be before blockEndNode in document order
 * 
 * @param {Node} blockStartNode 
 * @param {Node} blockEndNode 
 * @returns {{startChild: Node, endChild:Node, content: DocumentFragment}}
 */
function extractBlockContent(blockStartNode, blockEndNode) {
    // find common ancestor of blockStartNode and blockEndNode
    let commonAncestor

    let startAncestor = blockStartNode
    let endAncestor = blockEndNode

    const startAncestry = new Set([startAncestor])
    const endAncestry = new Set([endAncestor])

    while(!startAncestry.has(endAncestor) && !endAncestry.has(startAncestor)) {
        if(startAncestor.parentNode) {
            startAncestor = startAncestor.parentNode
            startAncestry.add(startAncestor)
        }
        if(endAncestor.parentNode) {
            endAncestor = endAncestor.parentNode
            endAncestry.add(endAncestor)
        }
    }

    if(startAncestry.has(endAncestor)) {
        commonAncestor = endAncestor
    }
    else {
        commonAncestor = startAncestor
    }

    const startAncestryToCommonAncestor = [...startAncestry].slice(0, [...startAncestry].indexOf(commonAncestor))
    const endAncestryToCommonAncestor = [...endAncestry].slice(0, [...endAncestry].indexOf(commonAncestor))

    const startChild = startAncestryToCommonAncestor.at(-1)
    const endChild = endAncestryToCommonAncestor.at(-1)

    // Extract DOM content in a documentFragment
    const contentFragment = blockStartNode.ownerDocument.createDocumentFragment()

    /** @type {Element[]} */
    const repeatedPatternArray = []
    let sibling = startChild.nextSibling

    while(sibling !== endChild) {
        repeatedPatternArray.push(sibling)
        sibling = sibling.nextSibling;
    }

    for(const sibling of repeatedPatternArray) {
        sibling.parentNode?.removeChild(sibling)
        contentFragment.appendChild(sibling)
    }

    return {
        startChild,
        endChild,
        content: contentFragment
    }
}




/**
 * 
 * @param {Node} ifOpeningMarkerNode 
 * @param {Node | undefined} ifElseMarkerNode 
 * @param {Node} ifClosingMarkerNode 
 * @param {string} ifBlockConditionExpression 
 * @param {Compartment} compartment 
 */
function fillIfBlock(ifOpeningMarkerNode, ifElseMarkerNode, ifClosingMarkerNode, ifBlockConditionExpression, compartment) {
    const conditionValue = compartment.evaluate(ifBlockConditionExpression)

    let startChild
    let endChild

    let markerNodes = new Set()

    let chosenFragment

    if(ifElseMarkerNode) {
        const {
            startChild: startIfThenChild,
            endChild: endIfThenChild,
            content: thenFragment
        } = extractBlockContent(ifOpeningMarkerNode, ifElseMarkerNode)

        const {
            startChild: startIfElseChild,
            endChild: endIfElseChild,
            content: elseFragment
        } = extractBlockContent(ifElseMarkerNode, ifClosingMarkerNode)

        chosenFragment = conditionValue ? thenFragment : elseFragment
        startChild = startIfThenChild
        endChild = endIfElseChild

        markerNodes
            .add(startIfThenChild).add(endIfThenChild)
            .add(startIfElseChild).add(endIfElseChild)
    }
    else {
        const {
            startChild: startIfThenChild,
            endChild: endIfThenChild,
            content: thenFragment
        } = extractBlockContent(ifOpeningMarkerNode, ifClosingMarkerNode)

        chosenFragment = conditionValue ? thenFragment : undefined
        startChild = startIfThenChild
        endChild = endIfThenChild

        markerNodes
            .add(startIfThenChild).add(endIfThenChild)
    }


    if(chosenFragment) {
        fillOdtElementTemplate(
            chosenFragment,
            compartment
        )

        endChild.parentNode.insertBefore(chosenFragment, endChild)
    }

    for(const markerNode of markerNodes) {
        try {
            // may throw if node already out of tree
            // might happen if 
            markerNode.parentNode.removeChild(markerNode)
        }
        catch(e) {}
    }

}


/**
 * 
 * @param {Node} startNode 
 * @param {string} iterableExpression 
 * @param {string} itemExpression 
 * @param {Node} endNode 
 * @param {Compartment} compartment 
 */
function fillEachBlock(startNode, iterableExpression, itemExpression, endNode, compartment) {
    //console.log('fillEachBlock', iterableExpression, itemExpression)
    //console.log('startNode', startNode.nodeType, startNode.nodeName)
    //console.log('endNode', endNode.nodeType, endNode.nodeName)

    const {startChild, endChild, content: repeatedFragment} = extractBlockContent(startNode, endNode)

    // Find the iterable in the data
    // PPP eventually, evaluate the expression as a JS expression
    let iterable = compartment.evaluate(iterableExpression)
    if(!iterable || typeof iterable[Symbol.iterator] !== 'function') {
        // when there is no iterable, silently replace with empty array
        iterable = []
    }

    // create each loop result
    // using a for-of loop to accept all iterable values
    for(const item of iterable) {
        /** @type {DocumentFragment} */
        // @ts-ignore
        const itemFragment = repeatedFragment.cloneNode(true)

        let insideCompartment = new Compartment({
            globals: Object.assign({}, compartment.globalThis, {[itemExpression]: item}),
            __options__: true
        })

        // recursive call to fillTemplatedOdtElement on itemFragment
        fillOdtElementTemplate(
            itemFragment,
            insideCompartment
        )

        endChild.parentNode.insertBefore(itemFragment, endChild)
    }

    // remove block marker elements
    startChild.parentNode.removeChild(startChild)
    endChild.parentNode.removeChild(endChild)
}


const IF = 'IF'
const EACH = 'EACH'

/**
 * 
 * @param {Element | DocumentFragment | Document} rootElement 
 * @param {Compartment} compartment 
 * @returns {void}
 */
export default function fillOdtElementTemplate(rootElement, compartment) {
    //console.log('fillTemplatedOdtElement', rootElement.nodeType, rootElement.nodeName)

    let currentlyOpenBlocks = []

    /** @type {Node | undefined} */
    let eachOpeningMarkerNode
    /** @type {Node | undefined} */
    let eachClosingMarkerNode

    let eachBlockIterableExpression, eachBlockItemExpression;


    /** @type {Node | undefined} */
    let ifOpeningMarkerNode
    /** @type {Node | undefined} */
    let ifElseMarkerNode
    /** @type {Node | undefined} */
    let ifClosingMarkerNode

    let ifBlockConditionExpression
    // Traverse "in document order"

    // @ts-ignore
    traverse(rootElement, currentNode => {
        //console.log('currentlyUnclosedBlocks', currentlyUnclosedBlocks)
        const insideAnOpenBlock = currentlyOpenBlocks.length >= 1

        if(currentNode.nodeType === Node.TEXT_NODE) {
            const text = currentNode.textContent || ''

            /**
             * looking for {#each x as y}
             */
            const eachStartMatch = text.match(eachStartMarkerRegex);

            if(eachStartMatch) {
                //console.log('startMatch', startMatch)

                currentlyOpenBlocks.push(EACH)

                if(insideAnOpenBlock) {
                    // do nothing 
                }
                else {
                    let [_, _iterableExpression, _itemExpression] = eachStartMatch

                    eachBlockIterableExpression = _iterableExpression
                    eachBlockItemExpression = _itemExpression
                    eachOpeningMarkerNode = currentNode
                }
            }


            /**
             * Looking for {/each}
             */
            const isEachClosingBlock = text.includes(eachClosingMarker)

            if(isEachClosingBlock) {

                //console.log('isEachClosingBlock', isEachClosingBlock)

                if(!eachOpeningMarkerNode)
                    throw new Error(`{/each} found without corresponding opening {#each x as y}`)

                if(currentlyOpenBlocks.at(-1) !== EACH)
                    throw new Error(`{/each} found while the last opened block was not an opening {#each x as y}`)

                if(currentlyOpenBlocks.length === 1) {
                    eachClosingMarkerNode = currentNode

                    // found an {#each} and its corresponding {/each}
                    // execute replacement loop
                    fillEachBlock(eachOpeningMarkerNode, eachBlockIterableExpression, eachBlockItemExpression, eachClosingMarkerNode, compartment)

                    eachOpeningMarkerNode = undefined
                    eachBlockIterableExpression = undefined
                    eachBlockItemExpression = undefined
                    eachClosingMarkerNode = undefined
                }
                else {
                    // ignore because it will be treated as part of the outer {#each}
                }

                currentlyOpenBlocks.pop()
            }


            /**
             * Looking for {#if ...}
             */
            const ifStartMatch = text.match(ifStartMarkerRegex);

            if(ifStartMatch) {
                currentlyOpenBlocks.push(IF)

                if(insideAnOpenBlock) {
                    // do nothing because the marker is too deep
                }
                else {
                    let [_, _ifBlockConditionExpression] = ifStartMatch

                    ifBlockConditionExpression = _ifBlockConditionExpression
                    ifOpeningMarkerNode = currentNode
                }
            }


            /**
             * Looking for {:else}
             */
            const hasElseMarker = text.includes(elseMarker);

            if(hasElseMarker) {
                if(!insideAnOpenBlock)
                    throw new Error('{:else} without a corresponding {#if}')

                if(currentlyOpenBlocks.length === 1) {
                    if(currentlyOpenBlocks[0] === IF) {
                        ifElseMarkerNode = currentNode
                    }
                    else
                        throw new Error('{:else} inside an {#each} but without a corresponding {#if}')
                }
                else {
                    // do nothing because the marker is too deep
                }
            }


            /**
             * Looking for {/if}
             */
            const hasClosingMarker = text.includes(closingIfMarker);

            if(hasClosingMarker) {
                if(!insideAnOpenBlock)
                    throw new Error('{/if} without a corresponding {#if}')

                if(currentlyOpenBlocks.length === 1) {
                    if(currentlyOpenBlocks[0] === IF) {
                        ifClosingMarkerNode = currentNode

                        // found an {#if} and its corresponding {/if}
                        // execute replacement loop
                        fillIfBlock(ifOpeningMarkerNode, ifElseMarkerNode, ifClosingMarkerNode, ifBlockConditionExpression, compartment)

                        ifOpeningMarkerNode = undefined
                        ifElseMarkerNode = undefined
                        ifClosingMarkerNode = undefined
                        ifBlockConditionExpression = undefined
                    }
                    else
                        throw new Error('{/if} inside an {#each} but without a corresponding {#if}')
                }
                else {
                    // do nothing because the marker is too deep
                }
            }


            /**
             * Looking for variables for substitutions
             */
            if(!insideAnOpenBlock) {
                // @ts-ignore
                if(currentNode.data) {
                    // @ts-ignore
                    const placesToFill = findPlacesToFillInString(currentNode.data, compartment)

                    if(placesToFill) {
                        const newText = placesToFill.fill()
                        // @ts-ignore
                        const newTextNode = currentNode.ownerDocument?.createTextNode(newText)
                        // @ts-ignore
                        currentNode.parentNode?.replaceChild(newTextNode, currentNode)
                    }
                }
            }
            else {
                // ignore because it will be treated as part of the outer {#each} block
            }
        }

        if(currentNode.nodeType === Node.ATTRIBUTE_NODE) {
            // Looking for variables for substitutions
            if(!insideAnOpenBlock) {
                // @ts-ignore
                if(currentNode.value) {
                    // @ts-ignore
                    const placesToFill = findPlacesToFillInString(currentNode.value, compartment)
                    if(placesToFill) {
                        // @ts-ignore
                        currentNode.value = placesToFill.fill()
                    }
                }
            }
            else {
                // ignore because it will be treated as part of the {#each} block
            }
        }
    })
}
