// Welcome to Surreal 1.3.4
// Documentation: https://github.com/gnat/surreal
// Locality of Behavior (LoB)
let surreal = (function () {
let $ = { // Convenience for internals.
    $: this, // Convenience for internals.
    plugins: [],

    // Table of contents and convenient call chaining sugar. For a familiar "jQuery like" syntax.
    // Check before adding new: https://youmightnotneedjquery.com/
    sugar(e) {
        if (!$.isNode(e) && !$.isNodeList(e)) { console.warn(`Surreal: Not a supported element / node / node list "${e}"`); return e }
        if ($.isNodeList(e)) e.forEach(_ => { $.sugar(_) }) // Add Surreal to all nodes from any()
        if (e.hasOwnProperty('hasSurreal')) return e // Surreal already added.

        // General
        e.run           = (f) => { return $.run(e, f) }
        e.remove        = () => { return $.remove(e) }

        // Classes and CSS.
        e.classAdd      = (name) => { return $.classAdd(e, name) }
        e.class_add     = e.add_class = e.addClass = e.classAdd // Alias
        e.classRemove   = (name) => { return $.classRemove(e, name) }
        e.class_remove  = e.remove_class = e.removeClass = e.classRemove // Alias
        e.classToggle   = (name, force) => { return $.classToggle(e, name, force) }
        e.class_toggle  = e.toggle_class = e.toggleClass = e.classToggle // Alias
        e.styles        = (value) => { return $.styles(e, value) }

        // Events.
        e.on            = (name, f) => { return $.on(e, name, f) }
        e.off           = (name, f) => { return $.off(e, name, f) }
        e.offAll        = (name) => { return $.offAll(e, name) }
        e.off_all       = e.offAll // Alias
        e.disable       = () => { return $.disable(e) }
        e.enable        = () => { return $.enable(e) }
        e.send          = (name, detail) => { return $.send(e, name, detail) }
        e.trigger       = e.send // Alias
        e.halt          = (ev, keepBubbling, keepDefault) => { return $.halt(ev, keepBubbling, keepDefault) }

        // Attributes.
        e.attribute     = (name, value) => { return $.attribute(e, name, value) }
        e.attributes    = e.attr = e.attribute // Alias

        // Add all plugins.
        $.plugins.forEach(function(func) { func(e) })

        e.hasSurreal = 1
        return e
    },
    // me() will return a single element or null. Selector not needed if used with inline <script>
    // If you select many elements, me() will return the first.
    // Example
    //  <div>
    //      Hello World!
    //      <script>me().style.color = 'red'</script>
    //  </div>
    me(selector=null, start=document, warning=true) {
        if (selector == null) return $.sugar(start.currentScript.parentElement) // Just local me() in <script>
        if (selector instanceof Event) return selector.currentTarget ? $.me(selector.currentTarget) : (console.warn(`Surreal: Event currentTarget is null. Please save your element because async will lose it`), null) // Events try currentTarget
        if (selector === '-' || selector === 'prev' || selector === 'previous') return $.sugar(start.currentScript.previousElementSibling) // Element directly before <script>
        if ($.isSelector(selector, start, warning)) return $.sugar(start.querySelector(selector)) // String selector.
        if ($.isNodeList(selector)) return $.me(selector[0]) // If we got a list, return the first element.
        if ($.isNode(selector)) return $.sugar(selector) // Valid element.
        return null // Invalid.
    },
    // any() is me() but will return an array of elements or empty [] if nothing is found.
    // You may optionally use forEach/map/filter/reduce.
    // Example: any('button')
    any(selector, start=document, warning=true) {
        if (selector == null) return $.sugar([start.currentScript.parentElement]) // Similar to me()
        if (selector instanceof Event) return selector.currentTarget ? $.any(selector.currentTarget) : (console.warn(`Surreal: Event currentTarget is null. Please save your element because async will lose it`), null) // Events try currentTarget
        if (selector === '-' || selector === 'prev' || selector === 'previous') return $.sugar([start.currentScript.previousElementSibling]) // Element directly before <script>
        if ($.isSelector(selector, start, warning)) return $.sugar(Array.from(start.querySelectorAll(selector))) // String selector.
        if ($.isNode(selector)) return $.sugar([selector]) // Single element. Convert to Array.
        if ($.isNodeList(selector)) return $.sugar(Array.from(selector)) // Valid NodeList or Array.
        return $.sugar([]) // Invalid.
    },
    // Run any function on element(s)
    run(e, f) {
        if (typeof f !== 'function') { console.warn(`Surreal: run(f) f must be a function`); return e }
        if ($.isNodeList(e)) e.forEach(_ => { $.run(_, f) })
        if ($.isNode(e)) { f(e); }
        return e
    },
    // Remove element(s)
    remove(e) {
        if ($.isNodeList(e)) e.forEach(_ => { $.remove(_) })
        if ($.isNode(e)) e.parentNode.removeChild(e)
        return // Special, end of chain.
    },
    // Add class to element(s).
    classAdd(e, name) {
        if (typeof name !== 'string') return e
        if (name.charAt(0) === '.') name = name.substring(1)
        if ($.isNodeList(e)) e.forEach(_ => { $.classAdd(_, name) })
        if ($.isNode(e)) e.classList.add(name)
        return e
    },
    // Remove class from element(s).
    classRemove(e, name) {
        if (typeof name !== 'string') return e
        if (name.charAt(0) === '.') name = name.substring(1)
        if ($.isNodeList(e)) e.forEach(_ => { $.classRemove(_, name) })
        if ($.isNode(e)) e.classList.remove(name)
        return e
    },
    // Toggle class in element(s).
    classToggle(e, name, force) {
        if (typeof name !== 'string') return e
        if (name.charAt(0) === '.') name = name.substring(1)
        if ($.isNodeList(e)) e.forEach(_ => { $.classToggle(_, name, force) })
        if ($.isNode(e)) e.classList.toggle(name, force)
        return e
    },
    // Add inline style to element(s).
    // Can use string or object formats.
    //  String format: "font-family: 'sans-serif'"
    //  Object format; { fontFamily: 'sans-serif', backgroundColor: '#000' }
    styles(e, value) {
        if (typeof value === 'string') { // Format: "font-family: 'sans-serif'"
            if ($.isNodeList(e)) e.forEach(_ => { $.styles(_, value) })
            if ($.isNode(e)) { $.attribute(e, 'style', ($.attribute(e, 'style') == null ? '' : $.attribute(e, 'style') + '; ') + value)  }
            return e
        }
        if (typeof value === 'object') { // Format: { fontFamily: 'sans-serif', backgroundColor: '#000' }
            if ($.isNodeList(e)) e.forEach(_ => { $.styles(_, value) })
            if ($.isNode(e)) { Object.assign(e.style, value)  }
            return e
        }
        return e
    },
    // Add event listener to element(s).
    // Match a sender: if (!event.target.matches(".selector")) return;
    //  https://developer.mozilla.org/en-US/docs/Web/API/Event
    //  Vanilla: document.querySelector(".thing").addEventListener("click", (e) => { alert("clicked") }
    on(e, name, f) {
        if ($.isNodeList(e)) e.forEach(_ => { $.on(_, name, f) })
        if ($.isNode(e)) e.addEventListener(name, f)
        return e
    },
    off(e, name, f) {
        if ($.isNodeList(e)) e.forEach(_ => { $.off(_, name, f) })
        if ($.isNode(e)) e.removeEventListener(name, f)
        return e
    },
    offAll(e) {
        if ($.isNodeList(e)) e.forEach(_ => { $.offAll(_) })
        if ($.isNode(e)) e.parentNode.replaceChild(e.cloneNode(true), e)
        return e
    },
    // Easy alternative to off(). Disables click, key, submit events.
    disable(e) {
        if ($.isNodeList(e)) e.forEach(_ => { $.disable(_) })
        if ($.isNode(e)) e.disabled = true
        return e
    },
    // For reversing disable()
    enable(e) {
        if ($.isNodeList(e)) e.forEach(_ => { $.enable(_) })
        if ($.isNode(e)) e.disabled = false
        return e
    },
    // Send / trigger event.
    // Vanilla: Events Dispatch: document.querySelector(".thing").dispatchEvent(new Event('click'))
    send(e, name, detail=null) {
        if ($.isNodeList(e)) e.forEach(_ => { $.send(_, name, detail) })
        if ($.isNode(e)) {
            const event = new CustomEvent(name, { detail: detail, bubbles: true })
            e.dispatchEvent(event)
        }
        return e
    },
    // Halt event. Default: Stops normal event actions and event propagation.
    halt(ev, keepBubbling=false, keepDefault=false) {
        if (ev instanceof Event) {
            if (!keepDefault) ev.preventDefault()
            if (!keepBubbling) ev.stopPropagation()
        }
        return ev
    },
    // Add or remove attributes from element(s)
    attribute(e, name, value=undefined) {
        // Get. (Format: "name", "value") Special: Ends call chain.
        if (typeof name === 'string' && value === undefined) {
            if ($.isNodeList(e)) return [] // Not supported for Get. For many elements, wrap attribute() in any(...).run(...) or any(...).forEach(...)
            if ($.isNode(e)) return e.getAttribute(name)
            return null // No value. Ends call chain.
        }
        // Remove.
        if (typeof name === 'string' && value === null) {
            if ($.isNodeList(e)) e.forEach(_ => { $.attribute(_, name, value) })
            if ($.isNode(e)) e.removeAttribute(name)
            return e
        }
        // Add / Set.
        if (typeof name === 'string') {
            if ($.isNodeList(e)) e.forEach(_ => { $.attribute(_, name, value) })
            if ($.isNode(e)) e.setAttribute(name, value)
            return e
        }
        // Format: { "name": "value", "blah": true }
        if (typeof name === 'object') {
            if ($.isNodeList(e)) e.forEach(_ => { Object.entries(name).forEach(([key, val]) => { $.attribute(_, key, val) }) })
            if ($.isNode(e)) Object.entries(name).forEach(([key, val]) => { $.attribute(e, key, val) })
            return e
        }
        return e
    },
    // Puts Surreal functions except for "restricted" in global scope.
    globalsAdd() {
        console.log(`Surreal: Adding convenience globals to window.`)
        let restricted = ['$', 'sugar', 'plugins']
        for (const [key, value] of Object.entries(this)) {
            if (!restricted.includes(key)) window[key] === undefined ? window[key] = value : console.warn(`Surreal: "${key}()" already exists on window. Skipping to prevent overwrite.`)
            if (!restricted.includes(key)) window.document[key] === undefined ? window.document[key] = value : console.warn(`Surreal: "${key}()" already exists on document. Skipping to prevent overwrite.`)
        }
    },
    // Used internally. Is this an element / node?
    isNode(e) {
        return (e instanceof HTMLElement || e instanceof SVGElement || e instanceof Document) ? true : false
    },
    // Used internally by DOM functions. Is this a list of elements / nodes?
    isNodeList(e) {
        return (e instanceof NodeList || Array.isArray(e)) ? true : false
    },
    // Used internally by DOM functions. Warning when selector is invalid. Likely missing a "#" or "."
    isSelector(selector="", start=document, warning=true) {
        if (typeof selector !== 'string') return false
        if (start.querySelector(selector) == null) {
            if (warning) console.log(`Surreal: "${selector}" not found, ignoring.`)
            return false
        }
        return true // Valid.
    },
}
// Finish up...
$.globalsAdd() // Full convenience.
console.log("Surreal: Loaded.")
return $
})() // End of Surreal.

