-- Copyright 2012-2014 Samplecount S.L.
--
-- Licensed under the Apache License, Version 2.0 (the "License");
-- you may not use this file except in compliance with the License.
-- You may obtain a copy of the License at
--
--     http://www.apache.org/licenses/LICENSE-2.0
--
-- Unless required by applicable law or agreed to in writing, software
-- distributed under the License is distributed on an "AS IS" BASIS,
-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-- See the License for the specific language governing permissions and
-- limitations under the License.

{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE TypeOperators #-}

{-|
Description: Types and functions for working with target tool chains
-}

module Development.Shake.Language.C.ToolChain (
    Linkage(..)
    -- ** Working with toolchains
  , ToolChain
  , ToolChainVariant(..)
  , toolDirectory       -- | Directory prefix for tools in a `ToolChain`, e.g. @\/usr\/local\/linux-armv5-eabi\/bin@.
  , toolPrefix          -- | Prefix string for tools in a `ToolChain`, e.g. @"linux-armv5-eabi-"@.
  , variant             -- | Toolchain variant.
  , compilerCommand     -- | Compiler command, usually used in the `compiler` action.
  , Compiler
  , compiler            -- | Compiler action for this `ToolChain`.
  , archiverCommand     -- | Archiver command, usually used in the `archiver` action.
  , Archiver
  , archiver            -- | Archiver action for this `ToolChain`.
  , linkerCommand       -- | Linker command, usually used in the `linker` action.
  , Linker
  , LinkResult(..)
  , linker              -- | Linker action for this `ToolChain`.
  , defaultBuildFlags   -- | Action returning the default `BuildFlags` for this `ToolChain`.

  -- ** Interfacing with other build systems
  , applyEnv
  , toEnv
  -- ** Utilities for toolchain writers
  , defaultToolChain
  , defaultCompiler
  , defaultArchiver
  , defaultLinker
  , toolFromString
  , tool
) where

import           Control.Applicative
import           Data.Char (toLower)
import           Data.List (isInfixOf, isSuffixOf)
import           Data.Monoid (mempty)
import           Development.Shake
import           Development.Shake.FilePath
import           Development.Shake.Util (needMakefileDependencies)
import           Development.Shake.Language.C.Label
import           Development.Shake.Language.C.BuildFlags
import           Development.Shake.Language.C.Language (languageOf)
import           Development.Shake.Language.C.Util

-- | Linkage type, static or shared.
data Linkage = Static | Shared deriving (Enum, Eq, Show)

-- | Link result type
data LinkResult =
    Executable      -- ^ Executable
  | SharedLibrary   -- ^ Shared (dynamically linked) library
  | LoadableLibrary -- ^ Dynamically loadable library
  deriving (Enum, Eq, Show)

-- | Toolchain variant.
data ToolChainVariant =
    Generic   -- ^ Unspecified toolchain
  | GCC       -- ^ GNU Compiler Collection (gcc) toolchain
  | LLVM      -- ^ Low-Level Virtual Machine (LLVM) toolchain
  deriving (Eq, Show)

-- | `Action` type for producing an object file from a source file.
type Compiler =
     ToolChain  -- ^ Toolchain
  -> BuildFlags -- ^ Compiler flags
  -> FilePath   -- ^ Input source file
  -> FilePath   -- ^ Output object file
  -> Action ()

-- | `Action` type for linking object files into an executable or a library.
type Linker   =
     ToolChain  -- ^ Toolchain
  -> BuildFlags -- ^ Linker flags
  -> [FilePath] -- ^ Input object files
  -> FilePath   -- ^ Output link product
  -> Action ()

-- | `Action` type for archiving object files into a static library.
type Archiver =
    ToolChain   -- ^ Toolchain
 -> BuildFlags  -- ^ Archiver flags
 -> [FilePath]  -- ^ Input object files
 -> FilePath    -- ^ Output object archive (static library)
 -> Action ()

data ToolChain = ToolChain {
    _variant :: ToolChainVariant
  , _toolDirectory :: Maybe FilePath
  , _toolPrefix :: String
  , _compilerCommand :: FilePath
  , _compiler :: Compiler
  , _archiverCommand :: FilePath
  , _archiver :: Archiver
  , _linkerCommand :: FilePath
  , _linker :: LinkResult -> Linker
  , _defaultBuildFlags :: Action (BuildFlags -> BuildFlags)
  }

mkLabel ''ToolChain

-- | Default compiler action.
defaultCompiler :: Compiler
defaultCompiler toolChain buildFlags input output = do
  need $ [input]
  let depFile = output <.> "d"
  command_ [] (tool toolChain compilerCommand)
    $  concatMapFlag "-isystem" (get systemIncludes buildFlags)
    ++ mapFlag "-I" (get userIncludes buildFlags)
    ++ defineFlags buildFlags
    ++ get preprocessorFlags buildFlags
    ++ compilerFlagsFor (languageOf input) buildFlags
    ++ ["-MD", "-MF", depFile]
    ++ ["-c", "-o", output, input]
  needMakefileDependencies depFile

-- | Default archiver action.
defaultArchiver :: Archiver
defaultArchiver toolChain buildFlags inputs output = do
    need inputs
    command_ [] (tool toolChain archiverCommand)
        $ get archiverFlags buildFlags
        ++ [output]
        ++ inputs

-- | Default linker action.
defaultLinker :: Linker
defaultLinker toolChain buildFlags inputs output = do
    let localLibs = get localLibraries buildFlags
        buildFlags' = append libraryPath (map takeDirectory localLibs)
                      -- Local libraries must be passed to the linker before system libraries they depend on
                    . prepend libraries (map (strip.dropExtension.takeFileName) localLibs)
                    $ buildFlags
    need $ inputs ++ localLibs
    command_ [] (tool toolChain linkerCommand)
          $  inputs
          ++ get linkerFlags buildFlags'
          ++ concatMapFlag "-L" (get libraryPath buildFlags')
          ++ concatMapFlag "-l" (get libraries buildFlags')
          ++ ["-o", output]
    where
      strip ('l':'i':'b':rest) = rest
      strip x = x

-- | Default toolchain.
--
-- Probably not useful without modification.
defaultToolChain :: ToolChain
defaultToolChain =
    ToolChain {
        _variant = GCC
      , _toolDirectory = Nothing
      , _toolPrefix = ""
      , _compilerCommand = "gcc"
      , _compiler = defaultCompiler
      , _archiverCommand = "ar"
      , _archiver = defaultArchiver
      , _linkerCommand = "gcc"
      , _linker = \linkResult linkerCmd ->
          case linkResult of
            Executable -> defaultLinker linkerCmd
            _          -> defaultLinker linkerCmd . append linkerFlags ["-shared"]
      , _defaultBuildFlags = return id
      }

-- | Given a tool chain command name, construct the command's full path, taking into account the toolchain's `toolPrefix`.
toolFromString ::
    ToolChain -- ^ Toolchain
 -> String    -- ^ Command name
 -> FilePath  -- ^ Full command path
toolFromString toolChain name =
  let c = _toolPrefix toolChain ++ name
  in maybe c (</> c) (_toolDirectory toolChain)

-- | Construct the full path of a predefined tool given a `ToolChain` accessor.
tool ::
    ToolChain               -- ^ Toolchain
 -> (ToolChain :-> String)  -- ^ Toolchain accessor
 -> FilePath                -- ^ Full command path
tool toolChain getter = toolFromString toolChain (get getter toolChain)

-- | Apply the current environment and return a modified toolchain.
--
-- This function is experimental and subject to change!
--
-- Currently recognised environment variables are
--
--  [@CC@] Path to @C@ compiler.
--
--  [@LD@] Path to linker.
--
--  [@SHAKE_TOOLCHAIN_VARIANT@] One of the values of 'ToolChainVariant' (case insensitive). If this variable is not present, an attempt is made to determine the toolchain variant from the @C@ compiler command.
applyEnv :: ToolChain -> Action ToolChain
applyEnv toolChain = do
  cc <- getEnv "CC"
  ld <- getEnv "LD"
  vendor <- getEnv "SHAKE_TOOLCHAIN_VARIANT"
  return $ maybe id (set compilerCommand) cc
         $ maybe id (set linkerCommand) ld
         . maybe id (set variant) ((vendor >>= parseVendor) <|> (cc >>= vendorFromCommand))
         $ toolChain
  where
    parseVendor s =
      case map toLower s of
        "gcc" -> Just GCC
        "llvm" -> Just LLVM
        "clang" -> Just LLVM
        _ -> Nothing
    vendorFromCommand path =
      let x = takeFileName path
      in if "gcc" `isInfixOf` x || "g++" `isInfixOf` x
         then Just GCC
         else if "clang" `isInfixOf` x
         then Just LLVM
         else Just Generic

-- | Export a 'ToolChain' definition to a list of environment variable mappings, suitable e.g. for calling third-party configure scripts in cross-compilation mode.
--
-- Needs some fleshing out; currently only works for "standard" binutil toolchains.
toEnv :: ToolChain -> Action [(String,String)]
toEnv tc = do
  flags <- (\f -> f mempty) <$> get defaultBuildFlags tc
  let cflags =  concatMapFlag "-I" (map escapeSpaces (get systemIncludes flags))
             ++ concatMapFlag "-I" (map escapeSpaces (get userIncludes flags))
             ++ defineFlags flags
             ++ get preprocessorFlags flags
             ++ compilerFlagsFor (Just C) flags
      cxxflags = cflags ++ compilerFlagsFor (Just Cpp) flags
      ldflags =  get linkerFlags flags
              ++ concatMapFlag "-L" (get libraryPath flags)
              ++ concatMapFlag "-l" (get libraries flags)
      c2cxx path = let x = takeFileName path
                   in if "gcc" `isSuffixOf` x
                      then (++"g++") . reverse . drop 3 . reverse $ path
                      else if "clang" `isSuffixOf` x
                           then path ++ "++"
                           else path
  let cc = tool tc compilerCommand
      cpp = toolFromString tc "cpp"
  cppExists <- doesFileExist cpp
  let cpp' = if cppExists then cpp else cc ++ " -E"
  return $ [
      ("CPP", cpp')
    , ("AR", tool tc archiverCommand)
    , ("NM", toolFromString tc "nm")
    , ("CC", tool tc compilerCommand)
    -- FIXME: Configure toolchain with compiler command per language?
    , ("CXX", c2cxx $ tool tc compilerCommand)
    , ("LD", toolFromString tc "ld")
    , ("RANLIB", toolFromString tc "ranlib")
    , ("CFLAGS", unwords cflags)
    , ("CPPFLAGS", unwords cflags)
    , ("CXXFLAGS", unwords cxxflags)
    , ("LDFLAGS", unwords ldflags)
    ]