require unix/socket.fs

require socket-extensions.4th
require connections.4th
require commandline.4th
require motd.4th
require sendbuffer.4th

AF_INET constant SERVER_SOCKET_DOMAIN
  SOCK_STREAM
  SOCK_NONBLOCK or
        constant SERVER_SOCKET_TYPE
      0 constant SERVER_SOCKET_PROTOCOL
      0 constant SERVER_ADDR
  50000 constant SERVER_PORT
    128 constant SERVER_LISTEN_BACKLOG

\ Listening file descriptor.
variable listenfd
0 listenfd !

\ Idle detection.
variable idle false idle !
: server-idle? ( -- flag ) idle @ ;
: server-idle! ( flag -- ) idle ! ;

\ Temporary structs.
create con /CONNECTION allot
create saddr /sockaddr_in allot
create optval /option_value allot

: (assert-setsockopt) ( result-n -- )
  0< abort" making socket reusable failed" ;
: (optval-true!) ( -- )
  1 optval l! ;
: (make-reusable) ( socket-fd-n -- )
  (optval-true!)
  SOL_SOCKET SO_REUSEADDR optval /option_value setsockopt (assert-setsockopt) ;

: (saddr!) ( protocol-n sin_addr-n port-n -- )
  htons saddr port w!
  saddr sin_addr l!
  saddr family w! ;

: (assert-socket) ( result-n -- result-n )
  dup 0< abort" socket() failed." ;
: (assert-bind) ( result-n -- )
  0< abort" bind failed." ;
: (assert-listen) ( result-n -- )
  0< abort" listen failed." ;

: (erase-saddr) ( -- )
  saddr /sockaddr_in erase ;
: (create-socket)  ( -- )
  SERVER_SOCKET_DOMAIN
  SERVER_SOCKET_TYPE
  SERVER_SOCKET_PROTOCOL
    socket (assert-socket) listenfd ! ;
: (make-socket-reusable) ( -- )
  listenfd @ (make-reusable) ;
: (set-saddr) ( -- )
  SERVER_SOCKET_DOMAIN SERVER_ADDR SERVER_PORT (saddr!) ;
: (bind-socket) ( -- )
  listenfd @ saddr /sockaddr_in bind (assert-bind) ;
: (listen-socket) ( -- )
  listenfd @ SERVER_LISTEN_BACKLOG listen() (assert-listen) ;
: (server-info) ( -- )
  cr cr ." Server listening at port: " SERVER_PORT . cr ;
: initialize-server ( -- )
  (erase-saddr)
  (create-socket)
  (make-socket-reusable)
  (set-saddr)
  (bind-socket)
  (listen-socket)
  (server-info) ;

: (perform-disconnect) ( connection-addr -- )
  dup connection.connected false swap !
      connection.fd @ close() throw ;

: (close-clients) ( -- )
  connections.count 0= IF
    EXIT
  THEN
  connections.count 0 DO
    I connections.at connection.connected @ true = IF
      I connections.at (perform-disconnect)
    THEN
  LOOP ;

: (assert-close()) ( result-n -- )
  0<> abort" close failed" ;
: (close-server) ( -- )
  listenfd @ close() (assert-close()) ;

: (close-server-info) ( -- )
  cr ." Closed server connections." cr ;
: close-server ( -- )
  (close-clients) (close-server) (close-server-info) ;

: (queue-disconnect) ( connection-addr -- )
  dup (perform-disconnect) EVENT_CONNECTION_CLOSED events.enqueue ;

: (try-accept) ( -- c-result-n )
  listenfd @ 0 0 accept() ;
: (accept-error) ( accept-result-n -- )
  errno EAGAIN <> abort" accept error" drop ;
: (erase-connection) ( connection-addr -- )
  /CONNECTION erase ;
: (set-connection-number) ( -- )
  connections.last dup connections.indexOf 1+ swap connection.number ! ;
: (enqueue-new-connection) ( -- )
  connections.last EVENT_CONNECTION_NEW events.enqueue ;
: (store-connection) ( connection-addr -- )
  dup >r connections.append IF
    (set-connection-number)
    (enqueue-new-connection)
    rdrop
  ELSE
    ." Warning: failed to store connection, disconnecting client." cr
    r> (perform-disconnect)
  THEN ;
: (con!) ( fd-n connected-flag -- )
  con connection.connected !
  con connection.fd ! ;
: (accept-connection) ( fd-n -- )
  con (erase-connection)
  true (con!)
  con (store-connection) ;
: (server-idle-accept) ( -- )
  (try-accept) dup 0< IF
    (accept-error)
  ELSE
    (accept-connection)
  THEN ;

: (connected?) ( connection-addr -- flag )
  connection.connected @ ;
: (try-recv) ( connection-addr -- recv-result-n )
  dup connection.fd @
  swap connection.buffer
  CONNECTION_BUFFER_SIZE
  MSG_DONTWAIT
    recv ;
: (recv-error?) ( c-result-n -- flag ) 0< ;
: (recv) ( recv-result-n connection-addr -- )
  2dup connection.bufferlen ! swap
  0> IF
    EVENT_CONNECTION_RECV events.enqueue
  ELSE \ disconnected
    (queue-disconnect)
  THEN ;
