-- | Retrieve ghci options for your cabal project
module Distribution.Dev.Interactive ( 
  -- * Installing into your .ghci
  -- $install
  
  -- * Arguments
  -- $args
  
  -- * Exported functions
  cabalSet, packageOpts, loadCabal, lookForCabalFile, withOpts, LoadCabalRet(..)
  ) where

import Distribution.Text (display)
import Distribution.Compiler (
  buildCompilerFlavor, CompilerId(..), CompilerInfo(..), AbiTag(..),
  unknownCompilerInfo)
import Distribution.Verbosity (normal)
import Distribution.System (buildPlatform)
import Distribution.Package (PackageName(..), Dependency(..))
import Distribution.PackageDescription (
  FlagName(..), FlagAssignment, BuildInfo(..), hcOptions, allExtensions,
  PackageDescription(..), Executable(..), allBuildInfo)
import Distribution.PackageDescription.Parse (readPackageDescription)
import Distribution.PackageDescription.Configuration (
  finalizePackageDescription)
import Distribution.Simple.PackageIndex (lookupDependency)
import Distribution.Simple.LocalBuildInfo (LocalBuildInfo, installedPkgs)
import Data.Version (Version, showVersion)
import Distribution.Simple.BuildPaths (autogenModulesDir, cppHeaderName)

import System.FilePath (takeDirectory, (</>), takeExtension)
import System.Directory (
  getDirectoryContents, getCurrentDirectory, canonicalizePath)
import System.Info (compilerVersion)
import Data.Maybe (listToMaybe, fromMaybe)
import Control.Exception (try, SomeException)

type Deps = [(PackageName, Version)]

-- | Return value for 'loadCabal'
data LoadCabalRet =
  NoCabalFile | -- ^ No cabal file found
  MissingDeps [Dependency] | -- ^ Missing dependencies
  Pkg FilePath PackageDescription (Maybe LocalBuildInfo) -- ^ Successful loading and parsing of cabal file
  deriving Show

compiler ∷ CompilerInfo
compiler = unknownCompilerInfo compilerId NoAbiTag
  where
    compilerId = CompilerId buildCompilerFlavor compilerVersion

-- | Build a list of ghci options needed to load files from a cabal project
packageOpts
  ∷ FilePath -- ^ path to the .cabal file
  → PackageDescription -- ^ parsed package description
  → Maybe LocalBuildInfo -- ^ parsed build config
  → String -- ^ name of executable
  → Maybe [String]
packageOpts path pkg mlbi executable =
  maybe Nothing (\bi → Just $ ghcOpts path bi mlbi (listDeps bi =<< mlbi)) $
  listToMaybe $
    if executable == ""
    then allBuildInfo pkg
    else
      fmap buildInfo .
      filter (\x → exeName x == executable) .
      executables $ pkg

listDeps ∷ BuildInfo → LocalBuildInfo → Maybe Deps
listDeps bi lbi = sequence $ map find reqs 
  where
    reqs = targetBuildDepends bi
    db = installedPkgs lbi
    find dep@(Dependency pkg _) = do
      (fst . head → ver) ← return $ lookupDependency db dep
      return (pkg, ver)

-- | GHC options for a 'BuildInfo'
ghcOpts ∷ FilePath → BuildInfo → Maybe LocalBuildInfo → Maybe Deps → [String]
ghcOpts path bi mlbi deps = filter validGHCiFlag $ concat $
  maybe [] ((noPkgs:) . add "-package=" . map addDep) deps :
  map ($ bi) [
  hcOptions buildCompilerFlavor,
  addf "-X" display . allExtensions,
  addf "-i" (dir </>) . (autogendir:) . hsSourceDirs,
  add "-optP" . cppOptions,
  add "-optc" . ccOptions,
  add "-optl" . ldOptions,
  const ["-optP-include", "-optP" ++ (autogendir </> cppHeaderName)]
  -- Other cabal settings currently ignored by cabal-ghci: 
  -- frameworks cSources otherModules extraLibs extraLibsDirs includes
  ]
  where
    autogendir
      | Just lbi ← mlbi = autogenModulesDir lbi
      | otherwise = "dist/build/autogen"

    dir
      | s@(_:_) ← takeDirectory path = s
      | otherwise = "."
    add s = map (s++)
    addf ∷ String → (a → String) → [a] → [String]
    addf s f = map ((s++) . f)
    noPkgs = "-hide-all-packages"
    addDep (PackageName pkg, showVersion → ver) = pkg ++ "-" ++ ver

    -- flags sensible for GHCi
    validGHCiFlag "-O" = False
    validGHCiFlag ['-','O',n] | n `elem` ['0'..'9'] = False
    validGHCiFlag "-debug" = False
    validGHCiFlag "-rtsopts" = False
    validGHCiFlag "-threaded" = False
    validGHCiFlag "-ticky" = False
    validGHCiFlag _ = True

