# - Create libraries and executables using a common precompiled header (PCH.)
# 
# This module provides PCH versions of add_library and add_executable,
# for GCC and MSVC and Clang. It depends on the module "CMakeParseArguments",
# introduced in CMake 2.8.3
#
# The module defines the global variable:
#   PCH_SUPPORTED - If true, it is safe to use the other macros in the module.
#
# The main user functions provided behave as the standard add_executable() and
# add_library() functions, except that they specify a common PCH file.
#
#  pch_add_executable(name [WIN32] [MACOSX_BUNDLE]
#                  [EXCLUDE_FROM_ALL]
#                  PCH_HEADER <header>
#                  source1 source2 ... sourceN)
#
#  pch_add_library(name [SHARED | MODULE | STATIC]
#                  [EXCLUDE_FROM_ALL]
#                  PCH_HEADER <header>
#                  source1 source2 ... sourceN)
#
#=============================================================================
# Edgar Velázquez-Armendáriz, Cornell University (cs.cornell.edu - eva5)
# Distributed under the OSI-approved MIT License (the "License")
# 
# Copyright (c) 2011 Program of Computer Graphics, Cornell University
# 
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
# 
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
# 
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
#=============================================================================

include (CMakeParseArguments)

set (PCH_MSVC  FALSE)
set (PCH_GCC   FALSE)
set (PCH_CLANG FALSE)

if (MSVC OR (WIN32 AND CMAKE_CXX_COMPILER_ID MATCHES "Intel"))
  set (PCH_MSVC  TRUE)
elseif (CMAKE_CXX_COMPILER_ID MATCHES "Clang")
  set (PCH_CLANG TRUE)
elseif (CMAKE_CXX_COMPILER_ID MATCHES "GNU")
  set (PCH_GCC   TRUE)
elseif (NOT WIN32 AND CMAKE_CXX_COMPILER_ID MATCHES "Intel")
  set (PCH_INTEL TRUE)
endif ()



# Sets PCH_SUPPORTED to TRUE if the platform supports precompiled headers,
# otherwise it is set to FALSE. Do not try to use the
# pch_add_<library|executable> macros unless this is TRUE.
if (PCH_MSVC OR PCH_GCC OR PCH_CLANG OR PCH_INTEL)
  set (PCH_SUPPORTED TRUE)
else ()
  set (PCH_SUPPORTED FALSE)
endif ()



# Helper function to write a file only if its content changes. Used to
# avoid unnecessary rebuilds
function (pvt_write_if_changed filename text)
  set (needs_write TRUE)
  if (EXISTS "${filename}")
    file (READ "${filename}" text_current)
    if ("${text_current}" STREQUAL "${text}")
      set (needs_write FALSE)
    endif ()
  endif ()
  if (needs_write)
    file (WRITE "${filename}" "${text}")
  endif ()
endfunction ()



# Funtion to return the current count of PCH headers
function (pvt_get_pch_count out_var_name)
  get_property(pchcount GLOBAL PROPERTY "pch_count")
  if (NOT pchcount)
    set (pchcount 0)
  endif ()
  set (${out_var_name} ${pchcount} PARENT_SCOPE)
endfunction ()

# Function to increment by one the count of PCH headers
function (pvc_increment_pch_count)
  pvt_get_pch_count (pchcount)
  math (EXPR pchcount "${pchcount} + 1")
  set_property (GLOBAL PROPERTY "pch_count" ${pchcount})
endfunction ()



# Private function to generate the rules for compiling the PCH.
# Assumes pch_header does not contain directories, eg foo.h
# The variable contained in out_stub_src_var will hold the name of the
# generated source stub file.
function (PVT_ADD_PCH_RULE_MSVC pch_header_filename out_stub_src_var pch_subdir_external)
  get_filename_component (pch_header "${pch_header_filename}" NAME)
  get_filename_component (pch_header_path "${pch_header_filename}" PATH)
  get_filename_component (pch_header_name "${pch_header_filename}" NAME_WE)
  if(NOT pch_subdir_external)
    pvt_get_pch_count (pch_count)
    set(pch_subdir "pch.${pch_count}")
  else()
    set(pch_subdir "${pch_subdir_external}")
  endif()
  set (stub_src "${CMAKE_CURRENT_BINARY_DIR}/${pch_subdir}/${pch_header_name}_stub.cpp")
  set (stub_src_text
    "// Stub file for the PCH ${pch_header}. Generated by CMake.\n#include <${pch_header}>\n"
  )
  pvt_write_if_changed ("${stub_src}" "${stub_src_text}")
  set_property (SOURCE "${stub_src}" PROPERTY 
    COMPILE_FLAGS "/Yc\"${pch_header}\" /I\"${pch_header_path}\"")
  set (${out_stub_src_var} "${stub_src}" PARENT_SCOPE)
