packages feed

hpack-0.39.6: src/Hpack/Config.hs

{-# LANGUAGE NamedFieldPuns #-}
{-# LANGUAGE ConstraintKinds #-}
{-# LANGUAGE CPP #-}
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE DeriveAnyClass #-}
{-# LANGUAGE DeriveFoldable #-}
{-# LANGUAGE DeriveFunctor #-}
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE DeriveTraversable #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE LiberalTypeSynonyms #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE ViewPatterns #-}
module Hpack.Config (
-- | /__NOTE:__/ This module is exposed to allow integration of Hpack into
-- other tools.  It is not meant for general use by end users.  The following
-- caveats apply:
--
-- * The API is undocumented, consult the source instead.
--
-- * The exposed types and functions primarily serve Hpack's own needs, not
-- that of a public API.  Breaking changes can happen as Hpack evolves.
--
-- As an Hpack user you either want to use the @hpack@ executable or a build
-- tool that supports Hpack (e.g. @stack@ or @cabal2nix@).

  DecodeOptions(..)
, ProgramName(..)
, defaultDecodeOptions
, packageConfig
, DecodeResult(..)
, readPackageConfig
, readPackageConfigWithError

, renamePackage
, packageDependencies
, package
, section
, Package(..)
, CabalVersion(..)
, makeCabalVersion
, Dependencies(..)
, DependencyInfo(..)
, VersionConstraint(..)
, DependencyVersion(..)
, SourceDependency(..)
, GitRef
, GitUrl
, BuildTool(..)
, SystemBuildTools(..)
, GhcOption
, Verbatim(..)
, VerbatimValue(..)
, verbatimValueToString
, CustomSetup(..)
, Section(..)
, Library(..)
, Executable(..)
, Conditional(..)
, Cond(..)
, Flag(..)
, SourceRepository(..)
, Language(..)
, BuildType(..)
, GhcProfOption
, GhcjsOption
, CppOption
, AsmOption
, CcOption
, LdOption
, Path(..)
, Module(..)
#ifdef TEST
, renameDependencies
, Empty(..)
, pathsModuleFromPackageName

, LibrarySection(..)
, fromLibrarySectionInConditional
, formatOrList

, toBuildTool
#endif
) where

import           Imports

import           Data.Either
import           Data.Bitraversable
import           Data.Map.Lazy (Map)
import qualified Data.Map.Lazy as Map
import qualified Data.Aeson.Config.KeyMap as KeyMap
import           Data.Maybe
import           Data.Monoid (Last(..))
import           Data.Ord
import qualified Data.Text as T
import           Data.Text.Encoding (decodeUtf8)
import           Data.Scientific (Scientific)
import           System.Directory
import           System.FilePath
import           Control.Monad.State (MonadState, StateT, evalStateT)
import qualified Control.Monad.State as State
import           Control.Monad.Writer (MonadWriter, WriterT, runWriterT, tell)
import           Control.Monad.Except
import           Data.Version (Version, showVersion)
import qualified Data.Version as Version

import           Distribution.Pretty (prettyShow)
import qualified Distribution.SPDX.License as SPDX

import qualified Data.Yaml.Pretty as Yaml
import           Data.Aeson (object, (.=))
import           Data.Aeson.Config.Types
import           Data.Aeson.Config.FromValue hiding (decodeValue)
import qualified Data.Aeson.Config.FromValue as Config

import           Hpack.Error
import           Hpack.Syntax.Defaults
import           Hpack.Util hiding (expandGlobs)
import qualified Hpack.Util as Util
import           Hpack.Defaults
import qualified Hpack.Yaml as Yaml
import           Hpack.Syntax.DependencyVersion
import           Hpack.Syntax.Dependencies
import           Hpack.Syntax.BuildTools
import           Hpack.License
import           Hpack.CabalFile (parseVersion)
import           Hpack.Module

import qualified Path

import qualified Paths_hpack as Hpack (version)

defaultCabalVersion :: Version
defaultCabalVersion = Version.makeVersion [1,12]

package :: String -> String -> Package
package name version = Package {
    packageCabalVersion = CabalVersion defaultCabalVersion
  , packageName = name
  , packageVersion = version
  , packageSynopsis = Nothing
  , packageDescription = Nothing
  , packageHomepage = Nothing
  , packageBugReports = Nothing
  , packageCategory = []
  , packageStability = Nothing
  , packageAuthor = []
  , packageMaintainer = []
  , packageCopyright = []
  , packageBuildType = Simple
  , packageLicense = Nothing
  , packageLicenseFile = []
  , packageTestedWith = []
  , packageFlags = []
  , packageExtraSourceFiles = []
  , packageExtraDocFiles = []
  , packageExtraFiles = []
  , packageDataFiles = []
  , packageDataDir = Nothing
  , packageSourceRepository = Nothing
  , packageCustomSetup = Nothing
  , packageLibrary = Nothing
  , packageInternalLibraries = mempty
  , packageExecutables = mempty
  , packageTests = mempty
  , packageBenchmarks = mempty
  , packageVerbatim = []
  }

renamePackage :: String -> Package -> Package
renamePackage name p@Package{..} = p {
    packageName = name
  , packageExecutables = fmap (renameDependencies packageName name) packageExecutables
  , packageTests = fmap (renameDependencies packageName name) packageTests
  , packageBenchmarks = fmap (renameDependencies packageName name) packageBenchmarks
  }

renameDependencies :: String -> String -> Section a -> Section a
renameDependencies old new sect@Section{..} = sect {sectionDependencies = (Dependencies . Map.fromList . map rename . Map.toList . unDependencies) sectionDependencies, sectionConditionals = map renameConditional sectionConditionals}
  where
    rename dep@(name, version)
      | name == old = (new, version)
      | otherwise = dep

    renameConditional :: Conditional (Section a) -> Conditional (Section a)
    renameConditional (Conditional condition then_ else_) = Conditional condition (renameDependencies old new then_) (renameDependencies old new <$> else_)

packageDependencies :: Package -> [(String, DependencyInfo)]
packageDependencies Package{..} = nub . sortBy (comparing (lexicographically . fst)) $
     (concatMap deps packageExecutables)
  ++ (concatMap deps packageTests)
  ++ (concatMap deps packageBenchmarks)
  ++ maybe [] deps packageLibrary
  where
    deps xs = [(name, info) | (name, info) <- (Map.toList . unDependencies . sectionDependencies) xs]

section :: a -> Section a
section a = Section a [] mempty [] [] [] Nothing [] [] [] [] [] [] [] [] [] [] [] [] [] [] [] [] [] [] [] [] Nothing [] mempty mempty []

packageConfig :: FilePath
packageConfig = "package.yaml"

data CustomSetupSection = CustomSetupSection {
  customSetupSectionDependencies :: Maybe Dependencies
} deriving (Eq, Show, Generic, FromValue)

data LibrarySection = LibrarySection {
  librarySectionExposed :: Maybe Bool
, librarySectionVisibility :: Maybe String
, librarySectionExposedModules :: Maybe (List Module)
, librarySectionGeneratedExposedModules :: Maybe (List Module)
, librarySectionOtherModules :: Maybe (List Module)
, librarySectionGeneratedOtherModules :: Maybe (List Module)
, librarySectionReexportedModules :: Maybe (List String)
, librarySectionSignatures :: Maybe (List String)
} deriving (Eq, Show, Generic, FromValue)

instance Monoid LibrarySection where
  mempty = LibrarySection Nothing Nothing Nothing Nothing Nothing Nothing Nothing Nothing
  mappend = (<>)

instance Semigroup LibrarySection where
  a <> b = LibrarySection {
      librarySectionExposed = librarySectionExposed b <|> librarySectionExposed a
    , librarySectionVisibility = librarySectionVisibility b <|> librarySectionVisibility a
    , librarySectionExposedModules = librarySectionExposedModules a <> librarySectionExposedModules b
    , librarySectionGeneratedExposedModules = librarySectionGeneratedExposedModules a <> librarySectionGeneratedExposedModules b
    , librarySectionOtherModules = librarySectionOtherModules a <> librarySectionOtherModules b
    , librarySectionGeneratedOtherModules = librarySectionGeneratedOtherModules a <> librarySectionGeneratedOtherModules b
    , librarySectionReexportedModules = librarySectionReexportedModules a <> librarySectionReexportedModules b
    , librarySectionSignatures = librarySectionSignatures a <> librarySectionSignatures b
    }

data ExecutableSection = ExecutableSection {
  executableSectionMain :: Alias 'True "main-is" (Last FilePath)
, executableSectionOtherModules :: Maybe (List Module)
, executableSectionGeneratedOtherModules :: Maybe (List Module)
} deriving (Eq, Show, Generic, FromValue)

instance Monoid ExecutableSection where
  mempty = ExecutableSection mempty Nothing Nothing
  mappend = (<>)

instance Semigroup ExecutableSection where
  a <> b = ExecutableSection {
      executableSectionMain = executableSectionMain a <> executableSectionMain b
    , executableSectionOtherModules = executableSectionOtherModules a <> executableSectionOtherModules b
    , executableSectionGeneratedOtherModules = executableSectionGeneratedOtherModules a <> executableSectionGeneratedOtherModules b
    }

data VerbatimValue =
    VerbatimString String
  | VerbatimNumber Scientific
  | VerbatimBool Bool
  | VerbatimNull
  deriving (Eq, Show)

instance FromValue VerbatimValue where
  fromValue v = case v of
    String s -> return (VerbatimString $ T.unpack s)
    Number n -> return (VerbatimNumber n)
    Bool b -> return (VerbatimBool b)
    Null -> return VerbatimNull
    Object _ -> err
    Array _ -> err
    where
      err = typeMismatch (formatOrList ["String", "Number", "Bool", "Null"]) v

data Verbatim = VerbatimLiteral String | VerbatimObject (Map String VerbatimValue)
  deriving (Eq, Show)

instance FromValue Verbatim where
  fromValue v = case v of
    String s -> return (VerbatimLiteral $ T.unpack s)
    Object _ -> VerbatimObject <$> fromValue v
    _ -> typeMismatch (formatOrList ["String", "Object"]) v

data CommonOptions asmSources cSources cxxSources jsSources a = CommonOptions {
  commonOptionsSourceDirs :: Alias 'True "hs-source-dirs" (Maybe (List FilePath))
, commonOptionsDependencies :: Alias 'True "build-depends" (Maybe Dependencies)
, commonOptionsPkgConfigDependencies :: Alias 'False "pkgconfig-depends" (Maybe (List String))
, commonOptionsDefaultExtensions :: Maybe (List String)
, commonOptionsOtherExtensions :: Maybe (List String)
, commonOptionsLanguage :: Alias 'True "default-language" (Last (Maybe Language))
, commonOptionsMhsOptions :: Maybe (List GhcOption)
, commonOptionsGhcOptions :: Maybe (List GhcOption)
, commonOptionsGhcProfOptions :: Maybe (List GhcProfOption)
, commonOptionsGhcSharedOptions :: Maybe (List GhcOption)
, commonOptionsGhcjsOptions :: Maybe (List GhcjsOption)
, commonOptionsCppOptions :: Maybe (List CppOption)
, commonOptionsCcOptions :: Maybe (List CcOption)
, commonOptionsAsmOptions :: Maybe (List AsmOption)
, commonOptionsAsmSources :: asmSources
, commonOptionsCSources :: cSources
, commonOptionsCxxOptions :: Maybe (List CxxOption)
, commonOptionsCxxSources :: cxxSources
, commonOptionsJsSources :: jsSources
, commonOptionsExtraLibDirs :: Maybe (List FilePath)
, commonOptionsExtraLibraries :: Maybe (List FilePath)
, commonOptionsExtraFrameworksDirs :: Maybe (List FilePath)
, commonOptionsFrameworks :: Maybe (List String)
, commonOptionsIncludeDirs :: Maybe (List FilePath)
, commonOptionsInstallIncludes :: Maybe (List FilePath)
, commonOptionsLdOptions :: Maybe (List LdOption)
, commonOptionsBuildable :: Last Bool
, commonOptionsWhen :: Maybe (List (ConditionalSection asmSources cSources cxxSources jsSources a))
, commonOptionsBuildTools :: Alias 'True "build-tool-depends" (Maybe BuildTools)
, commonOptionsSystemBuildTools :: Maybe SystemBuildTools
, commonOptionsVerbatim :: Maybe (List Verbatim)
} deriving (Functor, Generic)

type ParseCommonOptions = CommonOptions ParseAsmSources ParseCSources ParseCxxSources ParseJsSources
instance FromValue a => FromValue (ParseCommonOptions a)

instance (Semigroup asmSources, Semigroup cSources, Semigroup cxxSources, Semigroup jsSources, Monoid asmSources, Monoid cSources, Monoid cxxSources, Monoid jsSources) => Monoid (CommonOptions asmSources cSources cxxSources jsSources a) where
  mempty = CommonOptions {
    commonOptionsSourceDirs = Alias Nothing
  , commonOptionsDependencies = Alias Nothing
  , commonOptionsPkgConfigDependencies = Alias Nothing
  , commonOptionsDefaultExtensions = Nothing
  , commonOptionsOtherExtensions = Nothing
  , commonOptionsLanguage = mempty
  , commonOptionsMhsOptions = Nothing
  , commonOptionsGhcOptions = Nothing
  , commonOptionsGhcProfOptions = Nothing
  , commonOptionsGhcSharedOptions = Nothing
  , commonOptionsGhcjsOptions = Nothing
  , commonOptionsCppOptions = Nothing
  , commonOptionsAsmOptions = Nothing
  , commonOptionsAsmSources = mempty
  , commonOptionsCcOptions = Nothing
  , commonOptionsCSources = mempty
  , commonOptionsCxxOptions = Nothing
  , commonOptionsCxxSources = mempty
  , commonOptionsJsSources = mempty
  , commonOptionsExtraLibDirs = Nothing
  , commonOptionsExtraLibraries = Nothing
  , commonOptionsExtraFrameworksDirs = Nothing
  , commonOptionsFrameworks = Nothing
  , commonOptionsIncludeDirs = Nothing
  , commonOptionsInstallIncludes = Nothing
  , commonOptionsLdOptions = Nothing
  , commonOptionsBuildable = mempty
  , commonOptionsWhen = Nothing
  , commonOptionsBuildTools = Alias Nothing
  , commonOptionsSystemBuildTools = Nothing
  , commonOptionsVerbatim = Nothing
  }
  mappend = (<>)

instance (Semigroup asmSources, Semigroup cSources, Semigroup cxxSources, Semigroup jsSources) => Semigroup (CommonOptions asmSources cSources cxxSources jsSources a) where
  a <> b = CommonOptions {
    commonOptionsSourceDirs = commonOptionsSourceDirs a <> commonOptionsSourceDirs b
  , commonOptionsDependencies = commonOptionsDependencies b <> commonOptionsDependencies a
  , commonOptionsPkgConfigDependencies = commonOptionsPkgConfigDependencies a <> commonOptionsPkgConfigDependencies b
  , commonOptionsDefaultExtensions = commonOptionsDefaultExtensions a <> commonOptionsDefaultExtensions b
  , commonOptionsOtherExtensions = commonOptionsOtherExtensions a <> commonOptionsOtherExtensions b
  , commonOptionsLanguage = commonOptionsLanguage a <> commonOptionsLanguage b
  , commonOptionsMhsOptions = commonOptionsMhsOptions a <> commonOptionsMhsOptions b
  , commonOptionsGhcOptions = commonOptionsGhcOptions a <> commonOptionsGhcOptions b
  , commonOptionsGhcProfOptions = commonOptionsGhcProfOptions a <> commonOptionsGhcProfOptions b
  , commonOptionsGhcSharedOptions = commonOptionsGhcSharedOptions a <> commonOptionsGhcSharedOptions b
  , commonOptionsGhcjsOptions = commonOptionsGhcjsOptions a <> commonOptionsGhcjsOptions b
  , commonOptionsCppOptions = commonOptionsCppOptions a <> commonOptionsCppOptions b
  , commonOptionsAsmOptions = commonOptionsAsmOptions a <> commonOptionsAsmOptions b
  , commonOptionsAsmSources = commonOptionsAsmSources a <> commonOptionsAsmSources b
  , commonOptionsCcOptions = commonOptionsCcOptions a <> commonOptionsCcOptions b
  , commonOptionsCSources = commonOptionsCSources a <> commonOptionsCSources b
  , commonOptionsCxxOptions = commonOptionsCxxOptions a <> commonOptionsCxxOptions b
  , commonOptionsCxxSources = commonOptionsCxxSources a <> commonOptionsCxxSources b
  , commonOptionsJsSources = commonOptionsJsSources a <> commonOptionsJsSources b
  , commonOptionsExtraLibDirs = commonOptionsExtraLibDirs a <> commonOptionsExtraLibDirs b
  , commonOptionsExtraLibraries = commonOptionsExtraLibraries a <> commonOptionsExtraLibraries b
  , commonOptionsExtraFrameworksDirs = commonOptionsExtraFrameworksDirs a <> commonOptionsExtraFrameworksDirs b
  , commonOptionsFrameworks = commonOptionsFrameworks a <> commonOptionsFrameworks b
  , commonOptionsIncludeDirs = commonOptionsIncludeDirs a <> commonOptionsIncludeDirs b
  , commonOptionsInstallIncludes = commonOptionsInstallIncludes a <> commonOptionsInstallIncludes b
  , commonOptionsLdOptions = commonOptionsLdOptions a <> commonOptionsLdOptions b
  , commonOptionsBuildable = commonOptionsBuildable a <> commonOptionsBuildable b
  , commonOptionsWhen = commonOptionsWhen a <> commonOptionsWhen b
  , commonOptionsBuildTools = commonOptionsBuildTools a <> commonOptionsBuildTools b
  , commonOptionsSystemBuildTools = commonOptionsSystemBuildTools b <> commonOptionsSystemBuildTools a
  , commonOptionsVerbatim = commonOptionsVerbatim a <> commonOptionsVerbatim b
  }

type ParseAsmSources = Maybe (List FilePath)
type ParseCSources = Maybe (List FilePath)
type ParseCxxSources = Maybe (List FilePath)
type ParseJsSources = Maybe (List FilePath)

type AsmSources = [Path]
type CSources = [Path]
type CxxSources = [Path]
type JsSources = [Path]

type WithCommonOptions asmSources cSources cxxSources jsSources a = Product (CommonOptions asmSources cSources cxxSources jsSources a) a

data Traverse m asmSources asmSources_ cSources cSources_ cxxSources cxxSources_ jsSources jsSources_ = Traverse {
  traverseAsmSources :: asmSources -> m asmSources_
, traverseCSources :: cSources -> m cSources_
, traverseCxxSources :: cxxSources -> m cxxSources_
, traverseJsSources :: jsSources -> m jsSources_
}

type Traversal t = forall m asmSources asmSources_ cSources cSources_ cxxSources cxxSources_ jsSources jsSources_. Monad m
  => Traverse m asmSources asmSources_ cSources cSources_ cxxSources cxxSources_ jsSources jsSources_
  -> t asmSources cSources cxxSources jsSources
  -> m (t asmSources_ cSources_ cxxSources_ jsSources_)

type Traversal_ t = forall m asmSources asmSources_ cSources cSources_ cxxSources cxxSources_ jsSources jsSources_ a. Monad m
  => Traverse m asmSources asmSources_ cSources cSources_ cxxSources cxxSources_ jsSources jsSources_
  -> t asmSources cSources cxxSources jsSources a
  -> m (t asmSources_ cSources_ cxxSources_ jsSources_ a)

traverseCommonOptions :: Traversal_ CommonOptions
traverseCommonOptions t@Traverse{..} c@CommonOptions{..} = do
  asmSources <- traverseAsmSources commonOptionsAsmSources
  cSources <- traverseCSources commonOptionsCSources
  cxxSources <- traverseCxxSources commonOptionsCxxSources
  jsSources <- traverseJsSources commonOptionsJsSources
  xs <- traverse (traverse (traverseConditionalSection t)) commonOptionsWhen
  return c {
      commonOptionsAsmSources = asmSources
    , commonOptionsCSources = cSources
    , commonOptionsCxxSources = cxxSources
    , commonOptionsJsSources = jsSources
    , commonOptionsWhen = xs
    }

traverseConditionalSection :: Traversal_ ConditionalSection
traverseConditionalSection t = \ case
  ThenElseConditional c -> ThenElseConditional <$> bitraverse (traverseThenElse t) return c
  FlatConditional c -> FlatConditional <$> bitraverse (traverseWithCommonOptions t) return c

traverseThenElse :: Traversal_ ThenElse
traverseThenElse t c@ThenElse{..} = do
  then_ <- traverseWithCommonOptions t thenElseThen
  else_ <- traverseWithCommonOptions t thenElseElse
  return c{thenElseThen = then_, thenElseElse = else_}

traverseWithCommonOptions :: Traversal_ WithCommonOptions
traverseWithCommonOptions t = bitraverse (traverseCommonOptions t) return

data ConditionalSection asmSources cSources cxxSources jsSources a =
    ThenElseConditional (Product (ThenElse asmSources cSources cxxSources jsSources a) Condition)
  | FlatConditional (Product (WithCommonOptions asmSources cSources cxxSources jsSources a) Condition)

instance Functor (ConditionalSection asmSources cSources cxxSources jsSources) where
  fmap f = \ case
    ThenElseConditional c -> ThenElseConditional (first (fmap f) c)
    FlatConditional c -> FlatConditional (first (bimap (fmap f) f) c)

type ParseConditionalSection = ConditionalSection ParseAsmSources ParseCSources ParseCxxSources ParseJsSources

instance FromValue a => FromValue (ParseConditionalSection a) where
  fromValue v
    | hasKey "then" v || hasKey "else" v = ThenElseConditional <$> fromValue v <* giveHint
    | otherwise = FlatConditional <$> fromValue v
    where
      giveHint = case v of
        Object o -> case (,,) <$> KeyMap.lookup "then" o <*> KeyMap.lookup "else" o <*> KeyMap.lookup "condition" o of
          Just (Object then_, Object else_, String condition) -> do
            when (KeyMap.null then_) $ "then" `emptyTryInstead` flatElse
            when (KeyMap.null else_) $ "else" `emptyTryInstead` flatThen
            where
              flatThen = flatConditional condition then_
              flatElse = flatConditional (negate_ condition) else_
          _ -> return ()
        _ -> return ()

      negate_ condition = "!(" <> condition <> ")"

      flatConditional condition sect = object [("when" .= KeyMap.insert "condition" (String condition) sect)]

      emptyTryInstead :: String -> Value -> Parser ()
      emptyTryInstead name sect = do
        fail $ "an empty " <> show name <> " section is not allowed, try the following instead:\n\n" ++ encodePretty sect

      encodePretty = T.unpack . decodeUtf8 . Yaml.encodePretty c
        where
          c :: Yaml.Config
          c = Yaml.setConfCompare f Yaml.defConfig
            where
              f a b = case (a, b) of
                ("condition", "condition") -> EQ
                ("condition", _) -> LT
                (_, "condition") -> GT
                _ -> compare a b

hasKey :: Key -> Value -> Bool
hasKey key (Object o) = KeyMap.member key o
hasKey _ _ = False

newtype Condition = Condition {
  conditionCondition :: Cond
} deriving (Eq, Show, Generic, FromValue)

data Cond = CondBool Bool | CondExpression String
  deriving (Eq, Show)

instance FromValue Cond where
  fromValue v = case v of
    String c -> return (CondExpression $ T.unpack c)
    Bool c -> return (CondBool c)
    _ -> typeMismatch "Boolean or String" v

data ThenElse asmSources cSources cxxSources jsSources a = ThenElse {
  thenElseThen :: WithCommonOptions asmSources cSources cxxSources jsSources a
, thenElseElse :: WithCommonOptions asmSources cSources cxxSources jsSources a
} deriving Generic

instance Functor (ThenElse asmSources cSources cxxSources jsSources) where
  fmap f c@ThenElse{..} = c{thenElseThen = map_ thenElseThen, thenElseElse = map_ thenElseElse}
    where
      map_ = bimap (fmap f) f

type ParseThenElse = ThenElse ParseAsmSources ParseCSources ParseCxxSources ParseJsSources

instance FromValue a => FromValue (ParseThenElse a)

data Empty = Empty
  deriving (Eq, Show)

instance Monoid Empty where
  mempty = Empty
  mappend = (<>)

instance Semigroup Empty where
  Empty <> Empty = Empty

instance FromValue Empty where
  fromValue _ = return Empty

newtype Language = Language String
  deriving (Eq, Show)

instance IsString Language where
  fromString = Language

instance FromValue Language where
  fromValue = fmap Language . fromValue

data BuildType =
    Simple
  | Configure
  | Make
  | Custom
  deriving (Eq, Show, Enum, Bounded)

instance FromValue BuildType where
  fromValue = withText $ \ (T.unpack -> t) -> do
    maybe err return (lookup t options)
    where
      err = fail ("expected one of " ++ formatOrList buildTypesAsString)
      buildTypes = [minBound .. maxBound]
      buildTypesAsString = map show buildTypes
      options = zip buildTypesAsString buildTypes

formatOrList :: [String] -> String
formatOrList xs = case reverse xs of
  [] -> ""
  x : [] -> x
  y : x : [] -> x ++ " or " ++ y
  x : ys@(_:_:_) -> intercalate ", " . reverse $ ("or " ++ x) : ys

type SectionConfigWithDefaults asmSources cSources cxxSources jsSources a = Product DefaultsConfig (WithCommonOptions asmSources cSources cxxSources jsSources a)

type PackageConfigWithDefaults asmSources cSources cxxSources jsSources = PackageConfig_
  (SectionConfigWithDefaults asmSources cSources cxxSources jsSources LibrarySection)
  (SectionConfigWithDefaults asmSources cSources cxxSources jsSources ExecutableSection)

type PackageConfig asmSources cSources cxxSources jsSources = PackageConfig_
  (WithCommonOptions asmSources cSources cxxSources jsSources LibrarySection)
  (WithCommonOptions asmSources cSources cxxSources jsSources ExecutableSection)

data PackageVersion = PackageVersion {unPackageVersion :: String}

instance FromValue PackageVersion where
  fromValue v = PackageVersion <$> case v of
    Number n -> return (scientificToVersion n)
    String s -> return (T.unpack s)
    _ -> typeMismatch "Number or String" v

data PackageConfig_ library executable = PackageConfig {
  packageConfigName :: Maybe String
, packageConfigVersion :: Maybe PackageVersion
, packageConfigSynopsis :: Maybe String
, packageConfigDescription :: Maybe String
, packageConfigHomepage :: Maybe (Maybe String)
, packageConfigBugReports :: Maybe (Maybe String)
, packageConfigCategory :: Maybe (List String)
, packageConfigStability :: Maybe String
, packageConfigAuthor :: Maybe (List String)
, packageConfigMaintainer :: Maybe (Maybe (List String))
, packageConfigCopyright :: Maybe (List String)
, packageConfigBuildType :: Maybe BuildType
, packageConfigLicense :: Maybe (Maybe String)
, packageConfigLicenseFile :: Maybe (List String)
, packageConfigTestedWith :: Maybe (List String)
, packageConfigFlags :: Maybe (Map String FlagSection)
, packageConfigExtraSourceFiles :: Maybe (List FilePath)
, packageConfigExtraDocFiles :: Maybe (List FilePath)
, packageConfigExtraFiles :: Maybe (List FilePath)
, packageConfigDataFiles :: Maybe (List FilePath)
, packageConfigDataDir :: Maybe FilePath
, packageConfigGithub :: Maybe GitHub
, packageConfigCodeberg :: Maybe GitHub
, packageConfigGit :: Maybe String
, packageConfigCustomSetup :: Maybe CustomSetupSection
, packageConfigLibrary :: Maybe library
, packageConfigInternalLibraries :: Maybe (Map String library)
, packageConfigExecutable :: Maybe executable
, packageConfigExecutables :: Maybe (Map String executable)
, packageConfigTests :: Maybe (Map String executable)
, packageConfigBenchmarks :: Maybe (Map String executable)
} deriving Generic

data GitHub = GitHub {
  _gitHubOwner :: String
, _gitHubRepo :: String
, _gitHubSubdir :: Maybe String
}

instance FromValue GitHub where
  fromValue v = do
    input <- fromValue v
    case map T.unpack $ T.splitOn "/" input of
      [owner, repo, subdir] -> return $ GitHub owner repo (Just subdir)
      [owner, repo] -> return $ GitHub owner repo Nothing
      _ -> fail $ "expected owner/repo or owner/repo/subdir, but encountered " ++ show input

data DefaultsConfig = DefaultsConfig {
  defaultsConfigDefaults :: Maybe (List Defaults)
} deriving (Generic, FromValue)

traversePackageConfig :: Traversal PackageConfig
traversePackageConfig t p@PackageConfig{..} = do
  library <- traverse (traverseWithCommonOptions t) packageConfigLibrary
  internalLibraries <- traverseNamedConfigs t packageConfigInternalLibraries
  executable <- traverse (traverseWithCommonOptions t) packageConfigExecutable
  executables <- traverseNamedConfigs t packageConfigExecutables
  tests <- traverseNamedConfigs t packageConfigTests
  benchmarks <- traverseNamedConfigs t packageConfigBenchmarks
  return p {
      packageConfigLibrary = library
    , packageConfigInternalLibraries = internalLibraries
    , packageConfigExecutable = executable
    , packageConfigExecutables = executables
    , packageConfigTests = tests
    , packageConfigBenchmarks = benchmarks
    }
  where
    traverseNamedConfigs = traverse . traverse . traverseWithCommonOptions

type ParsePackageConfig = PackageConfigWithDefaults ParseAsmSources ParseCSources ParseCxxSources ParseJsSources

instance FromValue ParsePackageConfig

liftIOEither :: (MonadIO m, Errors m) => IO (Either HpackError a) -> m a
liftIOEither action = liftIO action >>= liftEither

type FormatYamlParseError = FilePath -> Yaml.ParseException -> String

decodeYaml :: (FromValue a, MonadIO m, Warnings m, Errors m, State m) => FormatYamlParseError -> FilePath -> m a
decodeYaml formatYamlParseError file = do
  (warnings, a) <- liftIOEither $ first (ParseError . formatYamlParseError file) <$> Yaml.decodeYamlWithParseError file
  tell warnings
  decodeValue file a

data DecodeOptions = DecodeOptions {
  decodeOptionsProgramName :: ProgramName
, decodeOptionsTarget :: FilePath
, decodeOptionsUserDataDir :: Maybe FilePath
, decodeOptionsDecode :: FilePath -> IO (Either String ([String], Value))
, decodeOptionsFormatYamlParseError :: FilePath -> Yaml.ParseException -> String
}

defaultDecodeOptions :: DecodeOptions
defaultDecodeOptions = DecodeOptions "hpack" packageConfig Nothing Yaml.decodeYaml Yaml.formatYamlParseError

data DecodeResult = DecodeResult {
  decodeResultPackage :: Package
, decodeResultCabalVersion :: String
, decodeResultCabalFile :: FilePath
, decodeResultWarnings :: [String]
} deriving (Eq, Show)

readPackageConfig :: DecodeOptions -> IO (Either String DecodeResult)
readPackageConfig options = first (formatHpackError $ decodeOptionsProgramName options) <$> readPackageConfigWithError options

type Errors = MonadError HpackError
type Warnings = MonadWriter [String]
type State = MonadState SpecVersion

type ConfigM m = StateT SpecVersion (WriterT [String] (ExceptT HpackError m))

runConfigM :: Monad m => ConfigM m a -> m (Either HpackError (a, [String]))
runConfigM = runExceptT . runWriterT . (`evalStateT` NoSpecVersion)

readPackageConfigWithError :: DecodeOptions -> IO (Either HpackError DecodeResult)
readPackageConfigWithError (DecodeOptions _ file mUserDataDir readValue formatYamlParseError) = fmap (fmap addCabalFile) . runConfigM $ do
  (warnings, value) <- liftIOEither $ first ParseError <$> readValue file
  tell warnings
  config <- decodeValue file value
  dir <- liftIO $ takeDirectory <$> canonicalizePath file
  userDataDir <- liftIO $ maybe (getAppUserDataDirectory "hpack") return mUserDataDir
  toPackage formatYamlParseError userDataDir dir config
  where
    addCabalFile :: (Package, [String]) -> DecodeResult
    addCabalFile (pkg, warnings) = DecodeResult {
        decodeResultPackage = pkg
      , decodeResultCabalVersion = "cabal-version: " ++ showCabalVersion (packageCabalVersion pkg) ++ "\n\n"
      , decodeResultCabalFile = takeDirectory_ file </> packageName pkg <.> "cabal"
      , decodeResultWarnings = warnings
      }

    takeDirectory_ :: FilePath -> FilePath
    takeDirectory_ p
      | takeFileName p == p = ""
      | otherwise = takeDirectory p

deleteVerbatimField :: String -> [Verbatim] -> [Verbatim]
deleteVerbatimField name = map $ \ case
  literal@VerbatimLiteral {} -> literal
  VerbatimObject o -> VerbatimObject (Map.delete name o)

verbatimValueToString :: VerbatimValue -> String
verbatimValueToString = \ case
  VerbatimString s -> s
  VerbatimNumber n -> scientificToVersion n
  VerbatimBool b -> show b
  VerbatimNull -> ""

addPathsModuleToGeneratedModules :: Package -> Package
addPathsModuleToGeneratedModules pkg
  | packageCabalVersion pkg < makeCabalVersion [2] = pkg
  | otherwise = pkg {
      packageLibrary = fmap mapLibrary <$> packageLibrary pkg
    , packageInternalLibraries = fmap mapLibrary <$> packageInternalLibraries pkg
    , packageExecutables = fmap mapExecutable <$> packageExecutables pkg
    , packageTests = fmap mapExecutable <$> packageTests pkg
    , packageBenchmarks = fmap mapExecutable <$> packageBenchmarks pkg
    }
  where
    pathsModule = pathsModuleFromPackageName (packageName pkg)

    mapLibrary :: Library -> Library
    mapLibrary lib
      | pathsModule `elem` getLibraryModules lib = lib {
          libraryGeneratedModules = if pathsModule `elem` generatedModules then generatedModules else pathsModule : generatedModules
        }
      | otherwise = lib
      where
        generatedModules = libraryGeneratedModules lib

    mapExecutable :: Executable -> Executable
    mapExecutable executable
      | pathsModule `elem` executableOtherModules executable = executable {
          executableGeneratedModules = if pathsModule `elem` generatedModules then generatedModules else pathsModule : generatedModules
        }
      | otherwise = executable
      where
        generatedModules = executableGeneratedModules executable

data CabalVersion = CabalVersion Version | VerbatimCabalVersion String
  deriving (Eq, Ord, Show)

makeCabalVersion :: [Int] -> CabalVersion
makeCabalVersion = CabalVersion . Version.makeVersion

showCabalVersion :: CabalVersion -> String
showCabalVersion = \ case
  CabalVersion v -> showVersion v
  VerbatimCabalVersion v -> v

extractVerbatimCabalVersion :: [Verbatim] -> (Maybe CabalVersion, [Verbatim])
extractVerbatimCabalVersion verbatim = case listToMaybe (mapMaybe extractCabalVersion verbatim) of
  Nothing -> (Nothing, verbatim)
  Just verbatimVersion -> (Just cabalVersion, deleteVerbatimField "cabal-version" verbatim)
    where
      cabalVersion :: CabalVersion
      cabalVersion = case parseVersion verbatimVersion of
        Nothing -> VerbatimCabalVersion verbatimVersion
        Just v -> CabalVersion v
  where
    extractCabalVersion :: Verbatim -> Maybe String
    extractCabalVersion = \ case
      VerbatimLiteral _ -> Nothing
      VerbatimObject o -> case Map.lookup "cabal-version" o of
        Just v -> Just (verbatimValueToString v)
        Nothing -> Nothing

ensureRequiredCabalVersion :: Maybe (License SPDX.License) -> Package -> Package
ensureRequiredCabalVersion inferredLicense pkg@Package{..} = pkg {
    packageCabalVersion = version
  , packageLicense = formatLicense <$> license
  , packageVerbatim = verbatim
  }
  where
    makeVersion :: [Int] -> CabalVersion
    makeVersion = makeCabalVersion

    (verbatimCabalVersion, verbatim) = extractVerbatimCabalVersion packageVerbatim

    license :: Maybe (License String)
    license = fmap prettyShow <$> (parsedLicense <|> inferredLicense)

    parsedLicense :: Maybe (License SPDX.License)
    parsedLicense = parseLicense <$> packageLicense

    formatLicense :: License String -> String
    formatLicense = \ case
      MustSPDX spdx -> spdx
      CanSPDX _ spdx | version >= makeVersion [2,2] -> spdx
      CanSPDX cabal _ -> prettyShow cabal
      DontTouch original -> original

    mustSPDX :: Bool
    mustSPDX = maybe False f license
      where
        f = \case
          DontTouch _ -> False
          CanSPDX _ _ -> False
          MustSPDX _ -> True

    version :: CabalVersion
    version = fromMaybe inferredVersion verbatimCabalVersion

    inferredVersion :: CabalVersion
    inferredVersion = fromMaybe packageCabalVersion $ maximum [
        makeVersion [2,2] <$ guard mustSPDX
      , makeVersion [1,24] <$ packageCustomSetup
      , makeVersion [1,18] <$ guard (not (null packageExtraDocFiles))
      , makeVersion [3,14] <$ guard (not (null packageExtraFiles))
      , packageLibrary >>= libraryCabalVersion
      , internalLibsCabalVersion packageInternalLibraries
      , executablesCabalVersion packageExecutables
      , executablesCabalVersion packageTests
      , executablesCabalVersion packageBenchmarks
      ]

    libraryCabalVersion :: Section Library -> Maybe CabalVersion
    libraryCabalVersion sect = maximum [
        makeVersion [1,22] <$ guard (has libraryReexportedModules)
      , makeVersion [2,0]  <$ guard (has librarySignatures)
      , makeVersion [2,0] <$ guard (has libraryGeneratedModules)
      , makeVersion [3,12] <$ guard (libraryHasPackageInfoModule sect)
      , makeVersion [3,0] <$ guard (has libraryVisibility)
      , sectionCabalVersion (concatMap getLibraryModules) sect
      ]
      where
        has field = any (not . null . field) sect

    libraryHasPackageInfoModule :: Section Library -> Bool
    libraryHasPackageInfoModule =
      any (hasPackageInfoModule . libraryGeneratedModules)

    packageInfoModule :: Module
    packageInfoModule =
      Module ("PackageInfo_" ++ moduleNameFromPackageName packageName)

    hasPackageInfoModule :: [Module] -> Bool
    hasPackageInfoModule = any (== packageInfoModule)

    internalLibsCabalVersion :: Map String (Section Library) -> Maybe CabalVersion
    internalLibsCabalVersion internalLibraries
      | Map.null internalLibraries = Nothing
      | otherwise = foldr max (Just $ makeVersion [2,0]) versions
      where
        versions = libraryCabalVersion <$> Map.elems internalLibraries

    executablesCabalVersion :: Map String (Section Executable) -> Maybe CabalVersion
    executablesCabalVersion = foldr max Nothing . map executableCabalVersion . Map.elems

    executableCabalVersion :: Section Executable -> Maybe CabalVersion
    executableCabalVersion sect = maximum [
        makeVersion [2,0] <$ guard (executableHasGeneratedModules sect)
      , makeVersion [3,12] <$ guard (executableHasPackageInfoModule sect)
      , sectionCabalVersion (concatMap getExecutableModules) sect
      ]

    executableHasGeneratedModules :: Section Executable -> Bool
    executableHasGeneratedModules = any (not . null . executableGeneratedModules)

    executableHasPackageInfoModule :: Section Executable -> Bool
    executableHasPackageInfoModule =
      any (hasPackageInfoModule . executableGeneratedModules)

    sectionCabalVersion :: (Section a -> [Module]) -> Section a -> Maybe CabalVersion
    sectionCabalVersion getMentionedModules sect = maximum $ [
        makeVersion [2,2] <$ guard (sectionSatisfies (not . null . sectionCxxSources) sect)
      , makeVersion [2,2] <$ guard (sectionSatisfies (not . null . sectionCxxOptions) sect)
      , makeVersion [3,0] <$ guard (sectionSatisfies (not . null . sectionAsmOptions) sect)
      , makeVersion [3,0] <$ guard (sectionSatisfies (not . null . sectionAsmSources) sect)
      , makeVersion [2,0] <$ guard (sectionSatisfies (any hasMixins . unDependencies . sectionDependencies) sect)
      , makeVersion [3,0] <$ guard (sectionSatisfies (any hasSubcomponents . Map.keys . unDependencies . sectionDependencies) sect)
      , makeVersion [2,2] <$ guard (
              uses "RebindableSyntax"
          && (uses "OverloadedStrings" || uses "OverloadedLists")
          && pathsModule `elem` getMentionedModules sect)
      ] ++ map versionFromSystemBuildTool systemBuildTools
      where
        defaultExtensions :: [String]
        defaultExtensions = sectionAll sectionDefaultExtensions sect

        uses :: String -> Bool
        uses = (`elem` defaultExtensions)

        pathsModule :: Module
        pathsModule = pathsModuleFromPackageName packageName

        versionFromSystemBuildTool :: String -> Maybe CabalVersion
        versionFromSystemBuildTool name
          | name `elem` known_1_10 = Nothing
          | name `elem` known_1_14 = Just (makeVersion [1,14])
          | name `elem` known_1_22 = Just (makeVersion [1,22])
          | otherwise = Just (makeVersion [2,0])

        known_1_10 :: [String]
        known_1_10 = [
            "ghc"
          , "ghc-pkg"
          , "hugs"
          , "ffihugs"
          , "nhc98"
          , "hmake"
          , "jhc"
          , "lhc"
          , "lhc-pkg"
          , "uhc"
          , "gcc"
          , "ranlib"
          , "ar"
          , "strip"
          , "ld"
          , "tar"
          , "pkg-config"
          ] \\ [
          -- Support for these build tools has been removed from Cabal at some point
            "hugs"
          , "ffihugs"
          , "nhc98"
          , "ranlib"
          , "lhc"
          , "lhc-pkg"
          ]

        known_1_14 :: [String]
        known_1_14 = [
            "hpc"
          ]

        known_1_22 :: [String]
        known_1_22 = [
            "ghcjs"
          , "ghcjs-pkg"
          -- , "haskell-suite" // not a real build tool
          -- , "haskell-suite-pkg" // not a real build tool
          ]

        systemBuildTools :: [String]
        systemBuildTools = Map.keys $ unSystemBuildTools $ sectionAll sectionSystemBuildTools sect

    sectionSatisfies :: (Section a -> Bool) -> Section a -> Bool
    sectionSatisfies p sect = or [
        p sect
      , any (any (sectionSatisfies p)) (sectionConditionals sect)
      ]

    hasMixins :: DependencyInfo -> Bool
    hasMixins (DependencyInfo mixins _) = not (null mixins)

    hasSubcomponents :: String -> Bool
    hasSubcomponents = elem ':'

sectionAll :: Monoid b => (Section a -> b) -> Section a -> b
sectionAll f sect = f sect <> foldMap (foldMap $ sectionAll f) (sectionConditionals sect)

decodeValue :: (FromValue a, State m, Warnings m, Errors m) => FilePath -> Value -> m a
decodeValue file value = do
  (r, unknown, deprecated) <- liftEither $ first (DecodeValueError file) (Config.decodeValue value)
  case r of
    UnsupportedSpecVersion v -> do
      throwError $ HpackVersionNotSupported file v Hpack.version
    SupportedSpecVersion v a -> do
      tell (map formatUnknownField unknown)
      tell (map formatDeprecatedField deprecated)
      State.modify $ max v
      return a
  where
    prefix :: String
    prefix = file ++ ": "

    formatUnknownField :: String -> String
    formatUnknownField name = prefix ++ "Ignoring unrecognized field " ++ name

    formatDeprecatedField :: (String, String) -> String
    formatDeprecatedField (name, substitute) = prefix <> name <> " is deprecated, use " <> substitute <> " instead"

data SpecVersion = NoSpecVersion | SpecVersion Version
  deriving (Eq, Show, Ord)

toSpecVersion :: Maybe ParseSpecVersion -> SpecVersion
toSpecVersion = maybe NoSpecVersion (SpecVersion . unParseSpecVersion)

data CheckSpecVersion a = SupportedSpecVersion SpecVersion a | UnsupportedSpecVersion Version

instance FromValue a => FromValue (CheckSpecVersion a) where
  fromValue = withObject $ \ o -> o .:? "spec-version" >>= \ case
    Just (ParseSpecVersion v) | Hpack.version < v -> return $ UnsupportedSpecVersion v
    v -> SupportedSpecVersion (toSpecVersion v) <$> fromValue (Object o)

newtype ParseSpecVersion = ParseSpecVersion {unParseSpecVersion :: Version}

instance FromValue ParseSpecVersion where
  fromValue value = do
    s <- case value of
      Number n -> return (scientificToVersion n)
      String s -> return (T.unpack s)
      _ -> typeMismatch "Number or String" value
    case parseVersion s of
      Just v -> return (ParseSpecVersion v)
      Nothing -> fail ("invalid value " ++ show s)

data Package = Package {
  packageCabalVersion :: CabalVersion
, packageName :: String
, packageVersion :: String
, packageSynopsis :: Maybe String
, packageDescription :: Maybe String
, packageHomepage :: Maybe String
, packageBugReports :: Maybe String
, packageCategory :: [String]
, packageStability :: Maybe String
, packageAuthor :: [String]
, packageMaintainer :: [String]
, packageCopyright :: [String]
, packageBuildType :: BuildType
, packageLicense :: Maybe String
, packageLicenseFile :: [FilePath]
, packageTestedWith :: [String]
, packageFlags :: [Flag]
, packageExtraSourceFiles :: [Path]
, packageExtraDocFiles :: [Path]
, packageExtraFiles :: [Path]
, packageDataFiles :: [Path]
, packageDataDir :: Maybe FilePath
, packageSourceRepository :: Maybe SourceRepository
, packageCustomSetup :: Maybe CustomSetup
, packageLibrary :: Maybe (Section Library)
, packageInternalLibraries :: Map String (Section Library)
, packageExecutables :: Map String (Section Executable)
, packageTests :: Map String (Section Executable)
, packageBenchmarks :: Map String (Section Executable)
, packageVerbatim :: [Verbatim]
} deriving (Eq, Show)

data CustomSetup = CustomSetup {
  customSetupDependencies :: Dependencies
} deriving (Eq, Show)

data Library = Library {
  libraryExposed :: Maybe Bool
, libraryVisibility :: Maybe String
, libraryExposedModules :: [Module]
, libraryOtherModules :: [Module]
, libraryGeneratedModules :: [Module]
, libraryReexportedModules :: [String]
, librarySignatures :: [String]
} deriving (Eq, Show)

data Executable = Executable {
  executableMain :: Maybe FilePath
, executableOtherModules :: [Module]
, executableGeneratedModules :: [Module]
} deriving (Eq, Show)

data BuildTool = BuildTool String String | LocalBuildTool String
  deriving (Show, Eq, Ord)

data Section a = Section {
  sectionData :: a
, sectionSourceDirs :: [FilePath]
, sectionDependencies :: Dependencies
, sectionPkgConfigDependencies :: [String]
, sectionDefaultExtensions :: [String]
, sectionOtherExtensions :: [String]
, sectionLanguage :: Maybe Language
, sectionMhsOptions :: [GhcOption]
, sectionGhcOptions :: [GhcOption]
, sectionGhcProfOptions :: [GhcProfOption]
, sectionGhcSharedOptions :: [GhcOption]
, sectionGhcjsOptions :: [GhcjsOption]
, sectionCppOptions :: [CppOption]
, sectionAsmOptions :: [AsmOption]
, sectionAsmSources :: [Path]
, sectionCcOptions :: [CcOption]
, sectionCSources :: [Path]
, sectionCxxOptions :: [CxxOption]
, sectionCxxSources :: [Path]
, sectionJsSources :: [Path]
, sectionExtraLibDirs :: [FilePath]
, sectionExtraLibraries :: [FilePath]
, sectionExtraFrameworksDirs :: [FilePath]
, sectionFrameworks :: [FilePath]
, sectionIncludeDirs :: [FilePath]
, sectionInstallIncludes :: [FilePath]
, sectionLdOptions :: [LdOption]
, sectionBuildable :: Maybe Bool
, sectionConditionals :: [Conditional (Section a)]
, sectionBuildTools :: Map BuildTool DependencyVersion
, sectionSystemBuildTools :: SystemBuildTools
, sectionVerbatim :: [Verbatim]
} deriving (Eq, Show, Functor, Foldable, Traversable)

data Conditional a = Conditional {
  conditionalCondition :: Cond
, conditionalThen :: a
, conditionalElse :: Maybe a
} deriving (Eq, Show, Functor, Foldable, Traversable)

data FlagSection = FlagSection {
  _flagSectionDescription :: Maybe String
, _flagSectionManual :: Bool
, _flagSectionDefault :: Bool
} deriving (Eq, Show, Generic, FromValue)

data Flag = Flag {
  flagName :: String
, flagDescription :: Maybe String
, flagManual :: Bool
, flagDefault :: Bool
} deriving (Eq, Show)

toFlag :: (String, FlagSection) -> Flag
toFlag (name, FlagSection description manual def) = Flag name description manual def

data SourceRepository = SourceRepository {
  sourceRepositoryUrl :: String
, sourceRepositorySubdir :: Maybe String
} deriving (Eq, Show)

type Config asmSources cSources cxxSources jsSources =
  Product (CommonOptions asmSources cSources cxxSources jsSources Empty) (PackageConfig asmSources cSources cxxSources jsSources)

traverseConfig :: Traversal Config
traverseConfig t = bitraverse (traverseCommonOptions t) (traversePackageConfig t)

type ConfigWithDefaults = Product
  (CommonOptionsWithDefaults Empty)
  (PackageConfigWithDefaults ParseAsmSources ParseCSources ParseCxxSources ParseJsSources)

type CommonOptionsWithDefaults a = Product DefaultsConfig (CommonOptions ParseAsmSources ParseCSources ParseCxxSources ParseJsSources a)
type WithCommonOptionsWithDefaults a = Product DefaultsConfig (WithCommonOptions ParseAsmSources ParseCSources ParseCxxSources ParseJsSources a)

toPackage :: FormatYamlParseError -> FilePath -> FilePath -> ConfigWithDefaults -> ConfigM IO Package
toPackage formatYamlParseError userDataDir dir =
      expandDefaultsInConfig formatYamlParseError userDataDir dir
  >=> setDefaultLanguage "Haskell2010"
  >>> traverseConfig (expandForeignSources dir)
  >=> toPackage_ dir
  where
    setDefaultLanguage language config = first setLanguage config
      where
        setLanguage = (mempty { commonOptionsLanguage = Alias . Last $ Just (Just language) } <>)

expandDefaultsInConfig
  :: (MonadIO m, Warnings m, Errors m, State m) =>
     FormatYamlParseError
  -> FilePath
  -> FilePath
  -> ConfigWithDefaults
  -> m (Config ParseAsmSources ParseCSources ParseCxxSources ParseJsSources)
expandDefaultsInConfig formatYamlParseError userDataDir dir = bitraverse (expandGlobalDefaults formatYamlParseError userDataDir dir) (expandSectionDefaults formatYamlParseError userDataDir dir)

expandGlobalDefaults
  :: (MonadIO m, Warnings m, Errors m, State m) =>
     FormatYamlParseError
  -> FilePath
  -> FilePath
  -> CommonOptionsWithDefaults Empty
  -> m (CommonOptions ParseAsmSources ParseCSources ParseCxxSources ParseJsSources Empty)
expandGlobalDefaults formatYamlParseError userDataDir dir = do
  fmap (`Product` Empty) >>> expandDefaults formatYamlParseError userDataDir dir >=> \ (Product c Empty) -> return c

expandSectionDefaults
  :: (MonadIO m, Warnings m, Errors m, State m) =>
     FormatYamlParseError
  -> FilePath
  -> FilePath
  -> PackageConfigWithDefaults ParseAsmSources ParseCSources ParseCxxSources ParseJsSources
  -> m (PackageConfig ParseAsmSources ParseCSources ParseCxxSources ParseJsSources)
expandSectionDefaults formatYamlParseError userDataDir dir p@PackageConfig{..} = do
  library <- traverse (expandDefaults formatYamlParseError userDataDir dir) packageConfigLibrary
  internalLibraries <- traverse (traverse (expandDefaults formatYamlParseError userDataDir dir)) packageConfigInternalLibraries
  executable <- traverse (expandDefaults formatYamlParseError userDataDir dir) packageConfigExecutable
  executables <- traverse (traverse (expandDefaults formatYamlParseError userDataDir dir)) packageConfigExecutables
  tests <- traverse (traverse (expandDefaults formatYamlParseError userDataDir dir)) packageConfigTests
  benchmarks <- traverse (traverse (expandDefaults formatYamlParseError userDataDir dir)) packageConfigBenchmarks
  return p{
      packageConfigLibrary = library
    , packageConfigInternalLibraries = internalLibraries
    , packageConfigExecutable = executable
    , packageConfigExecutables = executables
    , packageConfigTests = tests
    , packageConfigBenchmarks = benchmarks
    }

expandDefaults
  :: forall a m. (MonadIO m, Warnings m, Errors m, State m) =>
     (FromValue a, Monoid a)
  => FormatYamlParseError
  -> FilePath
  -> FilePath
  -> WithCommonOptionsWithDefaults a
  -> m (WithCommonOptions ParseAsmSources ParseCSources ParseCxxSources ParseJsSources a)
expandDefaults formatYamlParseError userDataDir = expand []
  where
    expand ::
         [FilePath]
      -> FilePath
      -> WithCommonOptionsWithDefaults a
      -> m (WithCommonOptions ParseAsmSources ParseCSources ParseCxxSources ParseJsSources a)
    expand seen dir (Product DefaultsConfig{..} c) = do
      d <- mconcat <$> mapM (get seen dir) (fromMaybeList defaultsConfigDefaults)
      return (d <> c)

    get ::
         [FilePath]
      -> FilePath
      -> Defaults
      -> m (WithCommonOptions ParseAsmSources ParseCSources ParseCxxSources ParseJsSources a)
    get seen dir defaults = do
      file <- liftIOEither (ensure userDataDir dir defaults)
      seen_ <- checkCycle seen file
      let dir_ = takeDirectory file
      decodeYaml formatYamlParseError file >>= expand seen_ dir_

    checkCycle :: [FilePath] -> FilePath -> m [FilePath]
    checkCycle seen file = do
      canonic <- liftIO $ canonicalizePath file
      let seen_ = canonic : seen
      when (canonic `elem` seen) $ do
        throwError $ CycleInDefaults (reverse seen_)
      return seen_

toExecutableMap :: Warnings m => String -> Maybe (Map String a) -> Maybe a -> m (Maybe (Map String a))
toExecutableMap name executables mExecutable = do
  case mExecutable of
    Just executable -> do
      when (isJust executables) $ do
        tell ["Ignoring field \"executables\" in favor of \"executable\""]
      return $ Just (Map.fromList [(name, executable)])
    Nothing -> return executables

type GlobalOptions = CommonOptions AsmSources CSources CxxSources JsSources Empty

toPackage_ :: (MonadIO m, Warnings m, State m) => FilePath -> Product GlobalOptions (PackageConfig AsmSources CSources CxxSources JsSources) -> m Package
toPackage_ dir (Product g PackageConfig{..}) = do
  executableMap <- toExecutableMap packageName packageConfigExecutables packageConfigExecutable
  let
    globalVerbatim = commonOptionsVerbatim g
    globalOptions = g {commonOptionsVerbatim = Nothing}

    executableNames = maybe [] Map.keys executableMap

    toSect :: (Warnings m, Monoid a) => WithCommonOptions AsmSources CSources CxxSources JsSources a -> m (Section a)
    toSect = toSection packageName executableNames . first ((mempty <$ globalOptions) <>)

    toSections :: (Warnings m, Monoid a) => Maybe (Map String (WithCommonOptions AsmSources CSources CxxSources JsSources a)) -> m (Map String (Section a))
    toSections = maybe (return mempty) (traverse toSect)

    toLib = toLibrary dir packageName
    toExecutables = toSections >=> traverse (toExecutable dir packageName)

  mLibrary <- traverse (toSect >=> toLib) packageConfigLibrary
  internalLibraries <- toSections packageConfigInternalLibraries >>= traverse toLib

  executables <- toExecutables executableMap
  tests <- toExecutables packageConfigTests
  benchmarks <- toExecutables packageConfigBenchmarks

  licenseFileExists <- liftIO $ doesFileExist (dir </> "LICENSE")

  missingSourceDirs <- liftIO $ nub . sort <$> filterM (fmap not <$> doesDirectoryExist . (dir </>)) (
       maybe [] sectionSourceDirs mLibrary
    ++ concatMap sectionSourceDirs internalLibraries
    ++ concatMap sectionSourceDirs executables
    ++ concatMap sectionSourceDirs tests
    ++ concatMap sectionSourceDirs benchmarks
    )

  extraSourceFiles <- expandGlobs "extra-source-files" dir (fromMaybeList packageConfigExtraSourceFiles)
  extraDocFiles <- expandGlobs "extra-doc-files" dir (fromMaybeList packageConfigExtraDocFiles)
  extraFiles <- expandGlobs "extra-files" dir (fromMaybeList packageConfigExtraFiles)

  let dataBaseDir = maybe dir (dir </>) packageConfigDataDir

  dataFiles <- expandGlobs "data-files" dataBaseDir (fromMaybeList packageConfigDataFiles)

  let
    licenseFiles :: [String]
    licenseFiles = fromMaybeList $ packageConfigLicenseFile <|> do
      guard licenseFileExists
      Just (List ["LICENSE"])

  inferredLicense <- case (packageConfigLicense, licenseFiles) of
    (Nothing, [file]) -> do
      input <- liftIO (tryReadFile (dir </> file))
      case input >>= inferLicense of
        Nothing -> do
          tell ["Inferring license from file " ++ file ++ " failed!"]
          return Nothing
        license -> return license
    _ -> return Nothing

  let defaultBuildType :: BuildType
      defaultBuildType = maybe Simple (const Custom) mCustomSetup

      pkg = Package {
        packageCabalVersion = CabalVersion defaultCabalVersion
      , packageName
      , packageVersion = maybe "0.0.0" unPackageVersion packageConfigVersion
      , packageSynopsis = packageConfigSynopsis
      , packageDescription = packageConfigDescription
      , packageHomepage = homepage
      , packageBugReports = bugReports
      , packageCategory = fromMaybeList packageConfigCategory
      , packageStability = packageConfigStability
      , packageAuthor = fromMaybeList packageConfigAuthor
      , packageMaintainer = fromMaybeList maintainer
      , packageCopyright = fromMaybeList packageConfigCopyright
      , packageBuildType = fromMaybe defaultBuildType packageConfigBuildType
      , packageLicense = join packageConfigLicense
      , packageLicenseFile = licenseFiles
      , packageTestedWith = fromMaybeList packageConfigTestedWith
      , packageFlags = flags
      , packageExtraSourceFiles = extraSourceFiles
      , packageExtraDocFiles = extraDocFiles
      , packageExtraFiles = extraFiles
      , packageDataFiles = dataFiles
      , packageDataDir = packageConfigDataDir
      , packageSourceRepository = sourceRepository
      , packageCustomSetup = mCustomSetup
      , packageLibrary = mLibrary
      , packageInternalLibraries = internalLibraries
      , packageExecutables = executables
      , packageTests = tests
      , packageBenchmarks = benchmarks
      , packageVerbatim = fromMaybeList globalVerbatim
      }

  tell nameWarnings
  tell (formatMissingSourceDirs missingSourceDirs)

  return $ addPathsModuleToGeneratedModules $ ensureRequiredCabalVersion inferredLicense pkg
  where
    nameWarnings :: [String]
    packageName :: String
    (nameWarnings, packageName) = case packageConfigName of
      Nothing -> let inferredName = takeBaseName dir in
        (["Package name not specified, inferred " ++ show inferredName], inferredName)
      Just n -> ([], n)

    mCustomSetup :: Maybe CustomSetup
    mCustomSetup = toCustomSetup <$> packageConfigCustomSetup

    flags = map toFlag $ toList packageConfigFlags

    toList :: Maybe (Map String a) -> [(String, a)]
    toList = Map.toList . fromMaybe mempty

    formatMissingSourceDirs = map f
      where
        f name = "Specified source-dir " ++ show name ++ " does not exist"

    sourceRepository :: Maybe SourceRepository
    sourceRepository = codeberg <|> github <|> (`SourceRepository` Nothing) <$> packageConfigGit

    github :: Maybe SourceRepository
    github = toSourceRepository <$> packageConfigGithub
      where
        toSourceRepository :: GitHub -> SourceRepository
        toSourceRepository (GitHub owner repo subdir) = SourceRepository (githubBaseUrl ++ owner ++ "/" ++ repo) subdir

    codeberg :: Maybe SourceRepository
    codeberg = toSourceRepository <$> packageConfigCodeberg
      where
        toSourceRepository :: GitHub -> SourceRepository
        toSourceRepository (GitHub owner repo subdir) = SourceRepository (codebergBaseUrl ++ owner ++ "/" ++ repo) subdir

    homepage :: Maybe String
    homepage = case packageConfigHomepage of
      Just Nothing -> Nothing
      _ -> join packageConfigHomepage <|> fromCodeberg <|> fromGithub
      where
        fromCodeberg = (++ "#readme") . sourceRepositoryUrl <$> codeberg
        fromGithub = (++ "#readme") . sourceRepositoryUrl <$> github

    bugReports :: Maybe String
    bugReports = case packageConfigBugReports of
      Just Nothing -> Nothing
      _ -> join packageConfigBugReports <|> fromCodeberg <|> fromGithub
      where
        fromCodeberg = (++ "/issues") . sourceRepositoryUrl <$> codeberg
        fromGithub = (++ "/issues") . sourceRepositoryUrl <$> github

    maintainer :: Maybe (List String)
    maintainer = case (packageConfigAuthor, packageConfigMaintainer) of
      (Just _, Nothing) -> packageConfigAuthor
      (_, Just m) -> m
      _            -> Nothing

expandForeignSources
  :: (MonadIO m, Warnings m)
  => FilePath
  -> Traverse m ParseAsmSources AsmSources ParseCSources CSources ParseCxxSources CxxSources ParseJsSources JsSources
expandForeignSources dir = Traverse {
    traverseAsmSources = expand "asm-sources"
  , traverseCSources = expand "c-sources"
  , traverseCxxSources = expand "cxx-sources"
  , traverseJsSources = expand "js-sources"
  }
  where
    expand fieldName xs = do
      expandGlobs fieldName dir (fromMaybeList xs)

newtype Path = Path { unPath :: FilePath }
  deriving (Eq, Show, Ord)

instance IsString Path where
  fromString = Path

expandGlobs :: (MonadIO m, Warnings m) => String -> FilePath -> [String] -> m [Path]
expandGlobs name dir patterns = map Path <$> do
  (warnings, files) <- liftIO $ Util.expandGlobs name dir patterns
  tell warnings
  return files

toCustomSetup :: CustomSetupSection -> CustomSetup
toCustomSetup CustomSetupSection{..} = CustomSetup
  { customSetupDependencies = fromMaybe mempty customSetupSectionDependencies }

traverseSectionAndConditionals :: Monad m
  => (acc -> Section a -> m (acc, b))
  -> (acc -> Section a -> m (acc, b))
  -> acc
  -> Section a
  -> m (Section b)
traverseSectionAndConditionals fData fConditionals acc0 sect@Section{..} = do
  (acc1, x) <- fData acc0 sect
  xs <- traverseConditionals acc1 sectionConditionals
  return sect{sectionData = x, sectionConditionals = xs}
  where
    traverseConditionals = traverse . traverse . traverseSectionAndConditionals fConditionals fConditionals

getMentionedLibraryModules :: LibrarySection -> [Module]
getMentionedLibraryModules (LibrarySection _ _ exposedModules generatedExposedModules otherModules generatedOtherModules _ _)
  = fromMaybeList (exposedModules <> generatedExposedModules <> otherModules <> generatedOtherModules)

getLibraryModules :: Library -> [Module]
getLibraryModules Library{..} = libraryExposedModules ++ libraryOtherModules

getExecutableModules :: Executable -> [Module]
getExecutableModules Executable{..} = executableOtherModules

listModules :: FilePath -> Section a -> IO [Module]
listModules dir Section{..} = concat <$> mapM (getModules dir) sectionSourceDirs

removeConditionalsThatAreAlwaysFalse :: Section a -> Section a
removeConditionalsThatAreAlwaysFalse sect = sect {
    sectionConditionals = filter p $ sectionConditionals sect
  }
  where
    p = (/= CondBool False) . conditionalCondition

inferModules :: (MonadIO m, State m) =>
     FilePath
  -> String
  -> (a -> [Module])
  -> (b -> [Module])
  -> ([Module] -> [Module] -> a -> b)
  -> ([Module] -> a -> b)
  -> Section a
  -> m (Section b)
inferModules dir packageName_ getMentionedModules getInferredModules fromData fromConditionals sect_ = do
  specVersion <- State.get
  let
    pathsModule :: [Module]
    pathsModule = case specVersion of
      SpecVersion v | v >= Version.makeVersion [0,36,0] -> []
      _ -> [pathsModuleFromPackageName packageName_]

  removeConditionalsThatAreAlwaysFalse <$> traverseSectionAndConditionals
    (fromConfigSection fromData pathsModule)
    (fromConfigSection (\ [] -> fromConditionals) [])
    []
    sect_
  where
    fromConfigSection fromConfig pathsModule_ outerModules sect@Section{sectionData = conf} = do
      modules <- liftIO $ listModules dir sect
      let
        mentionedModules = concatMap getMentionedModules sect
        inferableModules = (modules \\ outerModules) \\ mentionedModules
        pathsModule = (pathsModule_ \\ outerModules) \\ mentionedModules
        r = fromConfig pathsModule inferableModules conf
      return (outerModules ++ getInferredModules r, r)

toLibrary :: (MonadIO m, State m) => FilePath -> String -> Section LibrarySection -> m (Section Library)
toLibrary dir name =
    inferModules dir name getMentionedLibraryModules getLibraryModules fromLibrarySectionTopLevel fromLibrarySectionInConditional
  where
    fromLibrarySectionTopLevel :: [Module] -> [Module] -> LibrarySection -> Library
    fromLibrarySectionTopLevel pathsModule inferableModules LibrarySection{..} =
      Library librarySectionExposed librarySectionVisibility exposedModules otherModules generatedModules reexportedModules signatures
      where
        (exposedModules, otherModules, generatedModules) =
          determineModules pathsModule inferableModules librarySectionExposedModules librarySectionGeneratedExposedModules librarySectionOtherModules librarySectionGeneratedOtherModules
        reexportedModules = fromMaybeList librarySectionReexportedModules
        signatures = fromMaybeList librarySectionSignatures

determineModules :: [Module] -> [Module] -> Maybe (List Module) -> Maybe (List Module) -> Maybe (List Module) -> Maybe (List Module) -> ([Module], [Module], [Module])
determineModules pathsModule inferable mExposed mGeneratedExposed mOther mGeneratedOther =
  (exposed, others, generated)
  where
    generated = fromMaybeList (mGeneratedExposed <> mGeneratedOther)
    exposed = maybe inferable fromList mExposed ++ fromMaybeList mGeneratedExposed
    others = maybe ((inferable \\ exposed) ++ pathsModule) fromList mOther ++ fromMaybeList mGeneratedOther

fromLibrarySectionInConditional :: [Module] -> LibrarySection -> Library
fromLibrarySectionInConditional inferableModules lib@(LibrarySection _ _ exposedModules _ otherModules _ _ _) =
  case (exposedModules, otherModules) of
    (Nothing, Nothing) -> addToOtherModules inferableModules (fromLibrarySectionPlain lib)
    _ -> fromLibrarySectionPlain lib
  where
    addToOtherModules xs r = r {libraryOtherModules = xs ++ libraryOtherModules r}

fromLibrarySectionPlain :: LibrarySection -> Library
fromLibrarySectionPlain LibrarySection{..} = Library {
    libraryExposed = librarySectionExposed
  , libraryVisibility = librarySectionVisibility
  , libraryExposedModules = fromMaybeList (librarySectionExposedModules <> librarySectionGeneratedExposedModules)
  , libraryOtherModules = fromMaybeList (librarySectionOtherModules <> librarySectionGeneratedOtherModules)
  , libraryGeneratedModules = fromMaybeList (librarySectionGeneratedOtherModules <> librarySectionGeneratedExposedModules)
  , libraryReexportedModules = fromMaybeList librarySectionReexportedModules
  , librarySignatures = fromMaybeList librarySectionSignatures
  }

getMentionedExecutableModules :: ExecutableSection -> [Module]
getMentionedExecutableModules (ExecutableSection (Alias (Last main)) otherModules generatedModules)=
  maybe id (:) (toModule . Path.fromFilePath <$> main) $ fromMaybeList (otherModules <> generatedModules)

toExecutable :: (MonadIO m, State m) => FilePath -> String -> Section ExecutableSection -> m (Section Executable)
toExecutable dir packageName_ =
    inferModules dir packageName_ getMentionedExecutableModules getExecutableModules fromExecutableSection (fromExecutableSection [])
  . expandMain
  where
    fromExecutableSection :: [Module] -> [Module] -> ExecutableSection -> Executable
    fromExecutableSection pathsModule inferableModules ExecutableSection{..} =
      (Executable (getLast $ unAlias executableSectionMain) (otherModules ++ generatedModules) generatedModules)
      where
        otherModules = maybe (inferableModules ++ pathsModule) fromList executableSectionOtherModules
        generatedModules = maybe [] fromList executableSectionGeneratedOtherModules

expandMain :: Section ExecutableSection -> Section ExecutableSection
expandMain = flatten . expand
  where
    expand :: Section ExecutableSection -> Section ([GhcOption], ExecutableSection)
    expand = fmap go
      where
        go exec@ExecutableSection{..} =
          let
            (mainSrcFile, ghcOptions) = maybe (Nothing, []) (first Just . parseMain) (getLast $ unAlias executableSectionMain)
          in
            (ghcOptions, exec{executableSectionMain = Alias $ Last mainSrcFile})

    flatten :: Section ([GhcOption], ExecutableSection) -> Section ExecutableSection
    flatten sect@Section{sectionData = (ghcOptions, exec), ..} = sect{
        sectionData = exec
      , sectionGhcOptions = sectionGhcOptions ++ ghcOptions
      , sectionConditionals = map (fmap flatten) sectionConditionals
      }

toSection :: forall a m. Warnings m => String -> [String] -> WithCommonOptions AsmSources CSources CxxSources JsSources a -> m (Section a)
toSection packageName_ executableNames = go
  where
    go (Product CommonOptions{..} a) = do
      (systemBuildTools, buildTools) <- maybe (return mempty) toBuildTools (unAlias commonOptionsBuildTools)

      conditionals <- mapM toConditional (fromMaybeList commonOptionsWhen)
      return Section {
        sectionData = a
      , sectionSourceDirs = nub $ fromMaybeList (unAlias commonOptionsSourceDirs)
      , sectionDependencies = fromMaybe mempty (unAlias commonOptionsDependencies)
      , sectionPkgConfigDependencies = fromMaybeList (unAlias commonOptionsPkgConfigDependencies)
      , sectionDefaultExtensions = fromMaybeList commonOptionsDefaultExtensions
      , sectionOtherExtensions = fromMaybeList commonOptionsOtherExtensions
      , sectionLanguage = join . getLast $ unAlias commonOptionsLanguage
      , sectionMhsOptions = fromMaybeList commonOptionsMhsOptions
      , sectionGhcOptions = fromMaybeList commonOptionsGhcOptions
      , sectionGhcProfOptions = fromMaybeList commonOptionsGhcProfOptions
      , sectionGhcSharedOptions = fromMaybeList commonOptionsGhcSharedOptions
      , sectionGhcjsOptions = fromMaybeList commonOptionsGhcjsOptions
      , sectionCppOptions = fromMaybeList commonOptionsCppOptions
      , sectionAsmOptions = fromMaybeList commonOptionsAsmOptions
      , sectionAsmSources = commonOptionsAsmSources
      , sectionCcOptions = fromMaybeList commonOptionsCcOptions
      , sectionCSources = commonOptionsCSources
      , sectionCxxOptions = fromMaybeList commonOptionsCxxOptions
      , sectionCxxSources = commonOptionsCxxSources
      , sectionJsSources = commonOptionsJsSources
      , sectionExtraLibDirs = fromMaybeList commonOptionsExtraLibDirs
      , sectionExtraLibraries = fromMaybeList commonOptionsExtraLibraries
      , sectionExtraFrameworksDirs = fromMaybeList commonOptionsExtraFrameworksDirs
      , sectionFrameworks = fromMaybeList commonOptionsFrameworks
      , sectionIncludeDirs = fromMaybeList commonOptionsIncludeDirs
      , sectionInstallIncludes = fromMaybeList commonOptionsInstallIncludes
      , sectionLdOptions = fromMaybeList commonOptionsLdOptions
      , sectionBuildable = getLast commonOptionsBuildable
      , sectionConditionals = conditionals
      , sectionBuildTools = buildTools
      , sectionSystemBuildTools = systemBuildTools <> fromMaybe mempty commonOptionsSystemBuildTools
      , sectionVerbatim = fromMaybeList commonOptionsVerbatim
      }
    toBuildTools :: BuildTools -> m (SystemBuildTools, Map BuildTool DependencyVersion)
    toBuildTools = fmap (mkSystemBuildTools &&& mkBuildTools) . mapM (toBuildTool packageName_ executableNames) . unBuildTools
      where
        mkSystemBuildTools :: [Either (String, VersionConstraint) b] -> SystemBuildTools
        mkSystemBuildTools = SystemBuildTools . Map.fromList . lefts

        mkBuildTools = Map.fromList . rights

    toConditional :: ConditionalSection AsmSources CSources CxxSources JsSources a -> m (Conditional (Section a))
    toConditional x = case x of
      ThenElseConditional (Product (ThenElse then_ else_) c) -> conditional c <$> go then_ <*> (Just <$> go else_)
      FlatConditional (Product sect c) -> conditional c <$> (go sect) <*> pure Nothing
      where
        conditional = Conditional . conditionCondition

type SystemBuildTool = (String, VersionConstraint)

toBuildTool :: Warnings m => String -> [String] -> (ParseBuildTool, DependencyVersion) -> m (Either SystemBuildTool (BuildTool, DependencyVersion))
toBuildTool packageName_ executableNames = \ case
  (QualifiedBuildTool pkg executable, v)
    | pkg == packageName_ && executable `elem` executableNames -> localBuildTool executable v
    | otherwise -> buildTool pkg executable v
  (UnqualifiedBuildTool executable, v)
    | executable `elem` executableNames -> localBuildTool executable v
    | Just pkg <- lookup executable legacyTools -> legacyBuildTool pkg executable v
    | executable `elem` legacySystemTools, DependencyVersion Nothing c <- v -> legacySystemBuildTool executable c
    | otherwise -> buildTool executable executable v
  where
    buildTool pkg executable v = return . Right $ (BuildTool pkg executable, v)

    systemBuildTool = return . Left

    localBuildTool executable v = return . Right $ (LocalBuildTool executable, v)
    legacyBuildTool pkg executable v = warnLegacyTool pkg executable >> buildTool pkg executable v
    legacySystemBuildTool executable c = warnLegacySystemTool executable >> systemBuildTool (executable, c)

    legacyTools = [
        ("gtk2hsTypeGen", "gtk2hs-buildtools")
      , ("gtk2hsHookGenerator", "gtk2hs-buildtools")
      , ("gtk2hsC2hs", "gtk2hs-buildtools")
      , ("cabal", "cabal-install")
      , ("grgen", "cgen")
      , ("cgen-hs", "cgen")
      ]
    legacySystemTools = [
        "ghc"
      , "git"
      , "llvm-config"
      , "gfortran"
      , "gcc"
      , "couchdb"
      , "mcc"
      , "nix-store"
      , "nix-instantiate"
      , "nix-hash"
      , "nix-env"
      , "nix-build"
      ]
    warnLegacyTool pkg name = tell ["Usage of the unqualified build-tool name " ++ show name ++ " is deprecated! Please use the qualified name \"" ++ pkg ++ ":" ++ name ++ "\" instead!"]
    warnLegacySystemTool name = tell ["Listing " ++ show name ++ " under build-tools is deprecated! Please list system executables under system-build-tools instead!"]

pathsModuleFromPackageName :: String -> Module
pathsModuleFromPackageName name =
  Module ("Paths_" ++ moduleNameFromPackageName name)

moduleNameFromPackageName :: String -> String
moduleNameFromPackageName = map f
 where
  f '-' = '_'
  f x = x