local Core = require "AsyncTasks.Core"
local Uuid = assert(require "uuid")
local url_encode = require "wsapi.util".url_encode

local m_localPort = KEPLER_LISTEN_PORT

module(..., package.seeall)

--
-- Checks that table 'table' has a field called 'name' of type 'expectedType'
-- If it is missing, returns 'optionalValue'
local function CheckField(table, name, expectedType, optionalValue)
	local value = table[name]
	-- is nil but it is required
	if not value then
		if not optionalValue then
			error( ("field '%s' is not optional"):format(name) )
		else
			return optionalValue
		end
	end
	if type(value) ~= expectedType then
		error( ("field '%s' must be a %s"):format(name, expectedType) )
	end
	return value
end

local mt = {
	__index = {
	},
	__metatable = false
}

--
-- Constructor: AsyncTask( parameters )
-- Returns a new asynchronous task.
-- @param selfModule is equivalent to _M (no need to pass it explicitly)
-- @param parameters is a table containing the following:
--   @field data a string with raw data for this task
--   @field url the address of the function that will service this task (mandatory)
--   @field due_in time in seconds when the task will be dispatched (default value is 0, which dispatches the task right away)
--   @field times number of times that the task will repeat. ("forever" means that the task will dispatch over and over again)
--   @field method the http method to use when dispatching the task (default value is POST, can GET or POST)
--   @field id the id of the task. Used whenever you need to refer to a task externally. It will auto generate an id if none supplied.
--   @field parameters a table of parameters to be sent when dispatching the task. They will be encoded as application/x-www-form-urlencoded
--   @field credentials whenever the task handler needs authentication, this field will supply that. 
--          It is a table with 'username', 'vcc' and 'password' fields.
--   @field headers is a table with additional http headers to pass along
--   @field cookies is a table with cookies to be sent in the request
--   @field redis_data isa a table with connection redis (ip, port) and timeout lock atribute
getmetatable(_M).__call = function(selfModule, parameters)
	local task = setmetatable({}, mt)
	
	task.id = parameters.id or Uuid.new()	-- supplied id or default
	task.data = parameters.data
	task.url = CheckField(parameters, "url", "string")
	task.due_in = CheckField(parameters, "due_in", "number", 0)
	task.headers = parameters.headers or {}
	task.cookies = parameters.cookies or {}
	task.redis_data = parameters.redis_data
	
	-- urlencode the cookies
	for k,v in pairs(task.cookies) do
		task.cookies[k] = url_encode(v)
	end
	
	if type(parameters.times) == "string" then
		if parameters.times ~= "forever" then
			error("field 'times' must be 'forever' or an integer value")
		else
			task.forever = true
		end
	else
		task.times = CheckField(parameters, "times", "number", 1)
	end
	
	-- check 'method' type
	local method = parameters.method
	if not method then
		task.method = "POST"
	else
		if type(method) == "string" then
			method = method:upper()
			if method == "POST" or method == "GET" then
				task.method = method
			else
				error("field 'method' must be 'POST' or 'GET'")
			end
		else
			error("field 'method' must be 'POST' or 'GET'")
		end
	end
	
	-- encode 'parameters' if present (using application/x-www-form-urlencoded)
	if type(parameters.parameters) == "table" then
		if task.data then
			error("cannot set 'data' and 'parameters' at the same time")
		end
		local t = {}
		for k,v in pairs(parameters.parameters) do
			t[#t + 1] = url_encode(tostring(k)) .. "=" .. url_encode(tostring(v))
		end
		task.parameters = table.concat(t, "&")
		if task.method == "POST" then
			task.headers["Content-Type"] = "application/x-www-form-urlencoded"
		end
	end
	
	-- if data was supplied, default to POST. If no Content-Type was specified, default to application/octet-stream
	if task.data then
		if task.method == "POST" and not task.headers["Content-Type"] then
			task.headers["Content-Type"] = "application/octet-stream"
		end
	end
	
	-- check for credentials
	local credentials = parameters.credentials
	if type(credentials) == "table" then
		CheckField(credentials, "username", "string")
		CheckField(credentials, "vcc", "string")
		CheckField(credentials, "password", "string")
	elseif type(credentials) ~= "nil" then
		error("field 'credentials' can only be a table")
	end
	task.credentials = credentials
	
	-- if method is GET then append the parameters or the data to the url
	if task.method == "GET" then
		local data = task.parameters or task.data
		if data then
			task.url = task.url .. "?" .. tostring(data)
		end
	-- if method is POST, then compute the content length
	elseif task.method == "POST" then
		task.headers["Content-Length"] = #(task.parameters or task.data or "")
	end
	
	-- consolidate on property 'data'
	task.data = task.data or task.parameters
	task.parameters = nil
	
	-- set up a full local url if the user didn't supply one
	if not string.match(task.url, "http") then
		if m_localPort then
			task.url = "http://localhost:" .. m_localPort .. task.url
		else
			task.url = "http://localhost" .. task.url
		end
	end
	
	local protocol, server, port = task.url:match("(%w+://)([^/:]+):?([%d]*)")
	task.urlScheme = { protocol = protocol, server = server, port = port }
	
	return task
end

--
-- task:Add(queue)
-- Adds a task to the given queue.
-- @param queue is the id of the queue to add the task to.
-- A task can only be added to one queue only.
mt.__index.Add = function(task, queue)
	return Core.AddTask(task, queue)
end

--
-- task:Cancel()
-- Cancels the dispatch of a queued task. If the task has already dispatched and it was a one time task, then this is a no-op.
mt.__index.Cancel = function(task)
	return Core.CancelTask(task.id)
end

--
-- Cancel, see task:Cancel().
-- @param task can be a task id (a string) or a task object.
function Cancel(task)
	if type(task) == "string" then
		return Core.CancelTask(task)
	else
		return task:Cancel()
	end
end
