module Main where

import Test.Framework
import Test.Framework.Providers.HUnit
import Test.HUnit

import Control.DeepSeq
import Control.Monad.Catch
import Control.Monad.Fix 
import Control.Monad.IO.Class
import Data.Typeable 
import GHC.Generics (Generic)

import Control.Concurrent.MVar
import Control.Wire
import Control.Wire.Unsafe.Event
import Data.Either
import Game.GoreAndAsh.Async 
import Game.GoreAndAsh.Core
import Prelude hiding (id, (.))

import qualified Data.Sequence as S 

-- | Application monad is monad stack build from given list of modules over base monad (IO or Identity)
type AppStack = ModuleStack [AsyncT, AsyncT] IO
newtype AppState = AppState (ModuleState AppStack)
  deriving (Generic)

instance NFData AppState 

-- | Wrapper around type family
newtype AppMonad a = AppMonad (AppStack a)
  deriving (Functor, Applicative, Monad, MonadFix, MonadIO, MonadThrow, MonadCatch, MonadAsync)

instance GameModule AppMonad AppState where 
  type ModuleState AppMonad = AppState
  runModule (AppMonad m) (AppState s) = do 
    (a, s') <- runModule m s 
    return (a, AppState s')
  newModuleState = AppState <$> newModuleState
  withModule _ = withModule (Proxy :: Proxy AppStack)
  cleanupModule (AppState s) = cleanupModule s 

-- | Arrow that is build over the monad stack
type AppWire a b = GameWire AppMonad a b

data TestException = TestException deriving Show 

instance Exception TestException

main :: IO ()
main = withModule (Proxy :: Proxy AppMonad) $ do
  defaultMainWithOpts [
      testGroup "async actions" [
        testCase "simple" asyncSimple
      , testCase "except" asyncExcept
      , testCase "except no catch" asyncExcept'
      , testCase "cancel" asyncCancel
      , testCase "cancel delayed" asyncCancelDelayed
      , testCase "factory" asyncFactory
      , testCase "factory ex normal" asyncFactoryEx
      , testCase "factory ex except" asyncFactoryEx'
      ]
    , testGroup "async bound actions" [
        testCase "simple" asyncBoundSimple
      , testCase "except" asyncBoundExcept
      , testCase "except no catch" asyncBoundExcept'
      , testCase "cancel" asyncCancelBound
      , testCase "cancel delayed" asyncCancelBoundDelayed
      , testCase "factory" asyncFactoryBound
      , testCase "factory ex normal" asyncFactoryBoundEx
      , testCase "factory ex except" asyncFactoryBoundEx'
      ]
    , testGroup "async sync actions" [
        testCase "simple" asyncSyncSimple
      , testCase "except" asyncSyncExcept
      , testCase "except no catch" asyncSyncExcept'
      , testCase "cancel" asyncCancelSync
      , testCase "factory" asyncFactorySync
      , testCase "factory ex normal" asyncSyncFactoryEx
      , testCase "factory ex except" asyncSyncFactoryEx'
      ]
    ] mempty

-- | Runs wire n times and return it result
runWire :: Int -> AppWire () a -> IO (Maybe a)
runWire n w = do 
  gs <- newGameState w
  go gs n Nothing
  where 
  go gs i ma = if i <= 0 then return ma
    else do 
      (ma', gs') <- stepGame gs (return ())
      go gs' (i-1) ma'

asyncSimple :: Assertion
asyncSimple = do 
  ma <- runWire 100 w 
  assertEqual "wire switch" (Just True) ma
  where
    w = proc _ -> do 
      e <- asyncAction (return True) -< ()
      rSwitch (pure False) -< ((), pure <$> e)

asyncExcept :: Assertion
asyncExcept = do 
  ma <- runWire 100 w 
  assertBool "returned is exception" (check ma) 
  where 
    check ma = case ma of 
      Nothing -> False 
      Just ea -> isLeft ea 

    w :: AppWire () (Either SomeException Bool)
    w = proc _ -> do 
      e <- asyncActionEx (throwM TestException) -< ()
      rSwitch (pure $ Right False) -< ((), pure <$> e)

asyncCancel :: Assertion
asyncCancel = do 
  ma <- runWire 100 w 
  assertEqual "wire switch" (Just False) ma
  where
    w = proc _ -> do 
      ce <- now -< ()
      e <- asyncActionC (return True) -< ce
      rSwitch (pure False) -< ((), pure <$> e)

asyncCancelDelayed :: Assertion
asyncCancelDelayed = do 
  ma <- runWire 100 w0 
  assertEqual "wire switch" (Just False) ma
  where
    w0 = switch $ proc _ -> do 
      evar <- liftGameMonadEvent1 (const $ liftIO newEmptyMVar) . now -< ()
      returnA -< (False, w <$> evar)

    w var = proc _ -> do 
      ce <- delay NoEvent . now -< ()
      e <- asyncActionC (io var) -< ce
      liftGameMonadEvent1 (const . liftIO $ putMVar var ()) -< ce
      rSwitch (pure False) -< ((), pure <$> e)

    io var = do 
      _ <- readMVar var
      return True

asyncFactory :: Assertion 
asyncFactory = do 
  ma <- runWire 100 w
  assertEqual "summed values" (Just 20) ma
  where
  w = proc _ -> do 
    eio <- wgen -< ()
    eas <- asyncActionFactory -< eio
    hold . accumE (\i as -> i + sum as) 0 -< eas 

  wgen :: AppWire () (Event (S.Seq (IO Int))) 
  wgen = go 2 10
    where
    go k 0 = never
    go k n = mkSFN $ \_ -> (Event $ S.replicate k (return 1), go k (n-1))

seqRights :: S.Seq (Either a b) -> S.Seq b
seqRights = fmap fromRight . S.filter isRight
  where 
  fromRight (Left _) = error "seqRights"
  fromRight (Right a) = a 

asyncFactoryEx :: Assertion 
asyncFactoryEx = do 
  ma <- runWire 100 w
  assertEqual "summed values" (Just 20) ma
  where
  w = proc _ -> do 
    eio <- wgen -< ()
    eas <- asyncActionFactoryEx -< eio
    hold . accumE foldResult 0 -< eas 

  foldResult :: Int -> S.Seq (Either SomeException Int) -> Int
  foldResult i eas = i + sum (seqRights eas)

  wgen :: AppWire () (Event (S.Seq (IO Int))) 
  wgen = go 2 10
    where
    go k 0 = never
    go k n = mkSFN $ \_ -> (Event $ S.replicate k (return 1), go k (n-1))

asyncFactoryEx' :: Assertion 
asyncFactoryEx' = do 
  ma <- runWire 100 w
  assertEqual "summed values" (Just 0) ma
  where
  w = proc _ -> do 
    eio <- wgen -< ()
    eas <- asyncActionFactoryEx -< eio
    hold . accumE foldResult 0 -< eas 

  foldResult :: Int -> S.Seq (Either SomeException Int) -> Int
  foldResult i eas = i + sum (seqRights eas)

  wgen :: AppWire () (Event (S.Seq (IO Int))) 
  wgen = go 2 10
    where
    go k 0 = never
    go k n = mkSFN $ \_ -> (Event $ S.replicate k (throwM TestException), go k (n-1))

asyncExcept' :: Assertion
asyncExcept' = do 
  ma <- (fmap Right $ runWire 100 w) `catchAll` (return . Left)
  assertBool "returned is exception" (isLeft ma) 
  where 
    w :: AppWire () Bool
    w = proc _ -> do 
      e <- asyncAction (throwM TestException) -< ()
      rSwitch (pure False) -< ((), pure <$> e)

asyncBoundSimple :: Assertion
asyncBoundSimple = do 
  ma <- runWire 100 w 
  assertEqual "wire switch" (Just True) ma
  where
    w = proc _ -> do 
      e <- asyncActionBound (return True) -< ()
      rSwitch (pure False) -< ((), pure <$> e)

asyncBoundExcept :: Assertion
asyncBoundExcept = do 
  ma <- runWire 100 w 
  assertBool "returned is exception" (check ma) 
  where 
    check ma = case ma of 
      Nothing -> False 
      Just ea -> isLeft ea 

    w :: AppWire () (Either SomeException Bool)
    w = proc _ -> do 
      e <- asyncActionBoundEx (throwM TestException) -< ()
      rSwitch (pure $ Right False) -< ((), pure <$> e)

asyncBoundExcept' :: Assertion
asyncBoundExcept' = do 
  ma <- (fmap Right $ runWire 100 w) `catchAll` (return . Left)
  assertBool "returned is exception" (isLeft ma) 
  where 
    w :: AppWire () Bool
    w = proc _ -> do 
      e <- asyncActionBound (throwM TestException) -< ()
      rSwitch (pure False) -< ((), pure <$> e)

asyncCancelBound :: Assertion
asyncCancelBound = do 
  ma <- runWire 100 w 
  assertEqual "wire switch" (Just False) ma
  where
    w = proc _ -> do 
      ce <- now -< ()
      e <- asyncActionBoundC (return True) -< ce
      rSwitch (pure False) -< ((), pure <$> e)

asyncCancelBoundDelayed :: Assertion
asyncCancelBoundDelayed = do 
  ma <- runWire 100 w0 
  assertEqual "wire switch" (Just False) ma
  where
    w0 = switch $ proc _ -> do 
      evar <- liftGameMonadEvent1 (const $ liftIO newEmptyMVar) . now -< ()
      returnA -< (False, w <$> evar)

    w var = proc _ -> do 
      ce <- delay NoEvent . now -< ()
      e <- asyncActionBoundC (io var) -< ce
      liftGameMonadEvent1 (const . liftIO $ putMVar var ()) -< ce
      rSwitch (pure False) -< ((), pure <$> e)

    io var = do 
      _ <- readMVar var
      return True

asyncFactoryBound :: Assertion 
asyncFactoryBound = do 
  ma <- runWire 100 w
  assertEqual "summed values" (Just 20) ma
  where
  w = proc _ -> do 
    eio <- wgen -< ()
    eas <- asyncActionBoundFactory -< eio
    hold . accumE (\i as -> i + sum as) 0 -< eas 

  wgen :: AppWire () (Event (S.Seq (IO Int))) 
  wgen = go 2 10
    where
    go k 0 = never
    go k n = mkSFN $ \_ -> (Event $ S.replicate k (return 1), go k (n-1))

asyncFactoryBoundEx :: Assertion 
asyncFactoryBoundEx = do 
  ma <- runWire 100 w
  assertEqual "summed values" (Just 20) ma
  where
  w = proc _ -> do 
    eio <- wgen -< ()
    eas <- asyncActionBoundFactoryEx -< eio
    hold . accumE foldResult 0 -< eas 

  foldResult :: Int -> S.Seq (Either SomeException Int) -> Int
  foldResult i eas = i + sum (seqRights eas)

  wgen :: AppWire () (Event (S.Seq (IO Int))) 
  wgen = go 2 10
    where
    go k 0 = never
    go k n = mkSFN $ \_ -> (Event $ S.replicate k (return 1), go k (n-1))

asyncFactoryBoundEx' :: Assertion 
asyncFactoryBoundEx' = do 
  ma <- runWire 100 w
  assertEqual "summed values" (Just 0) ma
  where
  w = proc _ -> do 
    eio <- wgen -< ()
    eas <- asyncActionBoundFactoryEx -< eio
    hold . accumE foldResult 0 -< eas 

  foldResult :: Int -> S.Seq (Either SomeException Int) -> Int
  foldResult i eas = i + sum (seqRights eas)

  wgen :: AppWire () (Event (S.Seq (IO Int))) 
  wgen = go 2 10
    where
    go k 0 = never
    go k n = mkSFN $ \_ -> (Event $ S.replicate k (throwM TestException), go k (n-1))

asyncSyncSimple :: Assertion
asyncSyncSimple = do 
  ma <- runWire 100 w 
  assertEqual "wire switch" (Just True) ma
  where
    w = proc _ -> do 
      e <- asyncSyncAction (return True) -< ()
      rSwitch (pure False) -< ((), pure <$> e)

asyncSyncExcept :: Assertion
asyncSyncExcept = do 
  ma <- runWire 100 w 
  assertBool "returned is exception" (check ma) 
  where 
    check ma = case ma of 
      Nothing -> False 
      Just ea -> isLeft ea 

    w :: AppWire () (Either SomeException Bool)
    w = proc _ -> do 
      e <- asyncSyncActionEx (throwM TestException) -< ()
      rSwitch (pure $ Right False) -< ((), pure <$> e)

asyncSyncExcept' :: Assertion
asyncSyncExcept' = do 
  ma <- (fmap Right $ runWire 100 w) `catchAll` (return . Left)
  assertBool "returned is exception" (isLeft ma) 
  where 
    w :: AppWire () Bool
    w = proc _ -> do 
      e <- asyncSyncAction (throwM TestException) -< ()
      rSwitch (pure False) -< ((), pure <$> e)

asyncCancelSync :: Assertion
asyncCancelSync = do 
  ma <- runWire 100 w 
  assertEqual "wire switch" (Just False) ma
  where
    w = proc _ -> do 
      ce <- now -< ()
      e <- asyncSyncActionC (return True) -< ce
      rSwitch (pure False) -< ((), pure <$> e)

asyncFactorySync :: Assertion 
asyncFactorySync = do 
  ma <- runWire 100 w
  assertEqual "summed values" (Just 20) ma
  where
  w = proc _ -> do 
    eio <- wgen -< ()
    eas <- asyncSyncActionFactory -< eio
    hold . accumE (\i as -> i + sum as) 0 -< eas 

  wgen :: AppWire () (Event (S.Seq (IO Int))) 
  wgen = go 2 10
    where
    go k 0 = never
    go k n = mkSFN $ \_ -> (Event $ S.replicate k (return 1), go k (n-1))

asyncSyncFactoryEx :: Assertion 
asyncSyncFactoryEx = do 
  ma <- runWire 100 w
  assertEqual "summed values" (Just 20) ma
  where
  w = proc _ -> do 
    eio <- wgen -< ()
    eas <- asyncSyncActionFactoryEx -< eio
    hold . accumE foldResult 0 -< eas 

  foldResult :: Int -> S.Seq (Either SomeException Int) -> Int
  foldResult i eas = i + sum (seqRights eas)

  wgen :: AppWire () (Event (S.Seq (IO Int))) 
  wgen = go 2 10
    where
    go k 0 = never
    go k n = mkSFN $ \_ -> (Event $ S.replicate k (return 1), go k (n-1))

asyncSyncFactoryEx' :: Assertion 
asyncSyncFactoryEx' = do 
  ma <- runWire 100 w
  assertEqual "summed values" (Just 0) ma
  where
  w = proc _ -> do 
    eio <- wgen -< ()
    eas <- asyncSyncActionFactoryEx -< eio
    hold . accumE foldResult 0 -< eas 

  foldResult :: Int -> S.Seq (Either SomeException Int) -> Int
  foldResult i eas = i + sum (seqRights eas)

  wgen :: AppWire () (Event (S.Seq (IO Int))) 
  wgen = go 2 10
    where
    go k 0 = never
    go k n = mkSFN $ \_ -> (Event $ S.replicate k (throwM TestException), go k (n-1))