{-# LANGUAGE ScopedTypeVariables #-}
-- | Communicate Yampa game and debugging GUI via TCP
module FRP.Titan.Debug.CommTCP
    ( mkTitanCommTCPBridge )
  where

-- External modules
import Control.Concurrent
import Control.Concurrent.STM
import Control.Exception
import Control.Monad
import Data.Maybe
import Network.BSD
import Network.Socket
import System.IO

-- Internal modules
import FRP.Titan.Debug.Comm

-- | Create a communication bridge using a local TCP server.
mkTitanCommTCPBridge :: IO ExternalBridge
mkTitanCommTCPBridge = do

  -- The communication bridge is composed of two TCP sockets:
  -- a sync one and an async one.
  --
  -- To control messages sent and received through these sockets,
  -- three mvars are used:
  -- - One for outgoing, sync messages  (out)
  -- - One for outgoing, async messages (event)
  -- - One for incoming, sync message   (get)
  --
  -- Sending and receiving messages is controlled via three MVars.
  --
  outChannel   <- atomically $ newTChan
  eventChannel <- atomically $ newTChan
  getChannel   <- atomically $ newTChan
  forkIO $ mkSendMsg outChannel getChannel
  forkIO $ mkSendEvent eventChannel
  let sendMsg msg = do atomically $ writeTChan outChannel msg
                       putStrLn $ "Wrote the sync message " ++ msg
      sendEvent msg = do atomically $ writeTChan eventChannel msg
                         putStrLn $ "Wrote the async message " ++ msg
      getMsg = do a <- (fromMaybe "") <$> (atomically $ tryReadTChan getChannel)
      -- getMsg = do a <- atomically $ readTChan getChannel
                  when (not (null a)) $ putStrLn $ "Got the sync message " ++ show a
                  return a
  return $ ExternalBridge errPrintLn sendMsg sendEvent getMsg

-- | Send communication channel that takes messages from an MVar and pushes
--   them out a socket.
mkSendMsg :: TChan String -> TChan String -> IO ()
mkSendMsg outChannel getChannel = void $
  forkIO $ serveSync "8081" (\msg -> do atomically $ writeTChan getChannel msg) (atomically $ readTChanAll outChannel)

readTChanAll :: TChan a -> STM [a]
readTChanAll tchan = reverse <$> readTChanAll' tchan []

readTChanAll' :: TChan a -> [a] -> STM [a]
readTChanAll' tchan acc = do
  mval <- tryReadTChan tchan
  case mval of
    Nothing  -> return acc
    Just val -> readTChanAll' tchan (val : acc)

serveSync :: String             -- ^ Port number or name; 514 is default
          -> (String -> IO ())  -- ^ Function to handle incoming messages
          -> (IO [String])      -- ^ Function to obtain outgoing messages
          -> IO ()
serveSync port handlerGet handlerSend = withSocketsDo $
  do -- Look up the port.  Either raises an exception or returns
     -- a nonempty list.
     addrinfos <- getAddrInfo
                  (Just (defaultHints {addrFlags = [AI_PASSIVE]}))
                  Nothing (Just port)
     let serveraddr = head addrinfos

     -- Create a socket
     sock <- socket (addrFamily serveraddr) Stream defaultProtocol

     -- Bind it to the address we're listening to
     bind sock (addrAddress serveraddr)

     -- Start listening for connection requests.  Maximum queue size
     -- of 5 connection requests waiting to be accepted.
     listen sock 5

     -- Create a lock to use for synchronizing access to the handler
     lock <- newMVar ()

     -- Loop forever waiting for connections.  Ctrl-C to abort.
     procRequests lock sock

  where
    -- | Process incoming connection requests
    procRequests :: MVar () -> Socket -> IO ()
    procRequests lock mastersock = forever $ void $ do
      (connsock, _clientaddr) <- accept mastersock
      -- handle lock clientaddr
      --    "syslogtcpserver.hs: client connnected"
      connhdl <- socketToHandle connsock ReadWriteMode
      putStrLn ("Socket connected.")
      hSetBuffering connhdl LineBuffering
      hPutStrLn connhdl "Hello 0"
      hFlush connhdl
      t1 <- forkIO $ procSend lock connhdl
      t2 <- forkIO $ procGet  lock connhdl
      -- hClose connhdl
      return ()

    -- | Process incoming messages
    procGet :: MVar () -> Handle -> IO ()
    procGet lock connhdl = do
      let processMessage = forever $ do
            message   <- hGetLine connhdl
            handleGet lock message
      catch processMessage (\(e :: IOException) -> putStrLn "Disconnected")

    -- | Process incoming messages
    procSend :: MVar () -> Handle -> IO ()
    procSend lock connhdl = do
      let processMessage = forever $ do
            responses  <- handleSend lock
            mapM_ (\msg -> hPutStrLn connhdl msg >> hFlush connhdl) responses
      catch processMessage (\(e :: IOException) -> putStrLn "Disconnected")

      -- handle lock clientaddr
      --    "syslogtcpserver.hs: client disconnected"

    -- Lock the handler before passing data to it.
    handleGet :: MVar () -> String -> IO ()
    handleGet lock msg =
      withMVarLock lock $ handlerGet msg

    -- Lock the handler before passing data to it.
    handleSend :: MVar () -> IO [String]
    handleSend lock =
      withMVarLock lock $ handlerSend

-- | Event communication channel that takes event messages from an MVar and
--   pushes them out a socket.
mkSendEvent :: TChan String -> IO ()
mkSendEvent channel = void $
  forkIO $ serveAsync "8082" $ \handle -> forever $ do
    response <- atomically $ readTChan channel
    putStrLn $ "Sending to the event log: " ++ show response
    hPutStrLn handle response
    hFlush handle

serveAsync :: String                          -- ^ Port number or name; 514 is default
           -> (Handle -> IO ())   -- ^ Function to handle incoming messages
           -> IO ()
serveAsync port handlerfunc = withSocketsDo $
  do -- Look up the port.  Either raises an exception or returns
     -- a nonempty list.
     addrinfos <- getAddrInfo
                  (Just (defaultHints {addrFlags = [AI_PASSIVE]}))
                  Nothing (Just port)
     let serveraddr = head addrinfos

     -- Create a socket
     sock <- socket (addrFamily serveraddr) Stream defaultProtocol

     -- Bind it to the address we're listening to
     bind sock (addrAddress serveraddr)

     -- Start listening for connection requests.  Maximum queue size
     -- of 5 connection requests waiting to be accepted.
     listen sock 5

     -- Create a lock to use for synchronizing access to the handler
     lock <- newMVar ()

     -- Loop forever waiting for connections.  Ctrl-C to abort.
     procRequests lock sock

  where
    -- | Process incoming connection requests
    procRequests :: MVar () -> Socket -> IO ()
    procRequests lock mastersock = forever $ void $ do
      (connsock, _clientaddr) <- accept mastersock
      -- handle lock clientaddr
      --    "syslogtcpserver.hs: client connnected"
      forkIO $ procMessages lock connsock

    -- | Process incoming messages
    procMessages :: MVar () -> Socket -> IO ()
    procMessages lock connsock = do
      connhdl <- socketToHandle connsock ReadWriteMode
      putStrLn ("Socket connected.")
      hSetBuffering connhdl LineBuffering
      hPutStrLn connhdl "DHello 0"
      hFlush connhdl
      handle lock connhdl
      hClose connhdl

    -- Lock the handler before passing data to it.
    handle :: MVar () -> Handle -> IO ()
    -- This type is the same as
    -- handle :: MVar () -> SockAddr -> String -> IO ()
    handle lock handle =
      withMVarLock lock (handlerfunc handle)

-- * Aux

-- | Put a message in an MVar.
putInMVar :: MVar [String] -> String -> IO ()
putInMVar mvar s = modifyMVar_ mvar (return . (++ [s]))

withMVarLock :: MVar a -> IO b -> IO b
withMVarLock lock io = do
  a <- takeMVar lock
  r <- io
  putMVar lock a
  return r

errPrintLn :: String -> IO ()
errPrintLn s = hPutStrLn stderr s >> hFlush stderr