-----------------------------------------------------------------------------
-- |
-- Module      :  Distribution.Client.Init.Command
-- Copyright   :  (c) Brent Yorgey 2009
-- License     :  BSD-like
--
-- Maintainer  :  cabal-devel@haskell.org
-- Stability   :  provisional
-- Portability :  portable
--
-- Implementation of the 'cabal init' command, which creates an initial .cabal
-- file for a project.
--
-----------------------------------------------------------------------------

module Distribution.Client.Init.Command
  ( -- * Commands
    initCabal
  , incVersion

    -- * Helpers
  , getSimpleProject
  , getLibOrExec
  , getCabalVersion
  , getPackageName
  , getVersion
  , getLicense
  , getAuthorInfo
  , getHomepage
  , getSynopsis
  , getCategory
  , getExtraSourceFiles
  , getAppDir
  , getSrcDir
  , getGenTests
  , getTestDir
  , getLanguage
  , getGenComments
  , getModulesBuildToolsAndDeps
  ) where

import Prelude ()
import Distribution.Client.Compat.Prelude hiding (empty)

import System.IO
  ( hSetBuffering, stdout, BufferMode(..) )
import System.Directory
  ( getCurrentDirectory, doesDirectoryExist, getDirectoryContents )
import System.FilePath
  ( (</>), takeBaseName, equalFilePath )

import qualified Data.List.NonEmpty as NE
import qualified Data.Map as M
import Control.Monad
  ( (>=>) )
import Control.Arrow
  ( (&&&), (***) )

import Distribution.CabalSpecVersion
  ( CabalSpecVersion (..), showCabalSpecVersion )
import Distribution.Version
  ( Version, mkVersion, alterVersion, majorBoundVersion
  , orLaterVersion, earlierVersion, intersectVersionRanges, VersionRange )
import Distribution.ModuleName
  ( ModuleName )  -- And for the Text instance
import Distribution.InstalledPackageInfo
  ( InstalledPackageInfo, exposed )
import qualified Distribution.Package as P
import qualified Distribution.SPDX as SPDX
import Language.Haskell.Extension ( Language(..) )

import Distribution.Client.Init.Defaults
  ( defaultApplicationDir, defaultCabalVersion, myLibModule, defaultSourceDir )
import Distribution.Client.Init.FileCreators
  ( writeLicense, writeChangeLog, createDirectories, createLibHs, createMainHs
  , createTestSuiteIfEligible, writeCabalFile )
import Distribution.Client.Init.Prompt
  ( prompt, promptYesNo, promptStr, promptList, maybePrompt
  , promptListOptional )
import Distribution.Client.Init.Utils
  ( eligibleForTestSuite,  message )
import Distribution.Client.Init.Types
  ( InitFlags(..), PackageType(..), Category(..)
  , displayPackageType )
import Distribution.Client.Init.Heuristics
  ( guessPackageName, guessAuthorNameMail, guessMainFileCandidates,
    SourceFileEntry(..),
    scanForModules, neededBuildPrograms )

import Distribution.Simple.Flag
  ( maybeToFlag )
import Distribution.Simple.Setup
  ( Flag(..), flagToMaybe )
import Distribution.Simple.Configure
  ( getInstalledPackages )
import Distribution.Simple.Compiler
  ( PackageDBStack, Compiler )
import Distribution.Simple.Program
  ( ProgramDb )
import Distribution.Simple.PackageIndex
  ( InstalledPackageIndex, moduleNameIndex )
