' @rokucommunity/promises v0.5.0
' Create a new promise
function rooibos_promises_create() as dynamic
    'create a unique ID for this promise
    id = "promise-" + rooibos_promises_internal_createUuid()
    node = createObject("roSGNode", "rooibos_promises_Promise")
    node.id = id
    return node
end function

' TODO rename this to `then` once BrighterScript supports using keywords as namespaced function names
function rooibos_promises_onThen(promise as dynamic, callback as Function, context = "__INVALID__" as object) as dynamic
    return rooibos_promises_internal_on("then", promise, callback, context)
end function

' TODO rename this to `catch` once BrighterScript supports using keywords as namespaced function names
function rooibos_promises_onCatch(promise as dynamic, callback as Function, context = "__INVALID__" as object) as dynamic
    return rooibos_promises_internal_on("catch", promise, callback, context)
end function

' TODO rename this to `finally` once BrighterScript supports using keywords as namespaced function names
function rooibos_promises_onFinally(promise as dynamic, callback as Function, context = "__INVALID__" as object) as dynamic
    return rooibos_promises_internal_on("finally", promise, callback, context)
end function

' Takes an array of promises as input and returns a single Promise.
' This returned promise fulfills when all of the input's promises fulfill (including when an empty array is passed), with an array of the fulfillment values.
' It rejects when any of the input's promises rejects, with this first rejection reason.
function rooibos_promises_all(promiseArray as dynamic) as dynamic
    ' Create a deferred to be resolved later
    deferred = rooibos_promises_create()
    if type(promiseArray) = "roArray" and not promiseArray.isEmpty() then
        ' Track the state and results of all the promises
        state = {
            deferred: deferred
            results: []
            resolvedCount: 0
            total: promiseArray.count()
            done: false
        }
        for i = 0 to promiseArray.count() - 1
            promise = promiseArray[i]
            if rooibos_promises_isPromise(promise) then
                ' Watch for both resolved or rejected promises
                rooibos_promises_onThen(promise, sub(result as dynamic, context as dynamic)
                    ' Do not process any promises that come in late
                    ' This can happen if any of the other promises reject
                    if not context.state.done then
                        ' Always assign the result to the origin index so results are in the same
                        ' order as the supplied promiseArray
                        context.state.results[context.index] = result
                        context.state.resolvedCount++
                        if context.state.resolvedCount = context.state.total then
                            ' All the promises are resolved.
                            ' Resolve the deferred and make the state as complete
                            context.state.done = true
                            rooibos_promises_resolve(context.state.results, context.state.deferred)
                        end if
                    end if
                end sub, {
                    state: state
                    index: i
                })
                rooibos_promises_onCatch(promise, sub(error as dynamic, state as dynamic)
                    ' This shouldn't happen but if we somehow get a rejected promise after
                    ' the state is marked as done we should ignore this callback
                    if not state.done then
                        ' Immediately mark the state as done and reject the deferred
                        ' with the error from the rejected promise the first time any
                        ' promise rejects regardless where in the promise array it was
                        ' located.
                        state.done = true
                        rooibos_promises_reject(error, state.deferred)
                    end if
                end sub, state)
            else
                ' The value in the promise array is not a promise.
                ' Immediately set the result.
                state.results[i] = promise
                state.resolvedCount++
                if state.resolvedCount = state.total then
                    ' All the promises are resolved.
                    ' Resolve the deferred and make the state as complete
                    state.done = true
                    rooibos_promises_resolve(state.results, state.deferred)
                end if
            end if
        end for
    else
        if type(promiseArray) = "roArray" then
            ' Resolve when the array is empty
            rooibos_promises_resolve(promiseArray, deferred)
        else
            ' Reject if the supplied list is not an array
            try
                throw "Did not supply an array"
            catch e
                rooibos_promises_reject(e, deferred)
            end try
        end if
    end if
    return deferred
end function

