module DDC.Build.Builder
        ( BuilderConfig (..)
        , BuilderHost   (..)
        , Builder       (..)
        , BuilderResult (..)
        , builders

        , determineDefaultBuilder
        , determineDefaultBuilderHost)
where
import DDC.Build.Platform
import DDC.Base.Pretty                          hiding ((</>))
import Data.List
import System.FilePath                         
import System.Exit                              hiding (die)
import System.Process
import qualified DDC.Core.Salt.Platform         as Llvm


-- | Configuration information for a builder that is not platform specific.
data BuilderConfig
        = BuilderConfig
        { -- | Directory that holds the source for the runtime system
          --   and base library.
          builderConfigBaseSrcDir       :: FilePath 

          -- | Directory that holds the shared objects for the runtime
          --   system and base library.
        , builderConfigBaseLibDir       :: FilePath

          -- | Runtime library to link with.
        , builderConfigLibFile          :: FilePath -> FilePath -> FilePath }


-- | Builder information that we determine by interrogating the host platform.
--   This tells us what we need to know about the environment that we're 
--   building in, versions of software tools etc. This is separate 
data BuilderHost
        = BuilderHost
        { builderHostLlvmVersion        :: String }


-- | Actions to use to invoke external compilation tools.
data Builder
        = Builder
        { -- | The name of this platform.
          builderName           :: String

          -- | The platform the build is being performed on.
        , buildHost             :: Platform

          -- | The platform we're compiling code for.
        , buildTarget           :: Platform

          -- | The LLVM target specification.
          --   Gives the widths of pointers and primitive numeric types.
        , buildSpec             :: Llvm.Platform

          -- | Directory that holds the source for the runtime system
          --   and base library.
        , buildBaseSrcDir       :: FilePath

          -- | Directory that holds the shared objects for the runtime
          --   system and base library.
        , buildBaseLibDir       :: FilePath 

          -- | Invoke the C compiler
          --   to compile a .c file into a .o file.
        , buildCC               :: FilePath -> FilePath -> IO ()

          -- | Invoke the LLVM compiler
          --   to compile a .ll file into a .s file.
        , buildLlc              :: FilePath -> FilePath -> IO ()

          -- | Version string of the LLVM compiler suite we are using.
        , buildLlvmVersion      :: String 

          -- | Invoke the system assembler
          --   to assemble a .s file into a .o file.
        , buildAs               :: FilePath -> FilePath -> IO ()

          -- | Link an executable.
        , buildLdExe            :: [FilePath] -> FilePath -> IO () 

          -- | Link a static library.
        , buildLdLibStatic      :: [FilePath] -> FilePath -> IO ()

          -- | Link a shared library.
        , buildLdLibShared      :: [FilePath] -> FilePath -> IO () }


-- | The result of a build command.
--
--   We use these so that the called doesn't need to worry about
--   interpreting numeric exit codes. 
data BuilderResult
        -- | Build command completed successfully.
        = BuilderSuccess

        -- | Build command was cancelled or killed by the user.
        --   eg by Control-C on the console.
        | BuilderCanceled     

        -- | Build command failed. 
        --   There is probably something wrong with the generated file.
        --   Unrecognised exit codes also result in this BuilderResult.
        | BuilderFailed
        deriving (Show, Eq)


instance Show Builder where
 show builder
        = "Builder " ++ show (builderName builder)


instance Pretty Builder where
 ppr builder
        = vcat
        [ text "Builder Name : " <> text (builderName builder) 
        , empty
        , text "Host Platform"
        , indent 1 $ ppr $ buildHost builder 
        , empty
        , text "Target Platform"
        , indent 1 $ ppr $ buildTarget builder
        , empty
        , text "LLVM Target Spec"
        , indent 1 $ ppr $ buildSpec builder ]


