--
-- lua-promises : https://github.com/zserge/lua-promises
--

local M = {}
_G.promise = M

local deferred = {}
deferred.__index = deferred

local PENDING = 0
local RESOLVING = 1
local REJECTING = 2
local RESOLVED = 3
local REJECTED = 4

local function finish(deferred, state)
    state = state or REJECTED
    for i, f in ipairs(deferred.queue) do
        if state == RESOLVED then
            f:resolve(deferred.value)
        else
            f:reject(deferred.value)
        end
    end
    deferred.state = state
end

local function isfunction(f)
    if type(f) == 'table' then
        local mt = getmetatable(f)
        return mt ~= nil and type(mt.__call) == 'function'
    end
    return type(f) == 'function'
end

local function promise(deferred, next, success, failure, nonpromisecb)
    if type(deferred) == 'table' and type(deferred.value) == 'table' and isfunction(next) then
        local called = false
        local ok, err = pcall(next, deferred.value, function(v)
            if called then return end
            called = true
            deferred.value = v
            success()
        end, function(v)
            if called then return end
            called = true
            deferred.value = v
            failure()
        end)
        if not ok and not called then
            deferred.value = err
            failure()
        end
    else
        nonpromisecb()
    end
end

local function fire(deferred)
    local next
    if type(deferred.value) == 'table' then
        next = deferred.value.next
    end
    promise(deferred, next, function()
        deferred.state = RESOLVING
        fire(deferred)
    end, function()
        deferred.state = REJECTING
        fire(deferred)
    end, function()
        local ok
        local v
        if deferred.state == RESOLVING and isfunction(deferred.success) then
            ok, v = pcall(deferred.success, deferred.value)
        elseif deferred.state == REJECTING and isfunction(deferred.failure) then
            ok, v = pcall(deferred.failure, deferred.value)
            if ok then
                deferred.state = RESOLVING
            end
        end

        if ok ~= nil then
            if ok then
                deferred.value = v
            else
                deferred.value = v
                return finish(deferred)
            end
        end

        if deferred.value == deferred then
            deferred.value = pcall(error, 'resolving promise with itself')
            return finish(deferred)
        else
            promise(deferred, next, function()
                finish(deferred, RESOLVED)
            end, function(state)
                finish(deferred, state)
            end, function()
                finish(deferred, deferred.state == RESOLVING and RESOLVED)
            end)
        end
    end)
end

local function resolve(deferred, state, value)
    if deferred.state == 0 then
        deferred.value = value
        deferred.state = state
        fire(deferred)
    end
    return deferred
end

--
-- PUBLIC API
--
function deferred:resolve(value)
    return resolve(self, RESOLVING, value)
end

function deferred:reject(value)
    return resolve(self, REJECTING, value)
end

function M.new(options)
    if isfunction(options) then
        local d = M.new()
        local ok, err = pcall(options, d)
        if not ok then
            d:reject(err)
        end
        return d
    end
    options = options or {}
    local d
    d = {
        next = function(self, success, failure)
            local next = M.new({success = success, failure = failure, extend = options.extend})
            if d.state == RESOLVED then
                next:resolve(d.value)
            elseif d.state == REJECTED then
                next:reject(d.value)
            else
                table.insert(d.queue, next)
            end
            return next
        end,
        state = 0,
        queue = {},
        success = options.success,
        failure = options.failure,
    }
    d = setmetatable(d, deferred)
    if isfunction(options.extend) then
        options.extend(d)
    end
    return d
end

function M.all(args)
    local d = M.new()
    if #args == 0 then
        return d:resolve({})
    end
    local method = "resolve"
    local pending = #args
    local results = {}

    local function synchronizer(i, resolved)
        return function(value)
            results[i] = value
            if not resolved then
                method = "reject"
            end
            pending = pending - 1
            if pending == 0 then
                d[method](d, results)
            end
            return value
        end
    end

    for i = 1, pending do
        args[i]:next(synchronizer(i, true), synchronizer(i, false))
    end
    return d
end

function M.map(args, fn)
    local d = M.new()
    local results = {}
    local function donext(i)
        if i > #args then
            d:resolve(results)
        else
            fn(args[i]):next(function(res)
                table.insert(results, res)
                donext(i+1)
            end, function(err)
                d:reject(err)
            end)
        end
    end
    donext(1)
    return d
end

function M.first(args)
    local d = M.new()
    for _, v in ipairs(args) do
        v:next(function(res)
            d:resolve(res)
        end, function(err)
            d:reject(err)
        end)
    end
    return d
end
