{-# LANGUAGE DeriveDataTypeable #-}
{-# LANGUAGE UnicodeSyntax #-}

{-| This module contains a couple of utility functions for sending and receiving
    'Serialize'-able data over a handle. Because the size of the serialized
    value can depend on the value being sent, these functions employ a protocol
    in which first the size of the serialized data is sent as a 64-bit
    big-endian word, and then the serialized data itself is sent.
 -}
module LogicGrowsOnTrees.Utils.Handle
    (
    -- * Exceptions
      ConnectionLost(..)
    -- * Functions
    , filterEOFExceptions
    , receive
    , send
    ) where

import Prelude hiding (catch)

import Control.Exception (Exception,catch,throwIO)
import Control.Monad (unless)

import qualified Data.ByteString as BS
import Data.ByteString (hGet,hPut)
import Data.Serialize (Serialize,encode,decode,runGet,runPut,getWord64be,putWord64be)
import Data.Typeable (Typeable)

import System.IO (Handle,hFlush)
import System.IO.Error (isEOFError,ioeGetErrorType)

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

{-| This exception is thrown when the connection has been lost. -}
data ConnectionLost = ConnectionLost
  deriving (Show,Typeable)
instance Exception ConnectionLost

{-| Replaces EOF 'IOException's with the 'ConnectionLost' exception. -}
filterEOFExceptions = flip catch $
    \e  if isEOFError e || (show . ioeGetErrorType $ e) == "resource vanished"
        then throwIO ConnectionLost
        else throwIO e

{-| Receives a 'Serialize'-able value from a handle.

    Specifically, this function reads in a 64-bit big-endian word with the
    size of the raw data to be read, reads that much data in bytes into a
    'ByteString', and then deserializes the 'ByteString' to produce the
    resulting value.

    If the connection has been lost, it throws 'ConnectionLost'.
 -}
receive :: Serialize α  Handle  IO α
receive handle = filterEOFExceptions $ do
    size_bytes  hGet handle 8
    unless (BS.length size_bytes == 8) $ throwIO ConnectionLost
    let number_of_value_bytes =
            either error fromIntegral
            .
            runGet getWord64be
            $
            size_bytes
    value_bytes  hGet handle number_of_value_bytes
    unless (BS.length value_bytes == number_of_value_bytes) $ throwIO ConnectionLost
    return . either error id . decode $ value_bytes

{-| Sends a 'Serialize'-able value to a handle.

    Specifically, this function serializes the given value to a 'ByteString',
    and then writes the size of the serialized data in bytes as a 64-bit
    big-endian word followed by the raw data itself.

    If the connection has been lost, it throws 'ConnectionLost'.
 -}
send :: Serialize α  Handle  α  IO ()
send handle value = filterEOFExceptions $ do
    let encoded_value = encode value
    hPut handle . runPut . putWord64be . fromIntegral . BS.length $ encoded_value
    hPut handle encoded_value
    hFlush handle