-- builders -------------------------------------------------------------------
-- | All supported builders.
--   The host and target platforms are the same.
-- 
--   Supported builders are: 
--      @x86_32-darwin@, @x86_64-darwin@,
--      @x86_32-linux@,  @x86_64-linux@,
--      @x86_32-cygwin@,
--      @ppc32-linux@
--
builders :: BuilderConfig -> BuilderHost -> [Builder]
builders config host
 =      [ builder_X8632_Darwin config host Nothing
        , builder_X8664_Darwin config host Nothing
        , builder_X8632_Linux  config host
        , builder_X8664_Linux  config host
        , builder_PPC32_Linux  config host ]


-- defaultBuilder -------------------------------------------------------------
-- | Determine the default builder based on the 'arch' and 'uname' commands.
--   This assumes that the 'host' and 'target' platforms are the same.
--
--   If we don't recognise the result of 'arch' or 'uname', or don't have 
--   a default builder config for this platform then `Nothing`.
determineDefaultBuilder :: BuilderConfig -> IO (Maybe Builder)
determineDefaultBuilder config
 = do   mPlatform       <- determineHostPlatform
        mHost           <- determineDefaultBuilderHost

        case (mPlatform, mHost) of
         (Just (Platform ArchX86_32 (OsDarwin mVersion)), Just host)
                -> return $ Just (builder_X8632_Darwin config host mVersion)

         (Just (Platform ArchX86_64 (OsDarwin mVersion)), Just host)
                -> return $ Just (builder_X8664_Darwin config host mVersion)

         (Just (Platform ArchX86_32 OsLinux),  Just host)
                -> return $ Just (builder_X8632_Linux  config host)

         (Just (Platform ArchX86_64 OsLinux),  Just host)
                -> return $ Just (builder_X8664_Linux  config host)

         (Just (Platform ArchPPC_32 OsLinux),  Just host)
                -> return $ Just (builder_PPC32_Linux  config host)

         (Just (Platform ArchX86_32 OsCygwin), Just host)
                -> return $ Just (builder_X8632_Cygwin config host)

         (Just (Platform ArchX86_32 OsMingw),  Just host)
                -> return $ Just (builder_X8632_Mingw  config host)

         _      -> return Nothing


-- | Determine the default builder host configuration, 
--   this the default set of build tools that we can see in the current path.
determineDefaultBuilderHost :: IO (Maybe BuilderHost)
determineDefaultBuilderHost
 = do   
        -- Get the version of the LLVM suite in the current path.
        mStrLlvmVersion  <- determineHostLlvmVersion Nothing
        case mStrLlvmVersion of
         Nothing 
          -> return Nothing

         Just strLlvmVersion
          -> return  $ Just $ BuilderHost
                     { builderHostLlvmVersion = strLlvmVersion }


-- x86_32-darwin ----------------------------------------------------------------
builder_X8632_Darwin config host mVersion
 =      Builder 
        { builderName           = "x86_32-darwin" 
        , buildHost             = Platform ArchX86_32 (OsDarwin mVersion)
        , buildTarget           = Platform ArchX86_32 (OsDarwin mVersion)
        , buildSpec             = Llvm.platform32
        , buildBaseSrcDir       = builderConfigBaseSrcDir config
        , buildBaseLibDir       = builderConfigBaseLibDir config

        , buildLlvmVersion      = builderHostLlvmVersion  host
        , buildLlc    
                = \llFile sFile
                -> doCmd "LLVM compiler"        [(2, BuilderCanceled)]
                [ "opt -O3"
                , llFile
                , "| llc -O3 -march=x86 -relocation-model=pic"
                , "-o", sFile ]

        , buildCC
                = \cFile oFile
                -> doCmd "C compiler"           [(2, BuilderCanceled)]
                [ "cc -Werror -std=c99 -O3 -m32"
                , "-c", cFile
                , "-o", oFile
                , "-I" ++ builderConfigBaseSrcDir config </> "sea/runtime"
                , "-I" ++ builderConfigBaseSrcDir config </> "sea/primitive" ]

        , buildAs
                = \sFile oFile
                -> doCmd "assembler"            [(2, BuilderCanceled)]
                [ "llvm-mc -arch x86 -filetype=obj"

                  -- From LLVM 3.8 we need to set the -triple explicitly which includes
                  -- the macosx OS specifier. llc inserts a pragma into the output
                  -- .s files saying they're for a specific version of macosx. If we
                  -- don't set the same version in the triple passed to llvm-mc then
                  -- it throws a warning. Note that Darwin v14.5 is OSX v10.10.5 etc.
                , case mVersion of
                        Nothing -> ""
                        Just (major, _minor, _patch)
                         -> "-triple=x86-apple-macosx10." ++ show (major - 4)

                , "-o", oFile
                ,       sFile ]

        , buildLdExe
                = \oFiles binFile
                -> doCmd "linker"               [(2, BuilderCanceled)]
                [ "cc -m32 -Wl,-dead_strip" 
                , "-o", binFile
                , intercalate " " oFiles
                , builderConfigBaseLibDir config
                        </> builderConfigLibFile config
                                "libddc-runtime.a"
                                "libddc-runtime.dylib" ]

        , buildLdLibStatic
                = \oFiles libFile
                -> doCmd "linker"               [(2, BuilderCanceled)]
                $ ["ar r", libFile] ++ oFiles 

        , buildLdLibShared
                = \oFiles libFile
                -> doCmd "linker"               [(2, BuilderCanceled)]
                $ [ "cc -m32 -dynamiclib -undefined dynamic_lookup"
                  , "-o", libFile ] ++ oFiles
        }

