{-# LANGUAGE DeriveDataTypeable, RankNTypes #-}
module IdeSession.RPC.API (
    -- * External exceptions
    ExternalException(..)
  , serverKilledException
    -- * Client-server communication
  , RpcConversation(..)
  , Request(..)
  , Response(..)
    -- * Lazy bytestring with incremental Binary instance
  , IncBS(..)
    -- * IO utils
  , hPutFlush
  , ignoreIOExceptions
  , openPipeForWriting
  , openPipeForReading
  ) where

import Prelude hiding (take)
import Control.Applicative ((<$>))
import Control.Concurrent (threadDelay)
import Data.Binary (Binary)
import Data.Typeable (Typeable)
import System.IO (Handle, hFlush, openFile, IOMode(..), hPutChar, hGetChar)
import qualified Control.Exception as Ex
import qualified Data.Binary as Binary
import qualified Data.ByteString as BSS
import qualified Data.ByteString.Lazy.Char8 as BSL
import qualified Data.ByteString.Lazy.Internal as BSL

--------------------------------------------------------------------------------
-- Exceptions thrown by the RPC server are retrown locally as                 --
-- 'ExternalException's                                                       --
--------------------------------------------------------------------------------

-- | Exceptions thrown by the remote server
data ExternalException = ExternalException {
     -- | The output from the server on stderr
     externalStdErr    :: String
     -- | The local exception that was thrown and alerted us to the problem
   , externalException :: Maybe Ex.IOException
   }
  deriving (Eq, Typeable)

instance Show ExternalException where
  show (ExternalException err Nothing) =
    "External exception: " ++ err
  show (ExternalException err (Just ex)) =
    "External exception: " ++ err ++ ". Local exception: " ++ show ex

instance Ex.Exception ExternalException

-- | Generic exception thrown if the server gets killed for unknown reason
serverKilledException :: Maybe Ex.IOException -> ExternalException
serverKilledException ex = ExternalException "Server killed" ex

{------------------------------------------------------------------------------
  Client-server communication
------------------------------------------------------------------------------}

data RpcConversation = RpcConversation {
    get :: forall a. (Typeable a, Binary a) => IO a
  , put :: forall a. (Typeable a, Binary a) => a -> IO ()
  }

data Request = Request IncBS | RequestShutdown
  deriving Show

newtype Response = Response IncBS

instance Binary Request where
  put (Request bs)         = Binary.putWord8 0 >> Binary.put bs
  put RequestShutdown      = Binary.putWord8 1

  get = do
    header <- Binary.getWord8
    case header of
      0 -> Request <$> Binary.get
      1 -> return RequestShutdown
      _ -> fail "Request.get: invalid header"

instance Binary Response where
  put (Response bs) = Binary.put bs
  get = Response <$> Binary.get

{------------------------------------------------------------------------------
  Lazy bytestring with an incremental Binary instance

  Note only does this avoid loading the entire ByteString into memory when
  serializing stuff, the standard Binary instance for Lazy bytestring is
  actually broken in 0.5 (http://hpaste.org/87401; fixed in 0.7, but even there
  still requires the length of the bytestring upfront).
------------------------------------------------------------------------------}

newtype IncBS = IncBS { unIncBS :: BSL.ByteString }

instance Binary IncBS where
  put (IncBS BSL.Empty)        = Binary.putWord8 0
  put (IncBS (BSL.Chunk b bs)) = do Binary.putWord8 1
                                    Binary.put b
                                    Binary.put (IncBS bs)

  get = go []
    where
      go :: [BSS.ByteString] -> Binary.Get IncBS
      go acc = do
        header <- Binary.getWord8
        case header of
          0 -> return . IncBS . BSL.fromChunks . reverse $ acc
          1 -> do b <- Binary.get ; go (b : acc)
          _ -> fail "IncBS.get: invalid header"

instance Show IncBS where
  show = show . unIncBS

{------------------------------------------------------------------------------
  Some IO utils
------------------------------------------------------------------------------}

-- | Write a bytestring to a buffer and flush
hPutFlush :: Handle -> BSL.ByteString -> IO ()
hPutFlush h bs = BSL.hPut h bs >> ignoreIOExceptions (hFlush h)

-- | Ignore IO exceptions
ignoreIOExceptions :: IO () -> IO ()
ignoreIOExceptions = Ex.handle ignore
  where
    ignore :: Ex.IOException -> IO ()
    ignore _ = return ()

-- | Open a pipe for writing
--
-- This is meant to be used together with 'openPipeForReading'
openPipeForWriting :: FilePath -> Int -> IO Handle
openPipeForWriting fp = go
  where
    go :: Int -> IO Handle
    go timeout = do
      -- We cannot open a pipe for writing without a corresponding reader
      mh <- Ex.try $ openFile fp WriteMode
      case mh of
        Left ex ->
          if timeout > delay
            then do threadDelay delay
                    go (timeout - delay)
            else Ex.throwIO (RPCPipeNotCreated ex)
        Right h -> do
          hPutChar h '!'
          hFlush h
          return h

    delay :: Int
    delay = 10000 -- 10 ms

data RPCPipeNotCreated = RPCPipeNotCreated Ex.IOException
    deriving Typeable
instance Ex.Exception RPCPipeNotCreated
instance Show RPCPipeNotCreated where
    show (RPCPipeNotCreated e) = "The bidirectional RPC pipe could not be opened. Exception was: " ++ show e

-- | Open a pipe for reading
--
-- This is meant to be used together with 'openPipeForWriting'
openPipeForReading :: FilePath -> Int -> IO Handle
openPipeForReading fp = \timeout -> do
    -- We _can_ open a pipe for reading without a corresponding writer
    h <- openFile fp ReadMode
    -- But if there is no corresponding writer, then trying to read from the
    -- pipe will report EOF. So we wait.
    go h timeout
    return h
  where
    go :: Handle -> Int -> IO ()
    go h timeout = do
      mc <- Ex.try $ hGetChar h
      case mc of
        Left ex ->
          if timeout > delay
            then do threadDelay delay
                    go h (timeout - delay)
            else Ex.throwIO (RPCPipeNotCreated ex)
        Right '!' ->
          return ()
        Right c ->
          Ex.throwIO (userError $ "openPipeForReading: Unexpected " ++ show c)

    delay :: Int
    delay = 10000 -- 10 ms