#.rst:
#
# The following functions are defined:
#
# .. cmake:command:: add_cython_target
#
# Create a custom rule to generate the source code for a Python extension module
# using cython.
#
#   add_cython_target(<Name> [<CythonInput>]
#                     [EMBED_MAIN]
#                     [C | CXX]
#                     [PY2 | PY3]
#                     [OUTPUT_VAR <OutputVar>])
#
# ``<Name>`` is the name of the new target, and ``<CythonInput>``
# is the path to a cython source file.  Note that, despite the name, no new
# targets are created by this function.  Instead, see ``OUTPUT_VAR`` for
# retrieving the path to the generated source for subsequent targets.
#
# If only ``<Name>`` is provided, and it ends in the ".pyx" extension, then it
# is assumed to be the ``<CythonInput>``.  The name of the input without the
# extension is used as the target name.  If only ``<Name>`` is provided, and it
# does not end in the ".pyx" extension, then the ``<CythonInput>`` is assumed to
# be ``<Name>.pyx``.
#
# The Cython include search path is amended with any entries found in the
# ``INCLUDE_DIRECTORIES`` property of the directory containing the
# ``<CythonInput>`` file.  Use ``include_directories`` to add to the Cython
# include search path.
#
# Options:
#
# ``EMBED_MAIN``
#   Embed a main() function in the generated output (for stand-alone
#   applications that initialize their own Python runtime).
#
# ``C | CXX``
#   Force the generation of either a C or C++ file.  By default, a C file is
#   generated, unless the C language is not enabled for the project; in this
#   case, a C++ file is generated by default.
#
# ``PY2 | PY3``
#   Force compilation using either Python-2 or Python-3 syntax and code
#   semantics.  By default, Python-2 syntax and semantics are used if the major
#   version of Python found is 2.  Otherwise, Python-3 syntax and sematics are
#   used.
#
# ``OUTPUT_VAR <OutputVar>``
#   Set the variable ``<OutputVar>`` in the parent scope to the path to the
#   generated source file.  By default, ``<Name>`` is used as the output
#   variable name.
#
# Defined variables:
#
# ``<OutputVar>``
#   The path of the generated source file.
#
# Cache variables that effect the behavior include:
#
# ``CYTHON_ANNOTATE``
#   whether to create an annotated .html file when compiling
#
# ``CYTHON_FLAGS``
#   additional flags to pass to the Cython compiler
#
# Example usage
# ^^^^^^^^^^^^^
#
# .. code-block:: cmake
#
#   find_package(Cython)
#
#   # Note: In this case, either one of these arguments may be omitted; their
#   # value would have been inferred from that of the other.
#   add_cython_target(cy_code cy_code.pyx)
#
#   add_library(cy_code MODULE ${cy_code})
#   target_link_libraries(cy_code ...)
#
#=============================================================================
# Copyright 2011 Kitware, Inc.
#
# 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.
#=============================================================================

# Configuration options.
set(CYTHON_ANNOTATE OFF
    CACHE BOOL "Create an annotated .html file when compiling *.pyx.")

set(CYTHON_FLAGS "" CACHE STRING
    "Extra flags to the cython compiler.")
mark_as_advanced(CYTHON_ANNOTATE CYTHON_FLAGS)
string(REGEX REPLACE " " ";" CYTHON_FLAGS_LIST "${CYTHON_FLAGS}")

find_package(PythonLibs REQUIRED)

set(CYTHON_CXX_EXTENSION "cxx")
set(CYTHON_C_EXTENSION "c")

get_property(languages GLOBAL PROPERTY ENABLED_LANGUAGES)

