# This find module originally created by Jeroen Koekkoek.
# Copyright (c) 2023, Jeroen Koekkoek
#
# This file is covered by a BSD 3-Clause License.
# See https://github.com/k0ekk0ek/cmake-sphinx/blob/master/LICENSE for details.

include(FindPackageHandleStandardArgs)

macro(_Sphinx_find_executable _exe)
  string(TOUPPER "${_exe}" _uc)
  # sphinx-(build|quickstart)-3 x.x.x
  # FIXME: This works on Fedora (and probably most other UNIX like targets).
  #        Windows targets and PIP installs might need some work.
  find_program(
    SPHINX_${_uc}_EXECUTABLE
    NAMES "sphinx-${_exe}-3" "sphinx-${_exe}" "sphinx-${_exe}.exe")
  mark_as_advanced(SPHINX_${_uc}_EXECUTABLE)

  if(SPHINX_${_uc}_EXECUTABLE)
    execute_process(
      COMMAND "${SPHINX_${_uc}_EXECUTABLE}" --version
      RESULT_VARIABLE _result
      OUTPUT_VARIABLE _output
      OUTPUT_STRIP_TRAILING_WHITESPACE)
    if(_result EQUAL 0 AND _output MATCHES " v?([0-9]+\\.[0-9]+\\.[0-9]+)$")
      set(SPHINX_${_uc}_VERSION "${CMAKE_MATCH_1}")
    endif()

    if(NOT TARGET Sphinx::${_exe})
      add_executable(Sphinx::${_exe} IMPORTED GLOBAL)
      set_target_properties(Sphinx::${_exe} PROPERTIES
        IMPORTED_LOCATION "${SPHINX_${_uc}_EXECUTABLE}")
    endif()
    set(Sphinx_${_exe}_FOUND TRUE)
  else()
    set(Sphinx_${_exe}_FOUND FALSE)
  endif()
  unset(_uc)
endmacro()

macro(_Sphinx_find_module _name _module)
  string(TOUPPER "${_name}" _Sphinx_uc)
  if(SPHINX_PYTHON_EXECUTABLE)
    execute_process(
      COMMAND ${SPHINX_PYTHON_EXECUTABLE} -m ${_module} --version
      RESULT_VARIABLE _result
      OUTPUT_VARIABLE _output
      OUTPUT_STRIP_TRAILING_WHITESPACE
      ERROR_QUIET)
    if(_result EQUAL 0)
      if(_output MATCHES " v?([0-9]+\\.[0-9]+\\.[0-9]+)$")
        set(SPHINX_${_Sphinx_uc}_VERSION "${CMAKE_MATCH_1}")
      endif()

      if(NOT TARGET Sphinx::${_name})
        set(SPHINX_${_Sphinx_uc}_EXECUTABLE "${SPHINX_PYTHON_EXECUTABLE} -m ${_module}")
        add_executable(Sphinx::${_name} IMPORTED GLOBAL)
        set_target_properties(Sphinx::${_name} PROPERTIES
          IMPORTED_LOCATION "${SPHINX_PYTHON_EXECUTABLE}")
      endif()
      set(Sphinx_${_name}_ARGS -m ${_module})
      set(Sphinx_${_name}_FOUND TRUE)
    else()
      set(Sphinx_${_name}_FOUND FALSE)
    endif()
  else()
    set(Sphinx_${_name}_FOUND FALSE)
  endif()
  unset(_Sphinx_uc)
endmacro()

macro(_Sphinx_find_extension _ext)
  if(SPHINX_PYTHON_EXECUTABLE)
    execute_process(
      COMMAND ${SPHINX_PYTHON_EXECUTABLE} -c "import ${_ext}"
      RESULT_VARIABLE _result)
    if(_result EQUAL 0)
      set(Sphinx_${_ext}_FOUND TRUE)
    else()
      set(Sphinx_${_ext}_FOUND FALSE)
    endif()
  endif()
endmacro()

#
# Find sphinx-build and sphinx-quickstart.
#

# Find sphinx-build shim.
_Sphinx_find_executable(build)

