-- Copyright 2012-2016 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.

{-|
Description: Toolchain definitions and utilities for Android

This module provides toolchain definitions and utilities for targeting Android.
See "Development.Shake.Language.C.Rules" for examples of how to use a target
toolchain.

The minimum required Android NDK revision is 11c.
-}
module Development.Shake.Language.C.Target.Android (
    target
  , sdkVersion
  , toolChain
  , abiString
  , gnustl
  , libcxx
  , native_app_glue
) where

import           Control.Category ((>>>))
import           Development.Shake.FilePath
import           Data.Version (Version(..), showVersion)
import           Development.Shake.Language.C.BuildFlags
import           Development.Shake.Language.C.Target
import           Development.Shake.Language.C.Label
import           Development.Shake.Language.C.ToolChain
import qualified System.Info as System

unsupportedArch :: Arch -> a
unsupportedArch arch = error $ "Unsupported Android target architecture " ++ show arch

toolChainPrefix :: Target -> String
toolChainPrefix x =
    case targetArch x of
        X86 _ -> "x86-"
        Arm Arm64 -> "aarch64-linux-android-"
        Arm _ -> "arm-linux-androideabi-"
        arch  -> unsupportedArch arch

toolPrefix_ :: Target -> String
toolPrefix_ x =
  case targetArch x of
    X86 _ -> "i686-linux-android-"
    _     -> toolChainPrefix x

osPrefix :: String
osPrefix = System.os ++ "-" ++ cpu
    where cpu = case System.arch of
                    "i386" -> "x86"
                    arch   -> arch

-- | Android target for architecture.
target :: Arch -> Target
target = Target Android (Platform "android")

mkDefaultBuildFlags :: FilePath -> Version -> Arch -> BuildFlags -> BuildFlags
mkDefaultBuildFlags ndk version arch =
      append compilerFlags [(Nothing, [sysroot, march])]
  >>> append compilerFlags (archCompilerFlags arch)
  >>> append compilerFlags [(Nothing, [
        "-fpic"
      , "-ffunction-sections"
      , "-funwind-tables"
      , "-fstack-protector"
      , "-no-canonical-prefixes"])]
  >>> append linkerFlags [sysroot, march]
  >>> append linkerFlags (archLinkerFlags arch)
  >>> append linkerFlags ["-Wl,--no-undefined", "-Wl,-z,relro", "-Wl,-z,now"]
  >>> append linkerFlags ["-no-canonical-prefixes"]
  >>> append archiverFlags ["crs"]
  where
    sysroot =    "--sysroot="
              ++ ndk
              </> "platforms"
              </> "android-" ++ show (head (versionBranch version))
              </> "arch-" ++ case arch of
                              (X86 _)     -> "x86"
                              (Arm Arm64) -> "arm64"
                              (Arm _)     -> "arm"
                              _           -> unsupportedArch arch
    march = "-march=" ++ case arch of
                          X86 I386   -> "i386"
                          X86 I686   -> "i686"
                          X86 X86_64 -> "x86_64"
                          Arm Armv5  -> "armv5te"
                          Arm Armv6  -> "armv5te"
                          Arm Armv7  -> "armv7-a"
                          Arm Arm64  -> "armv8-a"
                          _ -> unsupportedArch arch
    archCompilerFlags (Arm Armv7) = [(Nothing, ["-mfloat-abi=softfp", "-mfpu=neon"])]
    archCompilerFlags (Arm Arm64) = []
    archCompilerFlags (Arm _)     = [(Nothing, ["-mtune=xscale", "-msoft-float"])]
    archCompilerFlags _           = []
    archLinkerFlags (Arm Armv7)   = ["-Wl,--fix-cortex-a8"]
    archLinkerFlags _             = []

-- | Construct a version record from an integral Android SDK version.
--
-- prop> sdkVersion 19 == Version [19] []
sdkVersion :: Int -> Version
sdkVersion n = Version [n] []

gccToolChain :: FilePath -> Target -> FilePath
gccToolChain ndk target =
  ndk </> "toolchains"
      </> toolChainPrefix target ++ showVersion (Version [4,9] [])
      </> "prebuilt"
      </> osPrefix

-- | Construct an Android toolchain.
toolChain :: FilePath                     -- ^ NDK source directory
          -> Version                      -- ^ SDK version, see `sdkVersion`
          -> ToolChainVariant             -- ^ Toolchain variant
          -> Target                       -- ^ Build target, see `target`
          -> ToolChain                    -- ^ Resulting toolchain