' Takes an array of promises as input and returns a single Promise.
' This returned promise fulfills when all of the input's promises settle (including when an empty array is passed),
' with an array of objects that describe the outcome of each promise.
function rooibos_promises_allSettled(promiseArray as dynamic) as dynamic
    ' Create a deferred to be resolved later
    deferred = rooibos_promises_create()
    if type(promiseArray) = "roArray" and not promiseArray.isEmpty() then
        ' Track the state and results of all the promises
        state = {
            deferred: deferred
            results: []
            resolvedCount: 0
            total: promiseArray.count()
            done: false
        }
        for i = 0 to promiseArray.count() - 1
            promise = promiseArray[i]
            if rooibos_promises_isPromise(promise) then
                ' Watch for both resolved or rejected promises
                rooibos_promises_onThen(promise, sub(result as dynamic, context as dynamic)
                    ' Do not process any promises that come in late
                    ' This can happen if any of the other promises reject
                    if not context.state.done then
                        ' Always assign the result to the origin index so results are in the same
                        ' order as the supplied promiseArray
                        context.state.results[context.index] = {
                            status: "resolved"
                            value: result
                        }
                        context.state.resolvedCount++
                        if context.state.resolvedCount = context.state.total then
                            ' All the promises are resolved.
                            ' Resolve the deferred and make the state as complete
                            context.state.done = true
                            rooibos_promises_resolve(context.state.results, context.state.deferred)
                        end if
                    end if
                end sub, {
                    state: state
                    index: i
                })
                rooibos_promises_onCatch(promise, sub(error as dynamic, context as dynamic)
                    ' Do not process any promises that come in late
                    ' This can happen if any of the other promises reject
                    if not context.state.done then
                        ' Always assign the result to the origin index so results are in the same
                        ' order as the supplied promiseArray
                        context.state.results[context.index] = {
                            status: "rejected"
                            reason: error
                        }
                        context.state.resolvedCount++
                        if context.state.resolvedCount = context.state.total then
                            ' All the promises are resolved.
                            ' Resolve the deferred and make the state as complete
                            context.state.done = true
                            rooibos_promises_resolve(context.state.results, context.state.deferred)
                        end if
                    end if
                end sub, {
                    state: state
                    index: i
                })
            else
                ' The value in the promise array is not a promise.
                ' Immediately set the result.
                state.results[i] = {
                    status: "resolved"
                    value: promise
                }
                state.resolvedCount++
                if state.resolvedCount = state.total then
                    ' All the promises are resolved.
                    ' Resolve the deferred and make the state as complete
                    state.done = true
                    rooibos_promises_resolve(state.results, state.deferred)
                end if
            end if
        end for
    else
        if type(promiseArray) = "roArray" then
            ' Resolve when the array is empty
            rooibos_promises_resolve(promiseArray, deferred)
        else
            ' Reject if the supplied list is not an array
            try
                throw "Did not supply an array"
            catch e
                rooibos_promises_reject(e, deferred)
            end try
        end if
    end if
    return deferred
end function