endfunction ()



# Helper function to extract the configuration-dependent compile flags.
# "cfgname" name is usually Release, Debug, RelWithDebugInfo or RelMinSize
function (pvt_config_flags cfgname out_list_var_name)
  string (TOUPPER ${cfgname} cfg_upper)
  set (args_cxx "${CMAKE_CXX_FLAGS} ${CMAKE_CXX_FLAGS_${cfg_upper}}")
  separate_arguments (args_cxx)
  
  get_directory_property (dir_compile_defs COMPILE_DEFINITIONS_${cfg_upper})
  foreach (dir_def ${dir_compile_defs})
    list (APPEND args_cxx "-D${dir_def}")
  endforeach ()
  
  set (${out_list_var_name} ${args_cxx} PARENT_SCOPE)
endfunction ()



# Based on CMake's proposed module PCH_GCC4_v2.cmake (2010-07-26)
# http://www.cmake.org/Bug/view.php?id=1260
function (PVT_ADD_PCH_RULE pch_header_filename out_generated_files
       out_pch_filename extra_compile_flags pch_subdir_external)
  if (PCH_GCC)
    set (pch_ext "gch")
  elseif (PCH_CLANG)
    set (pch_ext "pch")
  elseif (PCH_INTEL)
    set (pch_ext "pchi")
  else ()
    message (AUTHOR_WARNING "Function being used for neither gcc, clang not intel")
  endif ()
  
  get_filename_component (pch_header "${pch_header_filename}" NAME)
  get_filename_component (pch_header_path "${pch_header_filename}" PATH)
  get_filename_component (pch_header_name "${pch_header_filename}" NAME_WE)
  if(NOT pch_subdir_external)
    pvt_get_pch_count (pch_count)
    set(pch_subdir "pch.${pch_count}")
  else()
    set(pch_subdir "${pch_subdir_external}")
  endif()
  set (pch_dir_cfgname "${CMAKE_CURRENT_BINARY_DIR}/${pch_subdir}/@CFGNAME@")
  set (pch_filename_cfgname "${pch_dir_cfgname}/${pch_header}.${pch_ext}")
  
  # Generate the stub file for the Intel Compiler
  if (PCH_INTEL)
    set (pch_stub_src "${CMAKE_CURRENT_BINARY_DIR}/${pch_subdir}/${pch_header_name}_stub.cpp")
    set (pch_stub_src_text
      "// Stub file for the PCH ${pch_header}. Generated by CMake.\n#include <${pch_header}>\n"
    )
    pvt_write_if_changed ("${pch_stub_src}" "${pch_stub_src_text}")
  endif ()
  
  # Build the arguments list for calling the compiler
  set (pch_args "")
  
  get_directory_property (pch_definitions COMPILE_DEFINITIONS)
  foreach (pch_def ${pch_definitions})
    list (APPEND pch_args "-D${pch_def}")
  endforeach ()

  # Add all the current include directories
  get_directory_property (pch_dirinc INCLUDE_DIRECTORIES)
  list(REMOVE_DUPLICATES pch_dirinc)
  foreach (pch_inc ${pch_dirinc})
    set(pch_inc_system OFF)
    if (CMAKE_CXX_IMPLICIT_INCLUDE_DIRECTORIES)
      get_filename_component(pch_inc_abs  "${pch_inc}" REALPATH)
      foreach(implicit_inc ${CMAKE_CXX_IMPLICIT_INCLUDE_DIRECTORIES})
        get_filename_component(implicit_inc_abs  "${implicit_inc}" REALPATH)
        if (pch_inc_abs STREQUAL implicit_inc_abs)
          set(pch_inc_system ON)
          break()
        endif()
      endforeach()
    endif()
    if (NOT pch_inc_system)
      list (APPEND pch_args "-I${pch_inc}")
    endif()
  endforeach ()
  
  # The OSX deployment target must match as well
  if (APPLE AND (PCH_GCC OR PCH_CLANG))
    if (CMAKE_OSX_DEPLOYMENT_TARGET)
      list(APPEND pch_args
        "-mmacosx-version-min=${CMAKE_OSX_DEPLOYMENT_TARGET}")
    endif()
  endif ()
  
  # Extra flag passed from the caller
  list(APPEND pch_args ${extra_compile_flags})
  
  # Some versions of gcc are not smart enough to identify the file type
  if (PCH_GCC OR PCH_CLANG)
    list(APPEND pch_args "-x" "c++-header")
  endif ()
  
  
  set (pch_generated "")
  
  # Helper internal macro: note that it depends on variables defined later on
  macro(_PCH_ADD_COMMAND)
    if (PCH_GCC OR PCH_CLANG)
      list (APPEND pch_args "-c" "-o" "${pch_filename}"
        "${pch_header_filename}")
    elseif (PCH_INTEL)
      list (APPEND pch_args "-c" "-pch-create" "${pch_filename}"
        "-I" "${pch_header_path}" "${pch_stub_src}")
    endif ()
    get_filename_component(_pch_location "${pch_filename}" PATH)
    add_custom_command (OUTPUT "${pch_filename}"
      COMMAND "${CMAKE_COMMAND}" -E make_directory "${_pch_location}"
      COMMAND "${CMAKE_CXX_COMPILER}" "${CMAKE_CXX_COMPILER_ARG1}"
              ${args_cxx} ${pch_args}
      DEPENDS "${pch_header_filename}"
      IMPLICIT_DEPENDS CXX "${pch_header_filename}"
    )
    list (APPEND pch_generated "${pch_filename}")
    unset (_pch_location)
  endmacro ()
  
  
  # Create the appropriate custom commands and custom target
  if (CMAKE_CONFIGURATION_TYPES)
    foreach (CFGNAME ${CMAKE_CONFIGURATION_TYPES})
      string (CONFIGURE "${pch_dir_cfgname}" pch_dir @ONLY)
      string (CONFIGURE "${pch_filename_cfgname}" pch_filename @ONLY)
      pvt_config_flags (${CFGNAME} args_cxx)
      _PCH_ADD_COMMAND ()
    endforeach ()
  else ()
    set (CFGNAME ".")
    string (CONFIGURE "${pch_dir_cfgname}" pch_dir @ONLY)
    string (CONFIGURE "${pch_filename_cfgname}" pch_filename @ONLY)
    if (CMAKE_BUILD_TYPE)
      pvt_config_flags (${CMAKE_BUILD_TYPE} args_cxx)
    else ()
      set (args_cxx "${CMAKE_CXX_FLAGS}")
      separate_arguments (args_cxx)
    endif ()
    _PCH_ADD_COMMAND ()
  endif ()
  
  # Set the properties on the generated files
  set_source_files_properties (${pch_generated} PROPERTIES
    GENERATED ON 
    HEADER_ONLY ON)
  
  set (CFGNAME "${CMAKE_CFG_INTDIR}")
  string (CONFIGURE "${pch_filename_cfgname}" pch_filename_final @ONLY)
  set (${out_pch_filename} ${pch_filename_final} PARENT_SCOPE)
