{-# LANGUAGE DeriveGeneric, GeneralizedNewtypeDeriving #-}
-- | Game messages displayed on top of the screen for the player to read
-- and then saved to player history.
module Game.LambdaHack.Client.UI.Msg
  ( -- * Msg
    Msg, toMsg
  , MsgClass(..), interruptsRunning, disturbsResting
    -- * Report
  , Report, nullReport, consReport, renderReport, anyInReport
    -- * History
  , History, newReport, emptyHistory, addToReport, archiveReport, lengthHistory
  , renderHistory
#ifdef EXPOSE_INTERNAL
    -- * Internal operations
  , isSavedToHistory, isDisplayed, bindsPronouns, msgColor
  , UAttrLine, RepMsgN, uToAttrLine, attrLineToU
  , emptyReport, snocReport, renderWholeReport, renderRepetition
  , scrapRepetition, renderTimeReport
#endif
  ) where

import Prelude ()

import Game.LambdaHack.Core.Prelude

import           Control.DeepSeq
import           Data.Binary
import qualified Data.EnumMap.Strict as EM
import           Data.Vector.Binary ()
import qualified Data.Vector.Unboxed as U
import           Data.Word (Word32)
import           GHC.Generics (Generic)

import           Game.LambdaHack.Client.UI.Overlay
import qualified Game.LambdaHack.Common.RingBuffer as RB
import           Game.LambdaHack.Common.Time
import qualified Game.LambdaHack.Definition.Color as Color

-- * UAttrLine

type UAttrLine = U.Vector Word32

uToAttrLine :: UAttrLine -> AttrLine
uToAttrLine v = map Color.AttrCharW32 $ U.toList v

attrLineToU :: AttrLine -> UAttrLine
attrLineToU l = U.fromList $ map Color.attrCharW32 l

-- * Msg

-- | The type of a single game message.
data Msg = Msg
  { msgLine  :: AttrLine  -- ^ the colours and characters of the message;
                          --   not just text, in case there was some colour
                          --   unrelated to msg class
  , msgClass :: MsgClass  -- ^ whether message should be displayed,
                          --   recorded in history, with what color, etc.
  }
  deriving (Show, Eq, Generic)

instance Binary Msg

toMsg :: Maybe (EM.EnumMap MsgClass Color.Color) -> MsgClass -> Text -> Msg
toMsg mem msgClass l =
  let findColorInConfig = EM.findWithDefault Color.White msgClass
      color = maybe (msgColor msgClass) findColorInConfig mem
      msgLine = textFgToAL color l
  in Msg {..}

data MsgClass =
    MsgAdmin
  | MsgBecome
  | MsgNoLonger
  | MsgLongerUs
  | MsgLonger
  | MsgItemCreation
  | MsgItemDestruction
  | MsgDeathGood
  | MsgDeathBad
  | MsgDeath
  | MsgDeathThreat
  | MsgLeader
  | MsgDiplomacy
  | MsgOutcome
  | MsgPlot
  | MsgLandscape
  | MsgTileDisco
  | MsgItemDisco
  | MsgActorSpot
  | MsgFirstEnemySpot
  | MsgItemSpot
  | MsgItemMove
  | MsgAction
  | MsgActionMinor
  | MsgEffectMajor
  | MsgEffect
  | MsgEffectMinor
  | MsgMisc
  | MsgHeardClose
  | MsgHeard
  | MsgFocus
  | MsgWarning
  | MsgRangedPowerfulWe
  | MsgRangedPowerfulUs
  | MsgRanged  -- our non-projectile actors are not hit
  | MsgRangedUs
  | MsgRare
  | MsgVeryRare
  | MsgMeleePowerfulWe
  | MsgMeleePowerfulUs
  | MsgMeleeInterestingWe
  | MsgMeleeInterestingUs
  | MsgMelee  -- our non-projectile actors are not hit
  | MsgMeleeUs
  | MsgDone
  | MsgAtFeetMajor
  | MsgAtFeet
  | MsgNumeric
  | MsgSpam
  | MsgMacro
  | MsgRunStop
  | MsgPrompt
  | MsgPromptFocus
  | MsgAlert
  | MsgStopPlayback
 deriving (Show, Read, Eq, Enum, Generic)

instance NFData MsgClass

instance Binary MsgClass

isSavedToHistory :: MsgClass -> Bool
isSavedToHistory MsgNumeric = False
isSavedToHistory MsgSpam = False
isSavedToHistory MsgMacro = False
isSavedToHistory MsgRunStop = False
isSavedToHistory MsgPrompt = False
isSavedToHistory MsgPromptFocus = False
isSavedToHistory MsgAlert = False
isSavedToHistory MsgStopPlayback = False
isSavedToHistory _ = True

isDisplayed :: MsgClass -> Bool
isDisplayed MsgRunStop = False
isDisplayed MsgNumeric = False
isDisplayed MsgSpam = False
isDisplayed MsgMacro = False
isDisplayed MsgStopPlayback = False
isDisplayed _ = True

interruptsRunning :: MsgClass -> Bool
interruptsRunning MsgHeard = False
  -- MsgHeardClose interrupts, even if running started while hearing close
interruptsRunning MsgEffectMinor = False
interruptsRunning MsgItemDisco = False
interruptsRunning MsgItemMove = False
interruptsRunning MsgActionMinor = False
interruptsRunning MsgAtFeet = False
interruptsRunning MsgNumeric = False
interruptsRunning MsgSpam = False
interruptsRunning MsgMacro = False
interruptsRunning MsgRunStop = False
interruptsRunning MsgPrompt = False
interruptsRunning MsgPromptFocus = False
  -- MsgAlert means something went wrong, so alarm
interruptsRunning _ = True

disturbsResting :: MsgClass -> Bool
disturbsResting MsgHeard = False
disturbsResting MsgHeardClose = False -- handled separately
disturbsResting MsgLeader = False -- handled separately
disturbsResting MsgEffectMinor = False
disturbsResting MsgItemDisco = False
disturbsResting MsgItemMove = False
disturbsResting MsgActionMinor = False
disturbsResting MsgAtFeet = False
disturbsResting MsgNumeric = False
disturbsResting MsgSpam = False
disturbsResting MsgMacro = False
disturbsResting MsgRunStop = False
disturbsResting MsgPrompt = False
disturbsResting MsgPromptFocus = False
  -- MsgAlert means something went wrong, so alarm
disturbsResting _ = True

-- Only player's non-projectile actors getting hit introduce subjects,
-- because only such hits are guaranteed to be perceived.
-- Here we also mark friends being hit, but that's a safe approximation.
-- We also mark the messages that use the introduced subjects
-- by referring to them via pronouns. They can't be moved freely either.
bindsPronouns :: MsgClass -> Bool
bindsPronouns MsgRangedPowerfulUs = True
bindsPronouns MsgRangedUs = True
bindsPronouns MsgMeleePowerfulUs = True
bindsPronouns MsgMeleeInterestingUs = True
bindsPronouns MsgMeleeUs = True
bindsPronouns MsgLongerUs = True
bindsPronouns _ = False

-- Only @White@ color gets replaced by this one.
msgColor :: MsgClass -> Color.Color
msgColor MsgAdmin = Color.White
msgColor MsgBecome = Color.BrBlue  -- similar color to cyan and role to Effect
msgColor MsgNoLonger = Color.Blue
msgColor MsgLongerUs = Color.White  -- not important enough
msgColor MsgLonger = Color.White  -- not important enough
msgColor MsgItemCreation = Color.BrBlue
msgColor MsgItemDestruction = Color.Blue
msgColor MsgDeathGood = Color.BrGreen
msgColor MsgDeathBad = Color.BrRed
msgColor MsgDeath = Color.White
msgColor MsgDeathThreat = Color.BrRed
msgColor MsgLeader = Color.White
msgColor MsgDiplomacy = Color.BrYellow
msgColor MsgOutcome = Color.BrWhite
msgColor MsgPlot = Color.White
msgColor MsgLandscape = Color.White
msgColor MsgTileDisco = Color.Magenta
msgColor MsgItemDisco = Color.BrMagenta
msgColor MsgActorSpot = Color.White  -- too common
msgColor MsgFirstEnemySpot = Color.Red
msgColor MsgItemSpot = Color.White
msgColor MsgItemMove = Color.White
msgColor MsgAction = Color.White
msgColor MsgActionMinor = Color.White
msgColor MsgEffectMajor = Color.BrCyan
msgColor MsgEffect = Color.Cyan
msgColor MsgEffectMinor = Color.White
msgColor MsgMisc = Color.White
msgColor MsgHeardClose = Color.BrYellow
msgColor MsgHeard = Color.Brown
msgColor MsgFocus = Color.Green
msgColor MsgWarning = Color.BrYellow
msgColor MsgRangedPowerfulWe = Color.Green
msgColor MsgRangedPowerfulUs = Color.Red
msgColor MsgRanged = Color.White
msgColor MsgRangedUs = Color.White
msgColor MsgRare = Color.Cyan
msgColor MsgVeryRare = Color.BrCyan
msgColor MsgMeleePowerfulWe = Color.Green
msgColor MsgMeleePowerfulUs = Color.Red
msgColor MsgMeleeInterestingWe = Color.Green
msgColor MsgMeleeInterestingUs = Color.Red
msgColor MsgMelee = Color.White
msgColor MsgMeleeUs = Color.White
msgColor MsgDone = Color.White
msgColor MsgAtFeetMajor = Color.White
msgColor MsgAtFeet = Color.White
msgColor MsgNumeric = Color.White
msgColor MsgSpam = Color.White
msgColor MsgMacro = Color.White
msgColor MsgRunStop = Color.White
msgColor MsgPrompt = Color.White
msgColor MsgPromptFocus = Color.Green
msgColor MsgAlert = Color.BrYellow
msgColor MsgStopPlayback = Color.BrYellow

-- * Report

data RepMsgN = RepMsgN {repMsg :: Msg, _repN :: Int}
  deriving (Show, Generic)

instance Binary RepMsgN

-- | The set of messages, with repetitions, to show at the screen at once.
newtype Report = Report [RepMsgN]
  deriving (Show, Binary)

-- | Empty set of messages.
emptyReport :: Report
emptyReport = Report []

-- | Test if the set of messages is empty.
nullReport :: Report -> Bool
nullReport (Report l) = null l

-- | Add a message to the end of the report.
snocReport :: Report -> Msg -> Int -> Report
snocReport (Report !r) y n =
  if null $ msgLine y then Report r else Report $ RepMsgN y n : r

-- | Add a message to the start of report.
consReport :: Msg -> Report -> Report
consReport Msg{msgLine=[]} rep = rep
consReport y (Report r) = Report $ r ++ [RepMsgN y 1]

-- | Render a report as a (possibly very long) 'AttrLine'. Filter out
-- messages not meant for display.
renderReport :: Report -> AttrLine
renderReport (Report r) =
  let rep = Report $ filter (isDisplayed . msgClass . repMsg) r
  in renderWholeReport rep

-- | Render a report as a (possibly very long) 'AttrLine'.
renderWholeReport :: Report -> AttrLine
renderWholeReport (Report []) = []
renderWholeReport (Report (x : xs)) =
  renderWholeReport (Report xs) <+:> renderRepetition x

renderRepetition :: RepMsgN -> AttrLine
renderRepetition (RepMsgN s 0) = msgLine s
renderRepetition (RepMsgN s 1) = msgLine s
renderRepetition (RepMsgN s n) = msgLine s ++ stringToAL ("<x" ++ show n ++ ">")

anyInReport :: (MsgClass -> Bool) -> Report -> Bool
anyInReport f (Report xns) = any (f . msgClass . repMsg) xns

-- * History

-- | The history of reports. This is a ring buffer of the given length
-- containing old archived history and two most recent reports stored
-- separately.
data History = History
  { newReport       :: Report
  , newTime         :: Time
  , oldReport       :: Report
  , oldTime         :: Time
  , archivedHistory :: RB.RingBuffer UAttrLine }
  deriving (Show, Generic)

instance Binary History

-- | Empty history of the given maximal length.
emptyHistory :: Int -> History
emptyHistory size =
  let ringBufferSize = size - 1  -- a report resides outside the buffer
  in History emptyReport timeZero emptyReport timeZero
             (RB.empty ringBufferSize U.empty)

scrapRepetition :: History -> Maybe History
scrapRepetition History{ newReport = Report newMsgs
                       , oldReport = Report oldMsgs
                       , .. } =
  case newMsgs of
    -- We take into account only first message of the new report,
    -- because others were deduplicated as they were added.
    -- We keep the message in the new report, because it should not
    -- vanish from the screen. In this way the message may be passed
    -- along many reports.
    RepMsgN s1 n1 : rest1 ->
      let commutative s = not $ bindsPronouns $ msgClass s
          f (RepMsgN s2 _) = msgLine s1 == msgLine s2
      in case break f rest1 of
        (_, []) | commutative s1 -> case break f oldMsgs of
          (noDup, RepMsgN s2 n2 : rest2) ->
            -- We keep the occurence of the message in the new report only.
            let newReport = Report $ RepMsgN s2 (n1 + n2) : rest1
                oldReport = Report $ noDup ++ rest2
            in Just History{..}
          _ -> Nothing
        (noDup, RepMsgN s2 n2 : rest2) | commutative s1
                                         || all (commutative . repMsg) noDup ->
          -- We keep the older (and so, oldest) occurence of the message,
          -- to avoid visual disruption by moving the message around.
          let newReport = Report $ noDup ++ RepMsgN s2 (n1 + n2) : rest2
              oldReport = Report oldMsgs
          in Just History{..}
        _ -> Nothing
    _ -> Nothing  -- empty new report

-- | Add a message to the new report of history, eliminating a possible
-- duplicate and noting its existence in the result.
addToReport :: History -> Msg -> Int -> Time -> (History, Bool)
addToReport History{..} msg n time =
  let newH = History{newReport = snocReport newReport msg n, newTime = time, ..}
  in case scrapRepetition newH of
    Just scrappedH -> (scrappedH, True)
    Nothing -> (newH, False)

-- | Archive old report to history, filtering out messages with 0 duplicates
-- and prompts. Set up new report with a new timestamp.
archiveReport :: History -> History
archiveReport History{newReport=Report newMsgs, ..} =
  let f (RepMsgN _ n) = n > 0
      newReportNon0 = Report $ filter f newMsgs
  in if nullReport newReportNon0
     then -- Drop empty new report.
          History emptyReport timeZero oldReport oldTime archivedHistory
     else let lU = map attrLineToU $ renderTimeReport oldTime oldReport
          in History emptyReport timeZero newReportNon0 newTime
             $ foldl' (\ !h !v -> RB.cons v h) archivedHistory (reverse lU)

renderTimeReport :: Time -> Report -> [AttrLine]
renderTimeReport !t (Report r) =
  let turns = t `timeFitUp` timeTurn
      rep = Report $ filter (isSavedToHistory . msgClass . repMsg) r
  in if nullReport rep
     then []
     else [stringToAL (show turns ++ ": ") ++ renderReport rep]

lengthHistory :: History -> Int
lengthHistory History{oldReport, archivedHistory} =
  RB.length archivedHistory
  + length (renderTimeReport timeZero oldReport)
      -- matches @renderHistory@

-- | Render history as many lines of text. New report is not rendered.
-- It's expected to be empty when history is shown.
renderHistory :: History -> [AttrLine]
renderHistory History{..} = map uToAttrLine (RB.toList archivedHistory)
                            ++ renderTimeReport oldTime oldReport