{-# LANGUAGE ConstraintKinds #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE GADTs #-}

-- | Make changes to project or global configuration.
module Stack.ConfigCmd
       (ConfigCmdSet(..)
       ,configCmdSetParser
       ,cfgCmdSet
       ,cfgCmdSetName
       ,cfgCmdName) where

import           Control.Applicative
import           Control.Monad
import           Control.Monad.Catch (throwM)
import           Control.Monad.IO.Class
import           Control.Monad.Logger
import qualified Data.ByteString as S
import qualified Data.HashMap.Strict as HMap
import           Data.Monoid
import           Data.Text (Text)
import qualified Data.Text as T
import qualified Data.Yaml as Yaml
import qualified Options.Applicative as OA
import qualified Options.Applicative.Types as OA
import           Path
import           Path.IO
import           Prelude -- Silence redundant import warnings
import           Stack.BuildPlan
import           Stack.Config (makeConcreteResolver, getProjectConfig, getImplicitGlobalProjectDir, LocalConfigStatus(..))
import           Stack.Constants
import           Stack.Types.Config
import           Stack.Types.Resolver

data ConfigCmdSet
    = ConfigCmdSetResolver AbstractResolver
    | ConfigCmdSetSystemGhc CommandScope
                            Bool
    | ConfigCmdSetInstallGhc CommandScope
                             Bool

data CommandScope
    = CommandScopeGlobal
      -- ^ Apply changes to the global configuration,
      --   typically at @~/.stack/config.yaml@.
    | CommandScopeProject
      -- ^ Apply changes to the project @stack.yaml@.

configCmdSetScope :: ConfigCmdSet -> CommandScope
configCmdSetScope (ConfigCmdSetResolver _) = CommandScopeProject
configCmdSetScope (ConfigCmdSetSystemGhc scope _) = scope
configCmdSetScope (ConfigCmdSetInstallGhc scope _) = scope

cfgCmdSet
    :: (StackMiniM env m, HasConfig env, HasGHCVariant env)
    => GlobalOpts -> ConfigCmdSet -> m ()
cfgCmdSet go cmd = do
    conf <- view configL
    configFilePath <-
        liftM
            toFilePath
            (case configCmdSetScope cmd of
                 CommandScopeProject -> do
                     mstackYamlOption <- forM (globalStackYaml go) resolveFile'
                     mstackYaml <- getProjectConfig mstackYamlOption
                     case mstackYaml of
                         LCSProject stackYaml -> return stackYaml
                         LCSNoProject -> liftM (</> stackDotYaml) (getImplicitGlobalProjectDir conf)
                         LCSNoConfig -> error "config command used when no local configuration available"
                 CommandScopeGlobal -> return (configUserConfigPath conf))
    -- We don't need to worry about checking for a valid yaml here
    (config :: Yaml.Object) <-
        liftIO (Yaml.decodeFileEither configFilePath) >>= either throwM return
    newValue <- cfgCmdSetValue cmd
    let cmdKey = cfgCmdSetOptionName cmd
        config' = HMap.insert cmdKey newValue config
    if config' == config
        then $logInfo
                 (T.pack configFilePath <>
                  " already contained the intended configuration and remains unchanged.")
        else do
            liftIO (S.writeFile configFilePath (Yaml.encode config'))
            $logInfo (T.pack configFilePath <> " has been updated.")

cfgCmdSetValue
    :: (StackMiniM env m, HasConfig env, HasGHCVariant env)
    => ConfigCmdSet -> m Yaml.Value
cfgCmdSetValue (ConfigCmdSetResolver newResolver) = do
    concreteResolver <- makeConcreteResolver newResolver
    case concreteResolver of
        -- Check that the snapshot actually exists
        ResolverSnapshot snapName -> void $ loadMiniBuildPlan snapName
        ResolverCompiler _ -> return ()
        -- TODO: custom snapshot support?  Would need a way to specify on CLI
        ResolverCustom _ _ -> error "'stack config set resolver' does not support custom resolvers"
    return (Yaml.String (resolverName concreteResolver))
cfgCmdSetValue (ConfigCmdSetSystemGhc _ bool) =
    return (Yaml.Bool bool)
cfgCmdSetValue (ConfigCmdSetInstallGhc _ bool) =
    return (Yaml.Bool bool)

cfgCmdSetOptionName :: ConfigCmdSet -> Text
cfgCmdSetOptionName (ConfigCmdSetResolver _) = "resolver"
cfgCmdSetOptionName (ConfigCmdSetSystemGhc _ _) = configMonoidSystemGHCName
cfgCmdSetOptionName (ConfigCmdSetInstallGhc _ _) = configMonoidInstallGHCName

cfgCmdName :: String
cfgCmdName = "config"

cfgCmdSetName :: String
cfgCmdSetName = "set"

configCmdSetParser :: OA.Parser ConfigCmdSet
configCmdSetParser =
    OA.hsubparser $
    mconcat
        [ OA.command
              "resolver"
              (OA.info
                   (ConfigCmdSetResolver <$>
                    OA.argument
                        readAbstractResolver
                        (OA.metavar "RESOLVER" <>
                         OA.help "E.g. \"nightly\" or \"lts-7.2\""))
                   (OA.progDesc
                        "Change the resolver of the current project. See https://docs.haskellstack.org/en/stable/yaml_configuration/#resolver for more info."))
        , OA.command
              (T.unpack configMonoidSystemGHCName)
              (OA.info
                   (ConfigCmdSetSystemGhc <$> scopeFlag <*> boolArgument)
                   (OA.progDesc
                        "Configure whether stack should use a system GHC installation or not."))
        , OA.command
              (T.unpack configMonoidInstallGHCName)
              (OA.info
                   (ConfigCmdSetInstallGhc <$> scopeFlag <*> boolArgument)
                   (OA.progDesc
                        "Configure whether stack should automatically install GHC when necessary."))
        ]

scopeFlag :: OA.Parser CommandScope
scopeFlag =
    OA.flag
        CommandScopeProject
        CommandScopeGlobal
        (OA.long "global" <>
         OA.help
             "Modify the global configuration (typically at \"~/.stack/config.yaml\") instead of the project stack.yaml.")

readBool :: OA.ReadM Bool
readBool = do
    s <- OA.readerAsk
    case s of
        "true" -> return True
        "false" -> return False
        _ -> OA.readerError ("Invalid value " ++ show s ++ ": Expected \"true\" or \"false\"")

boolArgument :: OA.Parser Bool
boolArgument = OA.argument readBool (OA.metavar "true/false")