{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE DeriveDataTypeable #-}

module Hans.Socket.Types where

import Hans.Addr (isWildcardAddr)
import Hans.Device.Types (Device)
import Hans.Network (Network(..),RouteInfo(..))
import Hans.Types (HasNetworkStack,NetworkStack)

import           Control.Exception (Exception,throwIO)
import qualified Data.ByteString.Lazy as L
import           Data.Typeable (Typeable)
import           Data.Word (Word16)


-- Socket Addresses ------------------------------------------------------------

type SockPort = Word16


-- Generic Socket Operations ---------------------------------------------------

data SocketConfig = SocketConfig { scRecvBufferSize :: !Int
                                   -- ^ Bytes to buffer
                                 } deriving (Show)

defaultSocketConfig :: SocketConfig
defaultSocketConfig  = SocketConfig { scRecvBufferSize = 4096 }

class Socket sock where

  -- | Close an open socket.
  sClose :: Network addr => sock addr -> IO ()


class (DataSocket (Client sock), Socket sock) => ListenSocket sock where

  type Client sock :: * -> *

  -- | Create a listening socket, with a backlog of n.
  sListen :: (HasNetworkStack ns, Network addr)
          => ns -> SocketConfig -> addr -> SockPort -> Int -> IO (sock addr)

  sAccept :: Network addr => sock addr -> IO (Client sock addr)


class Socket sock => DataSocket sock where

  -- | Connect this socket to one on a remote machine.
  sConnect :: (HasNetworkStack ns, Network addr)
           => ns
           -> SocketConfig
           -> Maybe Device
           -> addr           -- ^ Local address
           -> Maybe SockPort -- ^ Local port
           -> addr           -- ^ Remote host
           -> SockPort       -- ^ Remote port
           -> IO (sock addr)

  -- | Returns True iff there is currently space in the buffer to accept a
  -- write. Note, this is probably a bad thing to count on in a concurrent
  -- system ...
  sCanWrite :: Network addr => sock addr -> IO Bool

  -- | Send a chunk of data on a socket.
  sWrite :: Network addr => sock addr -> L.ByteString -> IO Int

  -- | Returns True iff there is data in the buffer that can be read.
  -- Note, this is probably a bad thing to count on in a concurrent
  -- system ...
  sCanRead :: Network addr => sock addr -> IO Bool

  -- | Read a chunk of data from a socket. Reading an empty result indicates
  -- that the socket has closed.
  sRead :: Network addr => sock addr -> Int -> IO L.ByteString

  -- | Non-blocking read from a socket. Reading an empty result means that the
  -- socket has closed, while reading a 'Nothing' result indicates that there
  -- was no data available.
  sTryRead :: Network addr => sock addr -> Int -> IO (Maybe L.ByteString)


-- Exceptions ------------------------------------------------------------------

data ConnectionException = AlreadyConnected
                           -- ^ This connection already exists.

                         | NoConnection
                           -- ^ No information about the other end of the
                           -- socket was present.

                         | NoPortAvailable
                           -- ^ All ports are in use.

                         | ConnectionRefused

                         | ConnectionClosing

                         | DoesNotExist
                           -- ^ The connection is already closed.
                           deriving (Show,Typeable)

data ListenException = AlreadyListening
                       -- ^ Something is already listening on this
                       -- host/port combination.
                       deriving (Show,Typeable)

data RoutingException = NoRouteToHost
                        -- ^ It's not possible to reach this host from this
                        -- source address.
                        deriving (Show,Typeable)

instance Exception ConnectionException
instance Exception ListenException
instance Exception RoutingException


-- Utilities -------------------------------------------------------------------

-- | Raise an exception when no route can be found to the destination.
route :: Network addr
      => NetworkStack -> Maybe Device -> addr -> addr -> IO (RouteInfo addr)

route ns mbDev src dst =
  do mbRoute <- route' ns mbDev src dst
     case mbRoute of
       Just ri -> return ri
       Nothing -> throwIO NoRouteToHost


-- | Return source routing information, when a route exists to the destination.
route' :: Network addr
       => NetworkStack -> Maybe Device -> addr -> addr
       -> IO (Maybe (RouteInfo addr))

route' ns mbDev src dst =
  do mbRoute <- lookupRoute ns dst
     case mbRoute of
       Just ri | maybe True (riDev ri ==) mbDev
                 && (src == riSource ri || isWildcardAddr src) ->
                 return (Just ri)

       _ -> return Nothing