endfunction ()



# Helper macro to set up the rules to create the PCH, add the dependencies
# and set up the appropriate include directories.
# The second to last argument is optional, it is the extra_compile_flags.
# The last argument is also optional, it is a specific subdirectory on which
# the PCH will be created for gcc and clang. Note that when this is used,
# the same value needs to be provided in PVT_PCH_USE
macro (PVT_PCH_SOURCES_GROUP header_filename_orig group_name
       out_extrasrcs_var out_pch_filename_var pch_optional_extra_compile_flags
       pch_optional_subdir)
  set (pch_generated "")
  pvc_increment_pch_count ()
  get_filename_component(header_filename "${header_filename_orig}" ABSOLUTE)
  if (PCH_MSVC)
    PVT_ADD_PCH_RULE_MSVC ("${header_filename}" pch_generated
      "${pch_optional_subdir}")
    set (${out_pch_target_var} "")
    set (${out_pch_filename_var} "")
  else ()
    PVT_ADD_PCH_RULE ("${header_filename}" pch_generated
      ${out_pch_filename_var} "${pch_optional_extra_compile_flags}"
      "${pch_optional_subdir}")
  endif ()
  
  set (${out_extrasrcs_var} "${header_filename}" ${pch_generated})
  source_group ("${group_name}" FILES ${${out_extrasrcs_var}})