: (recv-warning) ( -- ) ." Warning: recv failed, disconnecting client." cr ;
: (recv-error) ( recv-result-n connection-addr -- )
  errno EAGAIN <> IF
    (queue-disconnect) drop
    (recv-warning)
  ELSE
    2drop
  THEN ;
: (connection-recv) ( connection-addr -- )
  dup (try-recv) tuck (recv-error?) IF
    (recv-error)
  ELSE
    (recv)
  THEN ;

: (server-idle-recv) ( -- )
  true server-idle!
  connections.count 0= IF
    EXIT
  THEN
  connections.count 0 DO
    I connections.at dup (connected?) IF
      (connection-recv)
    ELSE
      drop
    THEN
  LOOP ;

: (to-string) ( n -- addr c )  s>d <# #s #> ;
: (connection.number>string) ( connection-addr -- c-addr u )
  connection.number @ (to-string) ;
: (connection.buffer>string) ( connection-addr -- c-addr u )
  dup connection.buffer swap connection.bufferlen @ ;
: (format-sendbuffer) ( from-connection-addr -- )
  >r sendbuffer-reset
     s" Anon "                     sendbuffer-append
     r@ (connection.number>string) sendbuffer-append
     s" : "                        sendbuffer-append
     r> (connection.buffer>string) sendbuffer-append
  sendbuffer-sanitize ;
: (connected?) ( connection-addr -- )
  connection.connected @ ;
: (different-connection?) ( from-connection-addr to-connection-addr -- )
  <> ;
: (send?) ( from-connection-addr to-connection-addr -- )
  tuck (different-connection?) swap (connected?) and ;
: (check-send) ( result-n -- )
  0< IF ." Warning: send failed." cr THEN ;
: (send-sendbuffer) ( to-connection-addr -- )
  connection.fd @ sendbuffer@ 0 send (check-send) ;
: (send) ( from-connection-addr to-connection-addr -- )
  (send-sendbuffer) ;
: (try-send) ( from-connection-addr to-connection-addr -- )
  2dup (send?) IF
    nip (send-sendbuffer)
  ELSE
    2drop
  THEN ;
: server-recv ( from-connection-addr eventid-n )
  drop dup (format-sendbuffer)
  connections.count 0 DO
    dup I connections.at (try-send)
  LOOP drop ;

: server-idle-accept ( eventdata-n eventid-n -- )
  2drop (server-idle-accept) ;
: server-idle-recv ( eventdata-n eventid-n -- )
  2drop (server-idle-recv) ;
variable (strstart)
variable (strend)
: (>str) ( startindex-n endindex-n str-addr -- c-addr u )
  tuck + -rot + tuck - ;
: (newline?) ( char -- flag ) 10 = ;
\ TODO: FIXME: refactor and create words to be able to conveniently
\ TODO: FIXME: send "Server: ..." messages. This will be useful in the repl too.
: (prepare-motd) ( -- )
\ TODO: FIXME: just write a proper parser at this point....
  sendbuffer-reset
  -1 (strstart) !
  -1 (strend) !
  motd@ 0 DO
    (strstart) @ -1 = IF
      I (strstart) !
    THEN
    dup I + c@ (newline?) IF
      I (strend) !
    THEN
    (strend) @ -1 <> IF
      s" Server: " sendbuffer-append
      dup (strstart) @ (strend) @ rot (>str) sendbuffer-append
      s\" \n" sendbuffer-append
      -1 (strstart) !
      -1 (strend) !
    THEN
  LOOP drop ;
: (prepare-empty-line) ( -- )
  sendbuffer-reset s\" Server: \n" sendbuffer-append ;
: (prepare-identity) ( connection-addr -- )
  sendbuffer-reset
  s\" Server: You are now known as \"Anon " sendbuffer-append
  (connection.number>string)                sendbuffer-append
  s\" \".\n"                                sendbuffer-append ;
: server-connection-new ( connection-addr eventid-n -- )
  drop ." New client connected!" cr
  dup (prepare-motd) (send-sendbuffer)
  dup (prepare-empty-line) (send-sendbuffer)
  dup (prepare-identity)
      (send-sendbuffer) ;

: server-connection-closed ( connection-addr eventid-n -- )
  2drop ." Client disconnected." cr ;

: server-commandline ( eventdata-n eventid-n -- )
  2drop commandline-ready? IF
    space commandline-getline ['] evaluate catch dup 0= IF
      drop ."  ok"
    ELSE
      ." error code: " . 2drop
    THEN
    cr
    commandline-reset 
  ELSE
    commandline-key? IF
      commandline-key commandline-handlekey
      commandline-redraw
    THEN
  THEN ;

: server-motd-changed ( eventdata-n eventid-n -- )
  2drop (prepare-motd)
  connections.count 0 DO
    I connections.at (send-sendbuffer)
  LOOP ;

' server-idle-accept       EVENT_IDLE              eventhandlers.append
' server-idle-recv         EVENT_IDLE              eventhandlers.append
' server-connection-new    EVENT_CONNECTION_NEW    eventhandlers.append
' server-connection-closed EVENT_CONNECTION_CLOSED eventhandlers.append
' server-recv              EVENT_CONNECTION_RECV   eventhandlers.append
' server-commandline       EVENT_COMMANDLINE       eventhandlers.append
' server-motd-changed      EVENT_MOTD_CHANGED      eventhandlers.append

initialize-server