{-# LANGUAGE NoMonomorphismRestriction, FlexibleContexts #-}
module Main (main) where

import System.Environment
         ( getArgs )
import System.Exit
         ( exitWith, ExitCode(..) )
import System.Console.GetOpt
         ( OptDescr(..), ArgDescr(..), ArgOrder(..), getOpt, usageInfo )
import Control.Monad
         ( when )
import Data.Version
         ( showVersion )

import Paths_hbuild
         ( version )

import Make.Rules.Dyn
import Distribution.ModuleName hiding (main)
import Data.List
import Make.Rule
import Distribution.Simple.Program as Cabal
import Distribution.Simple.Compiler
import Distribution.Text
import Data.Version
import Data.Maybe
import System.Directory
import System.FilePath
import Distribution.Simple.GHC
import Distribution.Verbosity
import Distribution.Simple.PackageIndex
import qualified Data.Map as M
import Distribution.InstalledPackageInfo hiding (exposed)
import qualified Distribution.InstalledPackageInfo as Pkg
import Distribution.Package
import Make.Goal
import Make.Module hiding (get)
import Data.Cache.Dynamic
import Data.Traversable hiding (mapM)
import Make.MakeM
import Make.JobControl
import Control.Applicative 
import Make.Graph
import Control.Monad.Trans
import Control.Monad.State.Strict
import Make.Memo
import Make.Rules.Dyn.Program.Builtin
import Make.Rules.Dyn.Program
import Make.Rules.Dyn.Types
import Data.DynamicC (DynamicC, TargetCxt, Cxt)

dist = "_hbuild"

cachefile = dist </> "cache"

main :: IO ()
main = do
  (opts, args) <- getOpts

  when (null args) (die usageMessage)
  modules <- mapM (\arg -> case simpleParse arg of
                            Just m -> return m
                            Nothing -> error $ "not a module name: " ++ arg) args :: IO [ModuleName]
  putStrLn $ "search path   : " ++ show (optSearchPath opts)
  putStrLn $ "target modules: " ++ show modules
  
  createDirectoryIfMissing True dist
  let imports = Cons defaultProgramUser (Cons defaultVerbosity (Cons (rulesIO ghc dist (optSearchPath opts)) (Cons (mkLib modules) Nil)))
  case optCommand opts of
    Infer -> do
             s <- execExpr opts . runModuleT imports $ (,,) <$> allModules <*> localModules <*> extensions
             (allmods,local,exts) <- fmap (fromMaybe (error "error while chasing dependencies")) (return s)
             let externals = allmods \\ local
             xs <- resolveToPackages externals
             putStr "build-depends: "
             putStrLn . intercalate ", " . map display $ xs
             putStrLn "other-modules: "
             printModules (local \\ modules)
             putStr "extensions: "
             putStrLn . intercalate ", " $ exts
    Build -> execExpr opts (runModuleT imports (traverse (fst . hi) modules)) >>= print


execExpr :: (Eq a) => Options -> 
  WApp (Rule (Pure IO)) (DynamicC TargetCxt) (DynamicC Cxt) a -> 
  IO (Maybe a)
execExpr opts e = do 
  jc <- poolThreadedJC (optThreads opts)
  withCacheFile cachefile $ \cache -> do 
                     flip evalStateT emptyGraph $ do
                          runGoal jc (matchGoal (Match matchIO))
                                      e  cache

printModules :: (Text a, Ord a) => [a] -> IO ()
printModules ms = putStrLn . unlines . map ("    "++) . map display . sort $ ms

resolveToPackages :: [ModuleName] -> IO [PackageId]
resolveToPackages ms = do 
  pc <- configureAllKnownPrograms minBound (addKnownPrograms [Cabal.ghcProgram,Cabal.ghcPkgProgram] emptyProgramConfiguration)
  ix <- getInstalledPackages minBound [GlobalPackageDB, UserPackageDB] pc
  let pkgs = filter Pkg.exposed $ allPackages ix
      -- is installedPackageId correct?
      m = M.fromList [(m, packageId p) | p <- pkgs, m <- exposedModules p]
  return $ nub $ catMaybes $ map (`M.lookup` m) ms 
      
die :: String -> IO a
die msg = putStr msg >> exitWith (ExitFailure 1)

-- GetOpt

data Options = Options {
    optHelp          :: Bool,
    optVersion       :: Bool,
    optSearchPath    :: [FilePath],
    optThreads       :: !Int,
    optCommand       :: !Command
  }

data Command = Infer | Build
defaultOptions :: Options
defaultOptions = Options {
    optHelp          = False,
    optVersion       = False,
    optSearchPath    = ["."],
    optThreads       = 1,
    optCommand       = Infer
  }

getOpts :: IO (Options, [String])
getOpts = do
  args <- getArgs
  case accumOpts $ getOpt RequireOrder optionDescriptions args of
    (opts, _,    _)
      | optHelp opts    -> printHelp
    (opts, args, [])
      | optVersion opts -> printVersion
      | otherwise       -> return (opts, args)
    (_,     _, errs)    -> printErrors errs
  where
    printErrors errs = die (unlines $ errs ++ ["see hbuild --help for useage"])
    printHelp = do
      putStrLn usageMessage
      putStrLn "hbuild is a build tool for Haskell code"
      exitWith ExitSuccess
    printVersion = do
      putStrLn $ "hbuild version "
              ++ showVersion version
      exitWith ExitSuccess
    accumOpts (opts, args, errs) =
      (foldr (flip (.)) id opts defaultOptions, args, errs)

usageMessage :: String
usageMessage = usageInfo usage optionDescriptions
  where usage = "Usage: hbuild [OPTION ...] [MODULE]\n\nOptions:"

optionDescriptions :: [OptDescr (Options -> Options)]
optionDescriptions =
  [ Option ['h'] ["help"]
      (NoArg (\opts -> opts { optHelp = True }))
      "Show this help text"
  , Option ['V'] ["version"]
      (NoArg (\opts -> opts { optVersion = True }))
      "Print version information"
  , Option ['i'] []
      (ReqArg (\dir opts -> opts { optSearchPath = optSearchPath opts ++ [dir] }) "DIR")
      "Sources search path"
  , Option ['j'] []
      (ReqArg (\n opts -> opts { optThreads = read n }) "N")
      "number of concurrent threads"
  ,Option [] ["build"]
      (NoArg (\opts -> opts { optCommand = Build }))
      "Build the modules"
  ,Option [] ["infer"]
    (NoArg (\opts -> opts { optCommand = Infer }))
    "Infer data for a .cabal file"
  ]