-- x86_64-darwin --------------------------------------------------------------
builder_X8664_Darwin config host mVersion
 =      Builder
        { builderName           = "x86_64-darwin"
        , buildHost             = Platform ArchX86_64 (OsDarwin mVersion)
        , buildTarget           = Platform ArchX86_64 (OsDarwin mVersion)
        , buildSpec             = Llvm.platform64
        , buildBaseSrcDir       = builderConfigBaseSrcDir config
        , buildBaseLibDir       = builderConfigBaseLibDir config

        , buildLlvmVersion      = builderHostLlvmVersion  host
        , buildLlc    
                = \llFile sFile
                -> doCmd "LLVM compiler"        [(2, BuilderCanceled)]
                [ "opt -O3"
                , llFile
                , "| llc -O3 -march=x86-64 -relocation-model=pic"
                , "-o", sFile ]

        , buildCC
                = \cFile oFile
                -> doCmd "C compiler"           [(2, BuilderCanceled)]
                [ "cc -Werror -std=c99 -O3 -m64"
                , "-c", cFile
                , "-o", oFile
                , "-I" ++ builderConfigBaseSrcDir config </> "sea/runtime"
                , "-I" ++ builderConfigBaseSrcDir config </> "sea/primitive" ]

        , buildAs
                = \sFile oFile
                -> doCmd "assembler"            [(2, BuilderCanceled)]
                [ "llvm-mc -filetype=obj"

                  -- From LLVM 3.8 we need to set the -triple explicitly which includes
                  -- the macosx OS specifier. llc inserts a pragma into the output
                  -- .s files saying they're for a specific version of macosx. If we
                  -- don't set the same version in the triple passed to llvm-mc then
                  -- it throws a warning. Note that Darwin v14.5 is OSX v10.10.5 etc.
                , case mVersion of
                        Nothing -> ""
                        Just (major, _minor, _patch)
                         -> "-triple=x86_64-apple-macosx10." ++ show (major - 4)

                , "-o", oFile
                ,       sFile ]

        , buildLdExe  
                = \oFiles binFile
                -> doCmd "linker"               [(2, BuilderCanceled)]
                [ "cc -m64 -Wl,-dead_strip" 
                , "-o", binFile
                , intercalate " " oFiles
                , builderConfigBaseLibDir config 
                        </> builderConfigLibFile config 
                                "libddc-runtime.a"
                                "libddc-runtime.dylib" ]

        , buildLdLibStatic
                = \oFiles libFile
                -> doCmd "linker"               [(2, BuilderCanceled)]
                $ ["ar r", libFile] ++ oFiles 

        , buildLdLibShared
                = \oFiles libFile
                -> doCmd "linker"               [(2, BuilderCanceled)]
                $ [ "cc -m64 -dynamiclib -undefined dynamic_lookup"
                  , "-o", libFile ] ++ oFiles
        }


