--[=[
This example serves a file/directory browser
It defaults to serving the current directory.
Usage: lua examples/serve_dir.lua [<port> [<dir>]]
]=]

local port = arg[1] or 8000
local dir = '../public/'

local new_headers = require "http.headers".new
local http_server = require "http.server"
local http_util = require "http.util"
local http_version = require "http.version"
local ce = require "cqueues.errno"
local lfs = require "lfs"
local lpeg = require "lpeg"
local uri_patts = require "lpeg_patterns.uri"
local cjson = require "cjson"
local mimetypes = require "mimetypes"
local api = require './piazza-api-lmdb'

local replyJSON = function(t, stream)
  stream.res_headers:upsert(":status", "200")
  stream.res_headers:append('content-type', 'application/json')
  stream:write_headers(stream.res_headers, false)
  stream:write_body_from_string( cjson.encode(t) )
end

--local mdb do
--	-- If available, use libmagic https://github.com/mah0x211/lua-magic
--	local ok, magic = pcall(require, "magic")
--	if ok then
--		mdb = magic.open(magic.MIME_TYPE+magic.PRESERVE_ATIME+magic.RAW+magic.ERROR)
--    local mgc_file = '/usr/share/misc/magic.mgc'
--		if mdb:load(mgc_file) ~= 0 then
--			error(magic:error())
--		end
--	end
--end

local uri_reference = uri_patts.uri_reference * lpeg.P(-1)

local default_server = string.format("%s/%s", http_version.name, http_version.version)

local xml_escape do
	local escape_table = {
		["'"] = "&apos;";
		["\""] = "&quot;";
		["<"] = "&lt;";
		[">"] = "&gt;";
		["&"] = "&amp;";
	}
	function xml_escape(str)
		str = string.gsub(str, "['&<>\"]", escape_table)
		str = string.gsub(str, "[%c\r\n]", function(c)
			return string.format("&#x%x;", string.byte(c))
		end)
		return str
	end
end

local human do -- Utility function to convert to a human readable number
	local suffixes = {
		[0] = "";
		[1] = "K";
		[2] = "M";
		[3] = "G";
		[4] = "T";
		[5] = "P";
	}
	local log = math.log
	if _VERSION:match("%d+%.?%d*") < "5.1" then
		log = require "compat53.module".math.log
	end
	function human(n)
		if n == 0 then return "0" end
		local order = math.floor(log(n, 2) / 10)
		if order > 5 then order = 5 end
		n = math.ceil(n / 2^(order*10))
		return string.format("%d%s", n, suffixes[order])
	end
end