' Takes an array of promises as input and returns a single Promise.
' This returned promise fulfills when any of the input's promises fulfills, with this first fulfillment value.
' It rejects when all of the input's promises reject (including when an empty array is passed), with an AggregateError containing an array of rejection reasons.
function rooibos_promises_any(promiseArray as dynamic) as dynamic
    ' Create a deferred to be resolved later
    deferred = rooibos_promises_create()
    if type(promiseArray) = "roArray" and not promiseArray.isEmpty() then
        ' Track the state and results of all the promises
        state = {
            deferred: deferred
            errors: []
            resolvedCount: 0
            total: promiseArray.count()
            done: false
        }
        for i = 0 to promiseArray.count() - 1
            promise = promiseArray[i]
            if rooibos_promises_isPromise(promise) then
                if promise.promiseState = "resolved" then
                    ' Do not process any promises that come in after the first resolved one
                    if not state.done then
                        state.done = true
                        rooibos_promises_resolve(promise.promiseResult, state.deferred)
                    end if
                else
                    ' Watch for both resolved or rejected promises
                    rooibos_promises_onThen(promise, sub(result as dynamic, state as dynamic)
                        ' Do not process any promises that come in after the first resolved one
                        if not state.done then
                            state.done = true
                            rooibos_promises_resolve(result, state.deferred)
                        end if
                    end sub, state)
                    rooibos_promises_onCatch(promise, sub(error as dynamic, context as dynamic)
                        ' Do not process any promises that come in late
                        ' This can happen if any of the other promises reject
                        if not context.state.done then
                            ' Always assign the result to the origin index so results are in the same
                            ' order as the supplied promiseArray
                            context.state.errors[context.index] = error
                            context.state.resolvedCount++
                            if context.state.resolvedCount = context.state.total then
                                ' All the promises are resolved.
                                ' Resolve the deferred and make the state as complete
                                context.state.done = true
                                try
                                    throw {
                                        message: "All promises were rejected"
                                        errors: context.state.errors
                                    }
                                catch e
                                    rooibos_promises_reject(e, context.state.deferred)
                                end try
                            end if
                        end if
                    end sub, {
                        state: state
                        index: i
                    })
                end if
            else
                ' The value in the promise array is not a promise.
                ' Immediately set the result.
                if not state.done then
                    state.done = true
                    rooibos_promises_resolve(promise, state.deferred)
                end if
            end if
        end for
    else
        ' We can't resolve with a promise if there are no promises to resolve
        try
            throw {
                message: "All promises were rejected"
                errors: []
            }
        catch e
            rooibos_promises_reject(e, deferred)
        end try
    end if
    return deferred
end function

' Takes an array of promises as input and returns a single Promise.
' This returned promise settles with the eventual state of the first promise that settles.
function rooibos_promises_race(promiseArray as dynamic) as dynamic
    ' Create a deferred to be resolved later
    deferred = rooibos_promises_create()
    if type(promiseArray) = "roArray" and not promiseArray.isEmpty() then
        ' Track the state and results of all the promises
        state = {
            deferred: deferred
            done: false
        }
        for i = 0 to promiseArray.count() - 1
            promise = promiseArray[i]
            if rooibos_promises_isPromise(promise) then
                if promise.promiseState = "resolved" then
                    ' Do not process any promises that come in after the first resolved one
                    if not state.done then
                        state.done = true
                        rooibos_promises_resolve(promise.promiseResult, state.deferred)
                    end if
                else if promise.promiseState = "rejected" then
                    ' Do not process any promises that come in after the first resolved one
                    if not state.done then
                        state.done = true
                        rooibos_promises_reject(promise.promiseResult, state.deferred)
                    end if
                else
                    ' Watch for both resolved or rejected promises
                    rooibos_promises_onThen(promise, sub(result as dynamic, state as dynamic)
                        ' Do not process any promises that come in after the first resolved one
                        if not state.done then
                            state.done = true
                            rooibos_promises_resolve(result, state.deferred)
                        end if
                    end sub, state)
                    rooibos_promises_onCatch(promise, sub(error as dynamic, state as dynamic)
                        ' Do not process any promises that come in after the first resolved one
                        if not state.done then
                            state.done = true
                            rooibos_promises_reject(error, state.deferred)
                        end if
                    end sub, state)
                end if
            else
                ' The value in the promise array is not a promise.
                ' Immediately set the result.
                if not state.done then
                    state.done = true
                    rooibos_promises_resolve(promise, state.deferred)
                end if
            end if
        end for
    else
        ' We can't resolve with a promise if there are no promises to resolve
        try
            throw {
                message: "All promises were rejected"
                errors: []
            }
        catch e
            rooibos_promises_reject(e, deferred)
        end try
    end if
    return deferred
end function

function rooibos_promises_resolve(result as dynamic, promise = invalid as dynamic) as object
    if not rooibos_promises_isPromise(promise) then
        promise = rooibos_promises_create()
    end if
    if not rooibos_promises_isComplete(promise) then
        ' console.trace("[promises.resolve]", promise.id)
        if type(result) = "roAssociativeArray" then
            promise.removeField("promiseResult")
            promise.addFields({
                promiseResult: result
            })
        else
            promise.update({
                promiseResult: result
            }, true)
        end if
        promise.promiseState = "resolved"
    end if
    return promise