endmacro ()



# The implementation forces the inclusion of the precompiled header in all
# C++ sources. Note that the sources var "srcs_var" ought not to include
# the PCH stub source file.
# The last argument is optional, it is a specific subdirectory on which
# the PCH will be created for gcc and clang. Note that when this is used,
# the same value needs to be provided in PVT_PCH_SOURCES_GROUP
macro (PVT_PCH_USE pch_header_filename pch_filename target_name srcs_var
       pch_optional_subdir)
  get_filename_component (pch_header "${pch_header_filename}" NAME)
  # Assuming that the precompiled header is C++, the C files need to be
  # either compiled as C++ or not use the PCH. Objective C files cannot
  # use a C++ precompiled header at all.
  set (has_only_cxx_sources ON)
  foreach (pch_src ${${srcs_var}})
    if (pch_src MATCHES ".+\\.([cC]|[mM][mM])$")
      set (has_only_cxx_sources OFF)
      break ()
    endif ()
  endforeach ()
  
  if (PCH_MSVC)
    set (pch_flags " /Yu\"${pch_header}\" /FI\"${pch_header}\"")
  elseif (PCH_GCC OR PCH_CLANG)
    # FIXME: this is coupled with the internal behavior of PVT_ADD_PCH_RULE
    set(pch_subdir "${pch_optional_subdir}")
    if(NOT pch_subdir)
      pvt_get_pch_count (pch_count)
      set(pch_subdir "pch.${pch_count}")
    endif()
    set (pch_flags
      "-include \"${pch_subdir}/${CMAKE_CFG_INTDIR}/${pch_header}\"")
  elseif (PCH_INTEL)
    set (pch_flags "-pch-use \"${pch_filename}\"")
  else ()
    message (AUTHOR_WARNING "Unknown PCH environment")
  endif ()
  
  # If all sources are C++, set the property for the target, otherwise set
  # it in each file separately
  if (has_only_cxx_sources)
    set_property (TARGET ${target_name} APPEND PROPERTY
      COMPILE_FLAGS "${pch_flags}")
  else ()
    foreach (pch_src ${${srcs_var}})
      if (pch_src MATCHES ".+\\.[cCpPxX][cCpPxX]+$")
        set_property (SOURCE ${pch_src} APPEND PROPERTY
          COMPILE_FLAGS "${pch_flags}")
      endif()
    endforeach ()
  endif ()
  
  # Add the dependencies
  foreach (pch_src ${${srcs_var}})
    if (pch_src MATCHES ".+\\.[cCpPxX][cCpPxX]+$")
      set_property (SOURCE ${pch_src} PROPERTY
        OBJECT_DEPENDS "${pch_filename}")
    endif()
  endforeach ()
  
  # On non-MSVC add the base directory with the PCH to the include list in
  # order to allow builds using absolute paths
  # FIXME: this is coupled with the internal behavior of PVT_ADD_PCH_RULE
  if (NOT PCH_MSVC)
    set_property(TARGET ${target_name} APPEND PROPERTY
      INCLUDE_DIRECTORIES "${CMAKE_CURRENT_BINARY_DIR}")
  endif()
endmacro ()



