content: {- | Functions to work with Cabal file and its content.
  -}
  
  module Policeman.Cabal
      ( CabalError (..)
      , parseCabalFile
      , findCabalDescription
      , extractExposedModules
      , extractPackageName
      , extractPackageVersion
      ) where
  
  import Control.Monad.Except (throwError)
  import Distribution.ModuleName (ModuleName, components)
  import Distribution.PackageDescription (CondTree (..), GenericPackageDescription (..), Library (..),
                                          PackageDescription (..))
  import Distribution.PackageDescription.Parsec (parseGenericPackageDescriptionMaybe)
  import Distribution.Types.PackageId (PackageIdentifier (..))
  import Distribution.Types.Version (versionNumbers)
  import System.Directory (doesDirectoryExist, listDirectory)
  import System.FilePath (takeExtension, (</>))
  
  import Policeman.Core.Package (Module (..), PackageName (..))
  import Policeman.Core.Version (Version, versionFromIntList)
  
  import qualified Distribution.Types.PackageName as Cabal
  
  
  {- | Errors about dealing with @.cabal@ files.
  -}
  data CabalError
      -- | Directory where we're looking for cabal file, not found.
      = NoSuchDirectory FilePath
      -- | No @.cabal@ file under given path.
      | NoCabalFile FilePath
      -- | Multiple cabal files found.
      | MultipleCabalFiles (NonEmpty FilePath)
      -- | Error parsing cabal file.
      | CabalParseError
      deriving stock (Show, Eq)
  
  {- | This function takes a path to a directory, finds a cabal file
  under this directory, parses it and returns parsed content.
  -}
  findCabalDescription :: FilePath -> ExceptT CabalError IO GenericPackageDescription
  findCabalDescription dirPath = do
      unlessM (liftIO $ doesDirectoryExist dirPath) $ throwError $ NoSuchDirectory dirPath
      dirContent <- liftIO $ listDirectory dirPath
      let cabalFiles = filter (\p -> takeExtension p == ".cabal") dirContent
      case cabalFiles of
          [] -> throwError $ NoCabalFile dirPath
          [cabalFile] -> do
              let cabalPath = dirPath </> cabalFile
              cabalContent <- readFileBS cabalPath
              parseCabalFile cabalContent
          x:xs -> throwError $ MultipleCabalFiles (x :| xs)
  
  -- | Parses the given cabal file source and returns 'GenericPackageDescription'
  parseCabalFile :: ByteString -> ExceptT CabalError IO GenericPackageDescription
  parseCabalFile cabalContent = whenNothing (parseGenericPackageDescriptionMaybe cabalContent) $
      throwError CabalParseError  -- TODO: better error
  
  extractPackageName :: GenericPackageDescription -> PackageName
  extractPackageName =
      PackageName
      . toText
      . Cabal.unPackageName
      . pkgName
      . package
      . packageDescription
  
  extractPackageVersion :: GenericPackageDescription -> Maybe Version
  extractPackageVersion =
      versionFromIntList
      . versionNumbers
      . pkgVersion
      . package
      . packageDescription
  
  extractExposedModules :: GenericPackageDescription -> [Module]
  extractExposedModules =
      map toModule
      . concatMap (exposedModules . condTreeData)
      . maybeToList
      . condLibrary
    where
      toModule :: ModuleName -> Module
      toModule = Module . toText . intercalate "." . components
  