{-# LANGUAGE PatternSynonyms #-}

module Development.Shake.C ( -- * Types
                             CConfig (..)
                           , CCompiler (GCC, Clang, GHC, Other, GCCStd, GHCStd)
                           -- * Rules
                           , staticLibR
                           , objectFileR
                           , dynLibR
                           , cBin
                           , cToLib
                           -- * Actions
                           , ccAction
                           , staticLibA
                           -- * Helper functions
                           , cconfigToArgs
                           , ccToString
                           , ccFromString
                           , host
                           ) where

import           Control.Monad
import           Data.List                  (isSuffixOf)
import           Data.Semigroup
import           Development.Shake
import           Development.Shake.FilePath
import           System.Info

mkQualified :: Monoid a => Maybe a -> a -> a
mkQualified suff = h (g <$> [suff])
    where g = maybe id mappend
          h = foldr fmap id

host :: String
host = arch ++ withManufacturer os
    where withManufacturer "darwin" = "-apple-" ++ os
          withManufacturer _        = "-unknown-" ++ os

pattern GCCStd :: CCompiler
pattern GCCStd = GCC Nothing

pattern GHCStd :: CCompiler
pattern GHCStd = GHC Nothing

ccToString :: CCompiler -> String
ccToString Clang     = "clang"
ccToString (Other s) = s
ccToString (GCC pre) = mkQualified pre "gcc"
ccToString (GHC pre) = mkQualified pre "ghc"

arToString :: CCompiler -> String
arToString (GCC pre) = mkQualified pre "ar"
arToString (GHC pre) = mkQualified pre "ar"
arToString _         = "ar"

ccFromString :: String -> CCompiler
ccFromString "gcc" = GCC Nothing
ccFromString "clang" = Clang
ccFromString "ghc" = GHC Nothing
ccFromString s
    | "gcc" `isSuffixOf` s = GCC (Just (reverse . drop 3 . reverse $ s))
    | "ghc" `isSuffixOf` s = GHC (Just (reverse . drop 3 . reverse $ s))
ccFromString _ = GCC Nothing

data CCompiler = GCC { _prefix :: Maybe String }
               | Clang
               | Other String
               | GHC { _prefix :: Maybe String }
               deriving (Eq)

mapFlags :: String -> ([String] -> [String])
mapFlags s = fmap (s <>)

data CConfig = CConfig { includes  :: [String] -- ^ Directories to be included.
                       , libraries :: [String] -- ^ Libraries against which to link.
                       , libDirs   :: [String] -- ^ Directories to find libraries.
                       , extras    :: [String] -- ^ Extra flags to be passed to the compiler
                       }

-- | Rules for making a static library from C source files
cToLib :: CCompiler
       -> [FilePath] -- ^ C source files
       -> FilePattern -- ^ Static libary output
       -> CConfig
       -> Rules ()
cToLib cc sources lib cfg =
    mconcat [ foldr (>>) (pure ()) objRules
            , staticLibR cc (g sources) lib cfg
            ]
    where objRules = objectFileR cc <$> g sources <*> pure lib <*> pure cfg
          g = fmap (-<.> "o")

cBin :: CCompiler
     -> [FilePath] -- ^ C source files
     -> FilePattern -- ^ Binary file output
     -> CConfig
     -> Rules ()
cBin cc sources bin cfg = bin %> \out -> ccAction cc sources out cfg

ccAction :: CmdResult r
         => CCompiler
         -> [FilePath] -- ^ Source files
         -> FilePath -- ^ Binary file output
         -> CConfig
         -> Action r
ccAction cc sources out cfg =
    need sources >>
    (command [EchoStderr False] (ccToString cc) . (("-o" : out : sources) <>) . cconfigToArgs) cfg

cconfigToArgs :: CConfig -> [String]
cconfigToArgs (CConfig is ls ds es) = join [ mapFlags "-I" is, mapFlags "-l" ls, mapFlags "-L" ds, es ]

dynLibR :: CCompiler
        -> [FilePath]
        -> FilePattern
        -> CConfig
        -> Rules ()
dynLibR cc objFiles shLib cfg =
    shLib %> \out ->
        need objFiles >>
        command mempty (ccToString cc) ("-shared" : "-o" : out : objFiles <> cconfigToArgs cfg)

objectFileR :: CCompiler
            -> FilePath -- ^ C source file
            -> FilePattern -- ^ Object file output
            -> CConfig
            -> Rules ()
objectFileR cc srcFile objFile cfg =
    objFile %> \out ->
        need [srcFile] >>
        command mempty (ccToString cc) (srcFile : "-c" : "-fPIC" : "-o" : out : cconfigToArgs cfg)

staticLibA :: CmdResult r
           => CCompiler
           -> [FilePath] -- ^ Object files to be linked
           -> FilePattern -- ^ File pattern for static library outputs
           -> CConfig
           -> Action r
staticLibA ar objFiles stalib _ =
    need objFiles >>
    command mempty (arToString ar) ("rcs" : stalib : objFiles)

staticLibR :: CCompiler -- ^ ar binary
           -> [FilePath] -- ^ Object files to be linked
           -> FilePattern -- ^ File pattern for static library outputs
           -> CConfig
           -> Rules ()
staticLibR ar objFiles stalib cfg =
    stalib %> \out -> staticLibA ar objFiles out cfg