if(SPHINX_BUILD_EXECUTABLE)
  # Find sphinx-quickstart shim.
  _Sphinx_find_executable(quickstart)

  # Locate Python executable
  if(CMAKE_HOST_WIN32)
    # script-build on Windows located under (when PIP is used):
    # C:/Program Files/PythonXX/Scripts
    # C:/Users/username/AppData/Roaming/Python/PythonXX/Sripts
    #
    # Python modules are installed under:
    # C:/Program Files/PythonXX/Lib
    # C:/Users/username/AppData/Roaming/Python/PythonXX/site-packages
    #
    # To verify a given module is installed, use the Python base directory
    # and test if either Lib/module.py or site-packages/module.py exists.
    get_filename_component(_Sphinx_directory "${SPHINX_BUILD_EXECUTABLE}" DIRECTORY)
    get_filename_component(_Sphinx_directory "${_Sphinx_directory}" DIRECTORY)
    if(EXISTS "${_Sphinx_directory}/python.exe")
      set(SPHINX_PYTHON_EXECUTABLE "${_Sphinx_directory}/python.exe")
    endif()
    unset(_Sphinx_directory)
  else()
    file(READ "${SPHINX_BUILD_EXECUTABLE}" _Sphinx_script)
    if(_Sphinx_script MATCHES "^#!([^\n]+)")
      string(STRIP "${CMAKE_MATCH_1}" _Sphinx_shebang)
      if(EXISTS "${_Sphinx_shebang}")
        set(SPHINX_PYTHON_EXECUTABLE "${_Sphinx_shebang}")
      endif()
    endif()
    unset(_Sphinx_script)
    unset(_Sphinx_shebang)
  endif()
endif()

if(NOT SPHINX_PYTHON_EXECUTABLE)
  # Python executable cannot be extracted from shim shebang or path if e.g.
  # virtual environments are used, fallback to find package. Assume the
  # correct installation is found, the setup is probably broken in more ways
  # than one otherwise.
  find_package(Python3 QUIET COMPONENTS Interpreter)
  if(TARGET Python3::Interpreter)
    set(SPHINX_PYTHON_EXECUTABLE ${Python3_EXECUTABLE})
    # Revert to "python -m sphinx" if shim cannot be found.
    if(NOT SPHINX_BUILD_EXECUTABLE)
      _Sphinx_find_module(build sphinx)
      _Sphinx_find_module(quickstart sphinx.cmd.quickstart)
    endif()
  endif()
endif()

#
# Verify components are available.
#
if(SPHINX_BUILD_VERSION)
  # Breathe is required for Exhale
  if("exhale"  IN_LIST Sphinx_FIND_COMPONENTS AND NOT
     "breathe" IN_LIST Sphinx_FIND_COMPONENTS)
    list(APPEND Sphinx_FIND_COMPONENTS "breathe")
  endif()

  foreach(_Sphinx_component IN LISTS Sphinx_FIND_COMPONENTS)
    if(_Sphinx_component STREQUAL "build")
      # Do nothing, sphinx-build is always required.
      continue()
    elseif(_Sphinx_component STREQUAL "quickstart")
      # Do nothing, sphinx-quickstart is optional, but looked up by default.
      continue()
    endif()
    _Sphinx_find_extension(${_Sphinx_component})
  endforeach()
  unset(_Sphinx_component)

  #
  # Verify both executables are part of the Sphinx distribution.
  #
  if(SPHINX_QUICKSTART_VERSION AND NOT SPHINX_BUILD_VERSION STREQUAL SPHINX_QUICKSTART_VERSION)
    message(FATAL_ERROR "Versions for sphinx-build (${SPHINX_BUILD_VERSION}) "
                        "and sphinx-quickstart (${SPHINX_QUICKSTART_VERSION}) "
                        "do not match")
  endif()
endif()

find_package_handle_standard_args(
  Sphinx
  VERSION_VAR SPHINX_BUILD_VERSION
  REQUIRED_VARS SPHINX_BUILD_EXECUTABLE SPHINX_BUILD_VERSION
  HANDLE_COMPONENTS)