-- x86_32-linux ---------------------------------------------------------------
builder_X8632_Linux config host
 =      Builder
        { builderName           = "x86_32-linux"
        , buildHost             = Platform ArchX86_32 OsLinux
        , buildTarget           = Platform ArchX86_32 OsLinux
        , buildSpec             = Llvm.platform32
        , buildBaseSrcDir       = builderConfigBaseSrcDir config
        , buildBaseLibDir       = builderConfigBaseLibDir config

        , buildLlvmVersion      = builderHostLlvmVersion  host
        , buildLlc    
                = \llFile sFile
                -> doCmd "LLVM compiler"        [(2, BuilderCanceled)]
                [ "llc -O3 -march=x86 -relocation-model=pic" 
                ,       llFile 
                , "-o", sFile ]

        , buildCC
                = \cFile oFile
                -> doCmd "C compiler"           [(2, BuilderCanceled)]
                [ "gcc -Werror -std=c99 -O3 -m32 -fPIC"
                , "-c", cFile
                , "-o", oFile
                , "-I" ++ builderConfigBaseSrcDir config </> "sea/runtime"
                , "-I" ++ builderConfigBaseSrcDir config </> "sea/primitive" ]


        , buildAs
                = \sFile oFile
                -> doCmd "assembler"            [(2, BuilderCanceled)]
                [ "as --32"  
                , "-o", oFile
                ,       sFile ]

        , buildLdExe  
                = \oFiles binFile
                -> doCmd "linker"               [(2, BuilderCanceled)]
                [ "gcc -m32" 
                , "-o", binFile
                , intercalate " " oFiles
                , builderConfigBaseLibDir config 
                        </> builderConfigLibFile config
                                "libddc-runtime.a"
                                "libddc-runtime.so" ]

        , buildLdLibStatic
                = \oFiles libFile
                -> doCmd "linker"               [(2, BuilderCanceled)]
                $ ["ar r", libFile] ++ oFiles 

        , buildLdLibShared
                = \oFiles libFile
                -> doCmd "linker"               [(2, BuilderCanceled)]
                $ [ "gcc -shared", "-o", libFile ] ++ oFiles
        }


-- x86_64-linux ---------------------------------------------------------------
builder_X8664_Linux config host
 =      Builder
        { builderName           = "x86_64-linux"
        , buildHost             = Platform ArchX86_64 OsLinux
        , buildTarget           = Platform ArchX86_64 OsLinux
        , buildSpec             = Llvm.platform64
        , buildBaseSrcDir       = builderConfigBaseSrcDir config
        , buildBaseLibDir       = builderConfigBaseLibDir config

        , buildLlvmVersion      = builderHostLlvmVersion  host
        , buildLlc    
                = \llFile sFile
                -> doCmd "LLVM compiler"        [(2, BuilderCanceled)]
                [ "llc -O3 -march=x86-64 -relocation-model=pic" 
                , llFile 
                , "-o", sFile ]

        , buildCC
                = \cFile oFile
                -> doCmd "C compiler"           [(2, BuilderCanceled)]
                [ "gcc -Werror -std=c99 -O3 -m64 -fPIC"
                , "-c", cFile
                , "-o", oFile
                , "-I" ++ builderConfigBaseSrcDir config </> "sea/runtime"
                , "-I" ++ builderConfigBaseSrcDir config </> "sea/primitive" ]


        , buildAs
                = \sFile oFile
                -> doCmd "assembler"            [(2, BuilderCanceled)]
                [ "as --64"  
                , "-o", oFile
                , sFile ] 

        , buildLdExe  
                = \oFiles binFile
                -> doCmd "linker"               [(2, BuilderCanceled)]
                [ "gcc -m64"
                , "-o", binFile
                , intercalate " " oFiles
                , builderConfigBaseLibDir config
                        </> builderConfigLibFile config
                                "libddc-runtime.a"
                                "libddc-runtime.so" ]

        , buildLdLibStatic
                = \oFiles libFile
                -> doCmd "linker"               [(2, BuilderCanceled)]
                $ ["ar r", libFile] ++ oFiles 

        , buildLdLibShared
                = \oFiles libFile
                -> doCmd "linker"               [(2, BuilderCanceled)]
                $ [ "gcc -shared", "-o", libFile ] ++ oFiles
        }


