local setmetatable, type, ipairs, table, string = setmetatable, type, ipairs, table, string

local lpeg = require "lpeg"
local re = require "re"
local util = require "wsapi.util"

local _M = {}

local alpha = lpeg.R('AZ', 'az')
local number = lpeg.R('09')
local asterisk = lpeg.P('*')
local question_mark = lpeg.P('?')
local at_sign = lpeg.P('@')
local colon = lpeg.P(':')
local the_dot = lpeg.P('.')
local underscore = lpeg.P('_')
local forward_slash = lpeg.P('/')
local slash_or_dot = forward_slash + the_dot

local function cap_param(prefix, name, dot)
    local inner = (1 - lpeg.S('/' .. (dot or '')))^1
    local close = lpeg.P'/' + (dot or -1) + -1
    return {
      cap = lpeg.Carg(1) * slash_or_dot * lpeg.C(inner^1) * #close / function (params, item, delim) params[name] = util.url_decode(item) end,
      clean = slash_or_dot * inner^1 * #close,
      tag = "param",
      name = name,
      prefix = prefix
    }
end

local param_pre = lpeg.C(slash_or_dot) * colon * lpeg.C((alpha + number + underscore)^1)

local param = (param_pre * #(forward_slash + -1) / cap_param) +
              (param_pre * #the_dot / function (prefix, name) return cap_param(prefix, name, ".") end)

local function cap_opt_param(prefix, name, dot)
    local inner = (1 - lpeg.S('/' .. (dot or '')))^1
    local close = lpeg.P('/') + lpeg.P(dot or -1) + -1
    return {
      cap = (lpeg.Carg(1) * slash_or_dot * lpeg.C(inner) * #close / function (params, item, delim) params[name] = util.url_decode(item) end)^-1,
      clean = (slash_or_dot * inner * #lpeg.C(close))^-1,
      tag = "opt",
      name = name,
      prefix = prefix
    }
end

local opt_param_pre = lpeg.C(slash_or_dot) * question_mark * colon * lpeg.C((alpha + number + underscore)^1) * question_mark

local opt_param = (opt_param_pre * #(forward_slash + -1) / cap_opt_param) +
                  (opt_param_pre * #the_dot / function (prefix, name) return cap_opt_param(prefix, name, ".") end)

local splat = lpeg.P(lpeg.C(forward_slash + the_dot) * asterisk * #(forward_slash + the_dot + -1)) /
  function (prefix)
    return {
      cap = "*",
      tag = "splat",
      prefix = prefix
    }
  end

local rest = lpeg.C((1 - param - opt_param - splat)^1)

local function fold_captures(cap, acc)
  if type(cap) == "string" then
    return {
      cap = lpeg.P(cap) * acc.cap,
      clean = lpeg.P(cap) * acc.clean
    }
  end

  -- if we have a star match (match everything)
  if cap.cap == "*" then
    return {
      cap = (lpeg.Carg(1) * (cap.prefix * lpeg.C((1 - acc.clean)^0))^-1 / 
          function (params, splat)
            params.splat = params.splat or {}
            if splat and splat ~= "" then
              params.splat[#params.splat+1] = util.url_decode(splat)
            end
          end) * acc.cap,
      clean = (cap.prefix * (1 - acc.clean)^0)^-1 * acc.clean
    }
  end

  return {
    cap = cap.cap * acc.cap,
    clean = cap.clean * acc.clean
  }
end

local function fold_parts(parts, cap)
  
  if type(cap) == "string" then -- if the capture is a string
    parts[#parts+1] = {
      tag = "text",
      text = cap
    }
  else                          -- it must be a table capture
    parts[#parts+1] = {
      tag = cap.tag,
      prefix = cap.prefix,
      name = cap.name
    }
  end

  return parts
end

-- the right part (a bottom to top loop)
local function fold_right(t, f, acc)
  for i = #t, 1, -1 do
    acc = f(t[i], acc)
  end
  return acc
end

-- the left part (a top to bottom loop)
local function fold_left(t, f, acc)
  for i = 1, #t do
    acc = f(acc, t[i])
  end
  return acc
end

local route = lpeg.Ct((param + opt_param + splat + rest)^0 * -1) / function (caps)
  local forward_slash_at_end = lpeg.P('/')^-1 * -1
  return fold_right(caps, fold_captures, { cap = forward_slash_at_end, clean = forward_slash_at_end }), fold_left(caps, fold_parts, {})
end

local function build(parts, params)
  local res, i = {}, 1
  params = params or {}
  params.splat = params.splat or {}
  for _, part in ipairs(parts) do
    if part.tag == 'param' then
      if not params[part.name] then error('route parameter ' .. part.name .. ' does not exist') end
      local s = string.gsub (params[part.name], '([^%.@]+)', function (s) return util.url_encode(s) end)
      res[#res+1] = part.prefix .. s
    elseif part.tag == "splat" then
      local s = string.gsub (params.splat[i] or '', '([^/%.@]+)', function (s) return util.url_encode(s) end)
      res[#res+1] = part.prefix .. s
      i = i + 1
    elseif part.tag == "opt" then
      if params and params[part.name] then
        local s = string.gsub (params[part.name], '([^%.@]+)', function (s) return util.url_encode(s) end)
        res[#res+1] = part.prefix .. s
      end
    else
      res[#res+1] = part.text
    end
  end

  if #res > 0 then
    return table.concat(res)
  end
  
  return '/'
end

function _M.R(path)
  local p, b = route:match(path)
  
  return setmetatable({
    parser = p.cap,
    parts = b
  }, {
    __index = {
      match = function (t, s)
        local params = {}
        if t.parser:match(s, 1, params) then
          return params
        end
        return nil
      end,
      build = function (t, params)
        return build(t.parts, params)
      end
    }
  })

end

return setmetatable(_M, {
  __call = function (_, path)
    return _M.R(path)
  end
})
