{-# LANGUAGE CPP #-}

module HIndent.CabalFile
  ( getCabalExtensionsForSourcePath
  ) where

import Control.Monad
import qualified Data.ByteString as BS
import Data.List
import Data.Maybe
import Data.Traversable
import Distribution.ModuleName
import Distribution.PackageDescription
import Distribution.PackageDescription.Configuration
#if MIN_VERSION_Cabal(3, 6, 0)
import Distribution.Utils.Path (getSymbolicPath)
#endif
#if MIN_VERSION_Cabal(2, 2, 0)
import Distribution.PackageDescription.Parsec
#else
import Distribution.PackageDescription.Parse
#endif
import HIndent.Language
import HIndent.LanguageExtension
import HIndent.LanguageExtension.Conversion
import HIndent.LanguageExtension.Types
import Language.Haskell.Extension hiding (Extension)
import System.Directory
import System.FilePath

data Stanza = MkStanza
  { _stanzaBuildInfo :: BuildInfo
  , stanzaIsSourceFilePath :: FilePath -> Bool
  }

-- | Find the relative path of a child path in a parent, if it is a child
toRelative :: FilePath -> FilePath -> Maybe FilePath
toRelative parent child =
  let rel = makeRelative parent child
   in if rel == child
        then Nothing
        else Just rel

-- | Create a Stanza from `BuildInfo` and names of modules and paths
mkStanza :: BuildInfo -> [ModuleName] -> [FilePath] -> Stanza
mkStanza bi mnames fpaths =
  MkStanza bi $ \path ->
    let modpaths = fmap toFilePath $ otherModules bi ++ mnames
        inDir dir =
          case toRelative dir path of
            Nothing -> False
            Just relpath ->
              any (equalFilePath $ dropExtension relpath) modpaths
                || any (equalFilePath relpath) fpaths
     in any inDir $ hsSourceDirs' bi
  where

#if MIN_VERSION_Cabal(3, 6, 0)
        hsSourceDirs' = (map getSymbolicPath) . hsSourceDirs
#else
        hsSourceDirs' = hsSourceDirs
#endif
-- | Extract `Stanza`s from a package
packageStanzas :: PackageDescription -> [Stanza]
packageStanzas pd =
  let libStanza :: Library -> Stanza
      libStanza lib = mkStanza (libBuildInfo lib) (exposedModules lib) []
      exeStanza :: Executable -> Stanza
      exeStanza exe = mkStanza (buildInfo exe) [] [modulePath exe]
      testStanza :: TestSuite -> Stanza
      testStanza ts =
        mkStanza
          (testBuildInfo ts)
          (case testInterface ts of
             TestSuiteLibV09 _ mname -> [mname]
             _ -> [])
          (case testInterface ts of
             TestSuiteExeV10 _ path -> [path]
             _ -> [])
      benchStanza :: Benchmark -> Stanza
      benchStanza bn =
        mkStanza (benchmarkBuildInfo bn) []
          $ case benchmarkInterface bn of
              BenchmarkExeV10 _ path -> [path]
              _ -> []
   in mconcat
        [ maybeToList $ libStanza <$> library pd
        , exeStanza <$> executables pd
        , testStanza <$> testSuites pd
        , benchStanza <$> benchmarks pd
        ]

-- | Find cabal files that are "above" the source path
findCabalFiles :: FilePath -> FilePath -> IO (Maybe ([FilePath], FilePath))
findCabalFiles dir rel = do
  names <- getDirectoryContents dir
  cabalnames <-
    filterM (doesFileExist . (dir </>)) $ filter (isSuffixOf ".cabal") names
  case cabalnames of
    []
      | dir == "/" -> return Nothing
    [] -> findCabalFiles (takeDirectory dir) (takeFileName dir </> rel)
    _ -> return $ Just (fmap (dir </>) cabalnames, rel)

getGenericPackageDescription :: FilePath -> IO (Maybe GenericPackageDescription)
#if MIN_VERSION_Cabal(2, 2, 0)
getGenericPackageDescription cabalPath = do
  cabaltext <- BS.readFile cabalPath
  return $ parseGenericPackageDescriptionMaybe cabaltext
#else
getGenericPackageDescription cabalPath = do
  cabaltext <- readFile cabalPath
  case parsePackageDescription cabaltext of
    ParseOk _ gpd -> return $ Just gpd
    _ -> return Nothing
#endif
-- | Find the `Stanza` that refers to this source path
getCabalStanza :: FilePath -> IO (Maybe Stanza)
getCabalStanza srcpath = do
  abssrcpath <- canonicalizePath srcpath
  mcp <- findCabalFiles (takeDirectory abssrcpath) (takeFileName abssrcpath)
  case mcp of
    Just (cabalpaths, relpath) -> do
      stanzass <-
        for cabalpaths $ \cabalpath -> do
          genericPackageDescription <- getGenericPackageDescription cabalpath
          case genericPackageDescription of
            Nothing -> return []
            Just gpd -> do
              return $ packageStanzas $ flattenPackageDescription gpd
      return
        $ case filter (`stanzaIsSourceFilePath` relpath) $ mconcat stanzass of
            [] -> Nothing
            (stanza:_) -> Just stanza -- just pick the first one
    Nothing -> return Nothing

-- | Get language and extensions from the cabal file for this source path
getCabalExtensions :: FilePath -> IO (Language, [Extension])
getCabalExtensions srcpath = do
  mstanza <- getCabalStanza srcpath
  return
    $ case mstanza of
        Nothing -> (Haskell98, [])
        Just (MkStanza bi _) ->
          ( fromMaybe Haskell98 $ defaultLanguage bi
          , mapMaybe fromCabalExtension $ defaultExtensions bi)

-- | Get extensions from the cabal file for this source path
getCabalExtensionsForSourcePath :: FilePath -> IO [Extension]
getCabalExtensionsForSourcePath srcpath = do
  (lang, exts) <- getCabalExtensions srcpath
  return $ exts ++ implicitExtensions (convertLanguage lang)
