local setmetatable = setmetatable
local tostring     = tostring
local insert       = table.insert
local remove       = table.remove
local concat       = table.concat
local ipairs       = ipairs
local pairs        = pairs
local match        = string.match
local find         = string.find
local sub          = string.sub


local RANDOM_BOUNDARY = sub(tostring({}), 10)


local MultipartData = { RANDOM_BOUNDARY = RANDOM_BOUNDARY}


MultipartData.__index = MultipartData


setmetatable(MultipartData, {
  __call = function (cls, ...)
    return cls.new(...)
  end,
})


local function is_header(value)
  return match(value, "%S:%s*%S")
end


-- Create a table representation of multipart/data body
--
-- @param {string} body The multipart/data string body
-- @param {string} boundary The multipart/data boundary
-- @return {table} Lua representation of the body
local function decode(body, boundary)
  local result = {
    data    = {},
    indexes = {},
  }

  if not boundary then
    return result
  end

  local part_name
  local part_index    = 1
  local part_headers  = {}
  local part_value    = {}
  local part_value_ct = 0

  local end_boundary_length   = boundary and #boundary + 2
  local processing_part_value = false

  local position = 1
  local done     = false

  repeat
    local s = find(body, "[\r\n]", position)

    local line

    if s then
      line = sub(body, position, s - 1)
      position = s + 1

    else
      if position == 1 then
        line = body

      else
        line = sub(body, position)
      end

      done = true
    end

    if line == "" then
      if s and processing_part_value then
        part_value_ct             = part_value_ct + 1
        part_value[part_value_ct] = sub(body, s, s)
      end

    else
      if sub(line, 1, 2) == "--" and sub(line, 3, end_boundary_length) == boundary then
        processing_part_value = false

        if part_name ~= nil then
          if part_value[part_value_ct] == "\n" then
             part_value[part_value_ct] = nil
          end

          if part_value[part_value_ct - 1] == "\r" then
             part_value[part_value_ct - 1] = nil
          end

          result.data[part_index] = {
            name    = part_name,
            headers = part_headers,
            value   = concat(part_value)
          }

          if result.indexes[part_name] == nil then
            result.indexes[part_name] = {}
          end

          insert(result.indexes[part_name], part_index)

          -- Reset fields for the next part
          part_headers  = {}
          part_value    = {}
          part_value_ct = 0
          part_name     = nil
          part_index    = part_index + 1
        end

      else
        --Beginning of part
        if not processing_part_value and line:sub(1, 19):lower() == "content-disposition" then
          -- Extract part_name
          for v in line:gmatch("[^;]+") do
            if not is_header(v) then -- If it's not content disposition part
              local pos = v:match("^%s*[Nn][Aa][Mm][Ee]=()")
              if pos then
                local current_value = v:match("^%s*([^=]*)", pos):gsub("%s*$", "")
                part_name = sub(current_value, 2, #current_value - 1)
              end
            end
          end

          insert(part_headers, line)

          if s and sub(body, s, s + 3) == "\r\n\r\n" then
            processing_part_value = true
            position = s + 4
          end

        elseif not processing_part_value and is_header(line) then
          insert(part_headers, line)

          if s and sub(body, s, s + 3) == "\r\n\r\n" then
            processing_part_value = true
            position = s + 4
          end

        else
          processing_part_value = true

          -- The value part begins
          part_value_ct               = part_value_ct + 1
          part_value[part_value_ct]   = line

          if s then
            part_value_ct             = part_value_ct + 1
            part_value[part_value_ct] = sub(body, s, s)
          end
        end
      end
    end

  until done

  if part_name ~= nil then
    result.data[part_index] = {
      name    = part_name,
      headers = part_headers,
      value   = concat(part_value)
    }
    result.indexes[part_name] = { part_index }
  end

  return result
end

-- Creates a multipart/data body from a table
--
-- @param {table} t The table that contains the multipart/data body properties
-- @param {boundary} boundary The multipart/data boundary to use
-- @return {string} The multipart/data string body
local function encode(t, boundary)
  if not boundary then
    boundary = RANDOM_BOUNDARY
  end

  local result = {}
  local i = 0

  for _, v in ipairs(t.data) do
    if v.value then
      result[i + 1] = "--"
      result[i + 2] = boundary
      result[i + 3] = "\r\n"

      i = i + 3

      for _, header in ipairs(v.headers) do
        result[i + 1] = header
        result[i + 2] = "\r\n"

        i = i + 2
      end

      result[i + 1] = "\r\n"
      result[i + 2] = v.value
      result[i + 3] = "\r\n"

      i = i + 3
    end
  end

  if i == 0 then
    return ""
  end

  result[i + 1] = "--"
  result[i + 2] = boundary
  result[i + 3] = "--\r\n"

  return concat(result)
end


function MultipartData.new(data, content_type)
  local instance = setmetatable({}, MultipartData)

  if content_type then
    local boundary = match(content_type, ";%s*boundary=(%S+)")
    if boundary then
      if (sub(boundary, 1, 1) == '"' and sub(boundary, -1)  == '"') or
         (sub(boundary, 1, 1) == "'" and sub(boundary, -1)  == "'") then
        boundary = sub(boundary, 2, -2)
      end

      if boundary ~= "" then
        instance._boundary = boundary
      end
    end
  end

  instance._data = decode(data or "", instance._boundary)

  return instance
end


function MultipartData:get(name)
  -- Get first index for part
  local index = self._data.indexes[name]
  if not index then
    return nil
  end

  return self._data.data[index[1]]
end


function MultipartData:get_all()
  local result = {}

  for k, v in pairs(self._data.indexes) do
    -- Get first index for part
    result[k] = self._data.data[v[1]].value
  end

  return result
end


function MultipartData:get_as_array(name)
  local vals = {}

  local idx = self._data.indexes[name]
  if not idx then
    return vals
  end

  for _, index in ipairs(self._data.indexes[name]) do
    insert(vals, self._data.data[index].value)
  end

  return vals
end


local req_headers_mt = {
    __index = function (tb, key)
        key = key:lower()
        local value = rawget(tb, key)
        if value == nil and key:find('_', 1, true) then
            value = rawget(tb, key:gsub('_', '-'))
        end
        return value
    end
}

local function parse_headers(headers)
  local results = {}
  for _, header in ipairs(headers) do
    -- match key:value pattern without quotes (for header values)
    local key, value = string.match(header, "([^:]+):%s*(.+)")
    if key and value then
        -- clean up whitespace around key
        key = key:match("^%s*(.-)%s*$")
        results[key:lower()] = value
    end
  end

  return setmetatable(results, req_headers_mt)
end


function MultipartData:get_with_headers_as_array(name)
  local vals = {}

  local idx = self._data.indexes[name]
  if not idx then
    return vals
  end

  for _, index in ipairs(self._data.indexes[name]) do
    insert(vals, {
      value   = self._data.data[index].value,
      headers = parse_headers(self._data.data[index].headers)
    })
  end

  return vals
end

function MultipartData:get_all_as_arrays()
  -- Get all fields as arrays
  local result = {}

  for k in pairs(self._data.indexes) do
    result[k] = self:get_as_array(k)
  end

  return result
end

function MultipartData:get_all_with_arrays()
  -- Get repeating fields as arrays, rest as strings
  local result = {}

  for k, v in pairs(self._data.indexes) do
    if #v == 1 then
      result[k] = self._data.data[v[1]].value
    else
      result[k] = self:get_as_array(k)
    end
  end

  return result
end

function MultipartData:get_all_include_headers_with_arrays()
  -- Get repeating fields as arrays, rest as strings
  local result = {}

  for k, v in pairs(self._data.indexes) do
    if #v == 1 then
      result[k] = { {
        value   = self._data.data[v[1]].value,
        headers = parse_headers(self._data.data[v[1]].headers)
      }}
    else
      result[k] = self:get_with_headers_as_array(k)
    end
  end

  return result
end


function MultipartData:set_simple(name, value, filename, content_type)
    local headers = {'Content-Disposition: form-data; name="' , name , '"'}
    if filename then
      headers[4] = '; filename="'
      headers[5] = filename
      headers[6] = '"'
    end
    if content_type then
      headers[7] = "\r\ncontent-type: "
      headers[8] = content_type
    end
    headers = concat(headers)
    if self._data.indexes[name] then
      self._data.data[self._data.indexes[name][1]] = {
        name = name,
        value = value,
        headers = { headers }
      }

    else
      -- Find maximum index
      local max_index = 0
      for _, indexes in pairs(self._data.indexes) do
        for _, index in ipairs(indexes) do
          if index > max_index then
            max_index = index
          end
        end
      end
      -- Assign data to new index
      local part_index = max_index + 1
      self._data.indexes[name] = { part_index }
      self._data.data[part_index] = {
        name    = name,
        value   = value,
        headers = { headers }
      }
    end
end


function MultipartData:delete(name)
  -- If part name repeats, then delete all occurrences
  local indexes = self._data.indexes[name]

  if indexes ~= nil then
    for _, index in ipairs(indexes) do
      remove(self._data.data, index)
    end
    self._data.indexes[name] = nil

    -- need to recount index
    -- Deleted indexes can be anywhere,
    -- including between values of indexes for single part name.
    -- For every index, need to count how many deleted indexes are bellow it
    for key, index_vals in pairs(self._data.indexes) do
      for i, val in ipairs(index_vals) do
        local num_deleted = 0
        for _, del_index in ipairs(indexes) do
          if val > del_index then
            num_deleted = num_deleted + 1
          end
        end
        self._data.indexes[key][i] = val - num_deleted
      end
    end
  end
end


function MultipartData:tostring()
  return encode(self._data, self._boundary)
end


return MultipartData
