{- | This is the central package of vabal-lib,
     it contains the functions to analyze a cabal package and determine
     which versions of ghc can comply with the constraints
     imposed on the following dependencies:

     * base

     * Cabal (only constraints in the setup-depends stanza are important)
-}
module PackageSolver
( analyzePackage
, doesGhcVersionSupportPackage
) where

import           Distribution.Types.GenericPackageDescription
import           Distribution.PackageDescription.Configuration
import           Distribution.Types.PackageDescription
import           Distribution.Types.ComponentRequestedSpec
import           Distribution.Types.Library
import           Distribution.Types.Executable
import           Distribution.Types.ForeignLib
import           Distribution.Types.BuildInfo
import           Distribution.Types.SetupBuildInfo
import           Distribution.Types.TestSuite
import           Distribution.Types.Benchmark
import           Distribution.Version
import           Distribution.Types.Dependency
import           Distribution.Types.PackageName
import           Distribution.System
import           Distribution.Compiler
import           Data.Maybe (fromMaybe, maybeToList)
import           Control.Arrow (second)
import           CompilerConditions
import           GhcDatabase
import qualified Data.Set as S

makeCompilerInfo :: Version -> CompilerInfo
makeCompilerInfo v = unknownCompilerInfo (CompilerId GHC v) NoAbiTag

isBase :: Dependency -> Bool
isBase (Dependency packageName _) = unPackageName packageName == "base"

isCabalLib :: Dependency -> Bool
isCabalLib (Dependency packageName _) = unPackageName packageName == "Cabal"

extractConstraints :: (Dependency -> Bool) -> [Dependency] -> VersionRange
extractConstraints predicate deps =
    let constraints = depVerRange <$> filter predicate deps
    in foldr intersectVersionRanges anyVersion constraints

-- | This is our dead simple dependency solver
-- that helps us choose a base version that satisfies constraints imposed externally
-- (i.e. by the user or by the ghc we are trying to use to configure the package)
queryDependency :: VersionRange -> Dependency -> Bool
queryDependency allowedBaseRange dep@(Dependency _ range)
   | isBase dep =  not . isNoVersion $ intersectVersionRanges range allowedBaseRange
   | otherwise  = True

configurePackage :: FlagAssignment
                 -> VersionRange
                 -> CompilerInfo
                 -> GenericPackageDescription
                 -> Maybe PackageDescription
configurePackage flags allowedBaseRange compiler pkgDescr =
    let configuredPkg = finalizePD flags
                        (ComponentRequestedSpec True True)
                        (queryDependency allowedBaseRange)
                        buildPlatform
                        compiler
                        []
                        pkgDescr
    in case configuredPkg of
         Left _ -> Nothing
         Right (pd, _) -> Just pd


-- | Extracts all base constraints in the Package
constraintsForBase :: PackageDescription -> VersionRange
constraintsForBase pkgDescr =
    let setupDependencies = setupDepends <$> maybeToList (setupBuildInfo pkgDescr)

        projectDependencies = map targetBuildDepends $ concat
                            [ libBuildInfo <$> maybeToList (library pkgDescr)
                            , libBuildInfo <$> subLibraries pkgDescr
                            , buildInfo <$> executables pkgDescr
                            , foreignLibBuildInfo <$> foreignLibs pkgDescr
                            , testBuildInfo <$> testSuites pkgDescr
                            , benchmarkBuildInfo <$> benchmarks pkgDescr
                            ]

        dependencies = setupDependencies ++ projectDependencies
        baseConstraints = map (extractConstraints isBase) dependencies
    in foldr intersectVersionRanges anyVersion baseConstraints

-- | Extracts Cabal constraints in the setup-depends section of the Package
constraintsForCabalInSetupDepends :: PackageDescription -> VersionRange
constraintsForCabalInSetupDepends pkgDescr =
    let setupDependencies = setupDepends <$> maybeToList (setupBuildInfo pkgDescr)
        cabalConstraints = map (extractConstraints isCabalLib) setupDependencies
    in foldr intersectVersionRanges anyVersion cabalConstraints


-- | Given a full configuration (FlagAssignment + Compiler version range),
-- find ghc versions compatible with the constraints.
findCandidate :: FlagAssignment
              -> GenericPackageDescription
              -> GhcDatabase
              -> (VersionRange, CompilerInfo)
              -> S.Set Version
findCandidate flags pkgDescr db (vr, ci) = fromMaybe mempty $ do
    configuredPkg <- configurePackage flags vr ci pkgDescr
    let baseConstraints = constraintsForBase configuredPkg
    let cabalLibConstraints = constraintsForCabalInSetupDepends configuredPkg
    let db' = filterBaseVersionIn db baseConstraints
    let db'' = filterMinCabalVersionIn db' cabalLibConstraints
    return $ ghcVersions db''

-- | 'analyzePackage' uses the provided flag assignment
-- to configure the package description.
-- It returns a set of all ghc versions found in the "GhcDatabase" that satisfy
-- the base and Cabal (only Cabal in setup-depends is considered) constraints.
analyzePackage :: FlagAssignment              -- ^ Flags to apply when configuring the project
               -> GhcDatabase                 -- ^ Database containing metadata for all ghc versions to consider
               -> GenericPackageDescription   -- ^ The raw project description cabal file
               -> S.Set Version               -- ^ The versions of ghc that comply with the constraints of base and Cabal
analyzePackage flags ghcDb pkgDescr = {-# SCC "vabal-core" #-}
    let compilers = map (second makeCompilerInfo) -- second makeCompiler :: (VersionRange, Version) -> (VersionRange, CompilerInfo)
                  $ genCompilerAssignments ghcDb pkgDescr

    in mconcat $ map (findCandidate flags pkgDescr ghcDb) compilers


-- | 'doesGhcVersionSupportPackage' checks that the provided compiler version
-- is able to comply with the constraints of
-- base and Cabal (only Cabal in setup-depends is considered) of the package.
doesGhcVersionSupportPackage :: FlagAssignment              -- ^ Flags to apply when configuring the project
                             -> GhcDatabase                 -- ^ Database containing metadata for all known ghc versions
                             -> GenericPackageDescription   -- ^ The raw project description cabal file
                             -> Version                     -- ^ The version of ghc the user wants to use
                             -> Bool                        -- ^ Returns whether the version of ghc the user wants is fine
doesGhcVersionSupportPackage flags ghcDb pkgDescr selectedGhcVersion = fromMaybe False $ do
    let ghc = makeCompilerInfo selectedGhcVersion
    configuredPkg <- configurePackage flags anyVersion ghc pkgDescr
    -- Check if the selected ghc is one of the suggested ones
    -- If we can't find the base version for the selected ghc,
    -- then we don't recognize it and we say it may not be good.
    -- Otherwise we check if its base version fits inside
    -- the suggested base version range
    let suggestedBaseVersionRange = constraintsForBase configuredPkg
        suggestedCabalLibVersionRange = constraintsForCabalInSetupDepends configuredPkg
        selectedGhcBaseVersion = baseVersionForGhc ghcDb selectedGhcVersion
        selectedGhcCabalLibRange = cabalLibRangeForGhc ghcDb selectedGhcVersion


    baseVersionIsFine <- (`withinRange` suggestedBaseVersionRange)
                      <$> selectedGhcBaseVersion

    cabalVersionIsFine <- not
                     . isNoVersion
                     . intersectVersionRanges suggestedCabalLibVersionRange
                     <$> selectedGhcCabalLibRange

    return $ baseVersionIsFine && cabalVersionIsFine