cmake_minimum_required(VERSION 3.21)
project(game_resolver_cpp VERSION 0.1.2 LANGUAGES CXX)

option(GAME_RESOLVER_BUILD_TESTS "Build the test suite" ${PROJECT_IS_TOP_LEVEL})
option(GAME_RESOLVER_BUILD_EXAMPLES "Build the example programs" ${PROJECT_IS_TOP_LEVEL})
option(GAME_RESOLVER_BUILD_BENCHMARKS "Build the benchmark program" ${PROJECT_IS_TOP_LEVEL})
option(GAME_RESOLVER_BUILD_PYTHON "Build the Python extension module" OFF)
option(GAME_RESOLVER_WERROR "Treat compiler warnings as errors" OFF)
option(GAME_RESOLVER_SANITIZE "Build with sanitizers (see GAME_RESOLVER_SANITIZERS)" OFF)

# macOS の Apple clang 15 に同梱の ASan ランタイムは、最近の macOS 上で
# 「CHECK failed: sanitizer_malloc_mac.inc」で起動時に落ちる（自明なプログラムでも再現する
# ツールチェーン側の問題）。そのため Apple では既定を UBSan のみにする。
# ASan は Linux の CI ジョブでかける。明示指定すれば macOS でも有効にできる。
if(APPLE)
  set(GAME_RESOLVER_SANITIZERS "undefined" CACHE STRING "Comma separated sanitizer list")
else()
  set(GAME_RESOLVER_SANITIZERS "address,undefined" CACHE STRING "Comma separated sanitizer list")
endif()

set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_CXX_EXTENSIONS OFF)
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)

if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES)
  set(CMAKE_BUILD_TYPE Release CACHE STRING "Build type" FORCE)
endif()

# ------------------------------------------------------- 可視化アセットの埋め込み
# assets/ と third_party/d3-mini/ を C++ の生文字列リテラルにしたヘッダを configure 時に作る。
# こうしておくと tree_view.cpp が吐く HTML が 1 ファイルで完結し、実行時に何も探しに行かない。
#
# MSVC は 1 つの文字列リテラルを 65535 バイトまでしか許さないので、16 KB ずつに割って
# 配列にし、実行時に連結する（d3-mini.min.js が 57 KB あり、素直に書くと将来引っかかる）。
set(GR_ASSET_FILES
  ${CMAKE_CURRENT_SOURCE_DIR}/assets/tree_view.html.in
  ${CMAKE_CURRENT_SOURCE_DIR}/assets/tree_view.css
  ${CMAKE_CURRENT_SOURCE_DIR}/assets/tree_view.js
  ${CMAKE_CURRENT_SOURCE_DIR}/assets/editor.html.in
  ${CMAKE_CURRENT_SOURCE_DIR}/assets/editor.css
  ${CMAKE_CURRENT_SOURCE_DIR}/assets/editor.js
  ${CMAKE_CURRENT_SOURCE_DIR}/third_party/d3-mini/d3-mini.min.js
)
set_property(DIRECTORY APPEND PROPERTY CMAKE_CONFIGURE_DEPENDS ${GR_ASSET_FILES})

function(gr_emit_asset out_var function_name path)
  file(READ "${path}" _content)
  string(FIND "${_content}" ")GRASSET\"" _clash)
  if(NOT _clash EQUAL -1)
    message(FATAL_ERROR "${path}: 生文字列リテラルの終端子 )GRASSET\" が中身に含まれています")
  endif()

  string(LENGTH "${_content}" _len)
  set(_chunks "")
  set(_at 0)
  while(_at LESS _len)
    math(EXPR _take "${_len} - ${_at}")
    if(_take GREATER 16000)
      set(_take 16000)
    endif()
    string(SUBSTRING "${_content}" ${_at} ${_take} _piece)
    string(APPEND _chunks "    R\"GRASSET(${_piece})GRASSET\",\n")
    math(EXPR _at "${_at} + ${_take}")
  endwhile()

  string(APPEND ${out_var}
    "inline std::string ${function_name}() {\n"
    "  static const char* const kParts[] = {\n${_chunks}  };\n"
    "  return detail::join(kParts, sizeof(kParts) / sizeof(kParts[0]));\n"
    "}\n\n")
  set(${out_var} "${${out_var}}" PARENT_SCOPE)
endfunction()

set(GR_ASSET_BODY "")
gr_emit_asset(GR_ASSET_BODY tree_view_template ${CMAKE_CURRENT_SOURCE_DIR}/assets/tree_view.html.in)
gr_emit_asset(GR_ASSET_BODY tree_view_css ${CMAKE_CURRENT_SOURCE_DIR}/assets/tree_view.css)
gr_emit_asset(GR_ASSET_BODY tree_view_js ${CMAKE_CURRENT_SOURCE_DIR}/assets/tree_view.js)
gr_emit_asset(GR_ASSET_BODY editor_template ${CMAKE_CURRENT_SOURCE_DIR}/assets/editor.html.in)
gr_emit_asset(GR_ASSET_BODY editor_css ${CMAKE_CURRENT_SOURCE_DIR}/assets/editor.css)
gr_emit_asset(GR_ASSET_BODY editor_js ${CMAKE_CURRENT_SOURCE_DIR}/assets/editor.js)
gr_emit_asset(GR_ASSET_BODY d3_mini ${CMAKE_CURRENT_SOURCE_DIR}/third_party/d3-mini/d3-mini.min.js)