// Plugin: Effects
function pluginEffects(e) {
    // Fade out and remove element.
    // Equivalent to jQuery fadeOut(), but actually removes the element.
    function fadeOut(e, f=undefined, ms=1000, remove=true) {
        let thing = e
        if (surreal.isNodeList(e)) e.forEach(_ => { fadeOut(_, f, ms) })
        if (surreal.isNode(e)) {
            (async() => {
                surreal.styles(e, {transform: 'scale(1)', transition: `all ${ms}ms ease-out`, overflow: 'hidden'})
                await tick()
                surreal.styles(e, {transform: 'scale(0.9)', opacity: '0'})
                await sleep(ms, e)
                if (typeof f === 'function') f(thing) // Run custom callback?
                if (remove) surreal.remove(thing) // Remove element after animation is completed?
            })()
        }
    }
    // Fade in an element that has opacity under 1
    function fadeIn(e, f=undefined, ms=1000) {
        let thing = e
        if (surreal.isNodeList(e)) e.forEach(_ => { fadeIn(_, f, ms) })
        if (surreal.isNode(e)) {
            (async() => {
                let save = e.style // Store original style.
                surreal.styles(e, {transition: `all ${ms}ms ease-in`, overflow: 'hidden'})
                await tick()
                surreal.styles(e, {opacity: '1'})
                await sleep(ms, e)
                e.style = save // Revert back to original style.
                surreal.styles(e, {opacity: '1'}) // Ensure we're visible after reverting to original style.
                if (typeof f === 'function') f(thing) // Run custom callback?
            })()
        }
    }
    // Add sugar
    e.fadeOut  = (f, ms, remove) => { return fadeOut(e, f, ms, remove) }
    e.fade_out = e.fadeOut
    e.fadeIn   = (f, ms) => { return fadeIn(e, f, ms) }
    e.fade_in  = e.fadeIn
}

