{-# LINE 1 "src/System/Hardware/MercuryApi/Testing.hsc" #-}
{-# LANGUAGE OverloadedStrings #-}
{-# OPTIONS_HADDOCK hide #-}
{-|
Module      : System.Hardware.MercuryApi.Testing
Description : A serial transport for MercuryApi which replays data from a file
Copyright   : © Patrick Pelletier, 2017
License     : MIT
Maintainer  : code@funwithsoftware.org

This module is not meant to be used by the end user.  It exists for the
automated tests.  It provides a handler for URIs of the form
test:///path/to/file, where the file is in the format produced by
'opcodeListener'.  When Mercury API reads from the transport, the data
from a "Received:" line is returned.  When Mercury API writes to the
transport, the data is compared to the data in the "Sending:" line.
If the data does not match, an error message is printed, and
TMR_ERROR_TIMEOUT is returned.  (Timeout was chosen because it's the only
error that seems to be consistently propagated from the transport by
Mercury API.)
-}

module System.Hardware.MercuryApi.Testing
  ( registerTransportInit
  ) where

import Control.Applicative ( Applicative((<*>)), (<$>) )
import Control.Concurrent ( threadDelay )
import Control.Exception ( IOException, try )
import Control.Monad ( when )
import qualified Data.ByteString as B
import qualified Data.ByteString.Char8 as B8 ( unpack )
import Data.Char ( isDigit )
import Data.IORef ( IORef, writeIORef, readIORef, newIORef )
import Data.Monoid ( (<>) )
import qualified Data.Text as T
import qualified Data.Text.Encoding as T ( encodeUtf8 )
import qualified Data.Text.IO as T ( readFile, putStrLn )
import Data.Word ( Word8, Word32 )
import Foreign
    ( newStablePtr,
      Ptr,
      FunPtr,
      nullPtr,
      Storable(alignment, peek, peekByteOff, poke, pokeByteOff, sizeOf),
      freeStablePtr,
      deRefStablePtr,
      castStablePtrToPtr,
      castPtrToStablePtr,
      nullFunPtr,
      castPtr,
      copyArray )
import Foreign.C ( CString, withCAString, peekCAString )
import System.Info ( os )
import qualified System.IO.Unsafe as U ( unsafePerformIO )

import System.Hardware.MercuryApi hiding (read)




type RawStatus = Word32

successStatus :: RawStatus
successStatus = 0
{-# LINE 65 "src/System/Hardware/MercuryApi/Testing.hsc" #-}

failureStatus :: RawStatus
failureStatus = 16777217
{-# LINE 68 "src/System/Hardware/MercuryApi/Testing.hsc" #-}

data SerialState =
  SerialState
  { ssFilename :: String
  , ssNext :: IORef [T.Text]
  , ssLeftover :: IORef B.ByteString
  , ssSendTime :: IORef Double
  }

data SerialTransport =
  SerialTransport
  { stCookie :: Ptr ()
  , stOpen :: FunPtr (Ptr SerialTransport -> IO RawStatus)
  , stSendBytes :: FunPtr (Ptr SerialTransport -> Word32 -> Ptr Word8 -> Word32 -> IO RawStatus)
  , stReceiveBytes :: FunPtr (Ptr SerialTransport -> Word32 -> Ptr Word32 -> Ptr Word8 -> Word32 -> IO RawStatus)
  , stSetBaudRate :: FunPtr (Ptr SerialTransport -> Word32 -> IO RawStatus)
  , stShutdown :: FunPtr (Ptr SerialTransport -> IO RawStatus)
  , stFlush :: FunPtr (Ptr SerialTransport -> IO RawStatus)
  }

instance Storable SerialTransport where
  sizeOf _ = (56)
{-# LINE 90 "src/System/Hardware/MercuryApi/Testing.hsc" #-}
  alignment _ = 8

  peek p =
    SerialTransport
    <$> (\hsc_ptr -> peekByteOff hsc_ptr 0)       p
{-# LINE 95 "src/System/Hardware/MercuryApi/Testing.hsc" #-}
    <*> (\hsc_ptr -> peekByteOff hsc_ptr 8)         p
{-# LINE 96 "src/System/Hardware/MercuryApi/Testing.hsc" #-}
    <*> (\hsc_ptr -> peekByteOff hsc_ptr 16)    p
{-# LINE 97 "src/System/Hardware/MercuryApi/Testing.hsc" #-}
    <*> (\hsc_ptr -> peekByteOff hsc_ptr 24) p
{-# LINE 98 "src/System/Hardware/MercuryApi/Testing.hsc" #-}
    <*> (\hsc_ptr -> peekByteOff hsc_ptr 32)  p
{-# LINE 99 "src/System/Hardware/MercuryApi/Testing.hsc" #-}
    <*> (\hsc_ptr -> peekByteOff hsc_ptr 40)     p
{-# LINE 100 "src/System/Hardware/MercuryApi/Testing.hsc" #-}
    <*> (\hsc_ptr -> peekByteOff hsc_ptr 48)        p
{-# LINE 101 "src/System/Hardware/MercuryApi/Testing.hsc" #-}

  poke p x = do
    (\hsc_ptr -> pokeByteOff hsc_ptr 0)       p (stCookie x)
{-# LINE 104 "src/System/Hardware/MercuryApi/Testing.hsc" #-}
    (\hsc_ptr -> pokeByteOff hsc_ptr 8)         p (stOpen x)
{-# LINE 105 "src/System/Hardware/MercuryApi/Testing.hsc" #-}
    (\hsc_ptr -> pokeByteOff hsc_ptr 16)    p (stSendBytes x)
{-# LINE 106 "src/System/Hardware/MercuryApi/Testing.hsc" #-}
    (\hsc_ptr -> pokeByteOff hsc_ptr 24) p (stReceiveBytes x)
{-# LINE 107 "src/System/Hardware/MercuryApi/Testing.hsc" #-}
    (\hsc_ptr -> pokeByteOff hsc_ptr 32)  p (stSetBaudRate x)
{-# LINE 108 "src/System/Hardware/MercuryApi/Testing.hsc" #-}
    (\hsc_ptr -> pokeByteOff hsc_ptr 40)     p (stShutdown x)
{-# LINE 109 "src/System/Hardware/MercuryApi/Testing.hsc" #-}
    (\hsc_ptr -> pokeByteOff hsc_ptr 48)        p (stFlush x)
{-# LINE 110 "src/System/Hardware/MercuryApi/Testing.hsc" #-}

foreign import ccall "tm_reader.h TMR_setSerialTransport"
    c_TMR_setSerialTransport :: CString
                             -> FunPtr (Ptr SerialTransport -> Ptr () -> CString -> IO RawStatus)
                             -> IO RawStatus

foreign import ccall "wrapper"
    wrapOneArg :: (Ptr SerialTransport -> IO RawStatus)
               -> IO (FunPtr (Ptr SerialTransport -> IO RawStatus))

foreign import ccall "wrapper"
    wrapSendBytes :: (Ptr SerialTransport -> Word32 -> Ptr Word8 -> Word32 -> IO RawStatus)
                  -> IO (FunPtr (Ptr SerialTransport -> Word32 -> Ptr Word8 -> Word32 -> IO RawStatus))

foreign import ccall "wrapper"
    wrapReceiveBytes :: (Ptr SerialTransport -> Word32 -> Ptr Word32 -> Ptr Word8 -> Word32 -> IO RawStatus)
                     -> IO (FunPtr (Ptr SerialTransport -> Word32 -> Ptr Word32 -> Ptr Word8 -> Word32 -> IO RawStatus))

foreign import ccall "wrapper"
    wrapInit :: (Ptr SerialTransport -> Ptr () -> CString -> IO RawStatus)
             -> IO (FunPtr (Ptr SerialTransport -> Ptr () -> CString -> IO RawStatus))

funOpen :: FunPtr (Ptr SerialTransport -> IO RawStatus)
{-# NOINLINE funOpen #-}
funOpen = U.unsafePerformIO $ wrapOneArg testOpen

funSendBytes :: FunPtr (Ptr SerialTransport -> Word32 -> Ptr Word8 -> Word32 -> IO RawStatus)
{-# NOINLINE funSendBytes #-}
funSendBytes = U.unsafePerformIO $ wrapSendBytes testSendBytes

funReceiveBytes :: FunPtr (Ptr SerialTransport -> Word32 -> Ptr Word32 -> Ptr Word8 -> Word32 -> IO RawStatus)
{-# NOINLINE funReceiveBytes #-}
funReceiveBytes = U.unsafePerformIO $ wrapReceiveBytes testReceiveBytes

funShutdown :: FunPtr (Ptr SerialTransport -> IO RawStatus)
{-# NOINLINE funShutdown #-}
funShutdown = U.unsafePerformIO $ wrapOneArg testShutdown

funFlush :: FunPtr (Ptr SerialTransport -> IO RawStatus)
{-# NOINLINE funFlush #-}
funFlush = U.unsafePerformIO $ wrapOneArg testFlush

funTransportInit :: FunPtr (Ptr SerialTransport -> Ptr () -> CString -> IO RawStatus)
{-# NOINLINE funTransportInit #-}
funTransportInit = U.unsafePerformIO $ wrapInit testTransportInit

mkSerialTransport :: Ptr () -> SerialTransport
mkSerialTransport cookie =
  SerialTransport
  { stCookie = cookie
  , stOpen = funOpen
  , stSendBytes = funSendBytes
  , stReceiveBytes = funReceiveBytes
  , stSetBaudRate = nullFunPtr
  , stShutdown = funShutdown
  , stFlush = funFlush
  }

getState :: Ptr SerialTransport -> IO SerialState
getState p = do
  st <- peek p
  let stable = castPtrToStablePtr $ stCookie st
  deRefStablePtr stable

printIOException :: IOException -> IO ()
printIOException = print

testOpen :: Ptr SerialTransport -> IO RawStatus
testOpen p = do
  ss <- getState p
  eth <- try $ T.readFile (ssFilename ss)
  case eth of
    Left exc -> do
      printIOException exc
      return failureStatus
    Right txt -> do
      writeIORef (ssNext ss) (T.lines txt)
      return successStatus

parseTransportLine :: T.Text -> (Maybe TransportDirection, B.ByteString)
parseTransportLine txt =
  let txt' = T.takeWhile (/= '|') txt
      f d = if d == "Sending" then Tx else Rx
      (dir, mbs) = case T.splitOn ":" txt' of
                     [x] -> (Nothing, hexToBytes $ T.filter (/= ' ') x)
                     [d, x] -> (Just (f d),
                                Just $ T.encodeUtf8 $ T.dropWhile (not . isDigit) x)
                     _ -> (Nothing, Just "")
  in case mbs of
       Nothing -> (dir, "barf!")
       Just bs -> (dir, bs)

parseTransport :: [T.Text]
               -> Maybe (TransportDirection, B.ByteString, Double, [T.Text])
parseTransport [] = Nothing
parseTransport (t:ts) =
  let (Just dir, bs) = parseTransportLine t
      rest = takeWhile ((== Nothing) . fst) $ map parseTransportLine ts
      leftover = drop (length rest) ts
      bss = map snd rest
      tm = read $ B8.unpack bs
  in Just (dir, B.concat bss, tm, leftover)

takeNext :: SerialState -> IO (Maybe (TransportDirection, B.ByteString, Double))
takeNext ss = do
  let ref = ssNext ss
  ts <- readIORef ref
  let result = parseTransport ts
  case result of
    Nothing -> return Nothing
    Just (dir, bs, tm, ts') -> do
      writeIORef ref ts'
      return $ Just (dir, bs, tm)

testSendBytes :: Ptr SerialTransport
              -> Word32
              -> Ptr Word8
              -> Word32
              -> IO RawStatus
testSendBytes p len msg _ = do
  ss <- getState p
  nxt <- takeNext ss
  case nxt of
    Just (Tx, expected, tm) -> do
      writeIORef (ssSendTime ss) tm
      actual <- B.packCStringLen (castPtr msg, fromIntegral len)
      if actual == expected
        then return successStatus
        else do
        T.putStrLn ("expected <" <> bytesToHex expected <>
                    ">, but got <" <> bytesToHex actual <> ">")
        return failureStatus
    x -> do
      putStrLn $ "expected Tx, but got " ++ show x
      return failureStatus

computeDelay :: Double -> Double -> Int
computeDelay oldTime newTime =
  -- convert seconds to microseconds, and add a fudge factor of 10%
  ceiling $ (newTime - oldTime) * 1.1e6

getNextBytes :: SerialState -> IO (Either RawStatus B.ByteString)
getNextBytes ss = do
  leftover <- readIORef (ssLeftover ss)
  if B.null leftover
    then do
    nxt <- takeNext ss
    case nxt of
      Just (Rx, bs, tm) -> do
        sendTime <- readIORef (ssSendTime ss)
        threadDelay $ computeDelay sendTime tm
        return $ Right bs
      x -> do
        putStrLn $ "expected Rx, but got " ++ show x
        return $ Left failureStatus
    else return $ Right leftover

testReceiveBytes :: Ptr SerialTransport
                 -> Word32
                 -> Ptr Word32
                 -> Ptr Word8
                 -> Word32
                 -> IO RawStatus
testReceiveBytes p len returnLen msg _ = do
  ss <- getState p
  eth <- getNextBytes ss
  case eth of
    Left status -> return status
    Right bs -> do
      let (bs1, bs2) = B.splitAt (fromIntegral len) bs
      B.useAsCStringLen bs1 $ \(pChar, bsLen) -> do
        poke returnLen (fromIntegral len)
        copyArray msg (castPtr pChar) bsLen
      writeIORef (ssLeftover ss) bs2
      return successStatus

testFlush :: Ptr SerialTransport -> IO RawStatus
testFlush _ = return successStatus

testShutdown :: Ptr SerialTransport -> IO RawStatus
testShutdown p = do
  st <- peek p
  let stable = castPtrToStablePtr $ stCookie st
  freeStablePtr stable
  poke p st { stCookie = nullPtr }
  return successStatus

-- Somehow in Windows, we end up with an absolute path that
-- starts with "/C:", which doesn't work, so we need to strip
-- the leading slash.
hackPath :: String -> String -> String
hackPath "mingw32" = dropWhile (== '/')
hackPath _ = id

testTransportInit :: Ptr SerialTransport -> Ptr () -> CString -> IO RawStatus
testTransportInit p _ cstr = do
  fname <- hackPath os <$> peekCAString cstr
  ref <- newIORef []
  ref2 <- newIORef ""
  ref3 <- newIORef 0
  let ss = SerialState fname ref ref2 ref3
  stable <- newStablePtr ss
  let st = mkSerialTransport $ castStablePtrToPtr stable
  poke p st
  return successStatus

registerTransportInit :: IO ()
registerTransportInit = do
  withCAString "test" $ \name -> do
    status <- c_TMR_setSerialTransport name funTransportInit
    when (status /= successStatus) $ fail "TMR_setSerialTransport failed"