import { addScopeToNode } from '../scope'
import { evaluateLater } from '../evaluator'
import { directive } from '../directives'
import { reactive } from '../reactivity'
import { initTree } from '../lifecycle'
import { mutateDom } from '../mutation'
import { flushJobs } from '../scheduler'
import { warn } from '../utils/warn'
import { dequeueJob } from '../scheduler'

directive('for', (el, { expression }, { effect, cleanup }) => {
    let iteratorNames = parseForExpression(expression)

    let evaluateItems = evaluateLater(el, iteratorNames.items)
    let evaluateKey = evaluateLater(el,
        // the x-bind:key expression is stored for our use instead of evaluated.
        el._x_keyExpression || 'index'
    )

    el._x_prevKeys = []
    el._x_lookup = {}

    effect(() => loop(el, iteratorNames, evaluateItems, evaluateKey))

    cleanup(() => {
        Object.values(el._x_lookup).forEach(el => el.remove())

        delete el._x_prevKeys
        delete el._x_lookup
    })
})

let shouldFastRender = true

function loop(el, iteratorNames, evaluateItems, evaluateKey) {
    let isObject = i => typeof i === 'object' && ! Array.isArray(i)
    let templateEl = el

    evaluateItems(items => {
        // Prepare yourself. There's a lot going on here. Take heart,
        // every bit of complexity in this function was added for
        // the purpose of making Alpine fast with large datas.

        // Support number literals. Ex: x-for="i in 100"
        if (isNumeric(items) && items >= 0) {
            items = Array.from(Array(items).keys(), i => i + 1)
        }

        if (items === undefined) items = []

        let lookup = el._x_lookup
        let prevKeys = el._x_prevKeys
        let scopes = []
        let keys = []

        // In order to preserve DOM elements (move instead of replace)
        // we need to generate all the keys for every iteration up
        // front. These will be our source of truth for diffing.
        if (isObject(items)) {
            items = Object.entries(items).map(([key, value]) => {
                let scope = getIterationScopeVariables(iteratorNames, value, key, items)

                evaluateKey(value => keys.push(value), { scope: { index: key, ...scope} })

                scopes.push(scope)
            })
        } else {
            for (let i = 0; i < items.length; i++) {
                let scope = getIterationScopeVariables(iteratorNames, items[i], i, items)

                evaluateKey(value => keys.push(value), { scope: { index: i, ...scope} })

                scopes.push(scope)
            }
        }

        // Rather than making DOM manipulations inside one large loop, we'll
        // instead track which mutations need to be made in the following
        // arrays. After we're finished, we can batch them at the end.
        let adds = []
        let moves = []
        let removes = []
        let sames = []

        // First, we track elements that will need to be removed.
        for (let i = 0; i < prevKeys.length; i++) {
            let key = prevKeys[i]

            if (keys.indexOf(key) === -1) removes.push(key)
        }

        // Notice we're mutating prevKeys as we go. This makes it
        // so that we can efficiently make incremental comparisons.
        prevKeys = prevKeys.filter(key => ! removes.includes(key))

        let lastKey = 'template'

        // This is the important part of the diffing algo. Identifying
        // which keys (future DOM elements) are new, which ones have
        // or haven't moved (noting where they moved to / from).
        for (let i = 0; i < keys.length; i++) {
            let key = keys[i]

            let prevIndex = prevKeys.indexOf(key)

            if (prevIndex === -1) {
                // New key found.
                prevKeys.splice(i, 0, key)

                adds.push([lastKey, i])
            } else if (prevIndex !== i) {
                // A key has moved.
                let keyInSpot = prevKeys.splice(i, 1)[0]
                let keyForSpot = prevKeys.splice(prevIndex - 1, 1)[0]

                prevKeys.splice(i, 0, keyForSpot)
                prevKeys.splice(prevIndex, 0, keyInSpot)

                moves.push([keyInSpot, keyForSpot])
            } else {
                // This key hasn't moved, but we'll still keep track
                // so that we can refresh it later on.
                sames.push(key)
            }

            lastKey = key
        }

        // Now that we've done the diffing work, we can apply the mutations
        // in batches for both separating types work and optimizing
        // for browser performance.

        // We'll remove all the nodes that need to be removed,
        // letting the mutation observer pick them up and
        // clean up any side effects they had.
        for (let i = 0; i < removes.length; i++) {
            let key = removes[i]

            // Remove any queued effects that might run after the DOM node has been removed.
            if (!! lookup[key]._x_effects) {
                lookup[key]._x_effects.forEach(dequeueJob)
            }

            lookup[key].remove()

            lookup[key] = null
            delete lookup[key]
        }

        // Here we'll move elements around, skipping
        // mutation observer triggers by using "mutateDom".
        for (let i = 0; i < moves.length; i++) {
            let [keyInSpot, keyForSpot] = moves[i]

            let elInSpot = lookup[keyInSpot]
            let elForSpot = lookup[keyForSpot]

            let marker = document.createElement('div')

            mutateDom(() => {
                if (! elForSpot) warn(`x-for ":key" is undefined or invalid`, templateEl)

                elForSpot.after(marker)
                elInSpot.after(elForSpot)
                elForSpot._x_currentIfEl && elForSpot.after(elForSpot._x_currentIfEl)
                marker.before(elInSpot)
                elInSpot._x_currentIfEl && elInSpot.after(elInSpot._x_currentIfEl)
                marker.remove()
            })

            elForSpot._x_refreshXForScope(scopes[keys.indexOf(keyForSpot)])
        }

        // We can now create and add new elements.
        for (let i = 0; i < adds.length; i++) {
            let [lastKey, index] = adds[i]

            let lastEl = (lastKey === 'template') ? templateEl : lookup[lastKey]
            // If the element is a x-if template evaluated to true,
            // point lastEl to the if-generated node
            if (lastEl._x_currentIfEl) lastEl = lastEl._x_currentIfEl

            let scope = scopes[index]
            let key = keys[index]

            let clone = document.importNode(templateEl.content, true).firstElementChild

            let reactiveScope = reactive(scope)

            addScopeToNode(clone, reactiveScope, templateEl)

            clone._x_refreshXForScope = (newScope) => {
                Object.entries(newScope).forEach(([key, value]) => {
                    reactiveScope[key] = value
                })
            }

            mutateDom(() => {
                lastEl.after(clone)

                initTree(clone)
            })

            if (typeof key === 'object') {
                warn('x-for key cannot be an object, it must be a string or an integer', templateEl)
            }

            lookup[key] = clone
        }

        // If an element hasn't changed, we still want to "refresh" the
        // data it depends on in case the data has changed in an
        // "unobservable" way.
        for (let i = 0; i < sames.length; i++) {
            lookup[sames[i]]._x_refreshXForScope(scopes[keys.indexOf(sames[i])])
        }

        // Now we'll log the keys (and the order they're in) for comparing
        // against next time.
        templateEl._x_prevKeys = keys
    })
}