-- ppc32-linux ---------------------------------------------------------------
builder_PPC32_Linux config host
 =      Builder
        { builderName           = "ppc32-linux"
        , buildHost             = Platform ArchPPC_32 OsLinux
        , buildTarget           = Platform ArchPPC_32 OsLinux
        , buildSpec             = Llvm.platform32
        , buildBaseSrcDir       = builderConfigBaseSrcDir config
        , buildBaseLibDir       = builderConfigBaseLibDir config

        , buildLlvmVersion      = builderHostLlvmVersion  host
        , buildLlc    
                = \llFile sFile
                -> doCmd "LLVM compiler"        [(2, BuilderCanceled)]
                [ "llc -O3 -march=ppc32 -relocation-model=pic" 
                , llFile 
                , "-o", sFile ]

        , buildCC
                = \cFile oFile
                -> doCmd "C compiler"           [(2, BuilderCanceled)]
                [ "gcc -Werror -std=c99 -O3 -m32"
                , "-c", cFile
                , "-o", oFile
                , "-I" ++ builderConfigBaseSrcDir config </> "sea/runtime"
                , "-I" ++ builderConfigBaseSrcDir config </> "sea/primitive" ]

        , buildAs
                = \sFile oFile
                -> doCmd "assembler"            [(2, BuilderCanceled)]
                [ "as"
                , "-o", oFile
                , sFile ]

        , buildLdExe  
                = \oFiles binFile
                -> doCmd "linker"               [(2, BuilderCanceled)]
                [ "gcc -m32" 
                , "-o", binFile
                , intercalate " " $ map normalise oFiles
                , builderConfigBaseLibDir config
                        </> builderConfigLibFile config
                                "libddc-runtime.a"
                                "libddc-runtime.so" ]

        , buildLdLibStatic
                = \oFiles libFile
                -> doCmd "linker"               [(2, BuilderCanceled)]
                $ ["ar r", libFile] ++ oFiles 

        , buildLdLibShared
                = \oFiles libFile
                -> doCmd "linker"               [(2, BuilderCanceled)]
                $ [ "gcc -shared", "-o", libFile ] ++ oFiles
        }


-- x86_32-cygwin ---------------------------------------------------------------
builder_X8632_Cygwin config host
 =      Builder
        { builderName           = "x86_32-cygwin"
        , buildHost             = Platform ArchX86_32 OsCygwin
        , buildTarget           = Platform ArchX86_32 OsCygwin
        , buildSpec             = Llvm.platform32
        , buildBaseSrcDir       = builderConfigBaseSrcDir config
        , buildBaseLibDir       = builderConfigBaseLibDir config

        , buildLlvmVersion      = builderHostLlvmVersion  host
        , buildLlc    
                = \llFile sFile
                -> doCmd "LLVM compiler"        [(2, BuilderCanceled)]
                [ "llc -O3 -march=x86 " 
                , normalise llFile
                , "-o", normalise sFile ]

        , buildCC
                = \cFile oFile
                -> doCmd "C compiler"           [(2, BuilderCanceled)]
                [ "gcc-4 -Werror -std=c99 -O3 -m32"
                , "-c", cFile
                , "-o", oFile
                , "-I" ++ builderConfigBaseSrcDir config </> "sea/runtime"
                , "-I" ++ builderConfigBaseSrcDir config </> "sea/primitive" ]

        , buildAs
                = \sFile oFile
                -> doCmd "assembler"            [(2, BuilderCanceled)]
                [ "as --32"
                , "-o", normalise oFile
                , normalise sFile ]

    -- Note on Cygwin we need to use 'gcc-4' explicitly because plain 'gcc'
    -- is a symlink, which Windows doesn't really support.
        , buildLdExe  
                = \oFiles binFile
                -> doCmd "linker"               [(2, BuilderCanceled)]
                [ "gcc-4 -m32" 
                , "-o", normalise binFile
                , intercalate " " $ map normalise oFiles
                , normalise $ builderConfigBaseLibDir config
                        </> "libddc-runtime.a" ] 
                        -- configRuntimeLinkStrategy is ignored

        , buildLdLibStatic
                = \oFiles libFile
                -> doCmd "linker"               [(2, BuilderCanceled)]
                $ ["ar r", libFile] ++ oFiles 

        , buildLdLibShared
                = \oFiles libFile
                -> doCmd "linker"               [(2, BuilderCanceled)]
                $ [ "gcc -shared", "-o", libFile ] ++ oFiles
        }