end function

function rooibos_promises_reject(error as dynamic, promise = invalid as dynamic) as object
    if not rooibos_promises_isPromise(promise) then
        promise = rooibos_promises_create()
    end if
    if not rooibos_promises_isComplete(promise) then
        ' console.trace("[promises.reject]", promise.id)
        if type(error) = "roAssociativeArray" then
            promise.removeField("promiseResult")
            promise.addFields({
                promiseResult: error
            })
        else
            promise.update({
                promiseResult: error
            }, true)
        end if
        promise.promiseState = "rejected"
    end if
    return promise
end function

function rooibos_promises_isComplete(promise as object) as boolean
    return rooibos_promises_isPromise(promise) and (promise.promiseState = "resolved" or promise.promiseState = "rejected")
end function

' Determines if the given item is a promise.
'
' Will return true if at least one of the following conditions are true:
' - the SubType exactly equals "Promise"
' - the subtype ends with "_promise" case insensitive
' - the node has a field called "promiseState"
function rooibos_promises_isPromise(promise as dynamic) as boolean
    if not type(promise) = "roSGNode" then
        return false
    end if
    subType = lCase(promise.subType())
    if subType.endsWith("_promise") then
        return true
    end if
    if subType = "promise" then
        return true
    end if
    while true
        subType = promise.parentSubtype(subType)
        if lCase(subType).endsWith("_promise") then
            return true
        end if
        if subType = "" then
            exit while
        end if
    end while
    return promise.hasField("promiseState")
end function

' Remove all promise storage from the current m
sub rooibos_promises_clean()
    for each key in m
        if key.startsWith("__promises__") then
            m.delete(key)
        end if
    end for
end sub

'Allows chaining multiple promise operations in a row in a clean syntax
function rooibos_promises_chain(initialPromise as object, context = "__INVALID__" as object) as object
    return {
        _lastPromise: initialPromise
        _context: context
        then: function(callback as Function) as object
            m._lastPromise = rooibos_promises_onThen(m._lastPromise, callback, m._context)
            return m
        end function
        "catch": function(callback as Function) as object
            m._lastPromise = rooibos_promises_onCatch(m._lastPromise, callback, m._context)
            return m
        end function
        finally: function(callback as Function) as object
            m._lastPromise = rooibos_promises_onFinally(m._lastPromise, callback, m._context)
            return m
        end function
        toPromise: function() as object
            return m._lastPromise
        end function
    }
end function

' Makes sure the value supplied is a promise
function rooibos_promises_ensurePromise(value as object) as object
    if rooibos_promises_isPromise(value) then
        return value
    end if
    return rooibos_promises_resolve(value)
end function





' Clear storage for a given promise
sub rooibos_promises_internal_clearPromiseStorage(promise as object)
    m.delete("__promises__" + promise.id)
end sub

' Get the storage for a promise on `m`
function rooibos_promises_internal_getPromiseStorage(promise as object) as object
    id = "__promises__" + promise.id
    storage = m[id]
    if storage = invalid then
        ' unregister any observers on the promise to prevent multiple callbacks
        rooibos_promises_internal_unobserveFieldScoped(promise, "promiseState")
        rooibos_promises_internal_observeFieldScoped(promise, "promiseState", sub(event)
            'run the notification nexttick to prevent stackoverflow due to cascading promises all resolving in sequence
            rooibos_promises_internal_delay(sub(context)
                rooibos_promises_internal_notifyListeners(context.event)
            end sub, {
                event: event
            })
        end sub)
        storage = {
            promise: promise
            thenListeners: []
            catchListeners: []
            finallyListeners: []
        }
        m[id] = storage
    end if
    return storage
end function