# Adds a library using a common PCH.
# Usage:
#  pch_add_library(name [SHARED | MODULE | STATIC]
#                  [EXCLUDE_FROM_ALL]
#                  PCH_HEADER <header>
#                  source1 source2 ... sourceN)
function (pch_add_library _libname)
  CMAKE_PARSE_ARGUMENTS (_pch "SHARED;MODULE;STATIC;EXCLUDE_FROM_ALL" "PCH_HEADER" "" ${ARGN})
  if (NOT _pch_PCH_HEADER)
    message (FATAL_ERROR "Missing PCH header!")
  endif ()
  set (_libsrcs ${_pch_UNPARSED_ARGUMENTS})
  
  # Poor man's n-ary xor
  set (_pch_count 0)
  set (_pch_libtype "")
  if (_pch_SHARED)
    set (_pch_libtype "SHARED")
    math (EXPR _pch_count "${_pch_count} + 1")
  endif ()
  if (_pch_MODULE)
    set (_pch_libtype "MODULE")
    math (EXPR _pch_count "${_pch_count} + 1")
  endif ()
  if (_pch_STATIC)
    set (_pch_libtype "STATIC")
    math (EXPR _pch_count "${_pch_count} + 1")
  endif ()
  if (_pch_count GREATER 1)
    message (AUTHOR_WARNING "More than one library type specified. Using \"${_pch_libtype}\"")
  endif ()
  if (_pch_EXCLUDE_FROM_ALL)
    set (_pch_exclude "EXCLUDE_FROM_ALL")
  endif ()
  
  if (CMAKE_SYSTEM_NAME STREQUAL "Linux" AND 
      (PCH_GCC OR PCH_CLANG OR PCH_INTEL))
    set (pch_extra_compile_flags "-fPIC")
  endif ()
  string(REPLACE " " "_" _pch_subdir "${_libname}_pch")
  PVT_PCH_SOURCES_GROUP ("${_pch_PCH_HEADER}" "PCH Sources"
       _pch_srcs _pch_filename "${pch_extra_compile_flags}" ${_pch_subdir})
  add_library (${_libname} ${_pch_libtype} ${_pch_exclude}
    ${_libsrcs} ${_pch_srcs})
  PVT_PCH_USE ("${_pch_PCH_HEADER}" "${_pch_filename}" ${_libname}
    _libsrcs ${_pch_subdir})
  if (NOT PCH_MSVC AND _pch_filename)
    add_custom_target("${_libname}_pch" DEPENDS "${_pch_filename}")
    add_dependencies(${_libname} "${_libname}_pch")
  endif()
endfunction ()



# Adds an executable using a common PCH.
# Usage:
#  pch_add_executable(name [WIN32] [MACOSX_BUNDLE]
#                  [EXCLUDE_FROM_ALL]
#                  PCH_HEADER <header>
#                  source1 source2 ... sourceN)
function (pch_add_executable _exename)
  CMAKE_PARSE_ARGUMENTS (_pch "WIN32;MACOSX_BUNDLE;EXCLUDE_FROM_ALL" "PCH_HEADER" "" ${ARGN})
  if (NOT _pch_PCH_HEADER)
    message (FATAL_ERROR "Missing PCH header!")
  endif ()
  set (_exesrcs ${_pch_UNPARSED_ARGUMENTS})

  if (_pch_WIN32)
    set (_pch_exetype "WIN32")
  endif ()
  if (_pch_MACOSX_BUNDLE)
    set (_pch_exetype "MACOSX_BUNDLE")
  endif ()
  if (_pch_EXCLUDE_FROM_ALL)
    set (_pch_exclude "EXCLUDE_FROM_ALL")
  endif ()
  if (_pch_WIN32 AND _pch_MACOSX_BUNDLE)
    message (AUTHOR_WARNING "Both WIN32 and MACOSX_BUNDLE selected. Using \"${_pch_exetype}\".")
  endif ()
  
  string(REPLACE " " "_" _pch_subdir "${_exename}_pch")
  PVT_PCH_SOURCES_GROUP ("${_pch_PCH_HEADER}" "PCH Sources"
       _pch_srcs _pch_filename "" ${_pch_subdir})
  add_executable (${_exename} ${_pch_exetype} ${_pch_exclude}
    ${_exesrcs} ${_pch_srcs})
  PVT_PCH_USE ("${_pch_PCH_HEADER}" "${_pch_filename}" ${_exename}
    _exesrcs ${_pch_subdir})
  if (NOT PCH_MSVC AND _pch_filename)
    add_custom_target("${_exename}_pch" DEPENDS "${_pch_filename}")
    add_dependencies(${_exename} "${_exename}_pch")
  endif()
endfunction ()