{-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE TupleSections #-}

import Control.Exception
import Control.Monad
import Control.Applicative
import Data.List (isPrefixOf)
import Distribution.PackageDescription
import Distribution.Verbosity
import Distribution.Simple
import Distribution.Simple.BuildPaths
import Distribution.Simple.LocalBuildInfo
import Distribution.Simple.Setup
import Distribution.Simple.Utils
import Distribution.Simple.Program
import Distribution.Simple.Program.Db
import Distribution.Simple.Program.Find
import System.Posix.Files(createSymbolicLink)
import System.Directory
import System.FilePath
import System.Info(os)

main :: IO ()
main = do
  curDir <- getCurrentDirectory

  let confProgram' = confProgram
        { programFindLocation =  \v _ -> findProgramOnSearchPath v
                                         [ProgramSearchPathDir $ curDir </> "llvm"] "configure"
        }

  defaultMainWithHooks simpleUserHooks
      { confHook  = libClangConfHook
      , buildHook = libClangBuildHook
      , copyHook  = libClangCopyHook
      , cleanHook = libClangCleanHook

      , hookedPrograms = hookedPrograms simpleUserHooks ++
                         [ confProgram'
                         , libtoolProgram
                         , makeProgram
                         , swVersProgram
                         ]
      }

libclangLibraries :: [String]
libclangLibraries =
  [ "clang_static"
  , "clangARCMigrate"
  , "clangAST"
  , "clangAnalysis"
  , "clangBasic"
  , "clangCodeGen"
  , "clangDriver"
  , "clangEdit"
  , "clangFormat"
  , "clangFrontend"
  , "clangFrontendTool"
  , "clangIndex"
  , "clangLex"
  , "clangParse"
  , "clangRewriteCore"
  , "clangRewriteFrontend"
  , "clangSema"
  , "clangSerialization"
  , "clangTooling"
  , "LLVMBitReader"
  , "LLVMCore"
  , "LLVMMCParser"
  , "LLVMMC"
  , "LLVMOption"
  , "LLVMSupport"
  , "LLVMTransformUtils"
  ]

libclangSharedLibraries :: [String]
libclangSharedLibraries = ["clang", "LLVM-3.4"]

libClangConfHook :: (GenericPackageDescription, HookedBuildInfo) -> ConfigFlags
                 -> IO LocalBuildInfo
libClangConfHook (pkg, pbi) flags = do
  let verbosity = fromFlag (configVerbosity flags)
  lbi <- confHook simpleUserHooks (pkg, pbi) flags

  -- Compute some paths that need to be absolute.
  curDir <- getCurrentDirectory
  let llvmRepoDir   = curDir </> "llvm"
      llvmBuildDir  = curDir </> "build"
      llvmPrefixDir = llvmBuildDir </> "out"
      clangRepoDir  = curDir </> "clang"
      clangLinkPath = llvmRepoDir </> "tools" </> "clang"

  -- Infer which standard library to use.
  cppStdLib <- preferredStdLib (withPrograms lbi) flags
  let (linkCPPStdLib, confCPPStdLib) =
        case cppStdLib of
          LibStdCPP -> ("-lstdc++", "no")
          LibCPP    -> ("-lc++", "yes")

  let addLdOptions = onLocalLibBuildInfo
                   . onLdOptions
                   $ (++ [ "-lpthread"
                         , linkCPPStdLib
                         , "-lncurses"
                         ])

  -- Ensure that the LLVM build process sees clang.
  createDirectoryIfMissingVerbose verbosity True llvmPrefixDir
  clangLinkExists <- doesDirectoryExist clangLinkPath
  unless clangLinkExists $ createSymbolicLink clangRepoDir clangLinkPath

  notice verbosity "Configuring LLVM and Clang..."

  makefileExists <- doesFileExist $ llvmBuildDir </> "Makefile"
  unless makefileExists $
    inDir llvmBuildDir $
      runDbProgram verbosity confProgram (withPrograms lbi)
             [ "--enable-optimized"
             , "--enable-keep-symbols"
             , "--disable-jit"
             , "--disable-docs"
             , "--enable-shared"
             , "--enable-bindings=none"
             , "--enable-libcpp=" ++ confCPPStdLib
             , "--prefix=" ++ llvmPrefixDir
             ]

  return $ addLdOptions lbi

libClangBuildHook :: PackageDescription -> LocalBuildInfo -> UserHooks -> BuildFlags -> IO ()
libClangBuildHook pkg lbi usrHooks flags = do
    curDir <- getCurrentDirectory
    let verbosity = fromFlag (buildVerbosity flags)
        llvmPrefixDir = curDir </> "build" </> "out"
        llvmLibDir = llvmPrefixDir </> "lib"
        libclangLibrariesFiles = map mkStaticLib libclangLibraries

        addLLVMComponents = onIncludeDirs (++ [".", llvmPrefixDir </> "include"])
                          . onExtraLibs (++ libclangSharedLibraries)
                          . onExtraLibDirs (++ [llvmLibDir])

        addGHCArgs = onProgram ghcProgram
                   . onProgramOverrideArgs
                   $ (++ ["-optl-Wl,-rpath," ++
                          libdir (absoluteInstallDirs pkg lbi NoCopyDest)])

        addHsc2hsArgs = if os == "darwin"
                          then onProgram hsc2hsProgram
                             . onProgramOverrideArgs
                             $ (++ [ "--lflag=-Xlinker"
                                   , "--lflag=-rpath"
                                   , "--lflag=-Xlinker"
                                   , "--lflag=" ++ llvmLibDir
                                   ])
                          else id
                      
        lbi' = onLocalLibBuildInfo addLLVMComponents
             . onPrograms (addGHCArgs . addHsc2hsArgs)
             $ lbi

    notice verbosity "Building LLVM and Clang..."

    -- FIXME: We'd ideally like to use the -j option given to cabal-install itself.
    -- Alternatively we could use a command-specific option like
    -- 'cabal install --make-option=-j4', but see
    -- https://github.com/haskell/cabal/issues/1380 for why this doesn't work.
    -- For now we hardcore "-j4".
    inDir (curDir </> "build") $
      runDbProgram verbosity makeProgram (withPrograms lbi') ["-j4", "install"]

    -- OS X's linker _really_ wants to link dynamically, and it doesn't support
    -- the options you'd usually use to control that on Linux. We rename the
    -- libclang library to make sure the linker does what we intend.
    copyFileVerbose verbosity (llvmLibDir </> mkStaticLib "clang") 
                              (llvmLibDir </> mkStaticLib "clang_static")

    -- Build.
    buildHook simpleUserHooks (localPkgDescr lbi') lbi' usrHooks flags

    notice verbosity "Relinking..."

    let componentLibs = concatMap componentLibNames $ componentsConfigs lbi'
        libclangLibs = map (llvmLibDir </>) libclangLibrariesFiles
        mergeLibs = mergeLibraries verbosity lbi' libclangLibs

    -- Merge the libclang static libraries into each Haskell static library.
    forM_ componentLibs $ \componentLib -> do
      when (withVanillaLib lbi') $
        mergeLibs $ mkLibName componentLib

      when (withProfLib lbi') $
        mergeLibs $ mkProfLibName componentLib

mergeLibraries :: Verbosity -> LocalBuildInfo -> [FilePath] -> FilePath -> IO ()
mergeLibraries verbosity lbi libclangLibs libName = do
  let libtool = runLibtool verbosity lbi
      ar = runAr verbosity lbi
      bdir = buildDir lbi
      finalLib = bdir </> libName
      origLib = bdir </> libName <.> "orig"
      allLibs = origLib : libclangLibs

  copyFileVerbose verbosity finalLib origLib

  if os == "darwin"
     then libtool $ ["-static", "-o", finalLib] ++ allLibs
     else ar ["-M"] $ arScript finalLib allLibs

arScript :: FilePath -> [FilePath] -> String
arScript finalLib allLibs = unlines $ ["CREATE " ++ finalLib]
                                   ++ map ("ADDLIB " ++) allLibs
                                   ++ ["SAVE", "END"]

libClangCopyHook :: PackageDescription -> LocalBuildInfo -> UserHooks -> CopyFlags -> IO ()
libClangCopyHook pkg lbi hooks flags = do
  copyHook simpleUserHooks pkg lbi hooks flags

  curDir <- getCurrentDirectory
  let llvmLibDir = curDir </> "build" </> "out" </> "lib"
      verbosity = fromFlag (copyVerbosity flags)
      libCopyDir = libdir $ absoluteInstallDirs pkg lbi NoCopyDest

  notice verbosity "Installing libclang shared libraries..."
  copyFiles verbosity libCopyDir $ map ((llvmLibDir,) . mkSharedLib) libclangSharedLibraries

libClangCleanHook :: PackageDescription -> () -> UserHooks -> CleanFlags -> IO ()
libClangCleanHook pkg v hooks flags = do
  curDir <- getCurrentDirectory
  let verbosity = fromFlag (cleanVerbosity flags)
      buildDir = curDir </> "build"

  notice verbosity "Cleaning LLVM and Clang..."

  buildDirExists <- doesDirectoryExist buildDir
  when buildDirExists $ removeDirectoryRecursive buildDir

  cleanHook simpleUserHooks pkg v hooks flags

runLibtool :: Verbosity -> LocalBuildInfo -> [String] -> IO ()
runLibtool verbosity lbi args = do
  output <- getDbProgramOutput verbosity libtoolProgram (withPrograms lbi) args
  when (verbosity >= deafening) $
    putStrLn output

runAr :: Verbosity -> LocalBuildInfo -> [String] -> String -> IO ()
runAr v lbi args script =
    case lookupProgram arProgram (withPrograms lbi) of
      Nothing    -> die "Couldn't find required program 'ar'"
      Just cProg -> runProgramInvocation v (progWithStdin cProg)
  where
    progWithStdin prog = (programInvocation prog args') { progInvokeInput = Just script }
    args' = if v >= deafening then "-v" : args else args

mkStaticLib :: String -> String
mkStaticLib lname = mkLibName (LibraryName lname)

mkSharedLib :: String -> String
mkSharedLib lname = "lib" ++ lname <.> dllExtension

componentLibNames :: (ComponentName, ComponentLocalBuildInfo, [ComponentName]) -> [LibraryName]
componentLibNames (_, LibComponentLocalBuildInfo {..}, _) = componentLibraries
componentLibNames _                                       = []

confProgram, libtoolProgram, makeProgram, swVersProgram :: Program
confProgram    = simpleProgram "configure"
libtoolProgram = simpleProgram "libtool"
makeProgram    = simpleProgram "make"
swVersProgram  = simpleProgram "sw_vers"

inDir :: FilePath -> IO a -> IO a
inDir dir act = do
  curDir <- getCurrentDirectory
  bracket_ (setCurrentDirectory dir)
           (setCurrentDirectory curDir)
           act

data CPPStdLib = LibStdCPP
               | LibCPP
                 deriving (Eq, Show)

preferredStdLib :: ProgramDb -> ConfigFlags -> IO CPPStdLib
preferredStdLib pdb flags =
  case (preferLibStdCPP, preferLibCPP, os) of
    (Just True, _, _)     -> return LibStdCPP
    (_, Just True, _)     -> return LibCPP
    (_, _, "darwin")      -> darwinPreferredStdLib pdb flags
    _                     -> return LibStdCPP
  where
    preferLibCPP    = lookup (FlagName "preferlibcpp") $ configConfigurationsFlags flags
    preferLibStdCPP = lookup (FlagName "preferlibstdcpp") $ configConfigurationsFlags flags

darwinPreferredStdLib :: ProgramDb -> ConfigFlags -> IO CPPStdLib
darwinPreferredStdLib pdb flags = do
  let verbosity = fromFlag (configVerbosity flags)
  darwinVer <- getDbProgramOutput verbosity swVersProgram pdb ["-productVersion"]

  -- We want LibStdCPP for 10.8 and below, but LibCPP for anything newer.
  let libStdCPPVers = ["10.0", "10.1", "10.2", "10.3", "10.4", "10.5", "10.6", "10.7", "10.8"]
  return $ if any (`isPrefixOf` darwinVer) libStdCPPVers
             then LibStdCPP
             else LibCPP

type Lifter a b = (a -> a) -> b -> b

onLocalPkgDescr :: Lifter PackageDescription LocalBuildInfo
onLocalPkgDescr f lbi = lbi { localPkgDescr = f (localPkgDescr lbi) }

onPrograms :: Lifter ProgramDb LocalBuildInfo
onPrograms f lbi = lbi { withPrograms = f (withPrograms lbi) }

onLibrary :: Lifter Library PackageDescription
onLibrary f lpd = lpd { library = f <$> library lpd }

onLibBuildInfo :: Lifter BuildInfo Library
onLibBuildInfo f lib = lib { libBuildInfo = f (libBuildInfo lib) }

onLocalLibBuildInfo :: Lifter BuildInfo LocalBuildInfo
onLocalLibBuildInfo = onLocalPkgDescr . onLibrary . onLibBuildInfo

onIncludeDirs :: Lifter [FilePath] BuildInfo
onIncludeDirs f libbi = libbi { includeDirs = f (includeDirs libbi) }

onExtraLibs :: Lifter [FilePath] BuildInfo
onExtraLibs f libbi = libbi { extraLibs = f (extraLibs libbi) }

onExtraLibDirs :: Lifter [FilePath] BuildInfo
onExtraLibDirs f libbi = libbi { extraLibDirs = f (extraLibDirs libbi) }

onLdOptions :: Lifter [FilePath] BuildInfo
onLdOptions f libbi = libbi { ldOptions = f (ldOptions libbi) }

onProgram :: Program -> Lifter ConfiguredProgram ProgramDb
onProgram prog f pdb = case lookupProgram prog pdb of
                         Just cProg -> updateProgram (f cProg) pdb
                         Nothing    -> pdb

onProgramOverrideArgs :: Lifter [String] ConfiguredProgram
onProgramOverrideArgs f prog = prog { programOverrideArgs = f (programOverrideArgs prog) }
