content: {-# LANGUAGE RecordWildCards #-}
  {-# LANGUAGE TupleSections   #-}
  module Telegram.Bot.Simple.Conversation where
  
  import           Data.Bifunctor
  import           Data.Hashable              (Hashable)
  import           Data.HashMap.Strict        (HashMap)
  import qualified Data.HashMap.Strict        as HashMap
  import           Data.Maybe                 (fromMaybe)
  
  import qualified Telegram.Bot.API           as Telegram
  import           Telegram.Bot.Simple.BotApp
  import           Telegram.Bot.Simple.Eff
  
  -- | Make bot to have a separate state for each conversation.
  --
  -- Common use (to have a separate state for each chat):
  --
  -- @
  -- 'conversationBot' 'Telegram.updateChatId' bot
  -- @
  conversationBot
    :: (Eq conversation, Hashable conversation)
    => (Telegram.Update -> Maybe conversation)   -- ^ How to disambiguate conversations.
    -> BotApp model action
    -> BotApp (HashMap (Maybe conversation) model) (Maybe conversation, action)
  conversationBot toConversation BotApp{..} = BotApp
    { botInitialModel = conversationInitialModel
    , botAction       = conversationAction
    , botHandler      = conversationHandler
    , botJobs         = conversationJobs
    }
    where
      conversationInitialModel = HashMap.empty
  
      conversationAction update conversations = do
        conversation <- toConversation update
        let model = fromMaybe botInitialModel (HashMap.lookup (Just conversation) conversations)
        (Just conversation,) <$> botAction update model
  
      conversationHandler (conversation, action) conversations =
        bimap (conversation,) (\m -> HashMap.insert conversation m conversations) $
          botHandler action model
        where
          model = fromMaybe botInitialModel (HashMap.lookup conversation conversations)
  
      conversationJobs = map toConversationJob botJobs
  
      toConversationJob BotJob{..} = BotJob
        { botJobSchedule = botJobSchedule
        , botJobTask = HashMap.traverseWithKey $
            \conversation -> first (conversation,) . botJobTask
        }
  
  -- | Pass latest 'Telegram.Update' to all bot jobs.
  --
  -- This enables jobs to easily send notifications.
  useLatestUpdateInJobs
    :: BotApp model action
    -> BotApp (Maybe Telegram.Update, model) (Maybe Telegram.Update, action)
  useLatestUpdateInJobs BotApp{..} = BotApp
    { botInitialModel = (Nothing, botInitialModel)
    , botAction       = newAction
    , botHandler      = newHandler
    , botJobs         = newJobs
    }
      where
        newAction update (_, model) = (Just update,) <$> botAction update model
  
        newHandler (update, action) (_update, model) =
          bimap (update,) (update,) $
            -- re-enforcing update here is needed for actions issued in jobs
            setEffUpdate update (botHandler action model)
  
        newJobs = map addUpdateToJob botJobs
  
        addUpdateToJob BotJob{..} = BotJob
          { botJobSchedule = botJobSchedule
          , botJobTask = \(update, model) ->
              bimap (update,) (update,) (setEffUpdate update (botJobTask model))
          }
  