cmake_minimum_required(VERSION 3.16)

# std::filesystem needs macOS 10.15 or newer. Has to be set before project().
if(APPLE AND NOT CMAKE_OSX_DEPLOYMENT_TARGET)
  set(CMAKE_OSX_DEPLOYMENT_TARGET "10.15" CACHE STRING "Minimum macOS version")
endif()

project(antlrlab VERSION 1.0.0 LANGUAGES CXX)

set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_CXX_EXTENSIONS OFF)
if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES)
  set(CMAKE_BUILD_TYPE Release CACHE STRING "Build type" FORCE)
endif()

# The ANTLR tool (Java) and the C++ runtime must be the same version, because
# the serialized grammar that the tool writes is read by the runtime.
set(ANTLR_VERSION "4.13.2" CACHE STRING "ANTLR version to build against")

option(ANTLRLAB_USE_SYSTEM_RUNTIME
       "Use an installed ANTLR C++ runtime instead of downloading one" OFF)
option(ANTLRLAB_DOWNLOAD_JAR
       "Download antlr-${ANTLR_VERSION}-complete.jar at configure time and use it by default" OFF)
if(MSVC)
  option(ANTLRLAB_STATIC_CRT
         "Visual C++: link the C++ runtime statically, so the program needs no redistributable" ON)
elseif(MINGW)
  option(ANTLRLAB_STATIC_LINK
         "MinGW: link libgcc, libstdc++ and winpthread statically, so no DLLs have to ship with the program" ON)
endif()

find_package(Threads REQUIRED)

# Visual C++: the runtime and antlrlab have to use the same C runtime library.
if(MSVC)
  if(ANTLRLAB_STATIC_CRT)
    set(WITH_STATIC_CRT ON CACHE BOOL "" FORCE)   # read by the ANTLR runtime's own CMakeLists
    set(ANTLRLAB_MSVC_RUNTIME "MultiThreaded$<$<CONFIG:Debug>:Debug>")
  else()
    set(WITH_STATIC_CRT OFF CACHE BOOL "" FORCE)
    set(ANTLRLAB_MSVC_RUNTIME "MultiThreaded$<$<CONFIG:Debug>:Debug>DLL")
  endif()
endif()

# --------------------------------------------------------------- runtime ----
if(ANTLRLAB_USE_SYSTEM_RUNTIME)
  find_path(ANTLR4_INCLUDE_DIR antlr4-runtime.h
            PATH_SUFFIXES antlr4-runtime include/antlr4-runtime)
  find_library(ANTLR4_LIBRARY NAMES antlr4-runtime antlr4-runtime-static)
  if(NOT ANTLR4_INCLUDE_DIR OR NOT ANTLR4_LIBRARY)
    message(FATAL_ERROR
      "ANTLR C++ runtime not found. Install it, set ANTLR4_INCLUDE_DIR and "
      "ANTLR4_LIBRARY, or configure without -DANTLRLAB_USE_SYSTEM_RUNTIME=ON.")
  endif()
  set(ANTLRLAB_RUNTIME_LIBS ${ANTLR4_LIBRARY})
  set(ANTLRLAB_RUNTIME_INCLUDES ${ANTLR4_INCLUDE_DIR})
  set(ANTLRLAB_RUNTIME_DEFS "")
else()
  include(FetchContent)
  set(ANTLR_BUILD_CPP_TESTS OFF CACHE BOOL "" FORCE)
  set(ANTLR_BUILD_SHARED OFF CACHE BOOL "" FORCE)
  set(ANTLR_BUILD_STATIC ON CACHE BOOL "" FORCE)
  set(WITH_DEMO OFF CACHE BOOL "" FORCE)
  set(ANTLR4_INSTALL OFF CACHE BOOL "" FORCE)

  set(_antlr_fetch_args
      URL "https://github.com/antlr/antlr4/archive/refs/tags/${ANTLR_VERSION}.tar.gz"
      SOURCE_SUBDIR runtime/Cpp)
  if(CMAKE_VERSION VERSION_GREATER_EQUAL 3.24)
    list(APPEND _antlr_fetch_args DOWNLOAD_EXTRACT_TIMESTAMP TRUE)
  endif()
  FetchContent_Declare(antlr4 ${_antlr_fetch_args})
  FetchContent_MakeAvailable(antlr4)

  # The runtime forgets some standard #includes (<chrono> for one), which newer
  # standard libraries, notably Visual C++'s, no longer paper over. Supply them.
  if(TARGET antlr4_static)
    set(_compat "${CMAKE_CURRENT_SOURCE_DIR}/cmake/antlr_runtime_compat.h")
    if(MSVC)
      target_compile_options(antlr4_static PRIVATE "/FI${_compat}")
    else()
      target_compile_options(antlr4_static PRIVATE -include "${_compat}")
    endif()
  endif()

  set(ANTLRLAB_RUNTIME_LIBS antlr4_static)
  set(ANTLRLAB_RUNTIME_INCLUDES "${antlr4_SOURCE_DIR}/runtime/Cpp/runtime/src")
  set(ANTLRLAB_RUNTIME_DEFS ANTLR4CPP_STATIC)
endif()

