    -- | UI client options.
    module Game.LambdaHack.Client.UI.UIOptionsParse
      ( mkUIOptions, applyUIOptions
    #ifdef EXPOSE_INTERNAL
        -- * Internal operations
      , configError, readError, parseConfig
    #endif
      ) where

    import Prelude ()

    import Game.LambdaHack.Core.Prelude

    import           Control.DeepSeq
    import qualified Data.Ini as Ini
    import qualified Data.Ini.Reader as Ini
    import qualified Data.Ini.Types as Ini
    import qualified Data.Map.Strict as M
    import qualified Data.Text as T
    import           Data.Version
    import           System.FilePath
    import           Text.ParserCombinators.ReadP (readP_to_S)
    import           Text.Read

    import           Game.LambdaHack.Client.UI.HumanCmd
    import qualified Game.LambdaHack.Client.UI.Key as K
    import           Game.LambdaHack.Client.UI.UIOptions
    import           Game.LambdaHack.Common.ClientOptions
    import           Game.LambdaHack.Common.File
    import           Game.LambdaHack.Common.Kind
    import           Game.LambdaHack.Common.Misc
    import           Game.LambdaHack.Common.Save (compatibleVersion, delayPrint)
    import qualified Game.LambdaHack.Common.Save as Save
    import           Game.LambdaHack.Content.RuleKind

    configError :: String -> a
    configError :: forall a. String -> a
    configError String
    err = String -> a
    forall a. HasCallStack => String -> a
    error (String -> a) -> String -> a
    forall a b. (a -> b) -> a -> b
    $ String
    "Error when parsing configuration file. Please fix config.ui.ini or remove it altogether. The details:\n" String -> String -> String
    forall a. [a] -> [a] -> [a]
    ++ String
    err

    readError :: Read a => String -> a
    readError :: forall a. Read a => String -> a
    readError String
    s = (String -> a) -> (a -> a) -> Either String a -> a
    forall a c b. (a -> c) -> (b -> c) -> Either a b -> c
    either (String -> a
    forall a. String -> a
    configError (String -> a) -> (String -> String) -> String -> a
    forall b c a. (b -> c) -> (a -> b) -> a -> c
    . (String
    "when reading:\n" String -> String -> String
    forall a. [a] -> [a] -> [a]
    ++ String
    s String -> String -> String
    forall v. Show v => String -> v -> String
    `showFailure`)) a -> a
    forall a. a -> a
    id
                  (Either String a -> a) -> Either String a -> a
    forall a b. (a -> b) -> a -> b
    $ String -> Either String a
    forall a. Read a => String -> Either String a
    readEither String
    s

    parseConfig :: Ini.Config -> UIOptions
    parseConfig :: Config -> UIOptions
    parseConfig Config
    cfg =
      let uCommands :: [(KM, CmdTriple)]
    uCommands =
            let mkCommand :: (String, String) -> (KM, CmdTriple)
    mkCommand (String
    ident, String
    keydef) =
                  case String -> String -> Maybe String
    forall a. Eq a => [a] -> [a] -> Maybe [a]
    stripPrefix String
    "Cmd_" String
    ident of
                    Just String
    _ ->
                      let (String
    key, CmdTriple
    def) = String -> (String, CmdTriple)
    forall a. Read a => String -> a
    readError String
    keydef
                      in (String -> KM
    K.mkKM String
    key, CmdTriple
    def :: CmdTriple)
                    Maybe String
    Nothing ->
                      String -> (KM, CmdTriple)
    forall a. String -> a
    configError
                        (String -> (KM, CmdTriple)) -> String -> (KM, CmdTriple)
    forall a b. (a -> b) -> a -> b
    $ String
    "macro id should start with Cmd_ and it does not:"
                          String -> String -> String
    forall v. Show v => String -> v -> String
    `showFailure` String
    ident
                section :: [(String, String)]
    section = String -> Config -> [(String, String)]
    Ini.allItems String
    "additional_commands" Config
    cfg
            in ((String, String) -> (KM, CmdTriple))
    -> [(String, String)] -> [(KM, CmdTriple)]
    forall a b. (a -> b) -> [a] -> [b]
    map (String, String) -> (KM, CmdTriple)
    mkCommand [(String, String)]
    section
          uHeroNames :: [(Int, (Text, Text))]
    uHeroNames =
            let toNumber :: (String, String) -> (a, b)
    toNumber (String
    ident, String
    nameAndPronoun) =
                  case String -> String -> Maybe String
    forall a. Eq a => [a] -> [a] -> Maybe [a]
    stripPrefix String
    "HeroName_" String
    ident of
                    Just String
    n -> (String -> a
    forall a. Read a => String -> a
    readError String
    n, String -> b
    forall a. Read a => String -> a
    readError String
    nameAndPronoun)
                    Maybe String
    Nothing -> String -> (a, b)
    forall a. String -> a
    configError
                               (String -> (a, b)) -> String -> (a, b)
    forall a b. (a -> b) -> a -> b
    $ String
    "wrong hero name id" String -> String -> String
    forall v. Show v => String -> v -> String
    `showFailure` String
    ident
                section :: [(String, String)]
    section = String -> Config -> [(String, String)]
    Ini.allItems String
    "hero_names" Config
    cfg
            in ((String, String) -> (Int, (Text, Text)))
    -> [(String, String)] -> [(Int, (Text, Text))]
    forall a b. (a -> b) -> [a] -> [b]
    map (String, String) -> (Int, (Text, Text))
    forall {a} {b}. (Read a, Read b) => (String, String) -> (a, b)
    toNumber [(String, String)]
    section
          lookupFail :: forall b. String -> String -> b
          lookupFail :: forall b. String -> String -> b
    lookupFail String
    optionName String
    err =
            String -> b
    forall a. String -> a
    configError (String -> b) -> String -> b
    forall a b. (a -> b) -> a -> b
    $ String
    "config file access failed"
                          String -> (String, String, Config) -> String
    forall v. Show v => String -> v -> String
    `showFailure` (String
    err, String
    optionName, Config
    cfg)
          _getOptionMaybe :: forall a. Read a => String -> Maybe a
          _getOptionMaybe :: forall a. Read a => String -> Maybe a
    _getOptionMaybe String
    optionName =
            let ms :: Maybe String
    ms = String -> String -> Config -> Maybe String
    Ini.getOption String
    "ui" String
    optionName Config
    cfg
            in (String -> a) -> (a -> a) -> Either String a -> a
    forall a c b. (a -> c) -> (b -> c) -> Either a b -> c
    either (String -> String -> a
    forall b. String -> String -> b
    lookupFail String
    optionName) a -> a
    forall a. a -> a
    id (Either String a -> a)
    -> (String -> Either String a) -> String -> a
    forall b c a. (b -> c) -> (a -> b) -> a -> c
    . String -> Either String a
    forall a. Read a => String -> Either String a
    readEither (String -> a) -> Maybe String -> Maybe a
    forall (f :: * -> *) a b. Functor f => (a -> b) -> f a -> f b
    <$> Maybe String
    ms
          getOption :: forall a. Read a => String -> a
          getOption :: forall a. Read a => String -> a
    getOption String
    optionName =
            let s :: String
    s = String -> Maybe String -> String
    forall a. a -> Maybe a -> a
    fromMaybe (String -> String -> String
    forall b. String -> String -> b
    lookupFail String
    optionName String
    "")
                    (Maybe String -> String) -> Maybe String -> String
    forall a b. (a -> b) -> a -> b
    $ String -> String -> Config -> Maybe String
    Ini.getOption String
    "ui" String
    optionName Config
    cfg
            in (String -> a) -> (a -> a) -> Either String a -> a
    forall a c b. (a -> c) -> (b -> c) -> Either a b -> c
    either (String -> String -> a
    forall b. String -> String -> b
    lookupFail String
    optionName) a -> a
    forall a. a -> a
    id (Either String a -> a) -> Either String a -> a
    forall a b. (a -> b) -> a -> b
    $ String -> Either String a
    forall a. Read a => String -> Either String a
    readEither String
    s
          uVi :: Bool
    uVi = String -> Bool
    forall a. Read a => String -> a
    getOption String
    "movementViKeys_hjklyubn"
          uLeftHand :: Bool
    uLeftHand = String -> Bool
    forall a. Read a => String -> a
    getOption String
    "movementLeftHandKeys_axwdqezc"
          uChosenFontset :: Text
    uChosenFontset = String -> Text
    forall a. Read a => String -> a
    getOption String
    "chosenFontset"
          uAllFontsScale :: Double
    uAllFontsScale = String -> Double
    forall a. Read a => String -> a
    getOption String
    "allFontsScale"
          uFullscreenMode :: FullscreenMode
    uFullscreenMode = String -> FullscreenMode
    forall a. Read a => String -> a
    getOption String
    "fullscreenMode"
          uhpWarningPercent :: Int
    uhpWarningPercent = String -> Int
    forall a. Read a => String -> a
    getOption String
    "hpWarningPercent"
          uMsgWrapColumn :: Int
    uMsgWrapColumn = String -> Int
    forall a. Read a => String -> a
    getOption String
    "msgWrapColumn"
          uHistoryMax :: Int
    uHistoryMax = String -> Int
    forall a. Read a => String -> a
    getOption String
    "historyMax"
          uMaxFps :: Double
    uMaxFps = Double -> Double -> Double
    forall a. Ord a => a -> a -> a
    max Double
    1 (Double -> Double) -> Double -> Double
    forall a b. (a -> b) -> a -> b
    $ String -> Double
    forall a. Read a => String -> a
    getOption String
    "maxFps"
          uNoAnim :: Bool
    uNoAnim = String -> Bool
    forall a. Read a => String -> a
    getOption String
    "noAnim"
          uOverrideCmdline :: [String]
    uOverrideCmdline = [String] -> [String]
    glueSeed ([String] -> [String]) -> [String] -> [String]
    forall a b. (a -> b) -> a -> b
    $ String -> [String]
    words (String -> [String]) -> String -> [String]
    forall a b. (a -> b) -> a -> b
    $ String -> String
    forall a. Read a => String -> a
    getOption String
    "overrideCmdline"
          uFonts :: [(Text, FontDefinition)]
    uFonts =
            let toFont :: (String, String) -> (Text, b)
    toFont (String
    ident, String
    fontString) = (String -> Text
    T.pack String
    ident, String -> b
    forall a. Read a => String -> a
    readError String
    fontString)
                section :: [(String, String)]
    section = String -> Config -> [(String, String)]
    Ini.allItems String
    "fonts" Config
    cfg
            in ((String, String) -> (Text, FontDefinition))
    -> [(String, String)] -> [(Text, FontDefinition)]
    forall a b. (a -> b) -> [a] -> [b]
    map (String, String) -> (Text, FontDefinition)
    forall {b}. Read b => (String, String) -> (Text, b)
    toFont [(String, String)]
    section
          uFontsets :: [(Text, FontSet)]
    uFontsets =
            let toFontSet :: (String, String) -> (Text, b)
    toFontSet (String
    ident, String
    fontSetString) =
                  (String -> Text
    T.pack String
    ident, String -> b
    forall a. Read a => String -> a
    readError String
    fontSetString)
                section :: [(String, String)]
    section = String -> Config -> [(String, String)]
    Ini.allItems String
    "fontsets" Config
    cfg
            in ((String, String) -> (Text, FontSet))
    -> [(String, String)] -> [(Text, FontSet)]
    forall a b. (a -> b) -> [a] -> [b]
    map (String, String) -> (Text, FontSet)
    forall {b}. Read b => (String, String) -> (Text, b)
    toFontSet [(String, String)]
    section
          uMessageColors :: [(String, Color)]
    uMessageColors =
            ((String, String) -> (String, Color))
    -> [(String, String)] -> [(String, Color)]
    forall a b. (a -> b) -> [a] -> [b]
    map ((String -> Color) -> (String, String) -> (String, Color)
    forall b c d. (b -> c) -> (d, b) -> (d, c)
    forall (a :: * -> * -> *) b c d.
    Arrow a =>
    a b c -> a (d, b) (d, c)
    second String -> Color
    forall a. Read a => String -> a
    readError) ([(String, String)] -> [(String, Color)])
    -> [(String, String)] -> [(String, Color)]
    forall a b. (a -> b) -> a -> b
    $ String -> Config -> [(String, String)]
    Ini.allItems String
    "message_colors" Config
    cfg
      in UIOptions{Bool
    Double
    Int
    [String]
    [(Int, (Text, Text))]
    [(String, Color)]
    [(Text, FontSet)]
    [(Text, FontDefinition)]
    [(KM, CmdTriple)]
    Text
    FullscreenMode
    uCommands :: [(KM, CmdTriple)]
    uHeroNames :: [(Int, (Text, Text))]
    uVi :: Bool
    uLeftHand :: Bool
    uChosenFontset :: Text
    uAllFontsScale :: Double
    uFullscreenMode :: FullscreenMode
    uhpWarningPercent :: Int
    uMsgWrapColumn :: Int
    uHistoryMax :: Int
    uMaxFps :: Double
    uNoAnim :: Bool
    uOverrideCmdline :: [String]
    uFonts :: [(Text, FontDefinition)]
    uFontsets :: [(Text, FontSet)]
    uMessageColors :: [(String, Color)]
    uMessageColors :: [(String, Color)]
    uFontsets :: [(Text, FontSet)]
    uFonts :: [(Text, FontDefinition)]
    uOverrideCmdline :: [String]
    uNoAnim :: Bool
    uMaxFps :: Double
    uHistoryMax :: Int
    uMsgWrapColumn :: Int
    uhpWarningPercent :: Int
    uFullscreenMode :: FullscreenMode
    uAllFontsScale :: Double
    uChosenFontset :: Text
    uLeftHand :: Bool
    uVi :: Bool
    uHeroNames :: [(Int, (Text, Text))]
    uCommands :: [(KM, CmdTriple)]
    ..}

    glueSeed :: [String] -> [String]
    glueSeed :: [String] -> [String]
    glueSeed [] = []
    glueSeed (String
    "SMGen" : String
    s1 : String
    s2 : [String]
    rest) =
      (String
    "SMGen" String -> String -> String
    forall a. [a] -> [a] -> [a]
    ++ String
    " " String -> String -> String
    forall a. [a] -> [a] -> [a]
    ++ String
    s1 String -> String -> String
    forall a. [a] -> [a] -> [a]
    ++ String
    " " String -> String -> String
    forall a. [a] -> [a] -> [a]
    ++ String
    s2) String -> [String] -> [String]
    forall a. a -> [a] -> [a]
    : [String] -> [String]
    glueSeed [String]
    rest
    glueSeed (String
    s : [String]
    rest) = String
    s String -> [String] -> [String]
    forall a. a -> [a] -> [a]
    : [String] -> [String]
    glueSeed [String]
    rest

    -- | Read and parse UI config file.
    mkUIOptions :: RuleContent -> ClientOptions -> IO UIOptions
    mkUIOptions :: RuleContent -> ClientOptions -> IO UIOptions
    mkUIOptions RuleContent
    corule ClientOptions
    clientOptions = do
      let benchmark :: Bool
    benchmark = ClientOptions -> Bool
    sbenchmark ClientOptions
    clientOptions
          cfgUIName :: String
    cfgUIName = RuleContent -> String
    rcfgUIName RuleContent
    corule
          (Text
    configText, Config
    cfgUIDefault) = RuleContent -> (Text, Config)
    rcfgUIDefault RuleContent
    corule
      dataDir <- IO String
    appDataDir
      let path String
    bkp = String
    dataDir String -> String -> String
    </> String
    bkp String -> String -> String
    forall a. Semigroup a => a -> a -> a
    <> String
    cfgUIName
      cfgUser <- if benchmark then return Ini.emptyConfig else do
        cpExists <- doesFileExist (path "")
        if not cpExists
          then return Ini.emptyConfig
          else do
            sUser <- readFile (path "")
            return $! either (configError . ("Ini.parse sUser" `showFailure`)) id
                      $ Ini.parse sUser
      let cfgUI = (Map String String -> Map String String -> Map String String)
    -> Config -> Config -> Config
    forall k a. Ord k => (a -> a -> a) -> Map k a -> Map k a -> Map k a
    M.unionWith Map String String -> Map String String -> Map String String
    forall k a. Ord k => Map k a -> Map k a -> Map k a
    M.union Config
    cfgUser Config
    cfgUIDefault  -- user cfg preferred
          vExe1 = RuleContent -> Version
    rexeVersion RuleContent
    corule
          vExe2 =
            let optionName :: String
    optionName = String
    "version"
                -- Lenient to parse, and reject, old config files:
                s :: String
    s = String -> Maybe String -> String
    forall a. a -> Maybe a -> a
    fromMaybe String
    "" (Maybe String -> String) -> Maybe String -> String
    forall a b. (a -> b) -> a -> b
    $ String -> String -> Config -> Maybe String
    Ini.getOption String
    "version" String
    optionName Config
    cfgUser
                dummyVersion :: Version
    dummyVersion = [Int] -> Version
    makeVersion []
            in case ((Version, String) -> Bool)
    -> [(Version, String)] -> Maybe (Version, String)
    forall (t :: * -> *) a. Foldable t => (a -> Bool) -> t a -> Maybe a
    find ((String -> String -> Bool
    forall a. Eq a => a -> a -> Bool
    == String
    "") (String -> Bool)
    -> ((Version, String) -> String) -> (Version, String) -> Bool
    forall b c a. (b -> c) -> (a -> b) -> a -> c
    . (Version, String) -> String
    forall a b. (a, b) -> b
    snd) ([(Version, String)] -> Maybe (Version, String))
    -> [(Version, String)] -> Maybe (Version, String)
    forall a b. (a -> b) -> a -> b
    $ ReadP Version -> ReadS Version
    forall a. ReadP a -> ReadS a
    readP_to_S ReadP Version
    parseVersion String
    s of
              Just (Version
    ver, String
    "") -> Version
    ver
              Maybe (Version, String)
    _ -> Version
    dummyVersion
      if benchmark || compatibleVersion vExe1 vExe2 then do
        let conf = Config -> UIOptions
    parseConfig Config
    cfgUI
        -- Catch syntax errors in complex expressions ASAP.
        return $! deepseq conf conf
      else do
        cpExists <- doesFileExist (path "")
        when cpExists $ do
          renameFile (path "") (path "bkp.")
          moveAside <- Save.bkpAllSaves corule clientOptions
          let msg = Text
    "Config file" Text -> Text -> Text
    <+> String -> Text
    T.pack (String -> String
    path String
    "")
                    Text -> Text -> Text
    <+> Text
    "from an incompatible version '"
                    Text -> Text -> Text
    forall a. Semigroup a => a -> a -> a
    <> String -> Text
    T.pack (Version -> String
    showVersion Version
    vExe2)
                    Text -> Text -> Text
    forall a. Semigroup a => a -> a -> a
    <> Text
    "' detected while starting"
                    Text -> Text -> Text
    <+> String -> Text
    T.pack (Version -> String
    showVersion Version
    vExe1)
                    Text -> Text -> Text
    <+> Text
    "game."
                    Text -> Text -> Text
    <+> if Bool
    moveAside
                        then Text
    "The config file and savefiles have been moved aside."
                        else Text
    "The config file has been moved aside."
          delayPrint msg
        dataDirExists <- doesFileExist dataDir
        when dataDirExists $  -- may not exist, e.g., when testing
          tryWriteFile (path "") configText
        let confDefault = Config -> UIOptions
    parseConfig Config
    cfgUIDefault
        return confDefault

    -- | Modify client options with UI options.
    applyUIOptions :: COps -> UIOptions -> ClientOptions -> ClientOptions
    applyUIOptions :: COps -> UIOptions -> ClientOptions -> ClientOptions
    applyUIOptions COps{RuleContent
    corule :: RuleContent
    corule :: COps -> RuleContent
    corule} UIOptions
    uioptions =
         (\ClientOptions
    opts -> ClientOptions
    opts {schosenFontset =
            schosenFontset opts `mplus` Just (uChosenFontset uioptions)}) (ClientOptions -> ClientOptions)
    -> (ClientOptions -> ClientOptions)
    -> ClientOptions
    -> ClientOptions
    forall b c a. (b -> c) -> (a -> b) -> a -> c
    .
         (\ClientOptions
    opts -> ClientOptions
    opts {sallFontsScale =
            sallFontsScale opts `mplus` Just (uAllFontsScale uioptions)}) (ClientOptions -> ClientOptions)
    -> (ClientOptions -> ClientOptions)
    -> ClientOptions
    -> ClientOptions
    forall b c a. (b -> c) -> (a -> b) -> a -> c
    .
         (\ClientOptions
    opts -> ClientOptions
    opts {sfullscreenMode =
            sfullscreenMode opts `mplus` Just (uFullscreenMode uioptions)}) (ClientOptions -> ClientOptions)
    -> (ClientOptions -> ClientOptions)
    -> ClientOptions
    -> ClientOptions
    forall b c a. (b -> c) -> (a -> b) -> a -> c
    .
         (\ClientOptions
    opts -> ClientOptions
    opts {smaxFps =
            smaxFps opts `mplus` Just (uMaxFps uioptions)}) (ClientOptions -> ClientOptions)
    -> (ClientOptions -> ClientOptions)
    -> ClientOptions
    -> ClientOptions
    forall b c a. (b -> c) -> (a -> b) -> a -> c
    .
         (\ClientOptions
    opts -> ClientOptions
    opts {snoAnim =
            snoAnim opts `mplus` Just (uNoAnim uioptions)}) (ClientOptions -> ClientOptions)
    -> (ClientOptions -> ClientOptions)
    -> ClientOptions
    -> ClientOptions
    forall b c a. (b -> c) -> (a -> b) -> a -> c
    .
         (\ClientOptions
    opts -> ClientOptions
    opts {stitle =
            stitle opts `mplus` Just (rtitle corule)}) (ClientOptions -> ClientOptions)
    -> (ClientOptions -> ClientOptions)
    -> ClientOptions
    -> ClientOptions
    forall b c a. (b -> c) -> (a -> b) -> a -> c
    .
         (\ClientOptions
    opts -> ClientOptions
    opts {sfonts = uFonts uioptions}) (ClientOptions -> ClientOptions)
    -> (ClientOptions -> ClientOptions)
    -> ClientOptions
    -> ClientOptions
    forall b c a. (b -> c) -> (a -> b) -> a -> c
    .
         (\ClientOptions
    opts -> ClientOptions
    opts {sfontsets = uFontsets uioptions})