--[[

  Example: Wiki engine

  Author: Peter Odding <peter@peterodding.com>
  Last Change: June 17, 2011
  Homepage: http://peterodding.com/code/lua/apr/
  License: MIT

  export LD_PRELOAD='/usr/lib/libapr-1.so:/usr/lib/libaprutil-1.so'

]]

-- Load the Lua/APR binding.
local apr = require 'apr'

-- Usage message.
local usage = [[
Usage: wiki.lua [OPTIONS]
  -a, --address=ADDR   address to bind to
  -p, --port=NUM       port number to bind to
  -d, --database=FILE  name of SQLite database
]]

function main()
  -- Parse the command line arguments.
  local opts, args = apr.getopt(usage)
  -- Get configuration from options and/or defaults.
  local address = opts.address or '*'
  local port = tonumber(opts.port) or math.random(1001, 40000)
  local database = opendb(opts)
  -- Create the server socket.
  local server = assert(apr.socket_create())
  assert(server:bind(address, port))
  assert(server:listen(1))
  -- Give the user a link to the wiki.
  local host = address == '*' and 'localhost' or address
  local url = 'http://' .. host .. ':' .. port
  printf("Running wiki on %s ..", url)
  openbrowser(url)
  -- Start serving requests.
  while true do
    -- Protect the server against errors during a request.
    local status, message = pcall(accept, server, database)
    if not status then
      printf("Error while serving request: %q", message)
    end
  end
end

function opendb(opts)
  local database = assert(apr.dbd 'sqlite3')
  local filename = opts.database or apr.filepath_merge(
      assert(apr.user_homepath_get(assert(apr.user_get()))),
      apr.platform_get() == 'UNIX' and '.wiki.sqlite3' or 'wiki.sqlite3')
  assert(database:open(filename))
  assert(database:query [[
    create table if not exists pages (
      page_path varchar(250) not null,
      page_title varchar(250) not null,
      page_content text not null,
      primary key (page_path))
  ]])
  assert(database:query [[
    insert or ignore into pages
      (page_path, page_title, page_content)
    values
      ('/', 'Welcome', 'Welcome to your wiki!')
  ]])
  return database
end

function openbrowser(url)
  -- Try to launch Google Chrome in "application mode".
  local pathname = apr.filepath_which 'chromium-browser'
  if pathname then
    local process = assert(apr.proc_create(pathname))
    assert(process:cmdtype_set 'program/env')
    assert(process:exec{'--app=' .. url})
  end
  -- TODO gnome-open, kde-open, xdg-open, etc.
end

function accept(server, database)
  -- Wait for and accept an incoming connection request.
  local client = assert(server:accept())
  -- Store all request data in a table.concat
  local request = { client = client, database = database }
  -- Parse the request line (the first line of an HTTP request).
  request.reqline = assert(client:read())
  request.method, request.location, request.protocol =
      assert(request.reqline:match '^(%w+)%s+(%S+)%s+(%S+)')
  -- Get the raw headers (terminated by an empty line) and parse them.
  request.rawheaders = {}
  for line in client:lines() do
    if line:find '%S' then
      table.insert(request.rawheaders, line)
    else
      break
    end
  end
  local allheaders = table.concat(request.rawheaders, '\n')
  request.headers = assert(apr.parse_headers(allheaders))
  -- Route the request to the appropriate function.
  local response
  if request.location == '/favicon.ico' then
    response = favicon(request)
  else
    response = getpage(request)
    if not response then
      response = setpage(request)
    end
  end
  -- Send the response.
  if not response.status then response.status = '200 OK' end
  printf("%s %s -> %s", request.method, request.location, response.status)
  client:write(request.protocol, ' ', response.status, '\r\n')
  if not response.headers then response.headers = {} end
  if not response.headers['content-type'] then
    response.headers['content-type'] = 'text/html; charset=utf-8'
  end
  for name, value in pairs(response.headers) do
    client:write(name, ": ", value, "\r\n")
  end
  if response.content then
    client:write("\r\n", response.content)
  end
  assert(client:close())