# ------------------------------------------------------------------- jar ----
set(ANTLRLAB_JAR_PATH "")
if(ANTLRLAB_DOWNLOAD_JAR)
  set(_jar "${CMAKE_BINARY_DIR}/antlr-${ANTLR_VERSION}-complete.jar")
  if(NOT EXISTS "${_jar}")
    # antlr.org first, then the copy in the ANTLR website's GitHub repository.
    foreach(_url
        "https://www.antlr.org/download/antlr-${ANTLR_VERSION}-complete.jar"
        "https://raw.githubusercontent.com/antlr/website-antlr4/gh-pages/download/antlr-${ANTLR_VERSION}-complete.jar")
      message(STATUS "Downloading ${_url}")
      file(DOWNLOAD "${_url}" "${_jar}" STATUS _dl_status SHOW_PROGRESS)
      list(GET _dl_status 0 _dl_code)
      if(_dl_code EQUAL 0)
        break()
      endif()
      file(REMOVE "${_jar}")
      message(STATUS "  failed: ${_dl_status}")
    endforeach()
    if(NOT EXISTS "${_jar}")
      message(WARNING "Could not download the ANTLR jar. "
                      "Pass --antlr-jar or set ANTLR_JAR when running antlrlab.")
    endif()
  endif()
  if(EXISTS "${_jar}")
    file(TO_CMAKE_PATH "${_jar}" ANTLRLAB_JAR_PATH)
  endif()
endif()

# ---------------------------------------------------------------- target ----
add_executable(antlrlab src/main.cpp)
target_include_directories(antlrlab PRIVATE src ${ANTLRLAB_RUNTIME_INCLUDES})
target_link_libraries(antlrlab PRIVATE ${ANTLRLAB_RUNTIME_LIBS} Threads::Threads)
target_compile_definitions(antlrlab PRIVATE
  ${ANTLRLAB_RUNTIME_DEFS}
  LAB_VERSION="${PROJECT_VERSION}"
  LAB_ANTLR_VERSION="${ANTLR_VERSION}")
if(ANTLRLAB_JAR_PATH)
  target_compile_definitions(antlrlab PRIVATE LAB_DEFAULT_ANTLR_JAR="${ANTLRLAB_JAR_PATH}")
  # Also put the jar next to the program: antlrlab looks there too, which keeps
  # working when the build directory is moved, or its path has unusual characters.
  add_custom_command(TARGET antlrlab POST_BUILD
    COMMAND ${CMAKE_COMMAND} -E copy_if_different "${ANTLRLAB_JAR_PATH}" "$<TARGET_FILE_DIR:antlrlab>")
  install(FILES "${ANTLRLAB_JAR_PATH}" DESTINATION bin)
endif()

# std::filesystem lives in a separate library in GCC 8 and older, and in Clang
# with libc++ 8 and older.
include(CheckCXXSourceCompiles)
set(CMAKE_REQUIRED_FLAGS "-std=c++17")
if(MSVC)
  set(CMAKE_REQUIRED_FLAGS "/std:c++17")
endif()
check_cxx_source_compiles("#include <filesystem>
int main() { return std::filesystem::current_path().empty() ? 1 : 0; }" ANTLRLAB_FS_BUILTIN)
unset(CMAKE_REQUIRED_FLAGS)
if(NOT ANTLRLAB_FS_BUILTIN)
  foreach(_fslib stdc++fs c++fs)
    set(CMAKE_REQUIRED_LIBRARIES ${_fslib})
    check_cxx_source_compiles("#include <filesystem>
int main() { return std::filesystem::current_path().empty() ? 1 : 0; }" ANTLRLAB_FS_${_fslib})
    unset(CMAKE_REQUIRED_LIBRARIES)
    if(ANTLRLAB_FS_${_fslib})
      target_link_libraries(antlrlab PRIVATE ${_fslib})
      break()
    endif()
  endforeach()
endif()

if(WIN32)
  # Winsock (web UI), BCrypt (access token), Shell32 (command line, opening the
  # browser), DbgHelp (stack traces in crash reports)
  target_link_libraries(antlrlab PRIVATE ws2_32 bcrypt shell32 dbghelp)
  target_compile_definitions(antlrlab PRIVATE _WIN32_WINNT=0x0601 NOMINMAX WIN32_LEAN_AND_MEAN)
endif()

# Crash reports print a stack trace; ENABLE_EXPORTS (-rdynamic) makes it show function names.
if(NOT WIN32)
  set_target_properties(antlrlab PROPERTIES ENABLE_EXPORTS ON)
endif()

# The ANTLR runtime recurses deeply on real grammars. antlrlab runs it on a
# thread with a big stack it creates itself (src/isolate.hpp), so the main
# program only needs a normal stack; 8 MB matches the Linux default for the
# threads of the web server.
if(MSVC)
  target_link_options(antlrlab PRIVATE /STACK:8388608)
  set_property(TARGET antlrlab PROPERTY MSVC_RUNTIME_LIBRARY "${ANTLRLAB_MSVC_RUNTIME}")
  target_compile_options(antlrlab PRIVATE /utf-8 /EHsc /permissive- /W3)
  target_compile_definitions(antlrlab PRIVATE _CRT_SECURE_NO_WARNINGS _CRT_NONSTDC_NO_WARNINGS)
else()
  target_compile_options(antlrlab PRIVATE -Wall -Wextra)
  if(MINGW)
    target_link_options(antlrlab PRIVATE -Wl,--stack,8388608)
    if(ANTLRLAB_STATIC_LINK)
      target_link_options(antlrlab PRIVATE -static -static-libgcc -static-libstdc++)
    endif()
  endif()
endif()

install(TARGETS antlrlab RUNTIME DESTINATION bin)