import Distribution.Simple.Utils
  ( die' )

import Distribution.Solver.Types.PackageIndex
  ( elemByPackageName )

import Distribution.Client.IndexUtils
  ( getSourcePackages )
import Distribution.Client.Types
  ( SourcePackageDb(..) )
import Distribution.Client.Setup
  ( RepoContext(..) )

initCabal :: Verbosity
          -> PackageDBStack
          -> RepoContext
          -> Compiler
          -> ProgramDb
          -> InitFlags
          -> IO ()
initCabal verbosity packageDBs repoCtxt comp progdb initFlags = do

  installedPkgIndex <- getInstalledPackages verbosity comp packageDBs progdb
  sourcePkgDb <- getSourcePackages verbosity repoCtxt

  hSetBuffering stdout NoBuffering

  initFlags' <- extendFlags verbosity installedPkgIndex sourcePkgDb initFlags

  case license initFlags' of
    Flag SPDX.NONE -> return ()
    _              -> writeLicense initFlags'
  writeChangeLog initFlags'
  createDirectories (sourceDirs initFlags')
  createLibHs initFlags'
  createDirectories (applicationDirs initFlags')
  createMainHs initFlags'
  createTestSuiteIfEligible initFlags'
  success <- writeCabalFile initFlags'

  when success $ generateWarnings initFlags'

---------------------------------------------------------------------------
--  Flag acquisition  -----------------------------------------------------
---------------------------------------------------------------------------

-- | Fill in more details in InitFlags by guessing, discovering, or prompting
-- the user.
extendFlags :: Verbosity -> InstalledPackageIndex -> SourcePackageDb -> InitFlags -> IO InitFlags
extendFlags verbosity pkgIx sourcePkgDb =
      getSimpleProject
  >=> getLibOrExec
  >=> getCabalVersion
  >=> getPackageName verbosity sourcePkgDb False
  >=> getVersion
  >=> getLicense
  >=> getAuthorInfo
  >=> getHomepage
  >=> getSynopsis
  >=> getCategory
  >=> getExtraSourceFiles
  >=> getAppDir
  >=> getSrcDir
  >=> getGenTests
  >=> getTestDir
  >=> getLanguage
  >=> getGenComments
  >=> getModulesBuildToolsAndDeps pkgIx

-- | Combine two actions which may return a value, preferring the first. That
--   is, run the second action only if the first doesn't return a value.
infixr 1 ?>>
(?>>) :: IO (Maybe a) -> IO (Maybe a) -> IO (Maybe a)
f ?>> g = do
  ma <- f
  if isJust ma
    then return ma
    else g

-- | Ask if a simple project with sensible defaults should be created.
getSimpleProject :: InitFlags -> IO InitFlags
getSimpleProject flags = do
  simpleProj <-     return (flagToMaybe $ simpleProject flags)
                ?>> maybePrompt flags
                    (promptYesNo
                      "Should I generate a simple project with sensible defaults"
                      (Just True))
  return $ case maybeToFlag simpleProj of
    Flag True ->
      flags { interactive = Flag False
            , simpleProject = Flag True
            , packageType = Flag LibraryAndExecutable
            , cabalVersion = Flag defaultCabalVersion
            }
    simpleProjFlag@_ ->
      flags { simpleProject = simpleProjFlag }


-- | Get the version of the cabal spec to use.
--
-- The spec version can be specified by the InitFlags cabalVersion field. If
-- none is specified then the user is prompted to pick from a list of
-- supported versions (see code below).
getCabalVersion :: InitFlags -> IO InitFlags
getCabalVersion flags = do
  cabVer <-     return (flagToMaybe $ cabalVersion flags)
            ?>> maybePrompt flags (either (const defaultCabalVersion) id `fmap`
                                  promptList "Please choose version of the Cabal specification to use"
                                  [CabalSpecV1_10, CabalSpecV2_0, CabalSpecV2_2, CabalSpecV2_4, CabalSpecV3_0]
                                  (Just defaultCabalVersion) displayCabalVersion False)
            ?>> return (Just defaultCabalVersion)

  return $  flags { cabalVersion = maybeToFlag cabVer }

  where
    displayCabalVersion :: CabalSpecVersion -> String
    displayCabalVersion v = case v of
      CabalSpecV1_10 -> "1.10   (legacy)"
      CabalSpecV2_0  -> "2.0    (+ support for Backpack, internal sub-libs, '^>=' operator)"
      CabalSpecV2_2  -> "2.2    (+ support for 'common', 'elif', redundant commas, SPDX)"
      CabalSpecV2_4  -> "2.4    (+ support for '**' globbing)"
      CabalSpecV3_0  -> "3.0    (+ set notation for ==, common stanzas in ifs, more redundant commas, better pkgconfig-depends)"
      _              -> showCabalSpecVersion v



-- | Get the package name: use the package directory (supplied, or the current
--   directory by default) as a guess. It looks at the SourcePackageDb to avoid
--   using an existing package name.
getPackageName :: Verbosity -> SourcePackageDb -> Bool -> InitFlags -> IO InitFlags
getPackageName verbosity sourcePkgDb forceAsk flags = do
  guess <- maybe (getCurrentDirectory >>= guessPackageName) pure
             =<< traverse guessPackageName (flagToMaybe $ packageDir flags)

  pkgName' <- case (flagToMaybe $ packageName flags) >>= maybeForceAsk of
    Just pkgName -> return $ Just $ pkgName
    _ -> maybePrompt flags (prompt "Package name" (Just guess))
  let pkgName = fromMaybe guess pkgName'

  chooseAgain <- if isPkgRegistered pkgName
                   then do
                     answer' <- maybePrompt flags (promptYesNo (promptOtherNameMsg pkgName) (Just True))
                     case answer' of
                       Just answer -> return answer
                       _ -> die' verbosity $ inUseMsg pkgName
                 else
                   return False

  if chooseAgain
    then getPackageName verbosity sourcePkgDb True flags
    else return $ flags { packageName = Flag pkgName }

  where
    maybeForceAsk x = if forceAsk then Nothing else Just x

    isPkgRegistered pkg = elemByPackageName (packageIndex sourcePkgDb) pkg

    inUseMsg pkgName = "The name " ++ (P.unPackageName pkgName) ++
                       " is already in use by another package on Hackage."

    promptOtherNameMsg pkgName = (inUseMsg pkgName) ++
                                 " Do you want to choose a different name"

-- | Package version: use 0.1.0.0 as a last resort, but try prompting the user
--  if possible.
getVersion :: InitFlags -> IO InitFlags
getVersion flags = do
  let v = Just $ mkVersion [0,1,0,0]
  v' <-     return (flagToMaybe $ version flags)
        ?>> maybePrompt flags (prompt "Package version" v)
        ?>> return v
  return $ flags { version = maybeToFlag v' }

-- | Choose a license for the package.
--
-- The license can come from Initflags (license field), if it is not present
-- then prompt the user from a predefined list of licenses.
getLicense :: InitFlags -> IO InitFlags
getLicense flags = do
  elic <- return (fmap Right $ flagToMaybe $ license flags)
      ?>> maybePrompt flags (promptList "Please choose a license" listedLicenses (Just SPDX.NONE) prettyShow True)

  case elic of
      Nothing          -> return flags { license = NoFlag }
      Just (Right lic) -> return flags { license = Flag lic }
      Just (Left str)  -> case eitherParsec str of
          Right lic -> return flags { license = Flag lic }
          -- on error, loop
          Left err -> do
              putStrLn "The license must be a valid SPDX expression."
              putStrLn err
              getLicense flags
  where
    -- perfectly we'll have this and writeLicense (in FileCreators)
    -- in a single file
    listedLicenses =
      SPDX.NONE :
      map (\lid -> SPDX.License (SPDX.ELicense (SPDX.ELicenseId lid) Nothing))
      [ SPDX.BSD_2_Clause
      , SPDX.BSD_3_Clause
      , SPDX.Apache_2_0
      , SPDX.MIT
      , SPDX.MPL_2_0
      , SPDX.ISC

      , SPDX.GPL_2_0_only
      , SPDX.GPL_3_0_only
      , SPDX.LGPL_2_1_only
      , SPDX.LGPL_3_0_only
      , SPDX.AGPL_3_0_only

      , SPDX.GPL_2_0_or_later
      , SPDX.GPL_3_0_or_later
      , SPDX.LGPL_2_1_or_later
      , SPDX.LGPL_3_0_or_later
      , SPDX.AGPL_3_0_or_later
      ]

-- | The author's name and email. Prompt, or try to guess from an existing
--   darcs repo.
getAuthorInfo :: InitFlags -> IO InitFlags
getAuthorInfo flags = do
  (authorName, authorEmail)  <-
    (flagToMaybe *** flagToMaybe) `fmap` guessAuthorNameMail
  authorName'  <-     return (flagToMaybe $ author flags)
                  ?>> maybePrompt flags (promptStr "Author name" authorName)
                  ?>> return authorName

  authorEmail' <-     return (flagToMaybe $ email flags)
                  ?>> maybePrompt flags (promptStr "Maintainer email" authorEmail)
                  ?>> return authorEmail

  return $ flags { author = maybeToFlag authorName'
                 , email  = maybeToFlag authorEmail'
                 }

-- | Prompt for a homepage URL for the package.
getHomepage :: InitFlags -> IO InitFlags
getHomepage flags = do
  hp  <- queryHomepage
  hp' <-     return (flagToMaybe $ homepage flags)
         ?>> maybePrompt flags (promptStr "Project homepage URL" hp)
         ?>> return hp

  return $ flags { homepage = maybeToFlag hp' }

-- | Right now this does nothing, but it could be changed to do some
--   intelligent guessing.
queryHomepage :: IO (Maybe String)
queryHomepage = return Nothing     -- get default remote darcs repo?

-- | Prompt for a project synopsis.
getSynopsis :: InitFlags -> IO InitFlags
getSynopsis flags = do
  syn <-     return (flagToMaybe $ synopsis flags)
         ?>> maybePrompt flags (promptStr "Project synopsis" Nothing)

  return $ flags { synopsis = maybeToFlag syn }

-- | Prompt for a package category.
--   Note that it should be possible to do some smarter guessing here too, i.e.
--   look at the name of the top level source directory.
getCategory :: InitFlags -> IO InitFlags
getCategory flags = do
  cat <-     return (flagToMaybe $ category flags)
         ?>> fmap join (maybePrompt flags
                         (promptListOptional "Project category" [Codec ..]))
  return $ flags { category = maybeToFlag cat }

-- | Try to guess extra source files (don't prompt the user).
getExtraSourceFiles :: InitFlags -> IO InitFlags
getExtraSourceFiles flags = do
  extraSrcFiles <-     return (extraSrc flags)
                   ?>> Just `fmap` guessExtraSourceFiles flags

  return $ flags { extraSrc = extraSrcFiles }

defaultChangeLog :: FilePath
defaultChangeLog = "CHANGELOG.md"

-- | Try to guess things to include in the extra-source-files field.
--   For now, we just look for things in the root directory named
--   'readme', 'changes', or 'changelog', with any sort of
--   capitalization and any extension.
guessExtraSourceFiles :: InitFlags -> IO [FilePath]
guessExtraSourceFiles flags = do
  dir <-
    maybe getCurrentDirectory return . flagToMaybe $ packageDir flags
  files <- getDirectoryContents dir
  let extraFiles = filter isExtra files
  if any isLikeChangeLog extraFiles
    then return extraFiles
    else return (defaultChangeLog : extraFiles)

  where
    isExtra = likeFileNameBase ("README" : changeLogLikeBases)
    isLikeChangeLog = likeFileNameBase changeLogLikeBases
    likeFileNameBase candidates = (`elem` candidates) . map toUpper . takeBaseName
    changeLogLikeBases = ["CHANGES", "CHANGELOG"]

-- | Ask whether the project builds a library or executable.
getLibOrExec :: InitFlags -> IO InitFlags
getLibOrExec flags = do
  pkgType <-     return (flagToMaybe $ packageType flags)
           ?>> maybePrompt flags (either (const Executable) id `fmap`
                                   promptList "What does the package build"
                                   [Executable, Library, LibraryAndExecutable]
                                   Nothing displayPackageType False)
           ?>> return (Just Executable)

  -- If this package contains an executable, get the main file name.
  mainFile <- if pkgType == Just Library then return Nothing else
                    getMainFile flags

  return $ flags { packageType = maybeToFlag pkgType
                 , mainIs = maybeToFlag mainFile
                 }


-- | Try to guess the main file of the executable, and prompt the user to choose
-- one of them. Top-level modules including the word 'Main' in the file name
-- will be candidates, and shorter filenames will be preferred.
getMainFile :: InitFlags -> IO (Maybe FilePath)
getMainFile flags =
  return (flagToMaybe $ mainIs flags)
  ?>> do
    candidates <- guessMainFileCandidates flags
    let showCandidate = either (++" (does not yet exist, but will be created)") id
        defaultFile = listToMaybe candidates
    maybePrompt flags (either id (either id id) `fmap`
                       promptList "What is the main module of the executable"
                       candidates
                       defaultFile showCandidate True)
      ?>> return (fmap (either id id) defaultFile)

-- | Ask if a test suite should be generated for the library.
getGenTests :: InitFlags -> IO InitFlags
getGenTests flags = do
  genTests <-     return (flagToMaybe $ initializeTestSuite flags)
                  -- Only generate a test suite if the package contains a library.
              ?>> if (packageType flags) == Flag Executable then return (Just False) else return Nothing
              ?>> maybePrompt flags
                  (promptYesNo
                    "Should I generate a test suite for the library"
                    (Just True))
  return $ flags { initializeTestSuite = maybeToFlag genTests }

-- | Ask for the test suite root directory.
getTestDir :: InitFlags -> IO InitFlags
getTestDir flags = do
  dirs <- return (testDirs flags)
              -- Only need testDirs when test suite generation is enabled.
          ?>> if not (eligibleForTestSuite flags) then return (Just []) else return Nothing
          ?>> fmap (fmap ((:[]) . either id id)) (maybePrompt
                   flags
                   (promptList "Test directory" ["test"] (Just "test") id True))

  return $ flags { testDirs = dirs }

-- | Ask for the Haskell base language of the package.
getLanguage :: InitFlags -> IO InitFlags
getLanguage flags = do
  lang <-     return (flagToMaybe $ language flags)
          ?>> maybePrompt flags
                (either UnknownLanguage id `fmap`
                  promptList "What base language is the package written in"
                  [Haskell2010, Haskell98]
                  (Just Haskell2010) prettyShow True)
          ?>> return (Just Haskell2010)

  if invalidLanguage lang
    then putStrLn invalidOtherLanguageMsg >> getLanguage flags
    else return $ flags { language = maybeToFlag lang }

  where
    invalidLanguage (Just (UnknownLanguage t)) = any (not . isAlphaNum) t
    invalidLanguage _ = False

    invalidOtherLanguageMsg = "\nThe language must be alphanumeric. " ++
                              "Please enter a different language."

-- | Ask whether to generate explanatory comments.
getGenComments :: InitFlags -> IO InitFlags
getGenComments flags = do
  genComments <-     return (not <$> flagToMaybe (noComments flags))
                 ?>> maybePrompt flags (promptYesNo promptMsg (Just False))
                 ?>> return (Just False)
  return $ flags { noComments = maybeToFlag (fmap not genComments) }
  where
    promptMsg = "Add informative comments to each field in the cabal file (y/n)"

-- | Ask for the application root directory.
getAppDir :: InitFlags -> IO InitFlags
getAppDir flags = do
  appDirs <- noAppDirIfLibraryOnly
    ?>> guessAppDir flags
    ?>> promptUserForApplicationDir
    ?>> setDefault
  return $ flags { applicationDirs = appDirs }
  where
    -- If the packageType==Library, ignore defined appdir.
    noAppDirIfLibraryOnly :: IO (Maybe [String])
    noAppDirIfLibraryOnly
      | packageType flags == Flag Library = return $ Just []
      | otherwise = return $ applicationDirs flags

    -- Set the default application directory.
    setDefault :: IO (Maybe [String])
    setDefault = pure (Just [defaultApplicationDir])

    -- Prompt the user for the application directory (defaulting to "app").
    -- Returns 'Nothing' if in non-interactive mode, otherwise will always
    -- return a 'Just' value ('Just []' if no separate application directory).
    promptUserForApplicationDir :: IO (Maybe [String])
    promptUserForApplicationDir = fmap (either (:[]) id) <$> maybePrompt
      flags
      (promptList
       ("Application " ++ mainFile ++ "directory")
       [[defaultApplicationDir], ["src-exe"], []]
        (Just [defaultApplicationDir])
       showOption True)

    showOption :: [String] -> String
    showOption [] = "(none)"
    showOption (x:_) = x

    -- The name
    mainFile :: String
    mainFile = case mainIs flags of
      Flag mainPath -> "(" ++ mainPath ++ ") "
      _             -> ""

-- | Try to guess app directory. Could try harder; for the
--   moment just looks to see whether there is a directory called 'app'.
guessAppDir :: InitFlags -> IO (Maybe [String])
guessAppDir flags = do
  dir      <- maybe getCurrentDirectory return . flagToMaybe $ packageDir flags
  appIsDir <- doesDirectoryExist (dir </> "app")
  return $ if appIsDir
             then Just ["app"]
             else Nothing

-- | Ask for the source (library) root directory.
getSrcDir :: InitFlags -> IO InitFlags
getSrcDir flags = do
  srcDirs <- noSourceDirIfExecutableOnly
    ?>> guessSourceDir flags
    ?>> promptUserForSourceDir
    ?>> setDefault

  return $ flags { sourceDirs = srcDirs }

  where
    -- If the packageType==Executable, then ignore source dir
    noSourceDirIfExecutableOnly :: IO (Maybe [String])
    noSourceDirIfExecutableOnly
      | packageType flags == Flag Executable = return $ Just []
      | otherwise = return $ sourceDirs flags

    -- Set the default source directory.
    setDefault :: IO (Maybe [String])
    setDefault = pure (Just [defaultSourceDir])

    -- Prompt the user for the source directory (defaulting to "app").
    -- Returns 'Nothing' if in non-interactive mode, otherwise will always
    -- return a 'Just' value ('Just []' if no separate application directory).
    promptUserForSourceDir :: IO (Maybe [String])
    promptUserForSourceDir = fmap (either (:[]) id) <$> maybePrompt
      flags
      (promptList
       ("Library source directory")
       [[defaultSourceDir], ["lib"], ["src-lib"], []]
        (Just [defaultSourceDir])
       showOption True)

    showOption :: [String] -> String
    showOption [] = "(none)"
    showOption (x:_) = x


-- | Try to guess source directory. Could try harder; for the
--   moment just looks to see whether there is a directory called 'src'.
guessSourceDir :: InitFlags -> IO (Maybe [String])
guessSourceDir flags = do
  dir      <-
    maybe getCurrentDirectory return . flagToMaybe $ packageDir flags
  srcIsDir <- doesDirectoryExist (dir </> "src")
  return $ if srcIsDir
             then Just ["src"]
             else Nothing

-- | Check whether a potential source file is located in one of the
--   source directories.
isSourceFile :: Maybe [FilePath] -> SourceFileEntry -> Bool
isSourceFile Nothing        sf = isSourceFile (Just ["."]) sf
isSourceFile (Just srcDirs) sf = any (equalFilePath (relativeSourcePath sf)) srcDirs

-- | Get the list of exposed modules and extra tools needed to build them.
getModulesBuildToolsAndDeps :: InstalledPackageIndex -> InitFlags -> IO InitFlags
getModulesBuildToolsAndDeps pkgIx flags = do
  dir <- maybe getCurrentDirectory return . flagToMaybe $ packageDir flags

  sourceFiles0 <- scanForModules dir

  let sourceFiles = filter (isSourceFile (sourceDirs flags)) sourceFiles0

  Just mods <-      return (exposedModules flags)
           ?>> (return . Just . map moduleName $ sourceFiles)

  tools <-     return (buildTools flags)
           ?>> (return . Just . neededBuildPrograms $ sourceFiles)

  deps <-      return (dependencies flags)
           ?>> Just <$> importsToDeps flags
                        (fromString "Prelude" :  -- to ensure we get base as a dep
                           (   nub   -- only need to consider each imported package once
                             . filter (`notElem` mods)  -- don't consider modules from
                                                        -- this package itself
                             . concatMap imports
                             $ sourceFiles
                           )
                        )
                        pkgIx

  exts <-     return (otherExts flags)
          ?>> (return . Just . nub . concatMap extensions $ sourceFiles)

  -- If we're initializing a library and there were no modules discovered
  -- then create an empty 'MyLib' module.
  -- This gets a little tricky when 'sourceDirs' == 'applicationDirs' because
  -- then the executable needs to set 'other-modules: MyLib' or else the build
  -- fails.
  let (finalModsList, otherMods) = case (packageType flags, mods) of

        -- For an executable leave things as they are.
        (Flag Executable, _) -> (mods, otherModules flags)

        -- If a non-empty module list exists don't change anything.
        (_, (_:_)) -> (mods, otherModules flags)

        -- Library only: 'MyLib' in 'other-modules' only.
        (Flag Library, _) -> ([myLibModule], Nothing)

        -- For a 'LibraryAndExecutable' we need to have special handling.
        -- If we don't have a module list (Nothing or empty), then create a Lib.
        (_, []) ->
          if sourceDirs flags == applicationDirs flags
          then ([myLibModule], Just [myLibModule])
          else ([myLibModule], Nothing)

  return $ flags { exposedModules = Just finalModsList
                 , otherModules   = otherMods
                 , buildTools     = tools
                 , dependencies   = deps
                 , otherExts      = exts
                 }

-- | Given a list of imported modules, retrieve the list of dependencies that
-- provide those modules.
importsToDeps :: InitFlags -> [ModuleName] -> InstalledPackageIndex -> IO [P.Dependency]
importsToDeps flags mods pkgIx = do

  let modMap :: M.Map ModuleName [InstalledPackageInfo]
      modMap  = M.map (filter exposed) $ moduleNameIndex pkgIx

      modDeps :: [(ModuleName, Maybe [InstalledPackageInfo])]
      modDeps = map (id &&& flip M.lookup modMap) mods

  message flags "\nGuessing dependencies..."
  nub . catMaybes <$> traverse (chooseDep flags) modDeps

-- Given a module and a list of installed packages providing it,
-- choose a dependency (i.e. package + version range) to use for that
-- module.
chooseDep :: InitFlags -> (ModuleName, Maybe [InstalledPackageInfo])
          -> IO (Maybe P.Dependency)

chooseDep flags (m, Nothing)
  = message flags ("\nWarning: no package found providing " ++ prettyShow m ++ ".")
    >> return Nothing

chooseDep flags (m, Just [])
  = message flags ("\nWarning: no package found providing " ++ prettyShow m ++ ".")
    >> return Nothing

    -- We found some packages: group them by name.
chooseDep flags (m, Just ps)
  = case pkgGroups of
      -- if there's only one group, i.e. multiple versions of a single package,
      -- we make it into a dependency, choosing the latest-ish version (see toDep).
      [grp] -> Just <$> toDep grp
      -- otherwise, we refuse to choose between different packages and make the user
      -- do it.
      grps  -> do message flags ("\nWarning: multiple packages found providing "
                                 ++ prettyShow m
                                 ++ ": " ++ intercalate ", " (fmap (prettyShow . P.pkgName . NE.head) grps))
                  message flags "You will need to pick one and manually add it to the Build-depends: field."
                  return Nothing
  where
    pkgGroups = NE.groupBy ((==) `on` P.pkgName) (map P.packageId ps)

    desugar = maybe True (< CabalSpecV2_0) $ flagToMaybe (cabalVersion flags)

    -- Given a list of available versions of the same package, pick a dependency.
    toDep :: NonEmpty P.PackageIdentifier -> IO P.Dependency

    -- If only one version, easy.  We change e.g. 0.4.2  into  0.4.*
    toDep (pid:|[]) = return $ P.Dependency (P.pkgName pid) (pvpize desugar . P.pkgVersion $ pid) P.mainLibSet --TODO sublibraries

    -- Otherwise, choose the latest version and issue a warning.
    toDep pids  = do
      message flags ("\nWarning: multiple versions of " ++ prettyShow (P.pkgName . NE.head $ pids) ++ " provide " ++ prettyShow m ++ ", choosing the latest.")
      return $ P.Dependency (P.pkgName . NE.head $ pids)
                            (pvpize desugar . maximum . fmap P.pkgVersion $ pids)
                            P.mainLibSet --TODO take into account sublibraries

-- | Given a version, return an API-compatible (according to PVP) version range.
--
-- If the boolean argument denotes whether to use a desugared
-- representation (if 'True') or the new-style @^>=@-form (if
-- 'False').
--
-- Example: @pvpize True (mkVersion [0,4,1])@ produces the version range @>= 0.4 && < 0.5@ (which is the
-- same as @0.4.*@).
pvpize :: Bool -> Version -> VersionRange
pvpize False  v = majorBoundVersion v
pvpize True   v = orLaterVersion v'
           `intersectVersionRanges`
           earlierVersion (incVersion 1 v')
  where v' = alterVersion (take 2) v

-- | Increment the nth version component (counting from 0).
incVersion :: Int -> Version -> Version
incVersion n = alterVersion (incVersion' n)
  where
    incVersion' 0 []     = [1]
    incVersion' 0 (v:_)  = [v+1]
    incVersion' m []     = replicate m 0 ++ [1]
    incVersion' m (v:vs) = v : incVersion' (m-1) vs

-- | Generate warnings for missing fields etc.
generateWarnings :: InitFlags -> IO ()
generateWarnings flags = do
  message flags ""
  when (synopsis flags `elem` [NoFlag, Flag ""])
       (message flags "Warning: no synopsis given. You should edit the .cabal file and add one.")

  message flags "You may want to edit the .cabal file and add a Description field."
