{-# LANGUAGE RankNTypes, TupleSections #-}
-- | Verifying, aggregating and displaying binding of keys to commands.
module Game.LambdaHack.Client.UI.KeyBindings
  ( keyHelp, okxsN
  ) where

import Prelude ()

import Game.LambdaHack.Core.Prelude

import qualified Data.Map.Strict as M
import qualified Data.Text as T

import           Game.LambdaHack.Client.UI.Content.Input
import           Game.LambdaHack.Client.UI.Content.Screen
import           Game.LambdaHack.Client.UI.ContentClientUI
import           Game.LambdaHack.Client.UI.HumanCmd
import           Game.LambdaHack.Client.UI.ItemSlot
import qualified Game.LambdaHack.Client.UI.Key as K
import           Game.LambdaHack.Client.UI.Overlay
import           Game.LambdaHack.Client.UI.Slideshow
import           Game.LambdaHack.Common.Kind
import           Game.LambdaHack.Content.RuleKind
import qualified Game.LambdaHack.Definition.Color as Color

-- | Produce a set of help/menu screens from the key bindings.
--
-- When the intro screen mentions KP_5, this really is KP_Begin,
-- but since that is harder to understand we assume a different, non-default
-- state of NumLock in the help text than in the code that handles keys.
keyHelp :: COps -> CCUI -> Int -> [(Text, OKX)]
keyHelp COps{corule}
        CCUI{ coinput=coinput@InputContent{..}
            , coscreen=ScreenContent{rheight, rintroScreen, rmoveKeysScreen} }
        offset = assert (offset > 0) $
  let
    introBlurb =
      ""
      : map T.pack rintroScreen
      ++
      [ ""
      , "Press SPACE or PGDN for help and ESC to see the map again."
      ]
    movBlurb = map T.pack rmoveKeysScreen
    movBlurbEnd =
      [ "Press SPACE or scroll the mouse wheel to see the minimal command set."
      ]
    minimalBlurb =
      [ "The following commands, joined with the basic set above,"
      , "let you accomplish anything in the game, though"
      , "not necessarily with the fewest keystrokes. You can also"
      , "play the game exclusively with a mouse, or both mouse"
      , "and keyboard. (See the ending help screens for mouse commands.)"
      , "Lastly, you can select a command with arrows or mouse directly"
      , "from the help screen or the dashboard and execute it on the spot."
      , ""
      ]
    casualEnding =
      [ ""
      , "Press SPACE to see the detailed descriptions of all commands."
      ]
    categoryEnding =
      [ ""
      , "Press SPACE to see the next page of command descriptions."
      ]
    itemMenuEnding =
      [ ""
      , "Note how lower case item commands (pack an item, equip, stash)"
      , "let you move items into a particular item store."
      , ""
      , "Press SPACE to see the detailed descriptions of other item-related commands."
      ]
    itemRemainingEnding =
      [ ""
      , "Note how upper case item commands (manage Pack, Equipment,"
      , "Stash, etc.) let you view and organize items within"
      , "a particular item store. Once a menu is opened, you can"
      , "switch stores at will, so each of the commands only"
      , "determines the starting item store. Each store"
      , "is accessible from the dashboard, as well."
      , ""
      , "Press SPACE to see the next page of command descriptions."
      ]
    itemAllEnding =
      [ ""
      , "Note how lower case item commands (pack an item, equip, stash)"
      , "let you move items into a particular item store, while"
      , "upper case item commands (manage Pack, Equipment, Stash, etc.)"
      , "let you view and organize items within an item store."
      , "Once a store management menu is opened, you can switch"
      , "stores at will, so the multiple commands only determine"
      , "the starting item store. Each store is accessible"
      , "from the dashboard as well."
      , ""
      , "Press SPACE to see the next page of command descriptions."
      ]
    mouseBasicsBlurb =
      [ "Screen area and UI mode (exploration/aiming) determine"
      , "mouse click effects. First, we give an overview"
      , "of effects of each button over the game map area."
      , "The list includes not only left and right buttons, but also"
      , "the optional middle mouse button (MMB) and the mouse wheel,"
      , "which is also used over menus, to page-scroll them."
      , "(For mice without RMB, one can use Control key with LMB and for mice"
      , "without MMB, one can use C-RMB or C-S-LMB.)"
      , "Next we show mouse button effects per screen area,"
      , "in exploration mode and (if different) in aiming mode."
      , ""
      ]
    mouseBasicsEnding =
      [ ""
      , "Press SPACE to see mouse commands in exploration and aiming modes."
      ]
    lastHelpEnding =
      [ ""
      , "For more playing instructions see file PLAYING.md. Press PGUP or scroll"
      , "mouse wheel for previous pages and press SPACE or ESC to see the map again."
      ]
    keyL = 12
    pickLeaderDescription =
      [ fmt keyL "0, 1 ... 6" "pick a particular actor as the new leader"
      ]
    casualDescription = "Minimal cheat sheet for casual play"
    fmt n k h = " " <> T.justifyLeft n ' ' k <+> h
    fmts s = " " <> s
    introText = map fmts introBlurb
    movText = map fmts movBlurb
    movTextEnd = map fmts movBlurbEnd
    minimalText = map fmts minimalBlurb
    casualEnd = map fmts casualEnding
    categoryEnd = map fmts categoryEnding
    itemMenuEnd = map fmts itemMenuEnding
    itemRemainingEnd = map fmts itemRemainingEnding
    itemAllEnd = map fmts itemAllEnding
    mouseBasicsText = map fmts mouseBasicsBlurb
    mouseBasicsEnd = map fmts mouseBasicsEnding
    lastHelpEnd = map fmts lastHelpEnding
    keyCaptionN n = fmt n "keys" "command"
    keyCaption = keyCaptionN keyL
    okxs = okxsN coinput offset keyL (const False) True
    renumber y (km, (y0, x1, x2)) = (km, (y0 + y, x1, x2))
    mergeOKX :: OKX -> OKX -> OKX
    mergeOKX (ov1, ks1) (ov2, ks2) =
      (ov1 ++ ov2, ks1 ++ map (renumber $ length ov1) ks2)
    catLength cat = length $ filter (\(_, (cats, desc, _)) ->
      cat `elem` cats && (desc /= "" || CmdInternal `elem` cats)) bcmdList
    keyM = 13
    keyB = 31
    truncatem b = if T.length b > keyB
                  then T.take (keyB - 1) b <> "$"
                  else b
    fmm a b c = fmt keyM a $ fmt keyB (truncatem b) (" " <> truncatem c)
    areaCaption t = fmm t "LMB (left mouse button)" "RMB (right mouse button)"
    keySel :: (forall a. (a, a) -> a) -> K.KM
           -> [(CmdArea, Either K.KM SlotChar, Text)]
    keySel sel key =
      let cmd = case M.lookup key bcmdMap of
            Just (_, _, cmd2) -> cmd2
            Nothing -> error $ "" `showFailure` key
          caCmds = case cmd of
            ByAimMode AimModeCmd{exploration=ByArea lexp, aiming=ByArea laim} ->
              sort $ sel (lexp, laim \\ lexp)
            _ -> error $ "" `showFailure` cmd
          caMakeChoice (ca, cmd2) =
            let (km, desc) = case M.lookup cmd2 brevMap of
                  Just ks ->
                    let descOfKM km2 = case M.lookup km2 bcmdMap of
                          Just (_, "", _) -> Nothing
                          Just (_, desc2, _) -> Just (km2, desc2)
                          Nothing -> error $ "" `showFailure` km2
                    in case mapMaybe descOfKM ks of
                      [] -> error $ "" `showFailure` (ks, cmd2)
                      kmdesc3 : _ -> kmdesc3
                  Nothing -> (key, "(not described:" <+> tshow cmd2 <> ")")
            in (ca, Left km, desc)
      in map caMakeChoice caCmds
    okm :: (forall a. (a, a) -> a)
        -> K.KM -> K.KM -> [Text] -> [Text]
        -> OKX
    okm sel key1 key2 header footer =
      let kst1 = keySel sel key1
          kst2 = keySel sel key2
          f (ca1, Left km1, _) (ca2, Left km2, _) y =
            assert (ca1 == ca2 `blame` (kst1, kst2))
              [ (Left [km1], (y, keyM + 3, keyB + keyM + 3))
              , (Left [km2], (y, keyB + keyM + 5, 2 * keyB + keyM + 5)) ]
          f c d e = error $ "" `showFailure` (c, d, e)
          kxs = concat $ zipWith3 f kst1 kst2 [offset + length header..]
          render (ca1, _, desc1) (_, _, desc2) =
            fmm (areaDescription ca1) desc1 desc2
          menu = zipWith render kst1 kst2
      in (map textToAL $ "" : header ++ menu ++ footer, kxs)
  in concat
    [ [ ( rtitle corule <+> "- backstory"
        , (map textToAL introText, []) ) ]
    , if catLength CmdMinimal
         + length movText + length minimalText + length casualEnd
         + 5 > rheight then
        [ ( casualDescription <+> "(1/2)."
          , (map textToAL ([""] ++ movText ++ [""] ++ movTextEnd), []) )
        , ( casualDescription <+> "(2/2)."
          , okxs CmdMinimal (minimalText ++ [keyCaption]) casualEnd ) ]
      else
        [ ( casualDescription <> "."
          , okxs CmdMinimal
                 (movText ++ [""] ++ minimalText ++ [keyCaption])
                 casualEnd ) ]
    , if catLength CmdItemMenu + catLength CmdItem
         + 9 > rheight then
        [ ( categoryDescription CmdItemMenu <> "."
          , okxs CmdItemMenu [keyCaption] itemMenuEnd )
        , ( categoryDescription CmdItem <> "."
          , okxs CmdItem [keyCaption] itemRemainingEnd ) ]
      else
        [ ( categoryDescription CmdItemMenu <> "."
          , mergeOKX
              (okxs CmdItemMenu [keyCaption] [""])
              (okxs CmdItem
                    [categoryDescription CmdItem <> ".", "", keyCaption]
                    itemAllEnd) ) ]
    , if catLength CmdMove + catLength CmdAim
         + 9 > rheight then
        [ ( "All terrain exploration and alteration commands."
          , okxs CmdMove [keyCaption] (pickLeaderDescription ++ categoryEnd) )
        , ( categoryDescription CmdAim <> "."
          , okxs CmdAim [keyCaption] categoryEnd ) ]
      else
        [ ( "All terrain exploration and alteration commands."
          , mergeOKX
              (okxs CmdMove [keyCaption] (pickLeaderDescription ++ [""]))
              (okxs CmdAim
                    [categoryDescription CmdAim <> ".", "", keyCaption]
                    categoryEnd) ) ]
    , if 45 > rheight then
        [ ( "Mouse overview."
          , let (ls, _) = okxs CmdMouse
                               (mouseBasicsText ++ [keyCaption])
                               mouseBasicsEnd
            in (ls, []) )  -- don't capture mouse wheel, etc.
        , ( "Mouse in exploration and aiming modes."
          , mergeOKX
               (okm fst K.leftButtonReleaseKM K.rightButtonReleaseKM
                    [areaCaption "exploration"] [])
               (okm snd K.leftButtonReleaseKM K.rightButtonReleaseKM
                    [areaCaption "aiming mode"] categoryEnd) ) ]
      else
        [ ( "Mouse commands."
          , let (ls, _) = okxs CmdMouse
                               (mouseBasicsText ++ [keyCaption])
                               []
                okx0 = (ls, [])  -- don't capture mouse wheel, etc.
            in mergeOKX
                 (mergeOKX
                    okx0
                    (okm fst K.leftButtonReleaseKM K.rightButtonReleaseKM
                         [areaCaption "exploration"] []))
                 (okm snd K.leftButtonReleaseKM K.rightButtonReleaseKM
                      [areaCaption "aiming mode"] categoryEnd) ) ]
    , [ ( categoryDescription CmdMeta <> "."
        , okxs CmdMeta [keyCaption] lastHelpEnd ) ]
    ]

-- | Turn the specified portion of bindings into a menu.
okxsN :: InputContent -> Int -> Int -> (HumanCmd -> Bool) -> Bool -> CmdCategory
      -> [Text] -> [Text] -> OKX
okxsN InputContent{..} offset n greyedOut showManyKeys cat header footer =
  let fmt k h = " " <> T.justifyLeft n ' ' k <+> h
      coImage :: HumanCmd -> [K.KM]
      coImage cmd = M.findWithDefault (error $ "" `showFailure` cmd) cmd brevMap
      disp = T.intercalate " or " . map (T.pack . K.showKM)
      keyKnown km = case K.key km of
        K.Unknown{} -> False
        _ -> True
      keys :: [(Either [K.KM] SlotChar, (Bool, Text))]
      keys = [ (Left kmsRes, (greyedOut cmd, fmt keyNames desc))
             | (_, (cats, desc, cmd)) <- bcmdList
             , let kms = coImage cmd
                   knownKeys = filter keyKnown kms
                   keyNames =
                     disp $ (if showManyKeys then id else take 1) knownKeys
                   kmsRes = if desc == "" then knownKeys else kms
             , cat `elem` cats
             , desc /= "" || CmdInternal `elem` cats]
      f (ks, (_, tkey)) y = (ks, (y, 1, T.length tkey))
      kxs = zipWith f keys [offset + length header..]
      ts = map (False,) ("" : header) ++ map snd keys ++ map (False,) footer
      greyToAL (b, t) = if b then textFgToAL Color.BrBlack t else textToAL t
  in (map greyToAL ts, kxs)
