-- | 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(..))
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  CompilerId
compiler = 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