-- Port of https://github.com/rhysbrettbowen/promise_impl/blob/master/promise.js
-- and https://github.com/rhysbrettbowen/Aplus
local queue = {}

local State = {
    PENDING = "pending",
    FULFILLED = "fulfilled",
    REJECTED = "rejected",
}

local passthrough = function(x)
    return x
end
local errorthrough = function(x)
    error(x)
end

local function callable_table(callback)
    local mt = getmetatable(callback)
    return type(mt) == "table" and type(mt.__call) == "function"
end

local function is_callable(value)
    local t = type(value)
    return t == "function" or (t == "table" and callable_table(value))
end

local transition, resolve, run

local Promise = {
    is_promise = true,
    state = State.PENDING,
}
Promise.mt = {
    __index = Promise,
}

local do_async = function(callback)
    if Promise.async then
        Promise.async(callback)
    else
        table.insert(queue, callback)
    end
end

local reject = function(promise, reason)
    transition(promise, State.REJECTED, reason)
end

local fulfill = function(promise, value)
    transition(promise, State.FULFILLED, value)
end

transition = function(promise, state, value)
    if promise.state == state or promise.state ~= State.PENDING or
        (state ~= State.FULFILLED and state ~= State.REJECTED) or value == nil then
        return
    end

    promise.state = state
    promise.value = value
    run(promise)
end

function Promise:next(on_fulfilled, on_rejected)
    local promise = Promise.new()

    table.insert(self.queue, {
        fulfill = is_callable(on_fulfilled) and on_fulfilled or nil,
        reject = is_callable(on_rejected) and on_rejected or nil,
        promise = promise,
    })

    run(self)

    return promise
end

resolve = function(promise, x)
    if promise == x then
        reject(promise, "TypeError: cannot resolve a promise with itself")
        return
    end

    local x_type = type(x)

    if x_type ~= "table" then
        fulfill(promise, x)
        return
    end

    -- x is a promise in the current implementation
    if x.is_promise then
        -- 2.3.2.1 if x is pending, resolve or reject this promise after completion
        if x.state == State.PENDING then
            x:next(function(value)
                resolve(promise, value)
            end, function(reason)
                reject(promise, reason)
            end)
            return
        end
        -- if x is not pending, transition promise to x's state and value
        transition(promise, x.state, x.value)
        return
    end

    local called = false
    -- 2.3.3.1. Catches errors thrown by __index metatable
    local success, reason = pcall(function()
        local next = x.next
        if is_callable(next) then
            next(x, function(y)
                if not called then
                    resolve(promise, y)
                    called = true
                end
            end, function(r)
                if not called then
                    reject(promise, r)
                    called = true
                end
            end)
        else
            fulfill(promise, x)
        end
    end)

    if not success then
        if not called then
            reject(promise, reason)
        end
    end
end

run = function(promise)
    if promise.state == State.PENDING then
        return
    end

    do_async(function()
        -- drain promise.queue while allowing pushes from within callbacks
        local q = promise.queue
        local i = 0
        while i < #q do
            i = i + 1
            local obj = q[i]
            local success, result = pcall(function()
                local success = obj.fulfill or passthrough
                local failure = obj.reject or errorthrough
                local callback = promise.state == State.FULFILLED and success or failure
                return callback(promise.value)
            end)

            if not success then
                reject(obj.promise, result)
            else
                resolve(obj.promise, result)
            end
        end
        for j = 1, i do
            q[j] = nil
        end
    end)
end

function Promise.new(callback)
    local instance = {
        queue = {},
    }
    setmetatable(instance, Promise.mt)

    if callback then
        callback(function(value)
            resolve(instance, value)
        end, function(reason)
            reject(instance, reason)
        end)
    end

    return instance
end

function Promise:catch(callback)
    return self:next(nil, callback)
end

function Promise:resolve(value)
    fulfill(self, value)
end

function Promise:reject(reason)
    reject(self, reason)
end

function Promise.update()
    while true do
        local async = table.remove(queue, 1)

        if not async then
            break
        end

        async()
    end
end

-- resolve when all promises complete
function Promise.all(...)
    local promises = {...}
    local results = {}
    local state = State.FULFILLED
    local remaining = #promises

    local promise = Promise.new()

    local check_finished = function()
        if remaining > 0 then
            return
        end
        transition(promise, state, results)
    end

    for i, p in ipairs(promises) do
        p:next(function(value)
            results[i] = value
            remaining = remaining - 1
            check_finished()
        end, function(value)
            results[i] = value
            remaining = remaining - 1
            state = State.REJECTED
            check_finished()
        end)
    end

    check_finished()

    return promise
end

-- resolve with first promise to complete
function Promise.race(...)
    local promises = {...}
    local promise = Promise.new()

    Promise.all(...):next(nil, function(value)
        reject(promise, value)
    end)

    local success = function(value)
        fulfill(promise, value)
    end

    for _, p in ipairs(promises) do
        p:next(success)
    end

    return promise
end

return Promise