toolChain "" _ _ _ = error "Empty NDK directory"
toolChain ndk version GCC t =
    set variant GCC
  $ set toolDirectory (Just (gccToolChain ndk t </> "bin"))
  $ set toolPrefix (toolPrefix_ t)
  $ set compilerCommand "gcc"
  $ set archiverCommand "ar"
  $ set linkerCommand "g++"
  $ set defaultBuildFlags (return $ mkDefaultBuildFlags ndk version (targetArch t))
  $ defaultToolChain
toolChain ndk version LLVM t =
    set variant LLVM
  $ set toolDirectory (Just (ndk </> "toolchains"
                                 </> "llvm"
                                 </> "prebuilt"
                                 </> osPrefix
                                 </> "bin"))
  $ set compilerCommand "clang"
  $ set archiverCommand (gccToolChain ndk t </> "bin" </> toolPrefix_ t ++ "ar")
  $ set linkerCommand "clang++"
  $ set defaultBuildFlags (return $
    let flags = [ "-target", llvmTarget t
                , "-gcc-toolchain", gccToolChain ndk t ]
    in  mkDefaultBuildFlags ndk version (targetArch t)
      . append compilerFlags [(Nothing, flags)]
      . append linkerFlags flags
    )
  $ defaultToolChain
  where
    llvmTarget x =
      case targetArch x of
        Arm Armv5 -> "armv5te-none-linux-androideabi"
        Arm Armv7 -> "armv7-none-linux-androideabi"
        Arm Arm64 -> "aarch64-none-linux-android"
        X86 I386  -> "i686-none-linux-android"
        arch      -> unsupportedArch arch
toolChain _ _ tcVariant _ =
  error $ "Unsupported toolchain variant " ++ show tcVariant

-- | Valid Android ABI identifier for the given architecture.
abiString :: Arch -> String
abiString (Arm Armv5) = "armeabi"
abiString (Arm Armv6) = "armeabi"
abiString (Arm Armv7) = "armeabi-v7a"
abiString (Arm Arm64) = "arm64-v8a"
abiString (X86 _)     = "x86"
abiString arch        = unsupportedArch arch

-- | Source paths and build flags for the @native_app_glue@ module.
native_app_glue :: FilePath -- ^ NDK source directory
                -> ([FilePath], BuildFlags -> BuildFlags)
native_app_glue ndk =
  ( [ndk </> "sources/android/native_app_glue/android_native_app_glue.c"]
  , append systemIncludes [ndk </> "sources/android/native_app_glue"] )

-- | Build flags for building with and linking against the GNU @gnustl@ standard C++ library.
gnustl :: Version                     -- ^ GNU STL version
       -> Linkage                     -- ^ `Static` or `Shared`
       -> FilePath                    -- ^ NDK source directory
       -> Target                      -- ^ Build target, see `target`
       -> (BuildFlags -> BuildFlags)  -- ^ 'BuildFlags' modification function
gnustl version linkage ndk t =
    append systemIncludes [stlPath </> "include", stlPath </> "libs" </> abi </> "include"]
  . append libraryPath [stlPath </> "libs" </> abi]
  . append libraries [lib]
    where stlPath = ndk </> "sources/cxx-stl/gnu-libstdc++" </> showVersion version
          abi = abiString (targetArch t)
          lib = case linkage of
                  Static -> "gnustl_static"
                  Shared -> "gnustl_shared"

-- | Build flags for building with and linking against the LLVM @libc++@ standard C++ library.
libcxx :: Linkage                     -- ^ `Static` or `Shared`
       -> FilePath                    -- ^ NDK source directory
       -> Target                      -- ^ Build target, see `target`
       -> (BuildFlags -> BuildFlags)  -- ^ 'BuildFlags' modification function
libcxx linkage ndk t =
    append systemIncludes [ stl </> "llvm-libc++" </> "libcxx" </> "include"
                          -- NOTE: libcxx needs to be first in include path!
                          , stl </> "llvm-libc++abi" </> "libcxxabi" </> "include"
                          , ndk </> "sources" </> "android" </> "support" </> "include" ]
  . append compilerFlags [(Just Cpp, ["-stdlib=libc++"])]
  . append libraryPath [stl </> "llvm-libc++" </> "libs" </> abi]
  . prepend libraries [lib]
    where stl = ndk </> "sources" </> "cxx-stl"
          abi = abiString (targetArch t)
          lib = case linkage of
                  Static -> "c++_static"
                  Shared -> "c++_shared"