# Generate a conf.py template file using sphinx-quickstart.
#
# sphinx-quickstart allows for quiet operation and a lot of settings can be
# specified as command line arguments, therefore its not required to parse the
# generated conf.py.
function(_Sphinx_generate_confpy _target _cachedir)
  if(NOT TARGET Sphinx::quickstart)
    message(FATAL_ERROR "sphinx-quickstart is not available, needed by"
                        "sphinx_add_docs for target ${_target}")
  endif()

  if(NOT DEFINED SPHINX_PROJECT)
    set(SPHINX_PROJECT ${PROJECT_NAME})
  endif()

  if(NOT DEFINED SPHINX_AUTHOR)
    set(SPHINX_AUTHOR "${SPHINX_PROJECT} committers")
  endif()

  if(NOT DEFINED SPHINX_COPYRIGHT)
    string(TIMESTAMP "%Y, ${SPHINX_AUTHOR}" SPHINX_COPYRIGHT)
  endif()

  if(NOT DEFINED SPHINX_VERSION)
    set(SPHINX_VERSION "${PROJECT_VERSION_MAJOR}.${PROJECT_VERSION_MINOR}.${PROJECT_VERSION_PATCH}")
  endif()

  if(NOT DEFINED SPHINX_RELEASE)
    set(SPHINX_RELEASE "${PROJECT_VERSION}")
  endif()

  if(NOT DEFINED SPHINX_LANGUAGE)
    set(SPHINX_LANGUAGE "en")
  endif()

  if(NOT DEFINED SPHINX_MASTER)
    set(SPHINX_MASTER "index")
  endif()

  set(_known_exts autodoc doctest intersphinx todo coverage imgmath mathjax
                  ifconfig viewcode githubpages)

  if(DEFINED SPHINX_EXTENSIONS)
    foreach(_ext ${SPHINX_EXTENSIONS})
      set(_is_known_ext FALSE)
      foreach(_known_ext ${_known_exsts})
        if(_ext STREQUAL _known_ext)
          set(_opts "${opts} --ext-${_ext}")
          set(_is_known_ext TRUE)
          break()
        endif()
      endforeach()
      if(NOT _is_known_ext)
        if(_exts)
          set(_exts "${_exts},${_ext}")
        else()
          set(_exts "${_ext}")
        endif()
      endif()
    endforeach()
  endif()

  if(_exts)
    set(_exts "--extensions=${_exts}")
  endif()

  set(_templatedir "${CMAKE_CURRENT_BINARY_DIR}/${_target}.template")
  file(MAKE_DIRECTORY "${_templatedir}")
  string(REPLACE " " ";" _Sphinx_executable ${SPHINX_QUICKSTART_EXECUTABLE})
  execute_process(
    COMMAND ${_Sphinx_executable}
              -q --no-makefile --no-batchfile
              -p "${SPHINX_PROJECT}"
              -a "${SPHINX_AUTHOR}"
              -v "${SPHINX_VERSION}"
              -r "${SPHINX_RELEASE}"
              -l "${SPHINX_LANGUAGE}"
              --master "${SPHINX_MASTER}"
              ${_opts} ${_exts} "${_templatedir}"
    RESULT_VARIABLE _result
    OUTPUT_QUIET)
  unset(_Sphinx_executable)

  if(_result EQUAL 0 AND EXISTS "${_templatedir}/conf.py")
    file(COPY "${_templatedir}/conf.py" DESTINATION "${_cachedir}")
  endif()

  file(REMOVE_RECURSE "${_templatedir}")

  if(NOT _result EQUAL 0 OR NOT EXISTS "${_cachedir}/conf.py")
    message(FATAL_ERROR "Sphinx configuration file not generated for "
                        "target ${_target}")
  endif()
endfunction()

