{.push raises: [].}

import std/os
import std/posix
import std/tables

import encoding/charset
import config/config
import config/urimethodmap
import io/chafile
import io/dynstream
import io/packetreader
import io/packetwriter
import io/poll
import server/buffer
import server/bufferiface
import server/connectionerror
import server/loader
import server/loaderiface
import types/opt
import types/url
import types/winattrs
import utils/myposix
import utils/proctitle
import utils/sandbox
import utils/strwidth
import utils/twtstr

type
  ForkServer* = object
    pid*: Pid
    stream*: PosixStream
    estream*: PosixStream
    westream*: PosixStream

  ForkServerContext = object
    stream: PosixStream
    loaderStream: PosixStream
    pollData: PollData
    linkHintChars: seq[uint32]
    schemes: seq[string]

proc loadConfig*(forkserver: ForkServer; config: Config): int =
  forkserver.stream.withPacketWriter w:
    w.swrite(config{"doubleWidthAmbiguous"})
    w.swrite(config{"linkHintChars"})
    w.swrite(LoaderConfig(
      w3mCGICompat: config{"w3mCgiCompat"},
      cgiDir: config{"cgiDir"},
      tmpdir: config{"tmpdir"},
      configDir: config.dir,
      dataDir: config.dataDir,
      bookmark: config{"bookmark"},
      maxNetConnections: config{"maxNetConnections"},
    ))
    # client config for pager
    w.swrite(LoaderClientConfig(
      defaultHeaders: config{"defaultHeaders"},
      proxy: config{"proxy"},
      allowAllSchemes: true
    ))
    w.swrite(config{"urimethodmap"})
  do:
    return -1
  var process = -1
  forkserver.stream.withPacketReaderFire r:
    r.sread(process)
  return process

proc forkBuffer*(forkserver: ForkServer; config: BufferConfig; url: URL;
    attrs: WindowAttributes; ishtml: bool; charsetStack: seq[Charset];
    contentType: string): tuple[pid: int; cstream: PosixStream] =
  var sv {.noinit.}: array[2, cint]
  if socketpair(AF_UNIX, SOCK_STREAM, IPPROTO_IP, sv) != 0:
    return (-1, nil)
  var fail = false
  forkserver.stream.withPacketWriter w:
    w.swrite(config)
    w.swrite(url)
    w.swrite(attrs)
    w.swrite(ishtml)
    w.swrite(charsetStack)
    w.swrite(contentType)
    w.sendFd(sv[1])
  do:
    fail = true
  var bufferPid = -1
  if not fail:
    forkserver.stream.withPacketReaderFire r:
      r.sread(bufferPid)
  if bufferPid == -1:
    discard close(sv[0])
    return (-1, nil)
  return (bufferPid, newPosixStream(sv[0]))

proc forkLoader(ctx: var ForkServerContext; config: LoaderConfig;
    loaderStream: PosixStream; pagerPid: int; pagerConfig: LoaderClientConfig;
    urimethodmap: URIMethodMap): (int, PosixStream) =
  # loaderStream is a connection between main process <-> loader, but we
  # also need a connection between fork server <-> loader.
  # The naming here is very confusing, sorry about that.
  var sv {.noinit.}: array[2, cint]
  if socketpair(AF_UNIX, SOCK_STREAM, IPPROTO_IP, sv) != 0:
    loaderStream.sclose()
    return (-1, nil)
  stderr.flushFile()
  let pid = fork()
  if pid == 0:
    # child process
    ctx.stream.sclose()
    discard close(sv[0])
    let forkStream = newPosixStream(sv[1])
    setProcessTitle("cha loader")
    runFileLoader(config, loaderStream, forkStream, pagerPid, pagerConfig,
      urimethodmap)
    exitnow(1)
  else:
    discard close(sv[1])
    loaderStream.sclose()
    return (int(pid), newPosixStream(sv[0]))

