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

import Prelude
import T3.Match.Types
import T3.Game
import Control.Concurrent
import Control.Concurrent.Async
import Control.Monad.Trans.Either

import Control.Monad.State.Strict

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

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

data MatchData = MatchData
  { matchReq :: XO -> IO (Loc, Callback)
  , matchRespX :: Callback
  , matchRespO :: Callback
  , matchLog :: [Action] -> Board -> Result -> IO ()
  , matchBoard :: Board
  , matchActions :: [Action]
  , matchTimeoutLimit :: Maybe Seconds
  }

newtype Match a = Match { unMatch :: StateT MatchData (EitherT (IO ()) IO) a }
  deriving (Functor, Applicative, Monad, MonadIO, MonadState MatchData)

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

runMatch :: Maybe Seconds -> UserInit -> UserInit -> ([Action] -> Board -> Result -> IO ()) -> IO () -> IO ()
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 (unMatch $ run b) matchDat)
  either id (const $ return ()) matchResult
  done

sendGameState :: XO -> Match ()
sendGameState xo = do
  s <- get
  liftIO $ (respXO xo s) (Step (matchBoard s) Nothing)

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

recvAction :: XO -> Match Loc
recvAction xo = do
  req <- gets (flip matchReq xo)
  s <- get
  let timeoutResponse = forfeitIO s (Win $ yinYang xo) (Lose xo)
  timeoutOrLoc <- liftIO $
    maybe
      (fmap Right req)
      (\secs -> race (delay secs >> return timeoutResponse) req)
      (matchTimeoutLimit s)
  case timeoutOrLoc of
    Left timeout -> Match (lift $ left timeout)
    Right (loc, resp) -> do
      updateResp resp
      return loc
  where
    updateResp resp = do
      match <- get
      put $ case xo of
        X -> match { matchRespX = resp }
        O -> match { matchRespO = resp }

sendFinal :: XO -> Final -> Match ()
sendFinal xo final = do
  s <- get
  liftIO $ sendFinalIO s xo final

sendFinalIO :: MatchData -> XO -> Final -> IO ()
sendFinalIO s xo final = liftIO $ (respXO xo s) (Step (matchBoard s) (Just final))

tally :: Result -> Match ()
tally res = do
  s <- get
  liftIO $ matchLog s (matchActions s) (matchBoard s) res

tallyIO :: MatchData -> Result -> IO ()
tallyIO s res =  matchLog s (matchActions s) (matchBoard s) res

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

updateBoard :: Board -> Match ()
updateBoard b = do
  match <- get
  put $ match { matchBoard = b }

logAction :: XO -> Loc -> Match ()
logAction xo loc = do
  match <- get
  put $ match { matchActions = matchActions match ++ [Action xo loc] }

respXO :: XO -> MatchData -> Callback
respXO X = matchRespX
respXO O = matchRespO

instance Game Match  where
  move xo = do
    sendGameState xo
    recvAction xo
  forfeit w l = do
    s <- get
    liftIO $ forfeitIO s 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