end

function getpage(request)
  printf('searching for path %q', request.location)
  local query = assert(request.database:prepare 'select * from pages where page_path = %s')
  local resultset = assert(query:select(false, request.location))
  local page = assert(resultset:row())
  return { content = template(page.title, page.content) }
end

function setpage(request)
  local page = request.location:sub(2)
  local title = prettyname(page)
  if request.method == 'GET' then
    return {
      status = '404 Not Found',
      content = template('New page', [[
   <p>To create a new page please fill out and submit the form below.</p>
   <form action="/new" method=post>
   <p><label>Name of page:<br>
     <input type=text name=title value="{title}">
    </label></p>
   <p><label>Page content:<br>
     <textarea name=content>{content}</textarea>
    </label></p>
   <p><input type=submit value="Create page"></p>
   </form>
]], { title = title })
    }
  elseif request.method == 'POST' then
    local size = assert(tonumber(request.headers['Content-Length']))
    local data = assert(request.client:read(size))
    local params = assert(apr.parse_query_string(data))
    -- TODO Use request.database!
    return { content = 'Thanks!' }
  end
end

favicon_data = apr.base64_decode(([[
AAABAAEAEBAAAAEAIADBAQAAFgAAAIlQTkcNChoKAAAADUlIRFIAAAAQAAAAEAgGAAAAH/P/YQAA
AYhJREFUOI2lk72KVEEQhb+e9WdFxUBZzPQBBMXA0EcQNFIMDHyFfQZT30FMxMxAEyMFwUgwMFkD
DRbUddlhR293366qY3BnxxlnIqeD7qbhfOdUU5Uksc4araUGjh1d9OWVbPITqUfWkDdkPbKKvEIr
uBVkGVrh3J0naRHgxvErd5ccJEgw3QDBwfN7ywnqeJdTEoo8PCSQAkiIv4Q0OolaXlGCGdF/wybv
ISpSP5zRQxQUBUXmxIXbROtWALwChqKA2lQ0Ly4oOpChfkUC+7U/RF5wzkSUKSRD5MHEfi8Dwgtl
5y1WP4P3IMPLIZuXtgbnKMgzKFb/ged97HCXM9fuA0H0X5l8eIa8A3XIy5BAhs8BZo2UJNqPjxy8
3sa7HfBGqM2JhxQhX0gwA0hw9sYD2niP8ZvHU3GZiRUFqQI+NNO/JWCFtDHi9PWH5E8vmbx7wcb5
q2xevEkgkAMGBLI6k6WjYdp7ekvRKrKCWsbyd2DEKCVkBfpKRBla25zLj5QWAP+71p7GP7m3OlzV
gfYNAAAAAElFTkSuQmCC
]]):gsub('%s+', ''))

function favicon(request)
  return {
    headers = { ['content-type'] = 'image/x-icon' },
    content = favicon_data
  }
end

function prettyname(slug)
  return slug:gsub('[-_]', ' '):gsub('^.', string.upper)
end

function template(title, body, params)
  return expand([[
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN"
"http://www.w3.org/TR/html4/strict.dtd">
<html>
 <head>
  <title>{title}</title>
  <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
  <style type="text/css">
   body, input, textarea { font: 1em sans-serif; }
   h1 { margin-top: 0; border-bottom: 1px solid silver; }
   input[type=text], textarea { border: 1px solid silver; padding: 0.25em; width: 90%%; }
   textarea { height: 10em; }
  </style>
 </head>
 <body>
  <h1>{title}</h1>
  {content}
 </body>
</html>
]], {
    title = title,
    content = expand(body, params),
  })
end

function expand(text, params)
  return (text:gsub('{(%w+)}', function(name)
    return params and params[name] or ''
  end))
end

function printf(message, ...)
  io.stderr:write(message:format(...), '\n')
end

main()
