-- Rings application for WSAPI

local rings = require "rings"

local _M = {}

local init = [==[
  local app_name, bootstrap_code, is_file = ...
  if bootstrap_code then
    local bootstrap, err
    if string.match(bootstrap_code, "%w%.lua$") then
      bootstrap, err = loadfile(bootstrap_code)
    else
      bootstrap, err = loadstring(bootstrap_code)
    end
    if bootstrap then
      bootstrap()
    else
      error("could not load " .. bootstrap_code .. ": " .. err)
    end
  else
    _, package.path = remotedostring("return package.path")
    _, package.cpath = remotedostring("return package.cpath")
  end
  local common = require"wsapi.common"
  require"coxpcall"
  pcall = copcall
  xpcall = coxpcall
  local wsapi_error = {
       write = function (self, err)
         remotedostring("env.error:write(...)", err)
       end
  }
  local wsapi_input =  {
       read = function (self, n)
         return coroutine.yield("RECEIVE", n)
       end
  }
  local wsapi_meta = {
       __index = function (tab, k)
        if k == "headers" then
          local headers
          local _, all_headers = remotedostring([[
                if env.headers then
                  local out = {}
                  for k, v in pairs(env.headers) do
                    table.insert(out,
                     "[" .. string.format("%q", k) .. "]=" ..
                     string.format("%q", v))
                  end
                  return "return {" .. table.concat(out, ",") .. "}"
                end
                  ]])
          if all_headers then
      local v = loadstring(all_headers)()
      rawset(tab, k, v)
      return v
          end
        else
          local  _, v = remotedostring("return env[(...)]", k)
          rawset(tab, k, v)
          return v
        end
     end,
       __newindex = function (tab, k, v)
           rawset(tab, k, v)
           remotedostring("local k, v = ...; env[k] = v", k, v)
        end
  }
  local app = common.normalize_app(app_name, is_file)
  main_func = function ()
     local wsapi_env = { error = wsapi_error, input = wsapi_input }
     setmetatable(wsapi_env, wsapi_meta)
     local ok, status, headers, res = common.run_app(app, wsapi_env)
     if not ok then
       local msg = status
       headers = { ["Content-Type"] = "text/html" }
       if wsapi_env.STATUS ~= "" then
         status = wsapi_env.STATUS
         res = coroutine.wrap(function () coroutine.yield(msg) end)
       else
         status = 500
         res = coroutine.wrap(function () coroutine.yield(common.error_html(msg)) end)
       end
     end
     remotedostring("status = ...", status)
     for k, v in pairs(headers) do
       if type(v) == "table" then
         remotedostring("headers[(...)] = {}", k)
         for _, val in ipairs(v) do
           remotedostring("local k, v = ...; table.insert(headers[k], v)", k, val)
         end
       else
         remotedostring("local k, v = ...; headers[k] = v", k, v)
       end
     end
     local s, v = res()
     while s do
       if s == "RECEIVE" and v then
          s, v = res(coroutine.yield(s, v))
       else
          coroutine.yield("SEND", s)
       end
       s, v = res()
     end
     return "SEND", nil
         end
]==]

-- Returns a WSAPI application that runs the provided WSAPI application
-- in an isolated Lua environment
function _M.new(app_name, bootstrap, is_file)
  local data = { created_at = os.time() }
  setmetatable(data, { __index = _G })
  local state = rings.new(data)
  assert(state:dostring(init, app_name, bootstrap, is_file))
  local error = function (msg)
    data.status, data.headers, data.env = nil
    error(msg)
  end
  return function (wsapi_env)
    if state and wsapi_env == "close" then
      state:close()
      state = nil
    end
    if not state then
      return nil
    end
    if rawget(data, "status") then
      error("this state is already in use")
    end
    data.status = 500
    data.headers = {}
    data.env = wsapi_env
    local ok, flag, s =
     state:dostring([[
      main_coro = coroutine.wrap(main_func)
      return main_coro(...)
     ]])
    repeat
      if not ok then error(flag) end
      if flag == "RECEIVE" then
        ok, flag, s = state:dostring("return main_coro(...)",
          wsapi_env.input:read(s))
      elseif flag == "SEND" then
        break
      else
        error("Invalid command: " .. tostring(flag))
      end
    until flag == "SEND"
    local res
    if not s then
      res = function () return nil end
    else
      res = function ()
        if s then
          local res = s
          s = nil
          return res
        end
        local ok, flag, s = state:dostring("return main_coro()")
        while ok and flag and s do
          if flag == "RECEIVE" then
            ok, flag, s = state:dostring("return main_coro(...)",
                wsapi_env.input:read(s))
          elseif flag == "SEND" then
            return s
          else
            error("Invalid command: " .. tostring(flag))
          end
        end
        data.status, data.headers, data.env = nil
        if data.cleanup then
          state:close()
          state = nil
        end
        if not ok then error(flag) end
      end
    end
    return data.status, data.headers, res
  end, data
end

return _M
