require('utils.promise')

makuConfig = {
  debug = false,
  record = false
}

---- Utilities

function printTable(t)
  for k, v in pairs(t) do
    print(k, v)
  end
end

function dump(o)
  if type(o) == 'table' then
    local s = '{ '
    for k,v in pairs(o) do
      if type(k) ~= 'number' then k = '"'..k..'"' end
      s = s .. '['..k..'] = ' .. dump(v) .. ','
    end
    return s .. '} '
  else
    return tostring(o)
  end
end

function log(area, msg, format)
  local preface = (format and format.preface) and format.preface or ''
  if makuConfig.debug then
    print(preface..'['..area..'] '..msg)
  end
end

function record(action, params)
  if makuConfig.record then
    table.insert(timeline, {
      time = os.time(),
      action = action,
      params = params
    })
  end
end

function bubbleUp(finalName)
  if listeners[finalName] then
    for i, handler in ipairs(listeners[finalName]) do
      log('CONNECT '..finalName, 'Bubbling state up to '..dump(handler))
      handler(state[finalName])
    end
  end
end

function dispatchFinals(action, params)
  for _, dispatchFn in pairs(dispatchTable) do
    dispatchFn(action, params)
  end
end

--- Global functions

function passthrough (action, params)
  return action, params
end

function dispatch(action, params)
  log('ACTION', 'Dispatching '..action..' with params '..dump(params), { preface = '\n' })

  local parts = {}
  for i in string.gmatch(action, '([^.]+)') do
    table.insert(parts, i)
  end
  local actionId = table.remove(parts, #parts)
  local namespace = table.concat(parts, '.')

  log('TRANS '..namespace, 'Inspecting transiton for '..namespace)

  local compositeTransitions = {}

  if transitions[namespace] and transitions[namespace][actionId] then
    compositeTransitions = transitions[namespace][actionId]
    if type(compositeTransitions) == 'function' then compositeTransitions = {transitions[namespace][actionId]} end
    log('TRANS '..namespace, 'Transition reducers found: '..dump(compositeTransitions))

    -- iterate through transitional reducers
    local fns = {}
    for _, execTrans in ipairs(compositeTransitions) do
      local fn = function(result)
        local deferred = Promise:new()
        log('TRANS '..namespace, 'Inbound action: '..result[1]..', Inbound params: '..dump(result[2]))
        local nextAction, nextParams = execTrans(result[1], result[2])
        if (type(nextParams) == 'table' and nextParams.is_deferred) then
          nextParams:done(function(params)
            log('TRANS '..namespace, 'Async outbound action: '..nextAction..', Outbound params: '..dump(params))
            deferred:resolve({ nextAction, params})
          end)
        else
          log('TRANS '..namespace, 'Linear outbound action: '..nextAction..', Outbound params: '..dump(nextParams))
          deferred:resolve({ nextAction, nextParams })
        end
        return deferred
      end
      table.insert(fns, fn)
    end

    reduceAsync({action, params}, fns):done(function(result)
      if result[1] then
        dispatchFinals(result[1], result[2])
      end
    end)
  else
    log('TRANS '..namespace, 'Transition not found')
  end

end

function connect(finalName, handler)
  listeners[finalName] = listeners[finalName] or {}
  table.insert(listeners[finalName], handler)
  if state[finalName] then bubbleUp(finalName) end
end

return {
  init = function(params)
    log('INIT', 'Initializing maku')
    state = params.state or {}
    transitions = {}
    for _, transitionSpace in ipairs(params.transitions) do
      transitions[transitionSpace] = require('transitions.'..transitionSpace)
    end
    finals = {}
    for _, finalSpace in ipairs(params.finals) do
      finals[finalSpace] = require('finals.'..finalSpace)
    end
    listeners = {}
    timeline = {}
    dispatchTable = {}

    -- create dispatchTable
    for finalName, reducer in pairs(finals) do
      dispatchTable[finalName] = function(action, params)
        log('FINAL '..finalName, 'Inbound state: '..dump(state[finalName])..', Inbound action: '..action..', Inbound params: '..dump(params))
        record(action, params)
        local nextState = reducer(state[finalName], action, params)
        state[finalName] = nextState
        log('FINAL '..finalName, 'Outbound state: '..dump(state[finalName]))
        bubbleUp(finalName)
      end
    end

    if params.state then
      dispatchFinals('__HYDRATE')
    end
  end
}
