# SPDX-FileCopyrightText: Copyright (c) Stanford University, The Regents of the University of California, and others.
# SPDX-License-Identifier: BSD-3-Clause

set(CMAKE_POLICY_VERSION_MINIMUM 3.18)
cmake_minimum_required(VERSION 3.18)
cmake_policy(SET CMP0077 NEW)

set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED True)

project(svZeroDSolver LANGUAGES C CXX)

set(CMAKE_POSITION_INDEPENDENT_CODE ON)

# Force Eigen & superprojects to skip docs/tests/fortran no matter what
set(BUILD_TESTING OFF CACHE BOOL "" FORCE)
set(EIGEN_BUILD_DOC OFF CACHE BOOL "" FORCE)
set(EIGEN_TEST_FORTRAN OFF CACHE BOOL "" FORCE)
set(EIGEN_BUILD_BLAS OFF CACHE BOOL "" FORCE)

include(FetchContent)
set(PYBIND11_FINDPYTHON NEW CACHE STRING "" FORCE)
find_package(Python COMPONENTS Interpreter Development.Module REQUIRED)
message(STATUS "Using Python: ${Python_EXECUTABLE} (${Python_VERSION})")

# Enable assertions
# -----------------------------------------------------------------------------
# Optimized configurations define NDEBUG, which disables assert() and Eigen's
# eigen_assert(). Stripping it keeps the optimization settings but re-enables
# the assertions, much cheaper in CI than a separate Debug build.
set(ENABLE_ASSERTIONS OFF CACHE BOOL "Enable assert() and eigen_assert() in optimized builds")
if(ENABLE_ASSERTIONS)
  foreach(config RELEASE RELWITHDEBINFO MINSIZEREL)
    string(REPLACE "-DNDEBUG" "" CMAKE_CXX_FLAGS_${config} "${CMAKE_CXX_FLAGS_${config}}")
    string(REPLACE "/DNDEBUG" "" CMAKE_CXX_FLAGS_${config} "${CMAKE_CXX_FLAGS_${config}}")
  endforeach()

  # Force-included ahead of Eigen so that eigen_assert throws instead of
  # aborting. Restricted to C++ to leave the C sources of dependencies alone.
  set(ASSERTIONS_HEADER ${CMAKE_CURRENT_SOURCE_DIR}/src/Assertions.h)
  if(MSVC)
    add_compile_options($<$<COMPILE_LANGUAGE:CXX>:/FI${ASSERTIONS_HEADER}>)
  else()
    add_compile_options($<$<COMPILE_LANGUAGE:CXX>:-include>
                        $<$<COMPILE_LANGUAGE:CXX>:${ASSERTIONS_HEADER}>)
  endif()
endif()

# Enable code coverage
# -----------------------------------------------------------------------------
set(ENABLE_COVERAGE OFF CACHE BOOL "Enable code coverage")
# coverage
if(ENABLE_COVERAGE)
  # set compiler flags
  set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -O0 -coverage -g")

  # find required tools
  find_program(LCOV lcov REQUIRED)
  find_program(GENHTML genhtml REQUIRED)

  # add coverage target
  add_custom_target(coverage
    # gather data
    COMMAND ${LCOV} --directory . --capture --output-file coverage.info --ignore-errors gcov --exclude '/usr/include/*' --exclude '/usr/lib/*' --exclude '*/_deps/*'
    # generate report
    COMMAND ${GENHTML} --demangle-cpp -o coverage coverage.info
    WORKING_DIRECTORY ${CMAKE_BINARY_DIR})
endif()

if (WIN32 AND MSVC)
    # CMake ≥ 3.15 has a proper variable
    # Use dynamic CRT (/MD or /MDd) so EXE and DLL share the same heap.
    # This is required when passing STL containers across DLL boundaries.
    if (CMAKE_VERSION VERSION_GREATER_EQUAL "3.15")
        set(CMAKE_MSVC_RUNTIME_LIBRARY
                "MultiThreaded$<$<CONFIG:Debug>:Debug>DLL")
    else()
        # CMake < 3.15: /MD is the default, so no changes needed
    endif()
endif()
# -----------------------------------------------------------------------------
# Set the location to store the binaries and libraries created by this project.
# -----------------------------------------------------------------------------
#
#set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/lib)
#set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/lib)
#set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin)

# Need to add the -fPIC flag to build the interface shared library.
if(CMAKE_CXX_COMPILER_ID MATCHES "GNU")
  set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fPIC")
