-- FIXME See how much of this module can be deleted.
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE BangPatterns #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE DeriveDataTypeable #-}
{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE TupleSections #-}
{-# OPTIONS -fno-warn-unused-do-bind #-}

-- | Functions for the GHC package database.

module Stack.GhcPkg
  (findGhcPkgId
  ,findGhcPkgKey
  ,getGlobalDB
  ,EnvOverride
  ,envHelper
  ,createDatabase
  ,unregisterGhcPkgId
  ,getCabalPkgVer
  ,findGhcPkgHaddockHtml
  ,findGhcPkgDepends
  ,findTransitiveGhcPkgDepends
  ,listGhcPkgDbs
  ,ghcPkgExeName
  ,mkGhcPackagePath)
  where

import           Control.Applicative
import           Control.Monad
import           Control.Monad.Catch
import           Control.Monad.IO.Class
import           Control.Monad.Logger
import           Control.Monad.Trans.Control
import qualified Data.ByteString.Char8 as S8
import           Data.Either
import           Data.List
import qualified Data.Map as Map
import           Data.Maybe
import           Data.Set (Set)
import qualified Data.Set as Set
import           Data.Text (Text)
import qualified Data.Text as T
import qualified Data.Text.Encoding as T
import           Path (Path, Abs, Dir, toFilePath, parent, parseAbsDir)
import           Path.IO (dirExists, createTree)
import           Prelude hiding (FilePath)
import           Stack.Constants
import           Stack.Types
import           System.Directory (canonicalizePath, doesDirectoryExist)
import           System.FilePath (FilePath, searchPathSeparator, dropTrailingPathSeparator)
import           System.Process.Read

-- | Get the global package database
getGlobalDB :: (MonadIO m, MonadLogger m, MonadBaseControl IO m, MonadCatch m, MonadThrow m)
            => EnvOverride -> WhichCompiler -> m (Path Abs Dir)
getGlobalDB menv wc = do
    -- This seems like a strange way to get the global package database
    -- location, but I don't know of a better one
    bs <- ghcPkg menv wc [] ["list", "--global"] >>= either throwM return
    let fp = S8.unpack $ stripTrailingColon $ firstLine bs
    liftIO (canonicalizePath fp) >>= parseAbsDir
  where
    stripTrailingColon bs
        | S8.null bs = bs
        | S8.last bs == ':' = S8.init bs
        | otherwise = bs
    firstLine = S8.takeWhile (\c -> c /= '\r' && c /= '\n')

-- | Run the ghc-pkg executable
ghcPkg :: (MonadIO m, MonadLogger m, MonadBaseControl IO m, MonadCatch m, MonadThrow m)
       => EnvOverride
       -> WhichCompiler
       -> [Path Abs Dir]
       -> [String]
       -> m (Either ReadProcessException S8.ByteString)
ghcPkg menv wc pkgDbs args = do
    eres <- go
    r <- case eres of
            Left _ -> do
                mapM_ (createDatabase menv wc) pkgDbs
                go
            Right _ -> return eres
    return r
  where
    go = tryProcessStdout Nothing menv (ghcPkgExeName wc) args'
    args' = packageDbFlags pkgDbs ++ args

-- | Create a package database in the given directory, if it doesn't exist.
createDatabase :: (MonadIO m, MonadLogger m, MonadBaseControl IO m, MonadCatch m, MonadThrow m)
               => EnvOverride -> WhichCompiler -> Path Abs Dir -> m ()
createDatabase menv wc db = do
    exists <- dirExists db
    unless exists $ do
        -- Creating the parent doesn't seem necessary, as ghc-pkg
        -- seems to be sufficiently smart. But I don't feel like
        -- finding out it isn't the hard way
        createTree (parent db)
        _ <- tryProcessStdout Nothing menv (ghcPkgExeName wc) ["init", toFilePath db]
        return ()

-- | Get the name to use for "ghc-pkg", given the compiler version.
ghcPkgExeName :: WhichCompiler -> String
ghcPkgExeName Ghc = "ghc-pkg"
ghcPkgExeName Ghcjs = "ghcjs-pkg"

-- | Get the necessary ghc-pkg flags for setting up the given package database
packageDbFlags :: [Path Abs Dir] -> [String]
packageDbFlags pkgDbs =
          "--no-user-package-db"
        : map (\x -> ("--package-db=" ++ toFilePath x)) pkgDbs

-- | Get the value of a field of the package.
findGhcPkgField
    :: (MonadIO m, MonadLogger m, MonadBaseControl IO m, MonadCatch m, MonadThrow m)
    => EnvOverride
    -> WhichCompiler
    -> [Path Abs Dir] -- ^ package databases
    -> String -- ^ package identifier, or GhcPkgId
    -> Text
    -> m (Maybe Text)
findGhcPkgField menv wc pkgDbs name field = do
    result <-
        ghcPkg
            menv
            wc
            pkgDbs
            ["field", "--simple-output", name, T.unpack field]
    return $
        case result of
            Left{} -> Nothing
            Right lbs ->
                fmap (stripCR . T.decodeUtf8) $ listToMaybe $ S8.lines lbs
  where
    stripCR t = fromMaybe t (T.stripSuffix "\r" t)

-- | Get the id of the package e.g. @foo-0.0.0-9c293923c0685761dcff6f8c3ad8f8ec@.
findGhcPkgId :: (MonadIO m, MonadLogger m, MonadBaseControl IO m, MonadCatch m, MonadThrow m)
             => EnvOverride
             -> WhichCompiler
             -> [Path Abs Dir] -- ^ package databases
             -> PackageName
             -> m (Maybe GhcPkgId)
findGhcPkgId menv wc pkgDbs name = do
    mpid <- findGhcPkgField menv wc pkgDbs (packageNameString name) "id"
    case mpid of
        Just !pid -> return (parseGhcPkgId (T.encodeUtf8 pid))
        _ -> return Nothing

-- | Get the package key e.g. @foo_9bTCpMF7G4UFWJJvtDrIdB@.
--
-- NOTE: GHC > 7.10 only! Will always yield 'Nothing' otherwise.
findGhcPkgKey :: (MonadIO m, MonadLogger m, MonadBaseControl IO m, MonadCatch m, MonadThrow m)
             => EnvOverride
             -> WhichCompiler
             -> [Path Abs Dir] -- ^ package databases
             -> PackageName
             -> m (Maybe Text)
findGhcPkgKey menv wc pkgDbs name =
    findGhcPkgField menv wc pkgDbs (packageNameString name) "key"

-- | Get the version of the package
findGhcPkgVersion :: (MonadIO m, MonadLogger m, MonadBaseControl IO m, MonadCatch m, MonadThrow m)
                  => EnvOverride
                  -> WhichCompiler
                  -> [Path Abs Dir] -- ^ package databases
                  -> PackageName
                  -> m (Maybe Version)
findGhcPkgVersion menv wc pkgDbs name = do
    mv <- findGhcPkgField menv wc pkgDbs (packageNameString name) "version"
    case mv of
        Just !v -> return (parseVersion (T.encodeUtf8 v))
        _ -> return Nothing

-- | Get the Haddock HTML documentation path of the package.
findGhcPkgHaddockHtml :: (MonadIO m, MonadLogger m, MonadBaseControl IO m, MonadCatch m, MonadThrow m)
                      => EnvOverride
                      -> WhichCompiler
                      -> [Path Abs Dir] -- ^ package databases
                      -> String -- ^ PackageIdentifier or GhcPkgId
                      -> m (Maybe (PackageIdentifier, Path Abs Dir))
findGhcPkgHaddockHtml menv wc pkgDbs ghcPkgId = do
    mpath <- findGhcPkgField menv wc pkgDbs ghcPkgId "haddock-html"
    mid <- findGhcPkgField menv wc pkgDbs ghcPkgId "id"
    mversion <- findGhcPkgField menv wc pkgDbs ghcPkgId "version"
    let mpkgId = PackageIdentifier
            <$> (mid >>= parsePackageName . T.encodeUtf8)
            <*> (mversion >>= parseVersion . T.encodeUtf8)
    case (,) <$> mpath <*> mpkgId of
        Just (path0, pkgId) -> do
            let path = T.unpack path0
            exists <- liftIO $ doesDirectoryExist path
            path' <- if exists
                then liftIO $ canonicalizePath path
                else return path

            return $ fmap (pkgId,) (parseAbsDir path')
        _ -> return Nothing

-- | Finds dependencies of package, and all their dependencies, etc.
findTransitiveGhcPkgDepends
    :: (MonadIO m, MonadLogger m, MonadBaseControl IO m, MonadCatch m, MonadThrow m)
    => EnvOverride
    -> WhichCompiler
    -> [Path Abs Dir] -- ^ package databases
    -> PackageIdentifier
    -> m (Set PackageIdentifier)
findTransitiveGhcPkgDepends menv wc pkgDbs pkgId0 =
    liftM (Set.fromList . Map.elems)
    (go (packageIdentifierString pkgId0) Map.empty)
  where
    go pkgId res = do
        deps <- findGhcPkgDepends menv wc pkgDbs pkgId
        loop deps res
    loop [] res = return res
    loop (dep:deps) res = do
        if Map.member dep res
            then loop deps res
            else do
                let pkgId = ghcPkgIdString dep
                mname <- findGhcPkgField menv wc pkgDbs pkgId "name"
                mversion <- findGhcPkgField menv wc pkgDbs pkgId "version"
                let mident = do
                        name <- mname >>= parsePackageName . T.encodeUtf8
                        version <- mversion >>= parseVersion . T.encodeUtf8
                        Just $ PackageIdentifier name version
                    res' = maybe id (Map.insert dep) mident res
                res'' <- go pkgId res'
                -- FIXME is the Map.union actually necessary?
                loop deps (Map.union res res'')

-- | Get the dependencies of the package.
findGhcPkgDepends :: (MonadIO m, MonadLogger m, MonadBaseControl IO m, MonadCatch m, MonadThrow m)
                  => EnvOverride
                  -> WhichCompiler
                  -> [Path Abs Dir] -- ^ package databases
                  -> String -- ^ package identifier or GhcPkgId
                  -> m [GhcPkgId]
findGhcPkgDepends menv wc pkgDbs pkgId = do
    mdeps <- findGhcPkgField menv wc pkgDbs pkgId "depends"
    case mdeps of
        Just !deps -> return (mapMaybe (parseGhcPkgId . T.encodeUtf8) (T.words deps))
        _ -> return []

unregisterGhcPkgId :: (MonadIO m, MonadLogger m, MonadThrow m, MonadCatch m, MonadBaseControl IO m)
                    => EnvOverride
                    -> WhichCompiler
                    -> CompilerVersion
                    -> Path Abs Dir -- ^ package database
                    -> GhcPkgId
                    -> PackageIdentifier
                    -> m ()
unregisterGhcPkgId menv wc cv pkgDb gid ident = do
    eres <- ghcPkg menv wc [pkgDb] args
    case eres of
        Left e -> $logWarn $ T.pack $ show e
        Right _ -> return ()
  where
    -- TODO ideally we'd tell ghc-pkg a GhcPkgId instead
    args = "unregister" : "--user" : "--force" :
        (case cv of
            GhcVersion v | v < $(mkVersion "7.9") ->
                [packageIdentifierString ident]
            _ -> ["--ipid", ghcPkgIdString gid])

-- | Get the version of Cabal from the global package database.
getCabalPkgVer :: (MonadThrow m, MonadIO m, MonadLogger m, MonadBaseControl IO m, MonadCatch m)
               => EnvOverride -> WhichCompiler -> m Version
getCabalPkgVer menv wc =
    findGhcPkgVersion
        menv
        wc
        [] -- global DB
        cabalPackageName >>=
        maybe (throwM $ Couldn'tFindPkgId cabalPackageName) return

listGhcPkgDbs
    :: (MonadIO m, MonadLogger m, MonadBaseControl IO m, MonadCatch m, MonadThrow m)
    => EnvOverride -> WhichCompiler -> [Path Abs Dir] -> m [PackageIdentifier]
listGhcPkgDbs menv wc pkgDbs = do
    result <-
        ghcPkg
            menv
            wc
            pkgDbs
            ["list", "--simple-output"]
    return $
        case result of
            Left{} -> []
            Right lbs -> mapMaybe parsePackageIdentifier (S8.words lbs)

-- | Get the value for GHC_PACKAGE_PATH
mkGhcPackagePath :: Bool -> Path Abs Dir -> Path Abs Dir -> Path Abs Dir -> Text
mkGhcPackagePath locals localdb deps globaldb =
  T.pack $ intercalate [searchPathSeparator] $ concat
    [ [toFilePathNoTrailingSlash localdb | locals]
    , [toFilePathNoTrailingSlash deps]
    , [toFilePathNoTrailingSlash globaldb]
    ]

-- TODO: dedupe with copy in Stack.Setup
toFilePathNoTrailingSlash :: Path loc Dir -> FilePath
toFilePathNoTrailingSlash = dropTrailingPathSeparator . toFilePath