local util = require"wsapi.util"

local _M = {}

local function split_filename(path)
	local name_patt = "[/\\]?([^/\\]+)$"
	return (string.match(path, name_patt))
end

local function insert_field (tab, name, value, overwrite)
	if overwrite or not tab[name] then
		tab[name] = value
	else
		local t = type (tab[name])
		if t == "table" then
			table.insert (tab[name], value)
		else
			tab[name] = { tab[name], value }
		end
	end
end

local function parse_qs(qs, tab, overwrite)
	tab = tab or {}
	if type(qs) == "string" then
		local url_decode = util.url_decode
		for key, val in string.gmatch(qs, "([^&=]+)=([^&=]*)&?") do
			insert_field(tab, url_decode(key), url_decode(val), overwrite)
		end
	elseif qs then
		error("WSAPI Request error: invalid query string")
	end
	return tab
end

local function get_boundary(content_type)
	local boundary = string.match(content_type, "boundary%=(.-)$")
	return "--" .. tostring(boundary)
end

local function break_headers(header_data)
	local headers = {}
	for type, val in string.gmatch(header_data, '([^%c%s:]+):%s+([^\n]+)') do
		type = string.lower(type)
		headers[type] = val
	end
	return headers
end

local function read_field_headers(input, pos)
	local EOH = "\r\n\r\n"
	local s, e = string.find(input, EOH, pos, true)
	if s then
		return break_headers(string.sub(input, pos, s-1)), e+1
	else return nil, pos end
end

local function get_field_names(headers)
	local disp_header = headers["content-disposition"] or ""
	local attrs = {}
	for attr, val in string.gmatch(disp_header, ';%s*([^%s=]+)="(.-)"') do
		attrs[attr] = val
	end
	return attrs.name, attrs.filename and split_filename(attrs.filename)
end

local function read_field_contents(input, boundary, pos)
	local boundaryline = "\r\n" .. boundary
	local s, e = string.find(input, boundaryline, pos, true)
	if s then
		return string.sub(input, pos, s-1), s-pos, e+1
	else return nil, 0, pos end
end

local function file_value(file_contents, file_name, file_size, headers)
	local value = { contents = file_contents, name = file_name,
		size = file_size }
	for h, v in pairs(headers) do
		if h ~= "content-disposition" then
			value[h] = v
		end
	end
	return value
end

local function fields(input, boundary)
	local state, _ = { }
	_, state.pos = string.find(input, boundary, 1, true)
	state.pos = state.pos + 1
	return function (state, _)
		local headers, name, file_name, value, size
		headers, state.pos = read_field_headers(input, state.pos)
		if headers then
			name, file_name = get_field_names(headers)
			if file_name then
				value, size, state.pos = read_field_contents(input, boundary,
															state.pos)
				value = file_value(value, file_name, size, headers)
			else
				value, size, state.pos = read_field_contents(input, boundary,
															state.pos)
			end
		end
		return name, value
	end, state
end

function _M.parse_multipart_data(input, input_type, tab, overwrite)
	tab = tab or {}
	local boundary = get_boundary(input_type)
	for name, value in fields(input, boundary) do
		insert_field(tab, name, value, overwrite)
	end
	return tab
end

local function parse_post_data(wsapi_env, tab, overwrite)
	tab = tab or {}
	local input_type = wsapi_env.CONTENT_TYPE
	if string.find(input_type, "x-www-form-urlencoded", 1, true) then
		local length = tonumber(wsapi_env.CONTENT_LENGTH) or 0
		parse_qs(wsapi_env.input:read(length) or "", tab, overwrite)
	elseif string.find(input_type, "multipart/form-data", 1, true) then
		local length = tonumber(wsapi_env.CONTENT_LENGTH) or 0
		if length > 0 then
			_M.parse_multipart_data(wsapi_env.input:read(length) or "", input_type, tab, overwrite)
		end
	else
		local length = tonumber(wsapi_env.CONTENT_LENGTH) or 0
		if length > 0 then
			tab.post_data = wsapi_env.input:read(length) or ""
		else
			tab.post_data = ""
		end
	end
	return tab
