{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE DeriveAnyClass #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE TypeSynonymInstances #-}
{-# LANGUAGE MultiParamTypeClasses #-}

{-|
Module      : FSM2FSM
Description : An example that shows how two FSMs can exchange messages with each other.
Copyright   : (c) Max Amanshauser, 2016
License     : MIT
Maintainer  : max@lambdalifting.org
-}

module FSM2FSM (runFSM2FSMTests) where

import Control.Concurrent.QSem
import Data.Aeson
import Data.Text
import Data.Typeable
import Data.UUID
import Data.UUID.V4
import GHC.Generics
import Test.Tasty
import Test.Tasty.HUnit

import Mealstrom
import Mealstrom.FSMStore
import Mealstrom.PostgresJSONStore as PGJSON
import Mealstrom.MemoryStore       as MemStore

-- #################
-- # Payment Example
-- #################

-- #######
-- # FSM 1
-- #######
type PaymentKey   = UUID
data PaymentState = PaymentPending Int | PaymentPaid | PaymentAborted
    deriving (Eq,Show,Typeable,Generic,ToJSON,FromJSON)

-- Yes, if you abort payment after it has been partially paid, you lose money :-)
data PaymentEvent = ReceivedPayment UUID Int | AbortPayment
    deriving (Eq,Show,Typeable,Generic,ToJSON,FromJSON)

-- Credit our own bank account with sweet funds
data PaymentAction = PaymentUpdateAccount UUID Int
    deriving (Eq,Show,Typeable,Generic,ToJSON,FromJSON)

paymentTransition :: (PaymentState, PaymentEvent) -> (PaymentState,[PaymentAction])
paymentTransition (s,e) = case (s,e) of
        (PaymentPending _o, AbortPayment)         -> (PaymentAborted,[])
        (PaymentPending o,  ReceivedPayment ba i) -> if i >= o
                                                     then (PaymentPaid, [PaymentUpdateAccount ba i])
                                                     else (PaymentPending (o-i),[])
        (PaymentAborted,   _)                     -> (PaymentAborted, [])

paymentEffects :: (FSMStore st BankAccountKey BankAccountState BankAccountEvent BankAccountAction)
               =>  QSem
               ->  FSMHandle st wal BankAccountKey BankAccountState BankAccountEvent BankAccountAction
               ->  Msg PaymentAction
               ->  IO Bool
paymentEffects qsem h (Msg d (PaymentUpdateAccount acc amount)) = do

    -- send message to bankaccount FSM using the same msgId!
    upsert h acc (BankAccountBalance 0) [Msg d (BankAccountDeposit amount)]
    signalQSem qsem
    return True

instance MealyInstance PaymentKey PaymentState PaymentEvent PaymentAction

-- #######
-- # FSM 2
-- #######
type BankAccountKey   = UUID
data BankAccountState = BankAccountBalance Int
    deriving (Eq,Show,Typeable,Generic,ToJSON,FromJSON)

data BankAccountEvent = BankAccountDeposit Int
    deriving (Eq,Show,Typeable,Generic,ToJSON,FromJSON)

-- NOP
data BankAccountAction = BankAccountDummyAction
    deriving (Eq,Show,Typeable,Generic)

instance ToJSON BankAccountAction where
    toJSON _ = "BankAccountDummyAction"

instance FromJSON BankAccountAction where
    parseJSON "BankAccountDummyAction" = return BankAccountDummyAction

bankAccountTransition :: (BankAccountState, BankAccountEvent) -> (BankAccountState,[BankAccountAction])
bankAccountTransition =
    \case (BankAccountBalance i, BankAccountDeposit j) -> (BankAccountBalance $ i + j, [BankAccountDummyAction])

bankAccountEffects :: QSem -> Msg BankAccountAction -> IO Bool
bankAccountEffects qsem _ = signalQSem qsem >> return True

instance MealyInstance BankAccountKey BankAccountState BankAccountEvent BankAccountAction

-- #######
-- # TEST
-- #######
runFSM2FSMTests :: String -> TestTree
runFSM2FSMTests c =
    testGroup "FSM2FSM" [
        testCase "FSM2FSMPG" (runTest (PGJSON.mkStore c)(PGJSON.mkStore c)),
        testCase "FSM2FSMMem" (runTest (MemStore.mkStore :: Text -> IO(MemoryStore BankAccountKey BankAccountState BankAccountEvent BankAccountAction))
                                       (MemStore.mkStore :: Text -> IO(MemoryStore PaymentKey     PaymentState     PaymentEvent     PaymentAction)))
    ]
  where
    runTest c1 c2 = do
        sync          <- newQSem 0

        st1           <- c1 "FSM2FSMTestBank"

        let t1         = FSMTable bankAccountTransition (bankAccountEffects sync)
        let bankFsm    = FSMHandle st1 st1 t1 900 3

        -- Using the first handle we can instantiate the second one.
        st2           <- c2 "FSM2FSMTestPayments"

        let t2         = FSMTable paymentTransition (paymentEffects sync bankFsm)
        let paymentFsm = FSMHandle st2 st2 t2 900 3

        paymentId     <- nextRandom
        bankAccount   <- nextRandom

        msg1          <- mkMsg $ ReceivedPayment bankAccount 1000
        post  paymentFsm paymentId (PaymentPending 1000)
        _ <- patch paymentFsm paymentId [msg1]

        waitQSem sync
        waitQSem sync
        pymtstatus <- get paymentFsm paymentId
        pymtstatus @?= Just PaymentPaid

        -- Now check that the second FSM has been updated as well
        bankstatus <- get bankFsm bankAccount
        bankstatus @?= Just (BankAccountBalance 1000)