'
' Registers a listener for a promise for the then, catch, or finally events
' @param eventName - should be "then", "catch", or "finally"
'
function rooibos_promises_internal_on(eventName as string, promise as dynamic, callback as Function, context = {} as object) as dynamic
    if rooibos_promises_isPromise(promise) then
        newPromise = rooibos_promises_create()
        storage = rooibos_promises_internal_getPromiseStorage(promise)
        storage[eventName + "Listeners"].push({
            callback: callback
            context: context
            promise: newPromise
        })
        promiseState = promise.promiseState
        'trigger a change if the promise is already resolved
        if promiseState = "resolved" or promiseState = "rejected" then
            rooibos_promises_internal_delay(sub(details as object)
                details.promise.promiseState = details.promiseState
            end sub, {
                promise: promise
                promiseState: promiseState
            })
        end if
        return newPromise
    end if
    errorMessage = "Cannot register promises." + eventName + " for non-promise"
    throw errorMessage
    return invalid
end function

'
' Notify all the listeners of a promise that it has been completed
'
sub rooibos_promises_internal_notifyListeners(event as object)
    originalPromise = event.getRoSgNode()
    if rooibos_promises_isComplete(originalPromise) then
        ' unregister any observers once the promise is completed
        rooibos_promises_internal_unobserveFieldScoped(originalPromise, "promiseState")
        promiseStorage = rooibos_promises_internal_getPromiseStorage(originalPromise)
        ' Delete the storage for this promise since we are going to handled all of the current listeners.
        ' Any new listeners created as a result of the logic in the callbacks will
        ' register a new instance of the promise storage item. If a new storage item is created
        ' we will notify the new listeners when we are done with the current ones.
        rooibos_promises_internal_clearPromiseStorage(originalPromise)
        promiseState = originalPromise.promiseState
        promiseResult = originalPromise.promiseResult
        'handle .then() listeners
        for each listener in promiseStorage.thenListeners
            rooibos_promises_internal_processPromiseListener(originalPromise, listener, promiseState = "resolved", promiseResult)
        end for
        'handle .catch() listeners
        for each listener in promiseStorage.catchListeners
            rooibos_promises_internal_processPromiseListener(originalPromise, listener, promiseState = "rejected", promiseResult)
        end for
        'handle .finally() listeners
        for each listener in promiseStorage.finallyListeners
            rooibos_promises_internal_processPromiseListener(originalPromise, listener, true)
        end for
        if rooibos_promises_internal_hasStorage(originalPromise) then
            ' There were listeners added as a result of some of the callback notifications
            ' Re-trigger the notification process for the new listeners
            rooibos_promises_internal_delay(sub(event as object)
                rooibos_promises_internal_notifyListeners(event)
            end sub, event)
        end if
    end if
end sub

' Used to check if there is a storage item of listeners for the supplied promise
function rooibos_promises_internal_hasStorage(promise as dynamic) as boolean
    return m.doesExist("__promises__" + promise.id)
end function

' We use an internal value to represent unset. Check if the parameter is that value
function rooibos_promises_internal_isSet(value as dynamic) as boolean
    return not (rooibos_promises_internal_isNonEmptyString(value) and value = "__INVALID__")
end function

' Is the supplied value a valid String type and is not empty
' @param value - The variable to be checked
' @return true if value is a non-empty string, false otherwise
function rooibos_promises_internal_isNonEmptyString(value as dynamic) as boolean
    return (type(value) = "String" or type(value) = "roString") and value <> ""
end function