local function _reply(myserver, stream) -- luacheck: ignore 212
	-- Read in headers
	local req_headers = assert(stream:get_headers())
	local req_method = req_headers:get ":method"

	-- Log request to stdout
	assert(io.stdout:write(string.format('[%s] "%s %s HTTP/%g"  "%s" "%s"\n',
		os.date("%d/%b/%Y:%H:%M:%S %z"),
		req_method or "",
		req_headers:get(":path") or "",
		stream.connection.version,
		req_headers:get("referer") or "-",
		req_headers:get("user-agent") or "-"
	)))

	-- Build response headers
	local res_headers = new_headers()
  stream.res_headers = res_headers
	res_headers:append(":status", nil)
	res_headers:append("server", default_server)
	res_headers:append("date", http_util.imf_date())
  
  if req_method == 'POST' then
    local endpoint = req_headers:get(":path"):sub(6)  -- TODO: match exact proper path, and return 404 if no match;  shave-off the leading '/cgi' from the path
    if api[endpoint] ~= nil then
      local req = cjson.decode(stream:get_body_as_string())
      replyJSON( api[endpoint](req) , stream )
      return
    end
  end
	if req_method ~= "GET" and req_method ~= "HEAD" then
		res_headers:upsert(":status", "405")
		assert(stream:write_headers(res_headers, true))
		return
	end
  
  local path = req_headers:get(":path")
  local thumbnail_id = string.match(path, '/thumbnailsdb/(.+)%.png')
  if req_method == 'GET' and thumbnail_id ~= nil then
    res_headers:upsert(":status", "200")
		res_headers:append("content-type", "image/png")
    assert(stream:write_headers(res_headers, false))
    local src = api.getthumbnail(tonumber(thumbnail_id))
    stream:write_chunk(src, true)
    return
  end

	-- use LPEG to create a uri table with the URI in different forms:
	local uri_t = assert(uri_reference:match(path), "invalid path")
	path = http_util.resolve_relative_path("/", uri_t.path or nil)
	local real_path = dir .. path
	local file_type = lfs.attributes(real_path, "mode")
	if file_type == "directory" then
    -- check for index.html
    local fd, err, errno = io.open(real_path..'/index.html', "rb")
		if not fd then
                  -- directory listing
                  path = path:gsub("/+$", "") .. "/"
                  res_headers:upsert(":status", "200")
                  res_headers:append("content-type", "text/html; charset=utf-8")
                  assert(stream:write_headers(res_headers, req_method == "HEAD"))
                  if req_method ~= "HEAD" then
                    assert(stream:write_chunk(string.format([[
              <!DOCTYPE html>
              <html>
              <head>
                <title>Index of %s</title>
                <style>
                  a {
                    float: left;
                  }
                  a::before {
                    width: 1em;
                    float: left;
                    content: "\0000a0";
                  }
                  a.directory::before {
                    content: "📁";
                  }
                  table {
                    width: 800px;
                  }
                  td {
                    padding: 0 5px;
                    white-space: nowrap;
                  }
                  td:nth-child(2) {
                    text-align: right;
                    width: 3em;
                  }
                  td:last-child {
                    width: 1px;
                  }
                </style>
              </head>
              <body>
                <h1>Index of %s</h1>
                <table>
                  <thead><tr>
                    <th>File Name</th><th>Size</th><th>Modified</th>
                  </tr></thead>
                  <tbody>
              ]], xml_escape(path), xml_escape(path)), false))
                    -- lfs doesn't provide a way to get an errno for attempting to open a directory
                    -- See https://github.com/keplerproject/luafilesystem/issues/87
                    for filename in lfs.dir(real_path) do
                      if not (filename == ".." and path == "/") then -- Exclude parent directory entry listing from top level
                        local stats = lfs.attributes(real_path .. "/" .. filename)
                        if stats.mode == "directory" then
                          filename = filename .. "/"
                        end
                        assert(stream:write_chunk(string.format("\t\t\t<tr><td><a class='%s' href='%s'>%s</a></td><td title='%d bytes'>%s</td><td><time>%s</time></td></tr>\n",
                          xml_escape(stats.mode:gsub("%s", "-")),
                          xml_escape(http_util.encodeURI(path .. filename)),
                          xml_escape(filename),
                          stats.size,
                          xml_escape(human(stats.size)),
                          xml_escape(os.date("!%Y-%m-%d %X", stats.modification))
                        ), false))
                      end
                    end
                    assert(stream:write_chunk([[
                  </tbody>
                </table>
              </body>
              </html>
              ]], true))
                  end
    else
      res_headers:upsert(":status", "200")
			res_headers:append("content-type", 'text/html')
			assert(stream:write_headers(res_headers, req_method == "HEAD"))
			if req_method ~= "HEAD" then
				assert(stream:write_body_from_file(fd))
			end
    end
	elseif file_type == "file" then
		local fd, err, errno = io.open(real_path, "rb")
		local code
		if not fd then
			if errno == ce.ENOENT then
				code = "404"
			elseif errno == ce.EACCES then
				code = "403"
			else
				code = "503"
			end
			res_headers:upsert(":status", code)
			res_headers:append("content-type", "text/plain")
			assert(stream:write_headers(res_headers, req_method == "HEAD"))
			if req_method ~= "HEAD" then
				assert(stream:write_body_from_string("Fail!\n"..err.."\n"))
			end
		else
			res_headers:upsert(":status", "200")
			-- local mime_type = mdb and mdb:file(real_path) or "application/octet-stream"
      local mime_type = mimetypes.guess(real_path) or 'application/octet-stream'
			res_headers:append("content-type", mime_type)
			assert(stream:write_headers(res_headers, req_method == "HEAD"))
			if req_method ~= "HEAD" then
				assert(stream:write_body_from_file(fd))
			end
		end
	elseif file_type == nil then
		res_headers:upsert(":status", "404")
		assert(stream:write_headers(res_headers, true))
	else
		res_headers:upsert(":status", "403")
		assert(stream:write_headers(res_headers, true))
	end
end

local function reply(myserver, stream)
  xpcall(function()
    _reply(myserver, stream)
    end,
    function(err)
      print('ERROR: ', err)
      print(debug.traceback())
    end)
end

local myserver = assert(http_server.listen {
	host = "localhost";
	port = port;
	max_concurrent = 100;
	onstream = reply;
	onerror = function(myserver, context, op, err, errno) -- luacheck: ignore 212
		local msg = op .. " on " .. tostring(context) .. " failed"
		if err then
			msg = msg .. ": " .. tostring(err)
		end
		assert(io.stderr:write(msg, "\n"))
	end;
})

-- Manually call :listen() so that we are bound before calling :localname()
assert(myserver:listen())
do
	local bound_port = select(3, myserver:localname())
	assert(io.stderr:write(string.format("Now listening on port %d\n", bound_port)))
end
-- Start the main server loop
assert(myserver:loop())