{-# LANGUAGE MagicHash #-}
{-# LANGUAGE BangPatterns #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE ExistentialQuantification #-}

{-|
Module      : Std.IO.TCP
Description : TCP servers and clients
Copyright   : (c) Dong Han, 2018
License     : BSD
Maintainer  : winterland1989@gmail.com
Stability   : experimental
Portability : non-portable

This module provides an API for creating TCP servers and clients.

-}

module Std.IO.TCP (
  -- * TCP Client
    ClientConfig(..)
  , defaultClientConfig
  , initClient
  -- * TCP Server
  , ServerConfig(..)
  , defaultServerConfig
  , startServer
  , module Std.IO.SockAddr
  ) where

import           Control.Concurrent
import           Control.Concurrent.MVar
import           Control.Monad
import           Control.Monad.IO.Class
import           Data.Int
import           Data.Primitive.PrimArray
import           Foreign.C.Types
import           Foreign.Ptr
import           GHC.Ptr
import           Std.Foreign.PrimArray
import           Std.Data.Array
import           Std.IO.Buffered
import           Std.IO.Exception
import           Std.IO.SockAddr
import           Std.IO.Resource
import           Std.IO.UV.FFI
import           Std.IO.UV.Manager
import           Std.Data.Vector

initTCPStream :: HasCallStack => UVManager -> Resource UVStream
initTCPStream = initUVStream (\ loop handle ->
    throwUVIfMinus_ (uv_tcp_init loop handle))

initTCPExStream :: HasCallStack => CUInt -> UVManager -> Resource UVStream
initTCPExStream family = initUVStream (\ loop handle ->
    throwUVIfMinus_ (uv_tcp_init_ex loop handle family))

--------------------------------------------------------------------------------

-- | A TCP client configuration
--
data ClientConfig = ClientConfig
    { clientLocalAddr :: Maybe SockAddr
    , clientTargetAddr :: SockAddr
    , clientNoDelay :: Bool
    }

defaultClientConfig :: ClientConfig
defaultClientConfig = ClientConfig Nothing (SockAddrInet 8888 inetLoopback) True

initClient :: HasCallStack => ClientConfig -> Resource UVStream
initClient ClientConfig{..} = do
    uvm <- liftIO getUVManager
    client <- initTCPStream uvm
    let handle = uvsHandle client
    liftIO . withSockAddr clientTargetAddr $ \ targetPtr -> do
        forM_ clientLocalAddr $ \ clientLocalAddr' ->
            withSockAddr clientLocalAddr' $ \ localPtr ->
                -- bind is safe without withUVManager
                throwUVIfMinus_ (uv_tcp_bind handle localPtr 0)
        -- nodelay is safe without withUVManager
        when clientNoDelay $ throwUVIfMinus_ (uv_tcp_nodelay handle 1)
        withUVRequest uvm $ \ _ -> hs_uv_tcp_connect handle targetPtr
    return client

--------------------------------------------------------------------------------

-- | A TCP server configuration
--
data ServerConfig = ServerConfig
    { serverAddr       :: SockAddr
    , serverBackLog    :: Int
    , serverWorker     :: UVStream -> IO ()
    , serverWorkerNoDelay :: Bool
    }

-- | A default hello world server on localhost:8888
--
-- Test it with @main = startServer defaultServerConfig@, now try @nc -v 127.0.0.1 8888@
--
defaultServerConfig :: ServerConfig
defaultServerConfig = ServerConfig
    (SockAddrInet 8888 inetAny)
    128
    (\ uvs -> writeOutput uvs (Ptr "hello world"#) 11)
    True

-- | Start a server
--
-- Fork new worker thread upon a new connection.
--
startServer :: ServerConfig -> IO ()
startServer ServerConfig{..} = do
    serverManager <- getUVManager
    withResource (initTCPStream serverManager) $ \ (UVStream serverHandle serverSlot _ _) ->
        bracket
            (throwOOMIfNull $ hs_uv_accept_check_alloc serverHandle)
            (hs_uv_accept_check_close) $ \ check -> do
                throwUVIfMinus_ $ hs_uv_accept_check_init check
                withSockAddr serverAddr $ \ addrPtr -> do
                    m <- getBlockMVar serverManager serverSlot
                    acceptBuf <- newPinnedPrimArray ACCEPT_BUFFER_SIZE
                    let acceptBufPtr = (coerce (mutablePrimArrayContents acceptBuf :: Ptr UVFD))

                    withUVManager_ serverManager $ do
                        pokeBufferTable serverManager serverSlot acceptBufPtr 0
                        throwUVIfMinus_ (uv_tcp_bind serverHandle addrPtr 0)
                        throwUVIfMinus_ (hs_uv_listen serverHandle (fromIntegral serverBackLog))

                    forever $ do
                        takeMVar m

                        -- we lock uv manager here in case of next uv_run overwrite current accept buffer
                        acceptBufCopy <- withUVManager_ serverManager $ do
                            tryTakeMVar m
                            accepted <- peekBufferTable serverManager serverSlot
                            acceptBuf' <- newPrimArray accepted
                            copyMutablePrimArray acceptBuf' 0 acceptBuf 0 accepted
                            pokeBufferTable serverManager serverSlot acceptBufPtr 0
                            unsafeFreezePrimArray acceptBuf'

                        let accepted = sizeofPrimArray acceptBufCopy

                        forM_ [0..accepted-1] $ \ i -> do
                            let fd = indexPrimArray acceptBufCopy i
                            if fd < 0
                            -- minus fd indicate a server error and we should close server
                            then throwUVIfMinus_ (return fd)
                            -- It's important to use the worker thread's mananger instead of server's one!
                            else void . forkBa $ do
                                uvm <- getUVManager
                                withResource (initUVStream (\ loop handle -> do
                                    throwUVIfMinus_ (uv_tcp_init loop handle)
                                    throwUVIfMinus_ (hs_uv_tcp_open handle fd)) uvm) $ \ client -> do
                                    when serverWorkerNoDelay . throwUVIfMinus_ $
                                        -- safe without withUVManager
                                        uv_tcp_nodelay (uvsHandle client) 1
                                    serverWorker client

                        when (accepted == ACCEPT_BUFFER_SIZE) $
                            withUVManager_ serverManager (hs_uv_listen_resume serverHandle)