proc forkBuffer(ctx: var ForkServerContext; r: var PacketReader): int =
  var config: BufferConfig
  var url: URL
  var attrs: WindowAttributes
  var ishtml: bool
  var charsetStack: seq[Charset]
  var contentType: string
  r.sread(config)
  r.sread(url)
  r.sread(attrs)
  r.sread(ishtml)
  r.sread(charsetStack)
  r.sread(contentType)
  let fd = r.recvFd()
  stderr.flushFile()
  let pid = fork()
  if pid == 0:
    # child process
    ctx.stream.sclose()
    ctx.loaderStream.sclose()
    setBufferProcessTitle(url)
    let pid = getCurrentProcessId()
    let urandom = newPosixStream("/dev/urandom", O_RDONLY, 0)
    let pstream = newPosixStream(fd)
    var cacheId: int
    var loaderStream: PosixStream
    var istream: PosixStream
    pstream.withPacketReader r:
      r.sread(cacheId)
      loaderStream = newPosixStream(r.recvFd())
      istream = newPosixStream(r.recvFd())
    do: # EOF in pager; give up
      quit(1)
    let loader = newFileLoader(pid, loaderStream)
    # SIGPIPE remains ignored, because we don't want the buffer to signal
    # just because the pager unregistered it (this can also happen when a
    # buffer is cloned)
    enterBufferSandbox()
    launchBuffer(config, url, attrs, ishtml, charsetStack, loader, pstream,
      istream, urandom, cacheId, contentType, move(ctx.linkHintChars),
      move(ctx.schemes))
    doAssert false
  discard close(fd)
  return pid

proc forkCGI(ctx: var ForkServerContext; r: var PacketReader): int =
  var hasIstream: bool
  r.sread(hasIstream)
  let istream = if hasIstream: newPosixStream(r.recvFd()) else: nil
  let ostream = newPosixStream(r.recvFd())
  # hack to detect when the child died
  var hasOstreamOut2: bool
  r.sread(hasOstreamOut2)
  let ostreamOut2 = if hasOstreamOut2: newPosixStream(r.recvFd()) else: nil
  var env: seq[tuple[name, value: string]]
  var dir: string
  var cmd: string
  var basename: string
  r.sread(env)
  r.sread(dir)
  r.sread(cmd)
  r.sread(basename)
  let pid = fork()
  if pid == 0: # child
    ctx.stream.sclose()
    ctx.loaderStream.sclose()
    # we leave stderr open, so it can be seen in the browser console
    if istream != nil:
      istream.moveFd(STDIN_FILENO)
    else:
      closeStdin()
    ostream.moveFd(STDOUT_FILENO)
    # reset SIGCHLD to the default handler. this is useful if the child
    # process expects SIGCHLD to be untouched.
    # (e.g. git dies a horrible death with SIGCHLD as SIG_IGN)
    discard myposix.signal(SIGCHLD, myposix.SIG_DFL)
    # let's also reset SIGPIPE, which we ignored on init
    discard myposix.signal(SIGPIPE, myposix.SIG_DFL)
    const ExecErrorMsg = "Cha-Control: ConnectionError " &
      $int(ceFailedToExecuteCGIScript)
    let stdout = cast[ChaFile](stdout)
    for it in env:
      if twtstr.setEnv(it.name, it.value).isErr:
        discard stdout.writeLine(ExecErrorMsg & " failed to set env vars")
        exitnow(1)
    if chdir(cstring(dir)) != 0:
      discard stdout.writeLine(ExecErrorMsg &
        " failed to set working directory")
      exitnow(1)
    discard execl(cstring(cmd), cstring(basename), nil)
    let es = $strerror(errno)
    discard stdout.writeLine(ExecErrorMsg & ' ' & es.deleteChars({'\n', '\r'}))
    exitnow(1)
  else: # parent or error
    if istream != nil:
      istream.sclose()
    ostream.sclose()
    if ostreamOut2 != nil:
      ostreamOut2.sclose()
    return pid