-- x86_32-mingw ----------------------------------------------------------------
builder_X8632_Mingw config host
 =      Builder
        { builderName           = "x86_32-mingw"
        , buildHost             = Platform ArchX86_32 OsMingw
        , buildTarget           = Platform ArchX86_32 OsMingw
        , buildSpec             = Llvm.platform32
        , buildBaseSrcDir       = builderConfigBaseSrcDir config
        , buildBaseLibDir       = builderConfigBaseLibDir config

        , buildLlvmVersion      = builderHostLlvmVersion  host
        , buildLlc    
                = \llFile sFile
                -> doCmd "LLVM compiler"        [(2, BuilderCanceled)]
                [ "llc -O3 -march=x86 " 
                , normalise llFile
                , "-o", normalise sFile ]

        , buildCC
                = \cFile oFile
                -> doCmd "C compiler"           [(2, BuilderCanceled)]
                [ "gcc -Werror -std=c99 -O3 -m32"
                , "-c", cFile
                , "-o", oFile
                , "-I" ++ builderConfigBaseSrcDir config </> "sea/runtime"
                , "-I" ++ builderConfigBaseSrcDir config </> "sea/primitive" ]

        , buildAs
                = \sFile oFile
                -> doCmd "assembler"            [(2, BuilderCanceled)]
                [ "as --32"
                , "-o", normalise oFile
                , normalise sFile ]

        , buildLdExe  
                = \oFiles binFile
                -> doCmd "linker"               [(2, BuilderCanceled)]
                [ "gcc -m32" 
                , "-o", normalise binFile
                , intercalate " " $ map normalise oFiles
                , normalise $ builderConfigBaseLibDir config 
                        </> "libddc-runtime.a" ] 
                        -- configRuntimeLinkStrategy is ignored

        , buildLdLibStatic
                = \oFiles libFile
                -> doCmd "linker"               [(2, BuilderCanceled)]
                $ ["ar r", libFile] ++ oFiles 

        , buildLdLibShared
                = \oFiles libFile
                -> doCmd "linker"               [(2, BuilderCanceled)]
                $ [ "gcc -shared", "-o", libFile ] ++ oFiles
        }


-- Utils ----------------------------------------------------------------------
-- | Run a system command, and if it fails quit the program.
doCmd   :: String                       -- ^ Description of tool being invoked.
        -> [(Int, BuilderResult)]       -- ^ How to interpret exit codes.
        -> [String]                     -- ^ System command to run.
        -> IO ()

doCmd thing exitCodeMeanings cmdParts
 = do   
        code <- system cmd
        case code of
         ExitSuccess    
          -> return ()

         ExitFailure c
          |  Just meaning        <- lookup c exitCodeMeanings
          -> case meaning of
                BuilderSuccess  -> return ()
                BuilderCanceled -> exitWith $ ExitFailure 2
                BuilderFailed   -> die c

          | otherwise           -> die c

 where  cmd     = unwords cmdParts
        die c   = error
                $ unlines
                [ "System command failed when invoking external " ++ thing ++ "."
                , " Command was: " ++ cmd
                , " Exit code:   " ++ show c ]