-- | Allow to use cabal configuration (generated via the configure action of cabal).
-- Source directories and compilation options will be reused by Shaker.
module Shaker.Cabal.CabalInfo(
    defaultCabalInput
  )
   where

import Shaker.Type
import Shaker.Config
import Distribution.Simple.Configure (getPersistBuildConfig)
import Distribution.Simple.LocalBuildInfo (LocalBuildInfo, localPkgDescr)
import Distribution.ModuleName
import Distribution.PackageDescription(
  BuildInfo,targetBuildDepends,options,libBuildInfo,library,Library,hsSourceDirs,exposedModules, extensions, 
  Executable,buildInfo, modulePath, executables, exeName
  )
import DynFlags(
    DynFlags, verbosity, ghcLink, packageFlags, outputFile, hiDir, objectDir ,importPaths
    ,PackageFlag (ExposePackage)
    ,GhcLink (NoLink)
  )
import System.FilePath          ( (</>))
import System.Directory (doesFileExist)
import Distribution.Compiler(CompilerFlavor(GHC))
import Distribution.Package (Dependency(Dependency), PackageName(PackageName))
import Data.Maybe
import Data.List(nub,isSuffixOf)
import Control.Monad 

-- | Read the build information from cabal and output a shakerInput from it
defaultCabalInput :: IO ShakerInput
defaultCabalInput = readConf >>= localBuildInfoToShakerInput >>= checkInvalidMain 

readConf :: IO LocalBuildInfo
readConf = getPersistBuildConfig "dist"

-- | Extract useful information from localBuildInfo to a ShakerInput
localBuildInfoToShakerInput :: LocalBuildInfo -> IO ShakerInput
localBuildInfoToShakerInput lbi = do 
  defInput <- defaultInputInitialized 
  return defInput {
    compileInputs = cplInputs
    ,listenerInput = compileInputsToListenerInput cplInputs
  }
  where cplInputs = localBuildInfoToCompileInputs lbi

compileInputsToListenerInput :: [CompileInput] -> ListenerInput
compileInputsToListenerInput cplInputs = defaultListenerInput {
        fileListenInfo = nub $ map (\a -> FileListenInfo a defaultExclude  defaultHaskellPatterns) concatSources
 } 
 where concatSources = concatMap cfSourceDirs cplInputs
       
-- * Converter to CompileInput

-- | Extract informations : Convert executable and library to 
-- compile inputs
localBuildInfoToCompileInputs :: LocalBuildInfo -> [CompileInput]
localBuildInfoToCompileInputs lbi = executableAndLibToCompileInput (library pkgDescription) (executables pkgDescription)
 where pkgDescription = localPkgDescr lbi


-- | Dispatch the processing depending of the library content
executableAndLibToCompileInput :: Maybe Library -> [Executable] -> [CompileInput]
executableAndLibToCompileInput Nothing exes = 
  map executableToCompileInput exes
executableAndLibToCompileInput (Just lib) exes = 
  libraryToCompileInput lib : map executableToCompileInput exes

-- | Convert a cabal executable to a compileInput
-- The target of compilation will the main file
executableToCompileInput :: Executable -> CompileInput
executableToCompileInput executable = defaultCompileInput { 
  cfSourceDirs = mySourceDir
  ,cfDescription = "Executable : " ++ exeName executable
  ,cfCommandLineFlags = getCompileOptions bldInfo
  ,cfTargetFiles = map (</> modulePath executable ) mySourceDir
  ,cfDynFlags = toDynFlags mySourceDir (getLibDependencies bldInfo)
  }
  where bldInfo = buildInfo executable
        mySourceDir = hsSourceDirs bldInfo

-- | Convert a cabal library to a compileInput
-- The target of compilation will be all exposed modules
libraryToCompileInput :: Library -> CompileInput
libraryToCompileInput lib = defaultCompileInput {
  cfSourceDirs = mySourceDir
  ,cfDescription = "Library : " ++ show myModules
  ,cfCommandLineFlags = getCompileOptions bldInfo
  ,cfTargetFiles = myModules
  ,cfDynFlags = toDynFlags mySourceDir (getLibDependencies bldInfo)
 }
 where bldInfo = libBuildInfo lib
       myModules = map convertModuleNameToString $ exposedModules lib
       mySourceDir = hsSourceDirs bldInfo

-- | Create a dynFlags for ghc from a source directory and 
-- a liste of packages
toDynFlags :: [String] -> [String] -> DynFlags -> DynFlags
toDynFlags sourceDirs packagesToExpose dnFlags = dnFlags {
  importPaths = sourceDirs
  ,outputFile = Just "target/Main"
  ,objectDir = Just "target"
  ,hiDir = Just "target"
  ,verbosity = 1
  ,ghcLink = NoLink
  ,packageFlags = map ExposePackage packagesToExpose 
  } 

-- * Helper methods

getCompileOptions :: BuildInfo -> [String]
getCompileOptions myLibBuildInfo = ghcOptions ++ ghcExtensions
 where ghcOptions = fromMaybe [] $ lookup GHC (options myLibBuildInfo)
       ghcExtensions = map (\a -> "-X"++ show a) (extensions myLibBuildInfo)

getLibDependencies :: BuildInfo -> [String]
getLibDependencies bi = map getPackageName $ targetBuildDepends bi 

getPackageName :: Dependency -> String
getPackageName (Dependency (PackageName pn) _) = pn

convertModuleNameToString :: ModuleName -> String
convertModuleNameToString modName
 | null modArr = ""
 | otherwise = foldr1 (\w s -> w ++ '.':s) modArr
   where modArr = components modName 


-- | Check and filter all invalid main definission
checkInvalidMain :: ShakerInput -> IO ShakerInput 
checkInvalidMain shIn = mapM checkInvalidMain' (compileInputs shIn) >>= \newCplInp ->
  return $ shIn {compileInputs = newCplInp  }

checkInvalidMain' :: CompileInput -> IO CompileInput
checkInvalidMain' cplInput
 | any (".hs" `isSuffixOf`) oldTargets = do
    newTargets <- filterM doesFileExist oldTargets
    return cplInput {cfTargetFiles = newTargets}
 | otherwise = return cplInput
  where oldTargets = cfTargetFiles cplInput