function(sphinx_add_docs _target)
  set(_opts)
  set(_single_opts BUILDER OUTPUT_DIRECTORY SOURCE_DIRECTORY)
  set(_multi_opts BREATHE_PROJECTS)
  cmake_parse_arguments(_args "${_opts}" "${_single_opts}" "${_multi_opts}" ${ARGN})

  unset(SPHINX_BREATHE_PROJECTS)

  if(NOT _args_BUILDER)
    message(FATAL_ERROR "Sphinx builder not specified for target ${_target}")
  elseif(NOT _args_SOURCE_DIRECTORY)
    message(FATAL_ERROR "Sphinx source directory not specified for target ${_target}")
  else()
    if(NOT IS_ABSOLUTE "${_args_SOURCE_DIRECTORY}")
      get_filename_component(_sourcedir "${_args_SOURCE_DIRECTORY}" ABSOLUTE)
    else()
      set(_sourcedir "${_args_SOURCE_DIRECTORY}")
    endif()
    if(NOT IS_DIRECTORY "${_sourcedir}")
      message(FATAL_ERROR "Sphinx source directory '${_sourcedir}' for"
                          "target ${_target} does not exist")
    endif()
  endif()

  set(_builder "${_args_BUILDER}")
  if(_args_OUTPUT_DIRECTORY)
    set(_outputdir "${_args_OUTPUT_DIRECTORY}")
  else()
    set(_outputdir "${CMAKE_CURRENT_BINARY_DIR}/${_target}")
  endif()


  if(_args_BREATHE_PROJECTS)
    if(NOT Sphinx_breathe_FOUND)
      message(FATAL_ERROR "Sphinx extension 'breathe' is not available. Needed"
                          "by sphinx_add_docs for target ${_target}")
    endif()
    list(APPEND SPHINX_EXTENSIONS breathe)

    foreach(_doxygen_target ${_args_BREATHE_PROJECTS})
      if(TARGET ${_doxygen_target})
        list(APPEND _depends ${_doxygen_target})

        # Doxygen targets are supported. Verify that a Doxyfile exists.
        get_target_property(_dir ${_doxygen_target} BINARY_DIR)
        set(_doxyfile "${_dir}/Doxyfile.${_doxygen_target}")
        if(NOT EXISTS "${_doxyfile}")
          message(FATAL_ERROR "Target ${_doxygen_target} is not a Doxygen"
                              "target, needed by sphinx_add_docs for target"
                              "${_target}")
        endif()

        # Read the Doxyfile, verify XML generation is enabled and retrieve the
        # output directory.
        file(READ "${_doxyfile}" _contents)
        if(NOT _contents MATCHES "GENERATE_XML *= *YES")
          message(FATAL_ERROR "Doxygen target ${_doxygen_target} does not"
                              "generate XML, needed by sphinx_add_docs for"
                              "target ${_target}")
        elseif(_contents MATCHES "OUTPUT_DIRECTORY *= *([^ ][^\n]*)")
          string(STRIP "${CMAKE_MATCH_1}" _dir)
          set(_name "${_doxygen_target}")
          set(_dir "${_dir}/xml")
        else()
          message(FATAL_ERROR "Cannot parse Doxyfile generated by Doxygen"
                              "target ${_doxygen_target}, needed by"
                              "sphinx_add_docs for target ${_target}")
        endif()
      elseif(_doxygen_target MATCHES "([^: ]+) *: *(.*)")
        set(_name "${CMAKE_MATCH_1}")
        string(STRIP "${CMAKE_MATCH_2}" _dir)
      endif()

      if(_name AND _dir)
        if(_breathe_projects)
          set(_breathe_projects "${_breathe_projects}, \"${_name}\": \"${_dir}\"")
        else()
          set(_breathe_projects "\"${_name}\": \"${_dir}\"")
        endif()
        if(NOT _breathe_default_project)
          set(_breathe_default_project "${_name}")
        endif()
      endif()
    endforeach()
  endif()

  set(_cachedir "${CMAKE_CURRENT_BINARY_DIR}/${_target}.cache")
  file(MAKE_DIRECTORY "${_cachedir}")
  if(EXISTS "${_sourcedir}/_static")
    file(COPY "${_sourcedir}/_static" DESTINATION "${_cachedir}")
  endif()
  if(EXISTS "${_sourcedir}/_templates")
    file(COPY "${_sourcedir}/_templates" DESTINATION "${_cachedir}")
  endif()

  if(EXISTS "${_sourcedir}/conf.py")
    configure_file("${_sourcedir}/conf.py" "${_cachedir}/conf.py" @ONLY)
  else()
    _Sphinx_generate_confpy(${_target} "${_cachedir}")
  endif()

  if(_breathe_projects)
    file(APPEND "${_cachedir}/conf.py"
      "\nbreathe_projects = { ${_breathe_projects} }"
      "\nbreathe_default_project = '${_breathe_default_project}'")
  endif()

  string(REPLACE " " ";" _Sphinx_executable ${SPHINX_BUILD_EXECUTABLE})
  add_custom_target(
    ${_target} ALL
    COMMAND ${_Sphinx_executable}
              -b ${_builder}
              -d "${CMAKE_CURRENT_BINARY_DIR}/${_target}.cache/_doctrees"
              -c "${CMAKE_CURRENT_BINARY_DIR}/${_target}.cache"
              "${_sourcedir}"
              "${_outputdir}"
    COMMENT "Building ${_target} Sphinx document"
    DEPENDS ${_depends})
  unset(_Sphinx_executable)
endfunction()
