module Distribution.Simple.Program.Db (
    
    ProgramDb,
    emptyProgramDb,
    defaultProgramDb,
    restoreProgramDb,
    
    addKnownProgram,
    addKnownPrograms,
    lookupKnownProgram,
    knownPrograms,
    getProgramSearchPath,
    setProgramSearchPath,
    modifyProgramSearchPath,
    userSpecifyPath,
    userSpecifyPaths,
    userMaybeSpecifyPath,
    userSpecifyArgs,
    userSpecifyArgss,
    userSpecifiedArgs,
    lookupProgram,
    updateProgram,
    configuredPrograms,
    
    configureProgram,
    configureAllKnownPrograms,
    unconfigureProgram,
    lookupProgramVersion,
    reconfigurePrograms,
    requireProgram,
    requireProgramVersion,
  ) where
import Prelude ()
import Distribution.Compat.Prelude
import Distribution.Simple.Program.Types
import Distribution.Simple.Program.Find
import Distribution.Simple.Program.Builtin
import Distribution.Simple.Utils
import Distribution.Version
import Distribution.Text
import Distribution.Verbosity
import Control.Monad (join)
import Data.Tuple (swap)
import qualified Data.Map as Map
data ProgramDb = ProgramDb {
        unconfiguredProgs :: UnconfiguredProgs,
        progSearchPath    :: ProgramSearchPath,
        configuredProgs   :: ConfiguredProgs
    }
  deriving (Typeable)
type UnconfiguredProgram = (Program, Maybe FilePath, [ProgArg])
type UnconfiguredProgs   = Map.Map String UnconfiguredProgram
type ConfiguredProgs     = Map.Map String ConfiguredProgram
emptyProgramDb :: ProgramDb
emptyProgramDb = ProgramDb Map.empty defaultProgramSearchPath Map.empty
defaultProgramDb :: ProgramDb
defaultProgramDb = restoreProgramDb builtinPrograms emptyProgramDb
updateUnconfiguredProgs :: (UnconfiguredProgs -> UnconfiguredProgs)
                        -> ProgramDb -> ProgramDb
updateUnconfiguredProgs update progdb =
  progdb { unconfiguredProgs = update (unconfiguredProgs progdb) }
updateConfiguredProgs :: (ConfiguredProgs -> ConfiguredProgs)
                      -> ProgramDb -> ProgramDb
updateConfiguredProgs update progdb =
  progdb { configuredProgs = update (configuredProgs progdb) }
instance Show ProgramDb where
  show = show . Map.toAscList . configuredProgs