file(WRITE ${CMAKE_CURRENT_BINARY_DIR}/generated/game_resolver_assets.hpp
"// CMake が assets/ と third_party/d3-mini/ から生成する。直接編集しない。
// 元ファイルを直せば configure が走り直して作り直される。
#pragma once

#include <cstddef>
#include <string>

namespace game_resolver::assets {
namespace detail {

inline std::string join(const char* const* parts, std::size_t count) {
  std::string out;
  for (std::size_t i = 0; i < count; ++i) {
    out += parts[i];
  }
  return out;
}

}  // namespace detail

${GR_ASSET_BODY}}  // namespace game_resolver::assets
")

# ---------------------------------------------------------------- library
add_library(game_resolver
  src/version.cpp
  src/action.cpp
  src/payoff.cpp
  src/nash_equilibrium.cpp
  src/solver/nash_solver.cpp
  src/bayesian/bayesian_player.cpp
  src/bayesian/bayesian_nash_equilibrium.cpp
  src/extensive/extensive_form_game.cpp
  src/extensive/subgame_perfect_equilibrium.cpp
  src/extensive/tree_view.cpp
  src/extensive/html_util.cpp
  src/extensive/editor.cpp
)
add_library(game_resolver::game_resolver ALIAS game_resolver)

target_include_directories(game_resolver PUBLIC
  $<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>
  $<INSTALL_INTERFACE:include>
)
# 生成した game_resolver_assets.hpp は tree_view.cpp だけが使う。公開ヘッダではない。
target_include_directories(game_resolver PRIVATE ${CMAKE_CURRENT_BINARY_DIR}/generated)
target_compile_features(game_resolver PUBLIC cxx_std_20)

# 静的ライブラリを共有オブジェクトにリンクできるようにする。
# これが無いと Linux で Python 拡張モジュール (_core.so) のリンクが
# 「relocation R_X86_64_32 ... can not be used when making a shared object」で失敗する。
# macOS は常に PIC なので気づきにくい。利用者が自分の共有ライブラリに組み込む場合にも要る。
set_target_properties(game_resolver PROPERTIES POSITION_INDEPENDENT_CODE ON)

find_package(Threads REQUIRED)
target_link_libraries(game_resolver PUBLIC Threads::Threads)

if(MSVC)
  target_compile_options(game_resolver PRIVATE /W4)
  if(GAME_RESOLVER_WERROR)
    target_compile_options(game_resolver PRIVATE /WX)
  endif()
else()
  target_compile_options(game_resolver PRIVATE -Wall -Wextra -Wpedantic)
  if(GAME_RESOLVER_WERROR)
    target_compile_options(game_resolver PRIVATE -Werror)
  endif()
endif()

if(GAME_RESOLVER_SANITIZE)
  message(STATUS "Sanitizers: ${GAME_RESOLVER_SANITIZERS}")
  target_compile_options(game_resolver PUBLIC
    -fsanitize=${GAME_RESOLVER_SANITIZERS} -fno-omit-frame-pointer)
  target_link_options(game_resolver PUBLIC -fsanitize=${GAME_RESOLVER_SANITIZERS})
endif()

# ---------------------------------------------------------------- subdirs
if(GAME_RESOLVER_BUILD_EXAMPLES)
  add_subdirectory(examples)
endif()

if(GAME_RESOLVER_BUILD_BENCHMARKS)
  add_subdirectory(benchmarks)
endif()

if(GAME_RESOLVER_BUILD_PYTHON)
  add_subdirectory(python)
endif()

if(GAME_RESOLVER_BUILD_TESTS)
  enable_testing()
  add_subdirectory(tests)
endif()

# ---------------------------------------------------------------- install
include(GNUInstallDirs)
include(CMakePackageConfigHelpers)

install(TARGETS game_resolver
  EXPORT game_resolverTargets
  ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR}
  LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR}
  RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}
)
install(DIRECTORY include/ DESTINATION ${CMAKE_INSTALL_INCLUDEDIR})

install(EXPORT game_resolverTargets
  FILE game_resolverTargets.cmake
  NAMESPACE game_resolver::
  DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/game_resolver
)

configure_package_config_file(
  ${CMAKE_CURRENT_SOURCE_DIR}/cmake/game_resolverConfig.cmake.in
  ${CMAKE_CURRENT_BINARY_DIR}/game_resolverConfig.cmake
  INSTALL_DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/game_resolver
)
write_basic_package_version_file(
  ${CMAKE_CURRENT_BINARY_DIR}/game_resolverConfigVersion.cmake
  COMPATIBILITY SameMajorVersion
)
install(FILES
  ${CMAKE_CURRENT_BINARY_DIR}/game_resolverConfig.cmake
  ${CMAKE_CURRENT_BINARY_DIR}/game_resolverConfigVersion.cmake
  DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/game_resolver
)
