cmake_minimum_required(VERSION 3.15...4.1)

#### Check important configuration pre-requisites.
#

# Discourage in-source builds; they overwrite the hand-written Makefile, and generally cause confusion.
# Use `cmake . -B<dir>` or the CMake GUI to do an out-of-source build.
if (${CMAKE_SOURCE_DIR} STREQUAL ${CMAKE_BINARY_DIR})
    message(FATAL_ERROR "Out-of-source builds are recommended for this project.")
endif()

#### Extract version from include/libjsonnet.h so it can be put in the project version.
#

# Extract release version number from include/libjsonnet.h.
# We do this in various other (non-CMake-build) places too, e.g., the Python wheel build.

set(JSONNET_HEADER_VERSION_REGEX
    [=[^[ \t]*#[ \t]*define[ \t]+LIB_JSONNET_VERSION[ \t]+\"v([0-9.]+(-?[a-z][a-z0-9]*)?)\"[ \t]*$]=])
file(
    STRINGS ${CMAKE_CURRENT_SOURCE_DIR}/include/libjsonnet.h JSONNET_HEADER_VERSION
    LIMIT_COUNT 1
    REGEX ${JSONNET_HEADER_VERSION_REGEX})
string(REGEX REPLACE ${JSONNET_HEADER_VERSION_REGEX} "\\1" JSONNET_HEADER_VERSION "${JSONNET_HEADER_VERSION}")
message(VERBOSE "Extracted version from include/libjsonnet.h: ${JSONNET_HEADER_VERSION}")

#### Set CMake project.
#

project(
    jsonnet
    HOMEPAGE_URL "https://jsonnet.org/"
    LANGUAGES C CXX)

#### Unconditional CMake module includes.
#

include(GNUInstallDirs)

#### Set up cacheable/user-configurable state.
#

# User-configurable options.
option(BUILD_JSONNET "Build jsonnet command-line tool." ON)
option(BUILD_JSONNETFMT "Build jsonnetfmt command-line tool." ON)
option(BUILD_TESTS "Build and run jsonnet tests." ON)
option(BUILD_STATIC_LIBS "Build a static libjsonnet." ON)
option(BUILD_SHARED_LIBS "Build shared libjsonnet." ON)
option(BUILD_SHARED_BINARIES "Link binaries to the shared libjsonnet instead of the static one." OFF)
option(BUILD_MAN_PAGES "Build manpages." ON)
option(USE_SYSTEM_GTEST "Use system-provided gtest library" OFF)
option(USE_SYSTEM_JSON "Use the system-provided json library" OFF)
option(USE_SYSTEM_RAPIDYAML "Use the system-provided rapidyaml library" OFF)

#### Compute derived values from user-configurable state.
#

# Helper variable set to ON if there is any reason we need to build the jsonnet command binaries.
if(BUILD_TESTS OR BUILD_JSONNET OR BUILD_JSONNETFMT)
    set(BUILD_SOME_BINARIES ON)
else()
    set(BUILD_SOME_BINARIES OFF)
endif()

#### System package searches.
#

if(USE_SYSTEM_JSON)
    find_package(nlohmann_json 3.6.1 REQUIRED)
endif()

if(USE_SYSTEM_RAPIDYAML)
    find_package(ryml 0.10.0 REQUIRED)
endif()

#### Utility function to configure a target with our preferred C and C++ compilation flags.
#

function(configure_target_cc_props LIB_TARGET_NAME)
    set_property(TARGET ${LIB_TARGET_NAME} PROPERTY CXX_STANDARD 17)
    set_property(TARGET ${LIB_TARGET_NAME} PROPERTY CXX_EXTENSIONS OFF)
    set_property(TARGET ${LIB_TARGET_NAME} PROPERTY CXX_STANDARD_REQUIRED ON)
    set_property(TARGET ${LIB_TARGET_NAME} PROPERTY C_STANDARD 99)
    set_property(TARGET ${LIB_TARGET_NAME} PROPERTY C_EXTENSIONS OFF)
    set_property(TARGET ${LIB_TARGET_NAME} PROPERTY C_STANDARD_REQUIRED ON)

    if (NOT MSVC)
        target_compile_options(${LIB_TARGET_NAME}
            PRIVATE
                -Wall -Wextra -Wimplicit-fallthrough
                -pedantic
                $<$<COMPILE_LANGUAGE:CXX>:-Woverloaded-virtual>
        )
    endif()
endfunction()

#### Sub-directory CMakeLists includes.
#

add_subdirectory(stdlib)

#### Utility function to set up all the properties for an OBJECT library for the jsonnet core.
#
# This is done as a function because there separate object libraries for shared vs. static.
# Shared has slightly different compilation flags (it needs -fPIC).
#

function(configure_jsonnet_obj_target LIB_TARGET_NAME)
    configure_target_cc_props(${LIB_TARGET_NAME})
    target_sources(${LIB_TARGET_NAME}
        PRIVATE
            third_party/md5/md5.h
            third_party/md5/md5.cpp

            core/static_error.h
            core/desugarer.h
            core/unicode.h
            core/vm.h
            core/static_analysis.h
            core/path_utils.h
            core/state.h
            core/pass.h
            core/ast.h
            core/json.h
            core/string_utils.h
            core/formatter.h
            core/parser.h
            core/lexer.h

            core/desugarer.cpp
            core/formatter.cpp
            core/lexer.cpp
            core/libjsonnet.cpp
            core/parser.cpp
            core/pass.cpp
            core/path_utils.cpp
            core/static_analysis.cpp
            core/string_utils.cpp
            core/vm.cpp

        PUBLIC
            include/libjsonnet.h
            include/libjsonnet_fmt.h
    )

    target_link_libraries(${LIB_TARGET_NAME} PRIVATE stdlib_h)

    if(USE_SYSTEM_JSON)
        target_link_libraries(${LIB_TARGET_NAME} PRIVATE nlohmann_json::nlohmann_json)
    else()
        target_sources(${LIB_TARGET_NAME}
            PRIVATE
                third_party/json/nlohmann/json.hpp
        )
        target_include_directories(${LIB_TARGET_NAME}
            SYSTEM PRIVATE third_party/json)
    endif()

    if(USE_SYSTEM_RAPIDYAML)
        target_link_libraries(${LIB_TARGET_NAME} PRIVATE ryml::ryml)
        target_compile_definitions(${LIB_TARGET_NAME} PRIVATE USE_SYSTEM_RAPIDYAML)
    else()
        target_sources(${LIB_TARGET_NAME}
            PRIVATE
                third_party/rapidyaml/rapidyaml-0.10.0.hpp
                third_party/rapidyaml/rapidyaml.cpp
        )
    endif()

    target_include_directories(${LIB_TARGET_NAME}
        PUBLIC
            $<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>
            $<INSTALL_INTERFACE:include>
        PRIVATE
            third_party/md5
            $<$<NOT:$<BOOL:${USE_SYSTEM_RAPIDYAML}>>:${CMAKE_CURRENT_SOURCE_DIR}/third_party/rapidyaml>
        )
endfunction()

#### Utility function to set up library target properties for the C lib.
#

function(configure_jsonnet_lib_target LIB_TARGET_NAME OBJ_TARGET_NAME)
    configure_target_cc_props(${LIB_TARGET_NAME})
    target_link_libraries(${LIB_TARGET_NAME} PUBLIC ${OBJ_TARGET_NAME})
    set(JSONNET_PUBLIC_HEADERS include/libjsonnet.h include/libjsonnet_fmt.h)
    set_target_properties(${LIB_TARGET_NAME} PROPERTIES
        OUTPUT_NAME jsonnet
        PUBLIC_HEADER "${JSONNET_PUBLIC_HEADERS}")
endfunction()

#### Utility function to set up library target properties for the C++ lib.
#

function(configure_jsonnet_cpp_lib_target LIB_TARGET_NAME OBJ_TARGET_NAME)
    configure_target_cc_props(${LIB_TARGET_NAME})
    target_sources(${LIB_TARGET_NAME}
        PRIVATE
            cpp/libjsonnet++.cpp
            include/libjsonnet++.h
    )
    target_link_libraries(${LIB_TARGET_NAME} PUBLIC ${OBJ_TARGET_NAME})
    set(JSONNET_PUBLIC_HEADERS include/libjsonnet.h include/libjsonnet++.h)
    set_target_properties(${LIB_TARGET_NAME} PROPERTIES
        OUTPUT_NAME jsonnet++
        PUBLIC_HEADER "${JSONNET_PUBLIC_HEADERS}")
endfunction()

#### Static library target setup.
#

if(BUILD_STATIC_LIBS OR (BUILD_SOME_BINARIES AND NOT BUILD_SHARED_BINARIES))
    add_library(jsonnet_obj_static OBJECT)
    configure_jsonnet_obj_target(jsonnet_obj_static)

    add_library(jsonnet_static STATIC)
    configure_jsonnet_lib_target(jsonnet_static jsonnet_obj_static)

    add_library(jsonnet_cpp_static STATIC)
    configure_jsonnet_cpp_lib_target(jsonnet_cpp_static jsonnet_obj_static)

    if(BUILD_STATIC_LIBS)
        install(TARGETS jsonnet_static jsonnet_cpp_static)
    endif()
endif()

#### Shared library target setup.
#

if(BUILD_SHARED_LIBS OR (BUILD_SOME_BINARIES AND BUILD_SHARED_BINARIES))
    add_library(jsonnet_obj_shared OBJECT)
    set_property(TARGET jsonnet_obj_shared PROPERTY POSITION_INDEPENDENT_CODE ON)
    configure_jsonnet_obj_target(jsonnet_obj_shared)

    add_library(jsonnet_shared SHARED)
    configure_jsonnet_lib_target(jsonnet_shared jsonnet_obj_shared)
    set_target_properties(jsonnet_shared PROPERTIES
        VERSION     "${JSONNET_HEADER_VERSION}"
        SOVERSION   "0")

    add_library(jsonnet_cpp_shared SHARED)
    configure_jsonnet_cpp_lib_target(jsonnet_cpp_shared jsonnet_obj_shared)
    set_target_properties(jsonnet_cpp_shared PROPERTIES
        VERSION     "${JSONNET_HEADER_VERSION}"
        SOVERSION   "0")

    if(BUILD_SHARED_LIBS)
        install(TARGETS jsonnet_shared jsonnet_cpp_shared)
    endif()
endif()

#### Alias the library to use for binaries.
#

if(BUILD_SHARED_BINARIES)
    add_library(jsonnet_lib_for_binaries ALIAS jsonnet_shared)
    add_library(jsonnet_cpp_lib_for_binaries ALIAS jsonnet_cpp_shared)
else()
    add_library(jsonnet_lib_for_binaries ALIAS jsonnet_static)
    add_library(jsonnet_cpp_lib_for_binaries ALIAS jsonnet_cpp_static)
endif()

#### An object library for the utils.cpp code which is shared by both CLI binaries.
#

if(BUILD_SOME_BINARIES)
    add_library(jsonnet_cmd_utils OBJECT
        cmd/utils.cpp
        cmd/utils.h
    )
endif()

#### CLI (jsonnet and jsonnetfmt) binary targets.
#

if(BUILD_JSONNET OR BUILD_TESTS)
    add_executable(jsonnet
        cmd/jsonnet.cpp
        $<TARGET_OBJECTS:jsonnet_cmd_utils>
    )
    target_link_libraries(jsonnet PRIVATE jsonnet_lib_for_binaries)
    if(BUILD_JSONNET)
        install(TARGETS jsonnet)
    endif()
endif()

if(BUILD_JSONNETFMT OR BUILD_TESTS)
    add_executable(jsonnetfmt
        cmd/jsonnetfmt.cpp
        $<TARGET_OBJECTS:jsonnet_cmd_utils>
    )
    target_link_libraries(jsonnetfmt PRIVATE jsonnet_lib_for_binaries)
    if(BUILD_JSONNETFMT)
        install(TARGETS jsonnetfmt)
    endif()
endif()

#### GoogleTest based tests.
#

if(BUILD_TESTS)
    enable_testing()
    include(GoogleTest)

    if(USE_SYSTEM_GTEST)
        find_package(GTest REQUIRED)
    else()
        include(FetchContent)
        FetchContent_Declare(googletest
            GIT_REPOSITORY https://github.com/google/googletest.git
            GIT_TAG        v1.17.0
        )
        # For Windows: Prevent overriding the parent project's compiler/linker settings.
        set(gtest_force_shared_crt ON CACHE BOOL "" FORCE)
        # Prevent including GoogleTest in the generated install rules.
        set(INSTALL_GTEST OFF CACHE BOOL "" FORCE)
        FetchContent_MakeAvailable(googletest)
    endif()

    set(JSONNET_TEST_TARGETS)

    function(jsonnet_add_gtest TESTNAME JSONNET_LIB_TARGET)
        set(JSONNET_TEST_TARGETS ${JSONNET_TEST_TARGETS} ${TESTNAME} PARENT_SCOPE)
        add_executable(${TESTNAME} ${ARGN})
        target_link_libraries(${TESTNAME} ${JSONNET_LIB_TARGET} gtest gmock gtest_main)
        gtest_discover_tests(${TESTNAME}
            WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}
            PROPERTIES
                VS_DEBUGGER_WORKING_DIRECTORY "${PROJECT_SOURCE_DIR}"
                ENVIRONMENT "JSONNET_SOURCE_BASE=${PROJECT_SOURCE_DIR}"
        )
        set_target_properties(${TESTNAME} PROPERTIES FOLDER tests)
    endfunction()

    function(jsonnet_add_raw_test TESTNAME JSONNET_LIB_TARGET)
        set(JSONNET_TEST_TARGETS ${JSONNET_TEST_TARGETS} ${TESTNAME} PARENT_SCOPE)
        add_executable(${TESTNAME} ${ARGN})
        target_link_libraries(${TESTNAME} ${JSONNET_LIB_TARGET})
        set_target_properties(${TESTNAME} PROPERTIES FOLDER tests)
        add_test(NAME ${TESTNAME} COMMAND ${TESTNAME})
    endfunction()

    jsonnet_add_gtest(test_core_unicode jsonnet_lib_for_binaries core/unicode_test.cpp)
    jsonnet_add_gtest(test_core_lexer jsonnet_lib_for_binaries core/lexer_test.cpp)
    jsonnet_add_gtest(test_core_parser jsonnet_lib_for_binaries core/parser_test.cpp)
    jsonnet_add_gtest(test_core_libjsonnet jsonnet_lib_for_binaries core/libjsonnet_test.cpp)
    jsonnet_add_gtest(test_cpp_libjsonnet jsonnet_cpp_lib_for_binaries cpp/libjsonnet++_test.cpp)
    jsonnet_add_gtest(test_cpp_libjsonnet_locale jsonnet_cpp_lib_for_binaries cpp/libjsonnet_locale_test.cpp)

    jsonnet_add_raw_test(test_core_libjsonnet_native_callbacks jsonnet_lib_for_binaries core/libjsonnet_native_callbacks_test.c)

    # core/libjsonnet_file_test needs an input file
    add_executable(test_libjsonnet_file core/libjsonnet_file_test.c)
    target_link_libraries(test_libjsonnet_file jsonnet_lib_for_binaries)
    set_target_properties(test_libjsonnet_file PROPERTIES FOLDER tests)
    add_test(
        NAME test_libjsonnet_file
        COMMAND test_libjsonnet_file "test_suite/object.jsonnet"
        WORKING_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}"
    )

    add_subdirectory(test_suite)

    # `run_tests` target builds and runs all tests
    add_custom_target(run_tests COMMAND ${CMAKE_CTEST_COMMAND}
        DEPENDS
            jsonnet jsonnetfmt test_libjsonnet_file ${JSONNET_TEST_TARGETS}
    )
endif()  # if(BUILD_TESTS)

#### Man pages
#

if(BUILD_MAN_PAGES AND (BUILD_JSONNET OR BUILD_JSONNETFMT))
    find_program(HELP2MAN_BINARY NAMES help2man)
    if (HELP2MAN_BINARY)
        message(STATUS "help2man found, man pages will be generated.")
        file(MAKE_DIRECTORY "${PROJECT_BINARY_DIR}/man/man1")
        if(BUILD_JSONNET)
            add_custom_command(
                OUTPUT "${PROJECT_BINARY_DIR}/man/man1/jsonnet.1"
                COMMAND "${HELP2MAN_BINARY}"
                ARGS --section=1 --no-info --name="Jsonnet data templating language interpreter" --output="${PROJECT_BINARY_DIR}/man/man1/jsonnet.1" "$<TARGET_FILE:jsonnet>"
                DEPENDS jsonnet
            )
        endif()
        if(BUILD_JSONNETFMT)
            add_custom_command(
                OUTPUT "${PROJECT_BINARY_DIR}/man/man1/jsonnetfmt.1"
                COMMAND "${HELP2MAN_BINARY}"
                ARGS --section=1 --no-info --name="Jsonnet data templating language auto-formatter" --output="${PROJECT_BINARY_DIR}/man/man1/jsonnetfmt.1" "$<TARGET_FILE:jsonnetfmt>"
                DEPENDS jsonnetfmt
            )
        endif()

        add_custom_target(jsonnet-man ALL DEPENDS "${PROJECT_BINARY_DIR}/man/man1/jsonnet.1")
        add_custom_target(jsonnetfmt-man ALL DEPENDS "${PROJECT_BINARY_DIR}/man/man1/jsonnetfmt.1")

        install(FILES ${PROJECT_BINARY_DIR}/man/man1/jsonnet.1 DESTINATION "${CMAKE_INSTALL_MANDIR}/man1")
        install(FILES ${PROJECT_BINARY_DIR}/man/man1/jsonnetfmt.1 DESTINATION "${CMAKE_INSTALL_MANDIR}/man1")
    else()
        message(STATUS "help2man not found, man pages will not be generated.")
    endif()
endif()  # if(BUILD_MAN_PAGES)

#### CMake debugging outputs
#

# Note cmake_language GET_MESSAGE_LOG_LEVEL exists only in 3.25 and above.
if(CMAKE_VERSION VERSION_GREATER "3.25")
    include(CMakePrintHelpers)
    cmake_language(GET_MESSAGE_LOG_LEVEL CURRENT_CMAKE_LOG_LEVEL)
    if(CURRENT_CMAKE_LOG_LEVEL MATCHES "TRACE")
        # Print specific properties for specific targets
        cmake_print_properties(
            TARGETS
                to_c_array
                stdlib_h
                jsonnet_obj_static
                jsonnet_obj_shared
                jsonnet_static
                jsonnet_shared
                jsonnet_cpp_static
                jsonnet_cpp_shared
                jsonnet_cmd_utils
                jsonnet
                jsonnetfmt
                jsonnet-man
                jsonnetfmt-man
            PROPERTIES
                TYPE
                C_STANDARD
                CXX_STANDARD
                POSITION_INDEPENDENT_CODE
                PUBLIC_LINK_DEPENDS
                INTERFACE_LINK_DEPENDS
                PUBLIC_LINK_LIBRARIES
                INTERFACE_LINK_LIBRARIES
                PUBLIC_INCLUDE_DIRECTORIES
                INTERFACE_INCLUDE_DIRECTORIES
                OUTPUT_NAME
                PUBLIC_HEADER
                RUNTIME_OUTPUT_DIRECTORY
        )
    endif()
endif()