// This was taken from VueJS 2.* core. Thanks Vue!
function parseForExpression(expression) {
    let forIteratorRE = /,([^,\}\]]*)(?:,([^,\}\]]*))?$/
    let stripParensRE = /^\s*\(|\)\s*$/g
    let forAliasRE = /([\s\S]*?)\s+(?:in|of)\s+([\s\S]*)/
    let inMatch = expression.match(forAliasRE)

    if (! inMatch) return

    let res = {}
    res.items = inMatch[2].trim()
    let item = inMatch[1].replace(stripParensRE, '').trim()
    let iteratorMatch = item.match(forIteratorRE)

    if (iteratorMatch) {
        res.item = item.replace(forIteratorRE, '').trim()
        res.index = iteratorMatch[1].trim()

        if (iteratorMatch[2]) {
            res.collection = iteratorMatch[2].trim()
        }
    } else {
        res.item = item
    }

    return res
}

function getIterationScopeVariables(iteratorNames, item, index, items) {
    // We must create a new object, so each iteration has a new scope
    let scopeVariables = {}

    // Support array destructuring ([foo, bar]).
    if (/^\[.*\]$/.test(iteratorNames.item) && Array.isArray(item)) {
        let names = iteratorNames.item.replace('[', '').replace(']', '').split(',').map(i => i.trim())

        names.forEach((name, i) => {
            scopeVariables[name] = item[i]
        })
    // Support object destructuring ({ foo: 'oof', bar: 'rab' }).
    } else if (/^\{.*\}$/.test(iteratorNames.item) && ! Array.isArray(item) && typeof item === 'object') {
        let names = iteratorNames.item.replace('{', '').replace('}', '').split(',').map(i => i.trim())

        names.forEach(name => {
            scopeVariables[name] = item[name]
        })
    } else {
        scopeVariables[iteratorNames.item] = item
    }

    if (iteratorNames.index) scopeVariables[iteratorNames.index] = index

    if (iteratorNames.collection) scopeVariables[iteratorNames.collection] = items

    return scopeVariables
}

function isNumeric(subject){
    return ! Array.isArray(subject) && ! isNaN(subject)
}