-- | Load the current cabal project file and parse it
loadCabal
  ∷ FilePath -- ^ usually the current directory
  → FlagAssignment -- ^ list of cabal flag assignments
  → IO LoadCabalRet
loadCabal path flags = do
  mCabalFile ← lookForCabalFile =<< canonicalizePath path
  flip (maybe (return NoCabalFile))
    mCabalFile $ \cabalFile → do
    gdescr ← readPackageDescription normal cabalFile
    mlbi ← loadCabalConfig (takeDirectory cabalFile </> "dist" </> "setup-config")
    case finalizePackageDescription flags (const True)
      buildPlatform compiler [] gdescr of
        Left deps → return $ MissingDeps deps
        Right (descr, _) → return $ Pkg cabalFile descr mlbi

maybeNth ∷ Int → [a] → Maybe a
maybeNth 0 (x:_) = Just x
maybeNth n (_:xs) = maybeNth (n-1) xs
maybeNth _ _ = Nothing

maybeRead ∷ Read a ⇒ String → Maybe a
maybeRead s = case reads s of [(a, "")] → Just a; _ → Nothing

-- | Load the 'LocalBuildInfo' from dist/setup-config
loadCabalConfig ∷ FilePath → IO (Maybe LocalBuildInfo)
loadCabalConfig path = 
  -- TODO: don't ignore all exceptions
  fmap (either ignore id) $ try $
  fmap ((maybeRead =<<) . maybeNth 1 . lines) $ readFile path
  where
    ignore ∷ SomeException → Maybe a
    ignore _ = Nothing

-- | Find a .cabal file in the path or any of it's parent directories
lookForCabalFile
  ∷ FilePath -- ^ canonicalised path
  → IO (Maybe FilePath)
lookForCabalFile "/" = return Nothing
lookForCabalFile path = do
  files ← getDirectoryContents path
  let cabals = filter (\f →
                        takeExtension f == ".cabal"
                        && f /= ".cabal") files
  case cabals of
    [] → lookForCabalFile (takeDirectory path)
    [a] → return $ Just $ path </> a
    _ → return Nothing

-- | 'cabalSet' returns a list of ghci commands (seperated by newlines) that :set the 'packageOpts' of the current cabal project
cabalSet
  ∷ String -- ^ arguments seperated by spaces
  → IO String
cabalSet args =
  withOpts (words args)
    (\x → putStrLn x >> return "")
    ((\x → putStrLn x >> return x) .
        unlines . (map (":set "++)) . map show )

-- | Generalised version of 'cabalSet'
withOpts
  ∷ [String] -- ^ List of cabal flag arguments and executable name
  → (String → IO a) -- ^ Error continuation. Recieves an error message.
  → ([String] → IO a) -- ^ Success continuation. Recieves a list of ghci arguments.
  → IO a
withOpts args err go = do
  let (flags, executable) = parseArgs args
  here ← getCurrentDirectory
  ret ← loadCabal here flags
  case ret of
    NoCabalFile → err "Current directory is not a cabal project"
    MissingDeps deps → err $ "Missing dependencies: " ++ unwords (map show deps)
    Pkg path descr mlbi → do
      let mopts = packageOpts path descr mlbi executable
      case mopts of
        Nothing → err (
          if executable /= ""
          then "No such executable in cabal file"
          else "No library defined in cabal file")
        Just opts → go opts

parseArgs ∷ [String] → (FlagAssignment, String)
parseArgs args =
  (map (makeFlag . drop 2) . filter flag $ args, 
   fromMaybe "" . listToMaybe . filter (not . flag) $ args)
  where
    flag x = take 2 x == "-f"
        
makeFlag ∷ String → (FlagName, Bool)
makeFlag ('-':f) = (FlagName f, False)
makeFlag f = (FlagName f, True)

-- $install
-- @
--   $ head -n 4 >> ~/.ghci
--   :m + Distribution.Dev.Interactive
--   :def cabalset cabalSet
--   :cabalset -fcabal-ghci
--   :m - Distribution.Dev.Interactive
-- @
-- 
-- After you've added those lines into your .ghci file, use the @:cabalset@
-- command to reload your .cabal file.

-- $args
-- [@-fflag@] enable flag
-- 
-- [@-f-flag@] disable flag
-- 
-- [@exec@] load options for the exec executable