end

_M.methods = {}

local methods = _M.methods

function methods.__index(tab, name)
	local func
	if methods[name] then
		func = methods[name]
	else
		local route_name = name:match("link_([%w_]+)")
		if route_name then
			func = function (self, query, ...)
				return tab:route_link(route_name, query, ...)
			end
		end
	end
	tab[name] = func
	return func
end

function methods:qs_encode(query, url)
	local parts = {}
	for k, v in pairs(query or {}) do
		parts[#parts+1] = k .. "=" .. util.url_encode(v)
	end
	if #parts > 0 then
		return (url and (url .. "?") or "") .. table.concat(parts, "&")
	else
		return (url and url or "")
	end
end

function methods:route_link(route, query, ...)
	local builder = self.mk_app["link_" .. route]
	if builder then
		local uri = builder(self.mk_app, self.env, ...)
		local qs = self:qs_encode(query)
		return uri .. (qs ~= "" and ("?"..qs) or "")
	else
		error("there is no route named " .. route)
	end
end

function methods:link(url, query)
	local prefix = (self.mk_app and self.mk_app.prefix) or self.script_name
	local uri = prefix .. url
	local qs = self:qs_encode(query)
	return prefix .. url .. (qs ~= "" and ("?"..qs) or "")
end

function methods:absolute_link(url, query)
	local qs = self:qs_encode(query)
	return url .. (qs ~= "" and ("?"..qs) or "")
end

function methods:static_link(url)
	local prefix = (self.mk_app and self.mk_app.prefix) or self.script_name
	local is_script = prefix:match("(%.%w+)$")
	if not is_script then return self:link(url) end
	local vpath = prefix:match("(.*)/") or ""
	return vpath .. url
end

function methods:empty(s)
	return not s or string.match(s, "^%s*$")
end

function methods:empty_param(param)
	return self:empty(self.params[param])
end

function _M.new(wsapi_env, options)
	options = options or {}
	local req = {
		GET          = {},
		POST         = {},
		method       = wsapi_env.REQUEST_METHOD,
		path_info    = wsapi_env.PATH_INFO,
		query_string = wsapi_env.QUERY_STRING,
		script_name  = wsapi_env.SCRIPT_NAME,
		env          = wsapi_env,
		mk_app       = options.mk_app,
		doc_root     = wsapi_env.DOCUMENT_ROOT,
		app_path     = wsapi_env.APP_PATH
	}
	parse_qs(wsapi_env.QUERY_STRING, req.GET, options.overwrite)
	
	-- para que cada vez que cree este método no me parsee el POST
	setmetatable(req.POST, { __index = function (tab, name)
						setmetatable(tab, nil)
						parse_post_data(wsapi_env, tab)
						return rawget(tab, name)
					end } )
	
	--[=[ -- esto es como está wsapi hoy en upstream, pero no me sirve
	if options.delay_post then
		req.parse_post = function (self)
			parse_post_data(wsapi_env, self.POST, options.overwrite)
			self.parse_post = function () return nil, "postdata already parsed" end
			return self.POST
		end
	else
		parse_post_data(wsapi_env, req.POST, options.overwrite)
		req.parse_post = function () return nil, "postdata already parsed" end
	end
	--]=]
	
	req.params = {}
	setmetatable(req.params, { __index = function (tab, name)
						local var = req.GET[name] or req.POST[name]
						rawset(tab, name, var)
						return var
					end })
	req.cookies = {}
	local cookies = string.gsub(";" .. (wsapi_env.HTTP_COOKIE or "") .. ";",
								"%s*;%s*", ";")
	setmetatable(req.cookies, { __index = function (tab, name)
						name = name
						local pattern = ";" .. name .. 
							"=(.-);"
						local cookie = string.match(cookies, pattern)
						cookie = util.url_decode(cookie)
						rawset(tab, name, cookie)
						return cookie
					end } )
	return setmetatable(req, methods)
end

return _M