function(add_cython_target _name)
  set(options EMBED_MAIN C CXX PY2 PY3)
  set(options1 OUTPUT_VAR)
  cmake_parse_arguments(_args "${options}" "${options1}" "" ${ARGN})

  list(GET _args_UNPARSED_ARGUMENTS 0 _arg0)

  # if provided, use _arg0 as the input file path
  if(_arg0)
    set(_source_file ${_arg0})

  # otherwise, must determine source file from name, or vice versa
  else()
    get_filename_component(_name_ext "${_name}" EXT)

    # if extension provided, _name is the source file
    if(_name_ext)
      set(_source_file ${_name})
      get_filename_component(_name "${_source_file}" NAME_WE)

    # otherwise, assume the source file is ${_name}.pyx
    else()
      set(_source_file ${_name}.pyx)
    endif()
  endif()

  set(_embed_main FALSE)

  if("C" IN_LIST languages)
    set(_output_syntax "C")
  elseif("CXX" IN_LIST languages)
    set(_output_syntax "CXX")
  else()
    message(FATAL_ERROR "Either C or CXX must be enabled to use Cython")
  endif()

  if("${PYTHONLIBS_VERSION_STRING}" MATCHES "^2.")
    set(_input_syntax "PY2")
  else()
    set(_input_syntax "PY3")
  endif()

  if(_args_EMBED_MAIN)
    set(_embed_main TRUE)
  endif()

  if(_args_C)
    set(_output_syntax "C")
  endif()

  if(_args_CXX)
    set(_output_syntax "CXX")
  endif()

  if(_args_PY2)
    set(_input_syntax "PY2")
  endif()

  if(_args_PY3)
    set(_input_syntax "PY3")
  endif()

  set(embed_arg "")
  if(_embed_main)
    set(embed_arg "--embed")
  endif()

  set(cxx_arg "")
  set(extension "c")
  if(_output_syntax STREQUAL "CXX")
    set(cxx_arg "--cplus")
    set(extension "cxx")
  endif()

  set(py_version_arg "")
  if(_input_syntax STREQUAL "PY2")
    set(py_version_arg "-2")
  elseif(_input_syntax STREQUAL "PY3")
    set(py_version_arg "-3")
  endif()

  set(generated_file "${CMAKE_CURRENT_BINARY_DIR}/${_name}.${extension}")
  set_source_files_properties(${generated_file} PROPERTIES GENERATED TRUE)

  set(_output_var ${_name})
  if(_args_OUTPUT_VAR)
      set(_output_var ${_args_OUTPUT_VAR})
  endif()
  set(${_output_var} ${generated_file} PARENT_SCOPE)

  file(RELATIVE_PATH generated_file_relative
      ${CMAKE_BINARY_DIR} ${generated_file})

  set(comment "Generating ${_output_syntax} source ${generated_file_relative}")
  set(cython_include_directories "")
  set(pxd_dependencies "")
  set(c_header_dependencies "")

  # Get the include directories.
  get_source_file_property(pyx_location ${_source_file} LOCATION)
  get_filename_component(pyx_path ${pyx_location} PATH)
  get_directory_property(cmake_include_directories
                         DIRECTORY ${pyx_path}
                         INCLUDE_DIRECTORIES)
  list(APPEND cython_include_directories ${cmake_include_directories})

  # Determine dependencies.
  # Add the pxd file with the same basename as the given pyx file.
  get_filename_component(pyx_file_basename ${_source_file} NAME_WE)
  unset(corresponding_pxd_file CACHE)
  find_file(corresponding_pxd_file ${pyx_file_basename}.pxd
            PATHS "${pyx_path}" ${cmake_include_directories}
            NO_DEFAULT_PATH)
  if(corresponding_pxd_file)
    list(APPEND pxd_dependencies "${corresponding_pxd_file}")
  endif()

  # pxd files to check for additional dependencies
  set(pxds_to_check "${_source_file}" "${pxd_dependencies}")
  set(pxds_checked "")
  set(number_pxds_to_check 1)
  while(number_pxds_to_check GREATER 0)
    foreach(pxd ${pxds_to_check})
      list(APPEND pxds_checked "${pxd}")
      list(REMOVE_ITEM pxds_to_check "${pxd}")

      # look for C headers
      file(STRINGS "${pxd}" extern_from_statements
           REGEX "cdef[ ]+extern[ ]+from.*$")
      foreach(statement ${extern_from_statements})
        # Had trouble getting the quote in the regex
        string(REGEX REPLACE
               "cdef[ ]+extern[ ]+from[ ]+[\"]([^\"]+)[\"].*" "\\1"
               header "${statement}")
        unset(header_location CACHE)
        find_file(header_location ${header} PATHS ${cmake_include_directories})
        if(header_location)
          list(FIND c_header_dependencies "${header_location}" header_idx)
          if(${header_idx} LESS 0)
            list(APPEND c_header_dependencies "${header_location}")
          endif()
        endif()
      endforeach()

      # check for pxd dependencies
      # Look for cimport statements.
      set(module_dependencies "")
      file(STRINGS "${pxd}" cimport_statements REGEX cimport)
      foreach(statement ${cimport_statements})
        if(${statement} MATCHES from)
          string(REGEX REPLACE
                 "from[ ]+([^ ]+).*" "\\1"
                 module "${statement}")
        else()
          string(REGEX REPLACE
                 "cimport[ ]+([^ ]+).*" "\\1"
                 module "${statement}")
        endif()
        list(APPEND module_dependencies ${module})
      endforeach()

      # check for pxi dependencies
      # Look for include statements.
      set(include_dependencies "")
      file(STRINGS "${pxd}" include_statements REGEX include)
      foreach(statement ${include_statements})
        string(REGEX REPLACE
               "include[ ]+[\"]([^\"]+)[\"].*" "\\1"
               module "${statement}")
        list(APPEND include_dependencies ${module})
      endforeach()

      list(REMOVE_DUPLICATES module_dependencies)
      list(REMOVE_DUPLICATES include_dependencies)

      # Add modules to the files to check, if appropriate.
      foreach(module ${module_dependencies})
        unset(pxd_location CACHE)
        find_file(pxd_location ${module}.pxd
                  PATHS "${pyx_path}" ${cmake_include_directories}
                  NO_DEFAULT_PATH)
        if(pxd_location)
          list(FIND pxds_checked ${pxd_location} pxd_idx)
          if(${pxd_idx} LESS 0)
            list(FIND pxds_to_check ${pxd_location} pxd_idx)
            if(${pxd_idx} LESS 0)
              list(APPEND pxds_to_check ${pxd_location})
              list(APPEND pxd_dependencies ${pxd_location})
            endif() # if it is not already going to be checked
          endif() # if it has not already been checked
        endif() # if pxd file can be found
      endforeach() # for each module dependency discovered

      # Add includes to the files to check, if appropriate.
      foreach(_include ${include_dependencies})
        unset(pxi_location CACHE)
        find_file(pxi_location ${_include}
                  PATHS "${pyx_path}" ${cmake_include_directories}
                  NO_DEFAULT_PATH)
        if(pxi_location)
          list(FIND pxds_checked ${pxi_location} pxd_idx)
          if(${pxd_idx} LESS 0)
            list(FIND pxds_to_check ${pxi_location} pxd_idx)
            if(${pxd_idx} LESS 0)
              list(APPEND pxds_to_check ${pxi_location})
              list(APPEND pxd_dependencies ${pxi_location})
            endif() # if it is not already going to be checked
          endif() # if it has not already been checked
        endif() # if include file can be found
      endforeach() # for each include dependency discovered
    endforeach() # for each include file to check

    list(LENGTH pxds_to_check number_pxds_to_check)
  endwhile()

  # Set additional flags.
  set(annotate_arg "")
  if(CYTHON_ANNOTATE)
    set(annotate_arg "--annotate")
  endif()

  set(no_docstrings_arg "")
  if(CMAKE_BUILD_TYPE STREQUAL "Release" OR
     CMAKE_BUILD_TYPE STREQUAL "MinSizeRel")
    set(no_docstrings_arg "--no-docstrings")
  endif()

  set(cython_debug_arg "")
  set(embed_pos_arg "")
  set(line_directives_arg "")
  if(CMAKE_BUILD_TYPE STREQUAL "Debug" OR
     CMAKE_BUILD_TYPE STREQUAL "RelWithDebInfo")
    set(cython_debug_arg "--gdb")
    set(embed_pos_arg "--embed-positions")
    set(line_directives_arg "--line-directives")
  endif()

  # Include directory arguments.
  list(REMOVE_DUPLICATES cython_include_directories)
  set(include_directory_arg "")
  foreach(_include_dir ${cython_include_directories})
    set(include_directory_arg
        ${include_directory_arg} "--include-dir" "${_include_dir}")
  endforeach()

  list(REMOVE_DUPLICATES pxd_dependencies)
  list(REMOVE_DUPLICATES c_header_dependencies)

  # Add the command to run the compiler.
  add_custom_command(OUTPUT ${generated_file}
                     COMMAND ${CYTHON_EXECUTABLE}
                     ARGS ${cxx_arg} ${include_directory_arg} ${py_version_arg}
                          ${embed_arg} ${annotate_arg} ${no_docstrings_arg}
                          ${cython_debug_arg} ${embed_pos_arg}
                          ${line_directives_arg} ${CYTHON_FLAGS_LIST} ${pyx_location}
                          --output-file ${generated_file}
                     DEPENDS ${_source_file}
                             ${pxd_dependencies}
                     IMPLICIT_DEPENDS ${_output_syntax}
                                      ${c_header_dependencies}
                     COMMENT ${comment})

  # NOTE(opadron): I thought about making a proper target, but after trying it
  # out, I decided that it would be far too convenient to use the same name as
  # the target for the extension module (e.g.: for single-file modules):
  #
  # ...
  # add_cython_target(_module.pyx)
  # add_library(_module ${_module})
  # ...
  #
  # The above example would not be possible since the "_module" target name
  # would already be taken by the cython target.  Since I can't think of a
  # reason why someone would need the custom target instead of just using the
  # generated file directly, I decided to leave this commented out.
  #
  # add_custom_target(${_name} DEPENDS ${generated_file})

  # Remove their visibility to the user.
  set(corresponding_pxd_file "" CACHE INTERNAL "")
  set(header_location "" CACHE INTERNAL "")
  set(pxd_location "" CACHE INTERNAL "")
endfunction()