endif()

# -----------------------------------------------------------------------------
# Fetch Eigen
# -----------------------------------------------------------------------------
# Eigen is a header-only C++ template library for linear algebra.
#
# Tell FetchContent not to re-check for updates once Eigen is downloaded
set(FETCHCONTENT_UPDATES_DISCONNECTED TRUE)

FetchContent_Declare(
  Eigen
  GIT_REPOSITORY https://gitlab.com/libeigen/eigen.git
  GIT_TAG 3.4.0
  GIT_SHALLOW TRUE
  GIT_PROGRESS TRUE)

set(EIGEN_BUILD_DOC OFF)
set(BUILD_TESTING OFF)
set(EIGEN_BUILD_PKGCONFIG OFF)

FetchContent_MakeAvailable(Eigen)

# -----------------------------------------------------------------------------
# Fetch pybind11
# -----------------------------------------------------------------------------
# pybind11 is a library used to create Python bindings of existing C++ code.
#
FetchContent_Declare(
    pybind11
    URL      https://github.com/pybind/pybind11/archive/refs/tags/v3.1.0.tar.gz
    URL_HASH SHA256=ef712655692a2e9bf7bb7874c022564a45f91d847ddee987e720cd9e28849665
)

FetchContent_GetProperties(pybind11)

if(NOT pybind11_POPULATED)
    FetchContent_Populate(pybind11)
    add_subdirectory(${pybind11_SOURCE_DIR} ${pybind11_BINARY_DIR})
endif()

# -----------------------------------------------------------------------------
# Fetch nlohmann/json
# -----------------------------------------------------------------------------
# This is a C++ header-only library for reading JSON files.
#
FetchContent_Declare(
    json
    URL https://github.com/nlohmann/json/releases/download/v3.11.2/json.tar.xz
)

FetchContent_MakeAvailable(json)

# -----------------------------------------------------------------------------
# Fetch pybind11_json
# -----------------------------------------------------------------------------
# pybind11_json is an nlohmann::json to pybind11 bridge that converts 
# between nlohmann::json and py::object objects. 
#
FetchContent_Declare(
    pybind11_json
    URL      https://github.com/pybind/pybind11_json/archive/d0bf434be9d287d73a963ff28745542daf02c08f.tar.gz
    URL_HASH SHA256=6c1b5973e9afd788ea3d9f59e142cdf022e8331a482c192bc4d23a406bfdbb77
)

FetchContent_MakeAvailable(pybind11_json)

# -----------------------------------------------------------------------------
# Set executables for each application.
# -----------------------------------------------------------------------------
# There are two executables
#   1) svzerodsolver
#   2) svzerodcalibrator 
#
add_executable(svzerodsolver applications/svzerodsolver.cpp 
  $<TARGET_OBJECTS:svzero_algebra_library> 
  $<TARGET_OBJECTS:svzero_model_library> 
  $<TARGET_OBJECTS:svzero_solve_library> 
)

add_executable(svzerodcalibrator applications/svzerodcalibrator.cpp
  $<TARGET_OBJECTS:svzero_algebra_library> 
  $<TARGET_OBJECTS:svzero_optimize_library> 
  $<TARGET_OBJECTS:svzero_model_library> 
)

# -----------------------------------------------------------------------------
# Setup building of the svzerod Python extension module.
# -----------------------------------------------------------------------------
# Replace EXCLUDE_FROM_ALL with SHARED to test building the Python 
# shared library.

pybind11_add_module(svzerod applications/svzerod.cpp)
set(SVZEROD_VERSION "dev" CACHE STRING "Python package version")
target_compile_definitions(svzerod PRIVATE SVZEROD_VERSION="${SVZEROD_VERSION}")
if(WIN32 AND "${CMAKE_GENERATOR}" STREQUAL "MinGW Makefiles")
    message(STATUS ">> Applying static‑link flags to svzerod")

    include(CheckCXXCompilerFlag)
    set(_static_flags
            -static
            -static-libgcc
            -static-libstdc++
    )
    # test for winpthread support before adding
    check_cxx_compiler_flag("-static-libwinpthread" HAVE_WINPTHREAD)
    if(HAVE_WINPTHREAD)
        list(APPEND _static_flags -static-libwinpthread)
    else()
        find_package(Threads REQUIRED)
        target_link_libraries(svzerod PRIVATE Threads::Threads)
    endif()
    # apply to compile *and* link
    foreach(_f IN LISTS _static_flags)
        target_compile_options(svzerod PRIVATE ${_f})
        # CMake ≥3.13:
        target_link_options   (svzerod PRIVATE ${_f})
    endforeach()

    # verify in the configure log for static compilation:
    get_target_property(_ccopts svzerod COMPILE_OPTIONS)
    message(STATUS ">>> svzerod COMPILE_OPTIONS = ${_ccopts}")
    get_target_property(_lnkopts svzerod LINK_OPTIONS)
    message(STATUS ">>> svzerod LINK_OPTIONS   = ${_lnkopts}")
