local table_new = require "table.new"

local string_char = string.char
local string_upper = string.upper
local string_find = string.find
local string_sub = string.sub
local string_byte = string.byte
local string_format = string.format
local tonumber = tonumber
local table_concat = table.concat
local ngx_re_find = ngx.re.find
local ngx_re_gsub = ngx.re.gsub

-- Charset:
--   reserved = "!" / "*" / "'" / "(" / ")" / ";" / ":" /
--       "@" / "&" / "=" / "+" / "$" / "," / "/" / "?" / "%" / "#" / "[" / "]"
--   unreserved  = ALPHA / DIGIT / "-" / "." / "_" / "~"
--   other: * (meaning any char that is not mentioned above)

-- Reserved characters have special meaning in URI. Encoding/decoding it affects the semantics of the URI;
-- Unreserved characters are safe to use as part of HTTP message without encoding;
-- Other characters has not special meaning but may be not safe to use as part of HTTP message without encoding;

-- We should not unescape or escape reserved characters;
-- We should unescape but not escape unreserved characters;
-- We choose to unescape when processing and escape when forwarding for other characters

local RESERVED_CHARS = "!*'();:@&=+$,/?%#[]"

local chars_to_decode = table_new(256, 0)
do
  -- reserved
  for i = 1, #RESERVED_CHARS do
    chars_to_decode[string_byte(RESERVED_CHARS, i)] = true
  end

  -- unreserved and others default to nil
end


local ESCAPE_PATTERN = "[^!#$&'()*+,/:;=?@[\\]A-Z\\d\\-_.~%]"

local TMP_OUTPUT = table_new(16, 0)
local DOT = string_byte(".")
local SLASH = string_byte("/")


local function normalize_decode(m)
  local hex = m[1]
  local num = tonumber(hex, 16)

  -- from rfc3986 we should decode unreserved character
  -- and we choose to decode "others"
  if not chars_to_decode[num] then -- is not reserved(false or nil)
    return string_char(num)
  end

  return string_upper(m[0])
end


local function percent_escape(m)
  return string_format("%%%02X", string_byte(m[0]))
end

-- This function does slightly different things from its name.
-- It ensures the output to be safe to a part of HTTP message (headers or path)
-- and preserve origin semantics
local function escape(uri)
  if ngx_re_find(uri, ESCAPE_PATTERN, "joi") then
    return (ngx_re_gsub(uri, ESCAPE_PATTERN, percent_escape, "joi"))
  end

  return uri
end


local function normalize(uri, merge_slashes)
  -- check for simple cases and early exit
  if uri == "" or uri == "/" then
    return uri
  end

  -- check if uri needs to be percent-decoded
  -- (this can in some cases lead to unnecessary percent-decoding)
  if string_find(uri, "%", 1, true) then
    -- decoding percent-encoded triplets of unreserved characters
    uri = ngx_re_gsub(uri, "%([\\dA-F]{2})", normalize_decode, "joi")
  end

  -- check if the uri contains a dot
  -- (this can in some cases lead to unnecessary dot removal processing)
  -- notice: it's expected that /%2e./ is considered the same of /../
  if string_find(uri, ".", 1, true) == nil  then
    if not merge_slashes then
      return uri
    end

    if string_find(uri, "//", 1, true) == nil then
      return uri
    end
  end

  local output_n = 0

  while #uri > 0 do
    local FIRST = string_byte(uri, 1)
    local SECOND = FIRST and string_byte(uri, 2) or nil
    local THIRD = SECOND and string_byte(uri, 3) or nil
    local FOURTH = THIRD and string_byte(uri, 4) or nil

    if uri == "/." then -- /.
      uri = "/"

    elseif uri == "/.." then -- /..
      uri = "/"
      if output_n > 0 then
        output_n = output_n - 1
      end

    elseif uri == "." or uri == ".." then
      uri = ""

    elseif FIRST == DOT and SECOND == DOT and THIRD == SLASH then -- ../
      uri = string_sub(uri, 4)

    elseif FIRST == DOT and SECOND == SLASH then -- ./
      uri = string_sub(uri, 3)

    elseif FIRST == SLASH and SECOND == DOT and THIRD == SLASH then -- /./
      uri = string_sub(uri, 3)

    elseif FIRST == SLASH and SECOND == DOT and THIRD == DOT and FOURTH == SLASH then -- /../
      uri = string_sub(uri, 4)
      if output_n > 0 then
        output_n = output_n - 1
      end

    elseif merge_slashes and FIRST == SLASH and SECOND == SLASH then -- //
      uri = string_sub(uri, 2)

    else
      local i = string_find(uri, "/", 2, true)
      output_n = output_n + 1

      if i then
        local seg = string_sub(uri, 1, i - 1)
        TMP_OUTPUT[output_n] = seg
        uri = string_sub(uri, i)

      else
        TMP_OUTPUT[output_n] = uri
        uri = ""
      end
    end
  end

  if output_n == 0 then
    return ""
  end

  if output_n == 1 then
    return TMP_OUTPUT[1]
  end

  return table_concat(TMP_OUTPUT, nil, 1, output_n)
end


return {
  escape = escape,
  normalize = normalize,
}