// Add plugins here!
surreal.plugins.push(pluginEffects)
console.log("Surreal: Added plugins.")

// Add global shortcuts here!
// DOM.
const createElement = document.createElement.bind(document); const create_element = createElement
// Animation.
const rAF = typeof requestAnimationFrame !== 'undefined' && requestAnimationFrame
const rIC = typeof requestIdleCallback !== 'undefined' && requestIdleCallback
// Animation: Wait for next animation frame, non-blocking.
async function tick() {
    return await new Promise(resolve => { requestAnimationFrame(resolve) })
}
// Animation: Sleep, non-blocking.
async function sleep(ms, e) {
    return await new Promise(resolve => setTimeout(() => { resolve(e) }, ms))
}
// Loading: Why? So you don't clobber window.onload (predictable sequential loading)
// Example: <script>onloadAdd(() => { console.log("Page was loaded!") })</script>
// Example: <script>onloadAdd(() => { console.log("Lets do another thing without clobbering window.onload!") })</script>
const addOnload = (f) => {
    if (typeof window.onload === 'function') { // window.onload already is set, queue functions together (creates a call chain).
        let onload_old = window.onload
        window.onload = () => {
            onload_old()
            f()
        }
        return
    }
    window.onload = f // window.onload was not set yet.
}
const onloadAdd = addOnload; const onload_add = addOnload; const add_onload = addOnload
console.log("Surreal: Added shortcuts.")