instance Read ProgramDb where
  readsPrec p s =
    [ (emptyProgramDb { configuredProgs = Map.fromList s' }, r)
    | (s', r) <- readsPrec p s ]
instance Binary ProgramDb where
  put db = do
    put (progSearchPath db)
    put (configuredProgs db)
  get = do
    searchpath <- get
    progs      <- get
    return $! emptyProgramDb {
      progSearchPath  = searchpath,
      configuredProgs = progs
    }
restoreProgramDb :: [Program] -> ProgramDb -> ProgramDb
restoreProgramDb = addKnownPrograms
addKnownProgram :: Program -> ProgramDb -> ProgramDb
addKnownProgram prog = updateUnconfiguredProgs $
  Map.insertWith combine (programName prog) (prog, Nothing, [])
  where combine _ (_, path, args) = (prog, path, args)
addKnownPrograms :: [Program] -> ProgramDb -> ProgramDb
addKnownPrograms progs progdb = foldl' (flip addKnownProgram) progdb progs
lookupKnownProgram :: String -> ProgramDb -> Maybe Program
lookupKnownProgram name =
  fmap (\(p,_,_)->p) . Map.lookup name . unconfiguredProgs
knownPrograms :: ProgramDb -> [(Program, Maybe ConfiguredProgram)]
knownPrograms progdb =
  [ (p,p') | (p,_,_) <- Map.elems (unconfiguredProgs progdb)
           , let p' = Map.lookup (programName p) (configuredProgs progdb) ]
getProgramSearchPath :: ProgramDb -> ProgramSearchPath
getProgramSearchPath = progSearchPath
setProgramSearchPath :: ProgramSearchPath -> ProgramDb -> ProgramDb
setProgramSearchPath searchpath db = db { progSearchPath = searchpath }
modifyProgramSearchPath :: (ProgramSearchPath -> ProgramSearchPath)
                        -> ProgramDb
                        -> ProgramDb
modifyProgramSearchPath f db =
  setProgramSearchPath (f $ getProgramSearchPath db) db
userSpecifyPath :: String   
                -> FilePath 
                -> ProgramDb -> ProgramDb
userSpecifyPath name path = updateUnconfiguredProgs $
  flip Map.update name $ \(prog, _, args) -> Just (prog, Just path, args)
userMaybeSpecifyPath :: String -> Maybe FilePath
                     -> ProgramDb -> ProgramDb
userMaybeSpecifyPath _    Nothing progdb     = progdb
userMaybeSpecifyPath name (Just path) progdb = userSpecifyPath name path progdb
userSpecifyArgs :: String    
                -> [ProgArg] 
                -> ProgramDb
                -> ProgramDb
userSpecifyArgs name args' =
    updateUnconfiguredProgs
      (flip Map.update name $
         \(prog, path, args) -> Just (prog, path, args ++ args'))
  . updateConfiguredProgs
      (flip Map.update name $
         \prog -> Just prog { programOverrideArgs = programOverrideArgs prog
                                                 ++ args' })
userSpecifyPaths :: [(String, FilePath)]
                 -> ProgramDb
                 -> ProgramDb
userSpecifyPaths paths progdb =
  foldl' (\progdb' (prog, path) -> userSpecifyPath prog path progdb') progdb paths
userSpecifyArgss :: [(String, [ProgArg])]
                 -> ProgramDb
                 -> ProgramDb
userSpecifyArgss argss progdb =
  foldl' (\progdb' (prog, args) -> userSpecifyArgs prog args progdb') progdb argss
userSpecifiedPath :: Program -> ProgramDb -> Maybe FilePath
userSpecifiedPath prog =
  join . fmap (\(_,p,_)->p) . Map.lookup (programName prog) . unconfiguredProgs
userSpecifiedArgs :: Program -> ProgramDb -> [ProgArg]
userSpecifiedArgs prog =
  maybe [] (\(_,_,as)->as) . Map.lookup (programName prog) . unconfiguredProgs
lookupProgram :: Program -> ProgramDb -> Maybe ConfiguredProgram
lookupProgram prog = Map.lookup (programName prog) . configuredProgs
updateProgram :: ConfiguredProgram -> ProgramDb
                                   -> ProgramDb
updateProgram prog = updateConfiguredProgs $
  Map.insert (programId prog) prog
configuredPrograms :: ProgramDb -> [ConfiguredProgram]
configuredPrograms = Map.elems . configuredProgs
configureProgram :: Verbosity
                 -> Program
                 -> ProgramDb
                 -> IO ProgramDb
configureProgram verbosity prog progdb = do
  let name = programName prog
  maybeLocation <- case userSpecifiedPath prog progdb of
    Nothing   ->
      programFindLocation prog verbosity (progSearchPath progdb)
      >>= return . fmap (swap . fmap FoundOnSystem . swap)
    Just path -> do
      absolute <- doesExecutableExist path
      if absolute
        then return (Just (UserSpecified path, []))
        else findProgramOnSearchPath verbosity (progSearchPath progdb) path
             >>= maybe (die' verbosity notFound)
                       (return . Just . swap . fmap UserSpecified . swap)
      where notFound = "Cannot find the program '" ++ name
                     ++ "'. User-specified path '"
                     ++ path ++ "' does not refer to an executable and "
                     ++ "the program is not on the system path."
  case maybeLocation of
    Nothing -> return progdb
    Just (location, triedLocations) -> do
      version <- programFindVersion prog verbosity (locationPath location)
      newPath <- programSearchPathAsPATHVar (progSearchPath progdb)
      let configuredProg        = ConfiguredProgram {
            programId           = name,
            programVersion      = version,
            programDefaultArgs  = [],
            programOverrideArgs = userSpecifiedArgs prog progdb,
            programOverrideEnv  = [("PATH", Just newPath)],
            programProperties   = Map.empty,
            programLocation     = location,
            programMonitorFiles = triedLocations
          }
      configuredProg' <- programPostConf prog verbosity configuredProg
      return (updateConfiguredProgs (Map.insert name configuredProg') progdb)
configurePrograms :: Verbosity
                  -> [Program]
                  -> ProgramDb
                  -> IO ProgramDb
configurePrograms verbosity progs progdb =
  foldM (flip (configureProgram verbosity)) progdb progs
unconfigureProgram :: String -> ProgramDb -> ProgramDb
unconfigureProgram progname =
  updateConfiguredProgs $ Map.delete progname
configureAllKnownPrograms :: Verbosity
                          -> ProgramDb
                          -> IO ProgramDb
configureAllKnownPrograms verbosity progdb =
  configurePrograms verbosity
    [ prog | (prog,_,_) <- Map.elems notYetConfigured ] progdb
  where
    notYetConfigured = unconfiguredProgs progdb
      `Map.difference` configuredProgs progdb
reconfigurePrograms :: Verbosity
                    -> [(String, FilePath)]
                    -> [(String, [ProgArg])]
                    -> ProgramDb
                    -> IO ProgramDb
reconfigurePrograms verbosity paths argss progdb = do
  configurePrograms verbosity progs
   . userSpecifyPaths paths
   . userSpecifyArgss argss
   $ progdb
  where
    progs = catMaybes [ lookupKnownProgram name progdb | (name,_) <- paths ]
requireProgram :: Verbosity -> Program -> ProgramDb
               -> IO (ConfiguredProgram, ProgramDb)
requireProgram verbosity prog progdb = do
  
  progdb' <- case lookupProgram prog progdb of
    Nothing -> configureProgram verbosity prog progdb
    Just _  -> return progdb
  case lookupProgram prog progdb' of
    Nothing             -> die' verbosity notFound
    Just configuredProg -> return (configuredProg, progdb')
  where notFound       = "The program '" ++ programName prog
                      ++ "' is required but it could not be found."
lookupProgramVersion
  :: Verbosity -> Program -> VersionRange -> ProgramDb
  -> IO (Either String (ConfiguredProgram, Version, ProgramDb))
lookupProgramVersion verbosity prog range programDb = do
  
  programDb' <- case lookupProgram prog programDb of
    Nothing -> configureProgram verbosity prog programDb
    Just _  -> return programDb
  case lookupProgram prog programDb' of
    Nothing                           -> return $! Left notFound
    Just configuredProg@ConfiguredProgram { programLocation = location } ->
      case programVersion configuredProg of
        Just version
          | withinRange version range ->
            return $! Right (configuredProg, version ,programDb')
          | otherwise                 ->
            return $! Left (badVersion version location)
        Nothing                       ->
          return $! Left (unknownVersion location)
  where notFound       = "The program '"
                      ++ programName prog ++ "'" ++ versionRequirement
                      ++ " is required but it could not be found."
        badVersion v l = "The program '"
                      ++ programName prog ++ "'" ++ versionRequirement
                      ++ " is required but the version found at "
                      ++ locationPath l ++ " is version " ++ display v
        unknownVersion l = "The program '"
                      ++ programName prog ++ "'" ++ versionRequirement
                      ++ " is required but the version of "
                      ++ locationPath l ++ " could not be determined."
        versionRequirement
          | isAnyVersion range = ""
          | otherwise          = " version " ++ display range
requireProgramVersion :: Verbosity -> Program -> VersionRange
                      -> ProgramDb
                      -> IO (ConfiguredProgram, Version, ProgramDb)
requireProgramVersion verbosity prog range programDb =
  join $ either (die' verbosity) return `fmap`
  lookupProgramVersion verbosity prog range programDb