# Wrappers around socket primitives that take a 'local-network' object and are
# thus easier to test.
#
# The current semantics of fake port-connections don't match UNIX socket ones,
# but we'll improve them as we learn more.

container local-network [
  data:&:@:port-connection
]

# Port connections represent connections to ports on localhost.
# Before passing a local-network object to network functions
# `start-reading-socket` and `start-writing-socket`, add port-connections to
# the local-network.
#
# For reading, `receive-from-socket` will check for a
# port-connection on the port parameter that's been passed in. If there's
# no port-connection for that port, it will return nothing and log an error.
# If there is a port-connection for that port, it will transmit the contents
# to the provided sink.
#
# For writing, `start-writing-socket` returns a sink connecting the
# caller to the socket on the passed-in port.
container port-connection [
  port:num
  contents:text
]

def new-port-connection port:num, contents:text -> p:&:port-connection [
  local-scope
  load-ingredients
  p:&:port-connection <- new port-connection:type
  *p <- merge port, contents
]

def new-fake-network -> n:&:local-network [
  local-scope
  load-ingredients
  n:&:local-network <- new local-network:type
  local-network-ports:&:@:port-connection <- new port-connection:type, 0
  *n <- put *n, data:offset, local-network-ports
]

scenario write-to-fake-socket [
  local-scope
  single-port-network:&:local-network <- new-fake-network
  sink:&:sink:char, writer:num/routine <- start-writing-socket single-port-network, 8080
  sink <- write sink, 120/x
  close sink
  wait-for-routine writer
  tested-port-connections:&:@:port-connection <- get *single-port-network, data:offset
  tested-port-connection:port-connection <- index *tested-port-connections, 0
  contents:text <- get tested-port-connection, contents:offset
  10:@:char/raw <- copy *contents
  memory-should-contain [
    10:array:character <- [x]
  ]
]

def start-reading-from-network resources:&:resources, uri:text -> contents:&:source:char [
  local-scope
  load-ingredients
  {
    break-if resources
    # real network
    host:text, path:text <- split-at uri, 47/slash
    socket:num <- $open-client-socket host, 80/http-port
    assert socket, [contents]
    req:text <- interpolate [GET _ HTTP/1.1], path
    request-socket socket, req
    contents:&:source:char, sink:&:sink:char <- new-channel 10000
    start-running receive-from-socket socket, sink
    return
  }
  # fake network
#?   i:num <- copy 0
#?   data:&:@:resource <- get *fs, data:offset
#?   len:num <- length *data
#?   {
#?     done?:bool <- greater-or-equal i, len
#?     break-if done?
#?     tmp:resource <- index *data, i
#?     i <- add i, 1
#?     curr-filename:text <- get tmp, name:offset
#?     found?:bool <- equal filename, curr-filename
#?     loop-unless found?
#?     contents:&:source:char, sink:&:sink:char <- new-channel 30
#?     curr-contents:text <- get tmp, contents:offset
#?     start-running transmit-from-text curr-contents, sink
#?     return
#?   }
  return 0/not-found
]

def request-socket socket:num, s:text -> socket:num [
  local-scope
  load-ingredients
  write-to-socket socket, s
  $write-to-socket socket, 13/cr
  $write-to-socket socket, 10/lf
  # empty line to delimit request
  $write-to-socket socket, 13/cr
  $write-to-socket socket, 10/lf
]

def start-writing-socket network:&:local-network, port:num -> sink:&:sink:char, routine-id:num [
  local-scope
  load-ingredients
  source:&:source:char, sink:&:sink:char <- new-channel 30
  {
    break-if network
    socket:num <- $open-server-socket port
    session:num <- $accept socket
    # TODO Create channel implementation of write-to-socket.
    return sink, 0/routine-id
  }
  # fake network
  routine-id <- start-running transmit-to-fake-socket network, port, source
]

def transmit-to-fake-socket network:&:local-network, port:num, source:&:source:char -> network:&:local-network, source:&:source:char [
  local-scope
  load-ingredients
  # compute new port connection contents
  buf:&:buffer <- new-buffer 30
  {
    c:char, done?:bool, source <- read source
    break-unless c
    buf <- append buf, c
    break-if done?
    loop
  }
  contents:text <- buffer-to-array buf
  new-port-connection:&:port-connection <- new-port-connection port, contents
  # Got the contents of the channel, time to write to fake port.
  i:num <- copy 0
  port-connections:&:@:port-connection <- get *network, data:offset
  len:num <- length *port-connections
  {
    done?:bool <- greater-or-equal i, len
    break-if done?
    current:port-connection <- index *port-connections, i
    current-port:num <- get current, port:offset
    ports-match?:bool <- equal current-port, port
    i <- add i, 1
    loop-unless ports-match?
    # Found an existing connection on this port, overwrite.
    put-index *port-connections, i, *new-port-connection
    reply
  }
  # Couldn't find an existing connection on this port, initialize a new one.
  new-len:num <- add len, 1
  new-port-connections:&:@:port-connection <- new port-connection:type, new-len
  put *network, data:offset, new-port-connections
  i:num <- copy 0
  {
    done?:bool <- greater-or-equal i, len
    break-if done?
    tmp:port-connection <- index *port-connections, i
    put-index *new-port-connections, i, tmp
  }
  put-index *new-port-connections, len, *new-port-connection
]

def receive-from-socket socket:num, sink:&:sink:char -> sink:&:sink:char [
  local-scope
  load-ingredients
  {
    req:text, eof?:bool <- $read-from-socket socket, 4096/bytes
    bytes-read:num <- length *req
    i:num <- copy 0
    {
      done?:bool <- greater-or-equal i, bytes-read
      break-if done?
      c:char <- index *req, i  # todo: unicode
      sink <- write sink, c
      i <- add i, 1
      loop
    }
    loop-unless eof?
  }
  sink <- close sink
]

def write-to-socket socket:num, s:text [
  local-scope
  load-ingredients
  len:num <- length *s
  i:num <- copy 0
  {
    done?:bool <- greater-or-equal i, len
    break-if done?
    c:char <- index *s, i
    $write-to-socket socket, c
    i <- add i, 1
    loop
  }
]

# like split-first, but don't eat the delimiter
def split-at text:text, delim:char -> x:text, y:text [
  local-scope
  load-ingredients
  # empty text? return empty texts
  len:num <- length *text
  {
    empty?:bool <- equal len, 0
    break-unless empty?
    x:text <- new []
    y:text <- new []
    return
  }
  idx:num <- find-next text, delim, 0
  x:text <- copy-range text, 0, idx
  y:text <- copy-range text, idx, len
]

scenario text-split-at [
  local-scope
  x:text <- new [a/b]
  run [
    y:text, z:text <- split-at x, 47/slash
    10:@:char/raw <- copy *y
    20:@:char/raw <- copy *z
  ]
  memory-should-contain [
    10:array:character <- [a]
    20:array:character <- [/b]
  ]
]