proc setupForkServerEnv(config: LoaderConfig): Opt[void] =
  ?twtstr.setEnv("SERVER_SOFTWARE", "Chawan")
  ?twtstr.setEnv("SERVER_PROTOCOL", "HTTP/1.0")
  ?twtstr.setEnv("SERVER_NAME", "localhost")
  ?twtstr.setEnv("SERVER_PORT", "80")
  ?twtstr.setEnv("REMOTE_HOST", "localhost")
  ?twtstr.setEnv("REMOTE_ADDR", "127.0.0.1")
  ?twtstr.setEnv("GATEWAY_INTERFACE", "CGI/1.1")
  ?twtstr.setEnv("CHA_INSECURE_SSL_NO_VERIFY", "0")
  ?twtstr.setEnv("CHA_TMP_DIR", config.tmpdir)
  ?twtstr.setEnv("CHA_DIR", config.configDir)
  ?twtstr.setEnv("CHA_DATA_DIR", config.dataDir)
  ?twtstr.setEnv("CHA_BOOKMARK", config.bookmark)
  ok()

proc runForkServer*(controlStream, loaderStream: PosixStream; pagerPid: int) =
  setProcessTitle("cha forkserver")
  var ctx = ForkServerContext(stream: controlStream)
  discard myposix.signal(SIGCHLD, myposix.SIG_IGN)
  discard myposix.signal(SIGPIPE, myposix.SIG_IGN)
  ctx.stream.withPacketReader r:
    var config: LoaderConfig
    var clientConfig: LoaderClientConfig
    var linkHintChars: string
    var urimethodmapPaths: seq[string]
    r.sread(isCJKAmbiguous)
    r.sread(linkHintChars)
    r.sread(config)
    r.sread(clientConfig)
    r.sread(urimethodmapPaths)
    ctx.linkHintChars = linkHintChars.toPoints()
    # for CGI
    if setupForkServerEnv(config).isErr:
      quit(1)
    var urimethodmap: URIMethodMap
    for path in urimethodmapPaths:
      let ps = newPosixStream(path)
      if ps != nil:
        urimethodmap.parseURIMethodMap(ps.readAll())
        ps.sclose()
    urimethodmap.parseURIMethodMap(DefaultURIMethodMap)
    for key in urimethodmap.map.keys:
      ctx.schemes.add(key.until(':'))
    # returns a new stream that connects fork server <-> loader and
    # gives away main process <-> loader
    var (pid, loaderStream) = ctx.forkLoader(config, loaderStream, pagerPid,
      clientConfig, urimethodmap)
    ctx.stream.withPacketWriter w:
      w.swrite(pid)
    do:
      pid = -1
    if pid == -1:
      # Notified main process of failure; our job is done.
      quit(1)
    ctx.loaderStream = loaderStream
  do:
    quit(1)
  ctx.pollData.register(ctx.stream.fd, POLLIN)
  ctx.pollData.register(ctx.loaderStream.fd, POLLIN)
  block mainLoop:
    while true:
      ctx.pollData.poll(-1)
      for event in ctx.pollData.events:
        if (event.revents and POLLIN) != 0:
          if event.fd == ctx.stream.fd:
            ctx.stream.withPacketReader r:
              let pid = ctx.forkBuffer(r)
              ctx.stream.withPacketWriter w:
                w.swrite(pid)
              do:
                break mainLoop # EOF
            do:
              break mainLoop # EOF
          elif event.fd == ctx.loaderStream.fd:
            ctx.loaderStream.withPacketReader r:
              let pid = ctx.forkCGI(r)
              ctx.loaderStream.withPacketWriter w:
                w.swrite(pid)
              do:
                break mainLoop # EOF
            do:
              break mainLoop # EOF
        if (event.revents and POLLERR) != 0 or (event.revents and POLLHUP) != 0:
          break mainLoop # EOF
  ctx.stream.sclose()
  # Clean up when the main process crashed.
  discard kill(0, cint(SIGTERM))
  quit(0)

{.pop.} # raises: []