' Handle an individual promise listener
sub rooibos_promises_internal_processPromiseListener(originalPromise as object, storageItem as object, callCallback as boolean, promiseValue = "__INVALID__" as dynamic)
    newPromise = storageItem.promise
    callback = storageItem.callback
    context = storageItem.context
    hasContext = rooibos_promises_internal_isSet(context)
    hasPromiseValue = rooibos_promises_internal_isSet(promiseValue)
    'only call the callback if configured to do so
    if callCallback then
        try
            '.then and .catch take one or two parameters (`promiseValue` and optional `context`)
            if hasPromiseValue then
                if hasContext then
                    callbackResult = callback(promiseValue, context)
                else
                    callbackResult = callback(promiseValue)
                end if
                '.finally callback takes 1 optional parameter (`context`)
            else
                if hasContext then
                    callbackResult = callback(context)
                else
                    callbackResult = callback()
                end if
            end if
        catch e
            'the result is a rejected promise
            callbackResult = rooibos_promises_reject(e)
        end try
    else
        'use the current promise value to pass to the next promise (this is a .catch handler)
        if originalPromise.promiseState = "rejected" then
            callbackResult = rooibos_promises_reject(promiseValue)
        else
            callbackResult = promiseValue
        end if
    end if
    'if the .then() callback returned a promise. wait for it to resolve and THEN resolve the newPromise
    if rooibos_promises_isPromise(callbackResult) then
        callbackPromise = callbackResult
        'wait for the callback promise to complete
        rooibos_promises_onFinally(callbackPromise, sub(context as object)
            promiseState = context.callbackPromise.promiseState
            promiseResult = context.callbackPromise.promiseResult
            if promiseState = "resolved" then
                'the callback promise is complete. resolve the newPromise
                rooibos_promises_resolve(promiseResult, context.newPromise)
                return
            end if
            if promiseState = "rejected" then
                rooibos_promises_reject(promiseResult, context.newPromise)
                return
            end if
        end sub, {
            newPromise: newPromise
            callbackPromise: callbackPromise
        })
        'the .then() callback returned a non-promise. Resolve the newPromise immediately with this value
    else
        rooibos_promises_resolve(callbackResult, newPromise)
    end if
end sub

'
' Generates a new UUID
'
function rooibos_promises_internal_createUuid() as string
    if m.__promises__deviceInfo = invalid then
        m.__promises__deviceInfo = createObject("roDeviceInfo")
    end if
    return m.__promises__deviceInfo.getRandomUUID()
end function

' Makes a delayed call to the supplied function. Default behavior is essentially next tick.
' @param {Function} callback - The function to be called after a set delay
' @param {Dynamic} context - a single item of data to be passed into the callback when invoked
' @param {Float} [duration] - the amount of delay before invoking the callback
sub rooibos_promises_internal_delay(callback as Function, context as dynamic, duration = 0.0001 as float)
    timer = createObject("roSGNode", "Timer")
    timer.update({
        duration: duration
        repeat: false
        id: "__delay_" + rooibos_promises_internal_createUuid()
    }, true)
    m[timer.id] = {
        timer: timer
        callback: callback
        context: context
    }
    rooibos_promises_internal_observeFieldScoped(timer, "fire", sub(event as object)
        rooibos_promises_internal_unobserveFieldScoped(event.getRosgNode(), "fire")
        delayId = event.getNode()
        options = m[delayId]
        callback = options.callback
        try
            callback(options.context)
        catch e
        end try
        m.delete(delayId)
    end sub)
    timer.control = "start"
end sub

' Observes a node field using observeFieldScoped
' @param {roSGNode} node - The node to apply the observer
' @param {String} field - The name of the field to be monitored.
' @param {Dynamic} callback - The name or message port to be executed when the value of the field changes.
' @return true if field could be observed, false if not
function rooibos_promises_internal_observeFieldScoped(node as object, field as string, callback as dynamic, infoFields = [] as object)
    if not type(node) = "roSGNode" then
        return false
    else
        if type(callback) = "roFunction" or type(callback) = "Function" then
            callback = callback.toStr().tokenize(" ").peek()
        end if
        if not node.observeFieldScoped(field, callback, infoFields) then
            return false
        end if
    end if
    return true
end function

' Unobserve a node field using unobserveFieldScoped
' @param {roSGNode} node - The node to remove the observer from
' @param {String} field - The name of the field to no longer be monitored.
' @return true if field could be unobserved, false if not
function rooibos_promises_internal_unobserveFieldScoped(node as object, field as string)
    if not type(node) = "roSGNode" then
        return false
    else
        if not node.unobserveFieldScoped(field) then
            return false
        end if
    end if
    return true
end function'//# sourceMappingURL=./promises.bs.map