endif()

# -----------------------------------------------------------------------------
# Add source sub-directories.
# -----------------------------------------------------------------------------
add_subdirectory("src/algebra")
add_subdirectory("src/interface")
add_subdirectory("src/model")
add_subdirectory("src/optimize")
add_subdirectory("src/solve")

# -----------------------------------------------------------------------------
# Set header file include directories.
# -----------------------------------------------------------------------------
#
target_include_directories(svzerodsolver PUBLIC
  ${CMAKE_SOURCE_DIR}/src/algebra
  ${CMAKE_SOURCE_DIR}/src/model
  ${CMAKE_SOURCE_DIR}/src/solve
)

target_include_directories(svzerodcalibrator PUBLIC 
  ${CMAKE_SOURCE_DIR}/src/algebra
  ${CMAKE_SOURCE_DIR}/src/model 
  ${CMAKE_SOURCE_DIR}/src/optimize
  ${CMAKE_SOURCE_DIR}/src/solve 
)

target_include_directories(svzerod PUBLIC 
  ${CMAKE_SOURCE_DIR}/src/algebra
  ${CMAKE_SOURCE_DIR}/src/model 
  ${CMAKE_SOURCE_DIR}/src/optimize
  ${CMAKE_SOURCE_DIR}/src/solve 
)

# -----------------------------------------------------------------------------
# Set the libraries each application needs to link to.
# -----------------------------------------------------------------------------
# These are needed only for the header-only applications.
#
target_link_libraries(svzerodsolver PRIVATE Eigen3::Eigen)
target_link_libraries(svzerodsolver PRIVATE nlohmann_json::nlohmann_json)

target_link_libraries(svzerodcalibrator PRIVATE Eigen3::Eigen)
target_link_libraries(svzerodcalibrator PRIVATE nlohmann_json::nlohmann_json)

target_link_libraries(svzerod PRIVATE Eigen3::Eigen)
target_link_libraries(svzerod PRIVATE nlohmann_json::nlohmann_json)
target_link_libraries(svzerod PRIVATE pybind11_json)
target_link_libraries(svzerod PRIVATE svzero_algebra_library)
target_link_libraries(svzerod PRIVATE svzero_model_library)
target_link_libraries(svzerod PRIVATE svzero_optimize_library)
target_link_libraries(svzerod PRIVATE svzero_solve_library)


# Create distribution
set(ENABLE_DISTRIBUTION OFF CACHE BOOL "Enable installer build")
add_subdirectory("distribution")


# -----------------------------------------------------------------------------
# Enforce and check code format
# -----------------------------------------------------------------------------
# set paths of files to format
set(SDIR ${PROJECT_SOURCE_DIR}/src)

# format the code
add_custom_target(codeformat
  COMMAND npm run format
  COMMAND find ${SDIR}/*.h ${SDIR}/*.cpp | xargs clang-format -style=file:${PROJECT_SOURCE_DIR}/.clang-format -i)

# check code format
add_custom_target(codecheck
  COMMAND npm run check
  COMMAND find ${SDIR}/*.h ${SDIR}/*.cpp | xargs clang-format -style=file:${PROJECT_SOURCE_DIR}/.clang-format --dry-run --Werror)

set_target_properties(svzerod PROPERTIES
        LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/python
)

# standard install target locations
install(TARGETS svzerod
        LIBRARY DESTINATION "${CMAKE_INSTALL_LIBDIR}"
        ARCHIVE DESTINATION "${CMAKE_INSTALL_LIBDIR}"
        RUNTIME DESTINATION "${CMAKE_INSTALL_LIBDIR}"
)

# append correct suffix for windows python runtime files
if (WIN32)
    set_target_properties(svzerod PROPERTIES
            PREFIX ""
            SUFFIX ".pyd"
    )
endif()
