{-# LANGUAGE ConstraintKinds, FlexibleInstances, UndecidableInstances #-}
module Yesod.Worker.Types where

import Prelude
import Yesod

import Control.Applicative (Applicative (..))
import Control.Concurrent.STM (TVar)
import Control.Monad (liftM, ap)
import Control.Monad.Logger (LogSource, MonadLogger (..))
import Control.Monad.Trans.Control (MonadBaseControl (..))
import Control.Monad.Trans.Resource (InternalState, runInternalState, MonadThrow (..), monadThrow, MonadResourceBase)
import qualified Data.Sequence as S
import Database.Persist.Sql (SqlPersistT)
import Language.Haskell.TH.Syntax (Loc)
import System.Log.FastLogger (LogStr, toLogStr)


type JobQueue a = TVar (S.Seq a)

-- | Typeclass for customizing Worker settings
class Yesod site => YesodWorker site where
  -- | Your applications job type
  type Job

  -- | How to retreive the application queue
  queue :: site -> JobQueue Job

  -- | Number of concurrent workers
  workerCount :: site -> Int
  workerCount _ = 3

  -- | How to execute each job
  perform :: Job -> WorkerT site IO ()

  -- TODO: Allow different PersistBackends
  -- | Helper for running SQL queries inside a Worker
  runW :: SqlPersistT (WorkerT site IO) a -> WorkerT site IO a


data RunWorkerEnv site = RunWorkerEnv
  { rweSite     :: !site
  , rweLog      :: !(Loc -> LogSource -> LogLevel -> LogStr -> IO ())
  -- , rheOnError  :: !(ErrorResponse -> YesodApp)
  }

data WorkerData site = WorkerData
  { workerResource :: !InternalState
  , workerEnv      :: !(RunWorkerEnv site)
  }

newtype WorkerT site m a = WorkerT
  { unWorkerT :: WorkerData site -> m a
  }


instance MonadTrans (WorkerT site) where
  lift = WorkerT . const

instance Monad m => Functor (WorkerT site m) where
  fmap = liftM

instance Monad m => Applicative (WorkerT site m) where
  pure = return
  (<*>) = ap

instance MonadIO m => MonadIO (WorkerT site m) where
  liftIO = lift . liftIO

instance MonadBase b m => MonadBase b (WorkerT site m) where
  liftBase = lift . liftBase

-- TODO: absorb the instance declarations below
instance Monad m => Monad (WorkerT site m) where
  return = WorkerT . const . return
  WorkerT x >>= f = WorkerT $ \r -> x r >>= \x' -> unWorkerT (f x') r

instance MonadBaseControl b m => MonadBaseControl b (WorkerT site m) where
  data StM (WorkerT site m) a = StH (StM m a)
  liftBaseWith f = WorkerT $ \reader ->
    liftBaseWith $ \runInBase ->
      f $ liftM StH . runInBase . (\(WorkerT r) -> r reader)
  restoreM (StH base) = WorkerT $ const $ restoreM base

instance MonadThrow m => MonadThrow (WorkerT site m) where
  throwM = lift . monadThrow

instance (MonadIO m, MonadBase IO m, MonadThrow m) => MonadResource (WorkerT site m) where
  liftResourceT f = WorkerT $ \hd -> liftIO $ runInternalState f (workerResource hd)

instance MonadIO m => MonadLogger (WorkerT site m) where
  monadLoggerLog a b c d = WorkerT $ \hd ->
    liftIO $ rweLog (workerEnv hd) a b c (toLogStr d)

class MonadResource m => MonadWorker m where
  type WorkerSite m
  liftWorkerT :: WorkerT (WorkerSite m) IO a -> m a

instance MonadResourceBase m => MonadWorker (WorkerT site m) where
  type WorkerSite (WorkerT site m) = site
  liftWorkerT (WorkerT f) = WorkerT $ liftIO . f
{-# RULES "liftWorkerT (WorkerT site IO)" liftWorkerT = id #-}