module T3.Match
  ( module T3.Match.Types
  , Seconds(..)
  , runMatch
  , UserInit
  , Callback
  , StartCallback
  , delay
  ) where

import Control.Monad.Trans.Class
import Control.Monad.Trans.Either
import Control.Monad.State.Strict
import Control.Monad.Conc.ClassTmp

import T3.Game
import T3.Match.Types

type Callback m = Step -> m ()
type StartCallback m = MatchInfo -> Users -> Step -> m ()

newtype Seconds = Seconds Int
  deriving (Num, Show, Eq, Ord, Enum)

data MatchData m = MatchData
  { _matchReq :: XO -> m (Loc, Callback m)
  , _matchRespX :: Callback m
  , _matchRespO :: Callback m
  , _matchLog :: [Action] -> Board -> Result -> m ()
  , _matchBoard :: Board
  , _matchActions :: [Action]
  , _matchTimeoutLimit :: Maybe Seconds
  }

newtype MatchT m a = MatchT { unMatchT :: StateT (MatchData m) (EitherT (m ()) m) a }
  deriving (Functor, Applicative, Monad, MonadState (MatchData m))

instance MonadTrans MatchT where
  lift ma = MatchT . StateT $ \md -> do
    a <- EitherT (fmap return ma)
    return (a, md)

type UserInit m = (Callback m, m (Loc, Callback m))

runMatch :: MonadConc m => Maybe Seconds -> UserInit m -> UserInit m -> ([Action] -> Board -> Result -> m ()) -> m () -> m ()
runMatch timeoutLimit (xCB, xReq) (oCB, oReq) logger done = do
  let req X = xReq
      req O = oReq
  let cb X = xCB
      cb O = oCB
  let b = emptyBoard
  let matchDat = MatchData req (cb X) (cb O) logger b [] timeoutLimit
  matchResult <- runEitherT (evalStateT (unMatchT $ run b) matchDat)
  either id (const $ return ()) matchResult
  done

sendGameState :: MonadConc m => XO -> MatchT m ()
sendGameState xo = do
  md <- get
  lift $ (respXO xo md) (Step (_matchBoard md) Nothing)

delay :: MonadConc m => Seconds -> m ()
delay (Seconds n) = threadDelay (n * 1000000)

recvAction :: MonadConc m => XO -> MatchT m Loc
recvAction xo = do
  md <- get
  let req = _matchReq md xo
  let timeoutResponse = forfeitIO md (Win $ yinYang xo) (Lose xo)
  timeoutOrLoc <- lift $ do
    maybe
      (fmap Right req)
      (\secs -> race (delay secs >> return timeoutResponse) req)
      (_matchTimeoutLimit md)
  case timeoutOrLoc of
    Left timeout -> MatchT $ lift (left timeout)
    Right (loc, resp) -> do
      updateResp resp
      return loc
  where
    updateResp resp = do
      md <- get
      put $ case xo of
        X -> md{ _matchRespX = resp }
        O -> md{ _matchRespO = resp }

sendFinal :: MonadConc m => XO -> Final -> MatchT m ()
sendFinal xo f = do
  md <- get
  lift $ sendFinalIO md xo f

sendFinalIO :: MonadConc m => MatchData m -> XO -> Final -> m ()
sendFinalIO md xo f = (respXO xo md) (Step (_matchBoard md) (Just f))

tally :: MonadConc m => Result -> MatchT m ()
tally res = do
  md <- get
  lift $ tallyIO md res

tallyIO :: MonadConc m => MatchData m -> Result -> m ()
tallyIO md res = _matchLog md (_matchActions md) (_matchBoard md) res

forfeitIO :: MonadConc m => MatchData m -> Win XO -> Lose XO -> m ()
forfeitIO s (Win w) (Lose l) = do
  tallyIO s (Winner w)
  sendFinalIO s w WonByDQ
  sendFinalIO s l LossByDQ

updateBoard :: MonadConc m => Board -> MatchT m ()
updateBoard b = do
  md <- get
  put $ md{ _matchBoard = b }

logAction :: MonadConc m => XO -> Loc -> MatchT m ()
logAction xo loc = do
  md <- get
  put md{ _matchActions = _matchActions md ++ [Action xo loc] }

respXO :: MonadConc m => XO -> MatchData m -> Callback m
respXO X = _matchRespX
respXO O = _matchRespO

instance MonadConc m => Game (MatchT m) where
  move xo = do
    sendGameState xo
    recvAction xo
  forfeit w l = do
    md <- get
    lift $ forfeitIO md w l
  end (Win w) (Lose l) = do
    tally (Winner w)
    sendFinal w Won
    sendFinal l Loss
  tie = do
    tally Tie
    sendFinal X Tied
    sendFinal O Tied
  step b xo loc = do
    logAction xo loc
    updateBoard b