cmake_minimum_required(VERSION 3.19)

option(BUILD_PYTHON "export python module" off)

set(CMAKE_CXX_STANDARD 17 CACHE STRING "C++ version selection")
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_CXX_EXTENSIONS OFF)


list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_LIST_DIR}/cmake")
include(DynamicVersion)

extract_version_string(
    HEADER_FILE ${CMAKE_CURRENT_LIST_DIR}/include/urlparser.h
    VERSION_PREFIX URLPARSER_
    OUTPUT_VAR PROJECT_VERSION
)


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



project(liburlparser LANGUAGES CXX VERSION ${PROJECT_VERSION})
message(STATUS "Project: ${PROJECT_NAME}@v${PROJECT_VERSION}")
# Kept for docs/cpp templates (Doxyfile.in, footer.html.in) that still
# reference @PyProject_VERSION@.
set(PyProject_VERSION ${PROJECT_VERSION})

if (MSVC)
    add_compile_options(/EHsc)
    set(CMAKE_WINDOWS_EXPORT_ALL_SYMBOLS ON)
endif()
if (CYGWIN OR UNIX)
    add_compile_definitions(_GNU_SOURCE)
endif()


find_package(Python3 REQUIRED COMPONENTS Interpreter)

if(DEFINED SKBUILD)
    set(PYTHON_PROJECT_NAME "${SKBUILD_PROJECT_NAME}")
elseif(BUILD_PYTHON)
    set(PYTHON_PROJECT_NAME "${CMAKE_BINARY_DIR}")

    execute_process(
            COMMAND "${Python3_EXECUTABLE}" -m nanobind --cmake_dir
            OUTPUT_STRIP_TRAILING_WHITESPACE
            OUTPUT_VARIABLE NB_DIR
            ERROR_QUIET
            RESULT_VARIABLE NANOBIND_CHECK_RESULT
    )

    if(NOT NANOBIND_CHECK_RESULT EQUAL 0 OR NOT NB_DIR)
        message(WARNING "nanobind not found for ${Python3_EXECUTABLE}.")
        message(WARNING "Please install build requirements with: pip install -r requirements-dev.txt")
    else()
        message(STATUS "Found NanoBind at ${NB_DIR}")
        list(APPEND CMAKE_PREFIX_PATH "${NB_DIR}")
    endif()
endif()


# Fetch the Public Suffix List: tries a fresh download first every
# configure (cached per build directory via a marker file - see
# cmake/PublicSuffixList.cmake for why), overwriting the one real,
# tracked copy at src/public_suffix_list.dat in place. On a failed
# download it just warns and keeps using that existing file. The URL
# itself lives in a single place: URLPARSER_PUBLIC_SUFFIX_LIST_URL in
# include/urlparser.h.
include(${PROJECT_SOURCE_DIR}/cmake/PublicSuffixList.cmake)
fetch_public_suffix_list(
    HEADER_FILE ${PROJECT_SOURCE_DIR}/include/urlparser.h
    VERSION_PREFIX URLPARSER_
    TARGET_FILE ${PROJECT_SOURCE_DIR}/src/public_suffix_list.dat
    OUTPUT_VAR PUBLIC_SUFFIX_LIST_DAT
    URL_OUTPUT_VAR PUBLIC_SUFFIX_LIST_URL
)


file(GLOB SOURCES ${PROJECT_SOURCE_DIR}/src/*.cpp)

# Embed the PSL data file into a C++ header at CONFIGURE time (so it exists
# right after `cmake -B build`, not only after the first build). This
# removes the need to ship/locate a .dat file at runtime entirely.
include(${PROJECT_SOURCE_DIR}/cmake/EmbedFile.cmake)
set(GENERATED_HEADERS_DIR "${CMAKE_CURRENT_BINARY_DIR}/generated")
set(PUBLIC_SUFFIX_LIST_DAT_H "${GENERATED_HEADERS_DIR}/public_suffix_list_dat.h")
embed_file_as_string(
        "${PUBLIC_SUFFIX_LIST_DAT}"
        "${PUBLIC_SUFFIX_LIST_DAT_H}"
        "templates"
        "public_suffix_list_dat"
)


# --- C++ core library (static) ---
# Built in every mode (including scikit-build), so the Python extension
# links the very same library that gets shipped to C++ consumers.
# Install layout: the wheel carries include/, lib/ and cmake/ inside the
# `liburlparser` package (nanobind/pybind11 pattern, see get_cmake_dir() in
# the Python package); a plain `cmake --install` uses the usual prefix layout.
if(DEFINED SKBUILD)
    set(_urlparser_lib_dest   liburlparser/lib)
    set(_urlparser_inc_dest   liburlparser/include)
    set(_urlparser_cmake_dest liburlparser/cmake)
else()
    set(_urlparser_lib_dest   lib)
    set(_urlparser_inc_dest   include)
    set(_urlparser_cmake_dest lib/cmake/liburlparser)
endif()

add_library(urlparser STATIC ${SOURCES})
# The public target is `urlparser::urlparser` - the same name whether the
# library comes from FetchContent/add_subdirectory (this alias) or from an
# installed package (find_package(liburlparser), exported below).
add_library(urlparser::urlparser ALIAS urlparser)

set_target_properties(urlparser PROPERTIES
    POSITION_INDEPENDENT_CODE ON        # linked into the Python extension
    CXX_VISIBILITY_PRESET hidden
    VISIBILITY_INLINES_HIDDEN ON
    EXPORT_NAME urlparser               # installed as urlparser::urlparser (see install(EXPORT))
    OUTPUT_NAME urlparser               # liburlparser.a / urlparser.lib
)
target_include_directories(urlparser
    PUBLIC
        $<BUILD_INTERFACE:${PROJECT_SOURCE_DIR}/include>
        $<INSTALL_INTERFACE:${_urlparser_inc_dest}>
    PRIVATE
        ${PROJECT_SOURCE_DIR}/src
        ${GENERATED_HEADERS_DIR})


if(DEFINED PYTHON_PROJECT_NAME)
    # Try to import all Python components potentially needed by nanobind
    #set(Python3_FIND_STRATEGY LOCATION)
    find_package(Python 3.10
            REQUIRED COMPONENTS Interpreter Development.Module
            OPTIONAL_COMPONENTS Development.SABIModule)
    # Import nanobind through CMake's find_package mechanism
    find_package(nanobind CONFIG REQUIRED)

    set(NB_MODULE _urlparser_py)

    # Linked STABLE_ABI needs Python >= 3.12 (nanobind's floor for the
    # in-process stable ABI). Below that, build a regular per-version
    # extension instead of reaching for split-mode (STABLE_ABI +
    # BACKEND_MODULE): split-mode ships no nanobind runtime in the wheel
    # itself and instead depends on a separate 'nanobind-backend' package
    # at import time - a package that, in practice, isn't reliably
    # available for every interpreter/platform combination our CI builds
    # (manylinux cp310 in particular), so installs can fail outright with
    # "No matching distribution found for nanobind-backend". Linking the
    # nanobind runtime straight into the extension avoids that dependency
    # entirely, at the cost of one wheel per Python minor version below 3.12.
    if(Python_VERSION VERSION_GREATER_EQUAL "3.12")
        set(NB_ABI_ARGS STABLE_ABI)
    else()
        set(NB_ABI_ARGS)
    endif()

    # We are now ready to compile the actual extension module
    nanobind_add_module(
            # Name of the extension
            ${NB_MODULE}

            ${NB_ABI_ARGS}

            # Source code goes here
            ${PROJECT_SOURCE_DIR}/src/bindings/python/binding_py.cpp
    )

    target_link_libraries(${NB_MODULE} PRIVATE urlparser)

    if(DEFINED SKBUILD)
        install(TARGETS ${NB_MODULE}
                CONFIGURATIONS Release
                LIBRARY DESTINATION
                ${PYTHON_PROJECT_NAME})
    else()
        file(COPY ${PROJECT_SOURCE_DIR}/src/bindings/python/${PROJECT_NAME} DESTINATION ${PYTHON_PROJECT_NAME})

        set_target_properties(${NB_MODULE} PROPERTIES LIBRARY_OUTPUT_DIRECTORY ${PYTHON_PROJECT_NAME}/${PROJECT_NAME})
    endif()
endif()

# --- install: find_package(liburlparser CONFIG) -> urlparser::urlparser ---
include(CMakePackageConfigHelpers)
install(TARGETS urlparser EXPORT liburlparserTargets
    ARCHIVE DESTINATION ${_urlparser_lib_dest}
    LIBRARY DESTINATION ${_urlparser_lib_dest})
install(DIRECTORY include/ DESTINATION ${_urlparser_inc_dest})
install(EXPORT liburlparserTargets
    FILE liburlparserTargets.cmake
    NAMESPACE urlparser::
    DESTINATION ${_urlparser_cmake_dest})

# urlparser_LIB_PATH / urlparser_INCLUDE_PATH for consumers that want plain
# paths. The exact artifact name (liburlparser.a, urlparser.lib, ...) is only
# known to the generator, so the file is produced with file(GENERATE) - the
# generator expression is resolved when the build system is generated, and the
# result is installed next to the package config. Paths are written relative
# to the installed file itself, so the package stays relocatable.
file(RELATIVE_PATH _urlparser_rel_lib "/_/${_urlparser_cmake_dest}" "/_/${_urlparser_lib_dest}")
file(RELATIVE_PATH _urlparser_rel_inc "/_/${_urlparser_cmake_dest}" "/_/${_urlparser_inc_dest}")
string(REGEX REPLACE "/+$" "" _urlparser_rel_lib "${_urlparser_rel_lib}")
string(REGEX REPLACE "/+$" "" _urlparser_rel_inc "${_urlparser_rel_inc}")
file(GENERATE
    OUTPUT ${PROJECT_BINARY_DIR}/liburlparserPaths.cmake
    CONTENT
"# Generated by liburlparser's CMakeLists.txt - do not edit.
get_filename_component(urlparser_LIB_PATH
    \"\${CMAKE_CURRENT_LIST_DIR}/${_urlparser_rel_lib}/$<TARGET_FILE_NAME:urlparser>\" ABSOLUTE)
get_filename_component(urlparser_INCLUDE_PATH
    \"\${CMAKE_CURRENT_LIST_DIR}/${_urlparser_rel_inc}\" ABSOLUTE)
")

write_basic_package_version_file(
    ${PROJECT_BINARY_DIR}/liburlparserConfigVersion.cmake
    VERSION ${PROJECT_VERSION}
    COMPATIBILITY SameMajorVersion)
install(FILES
    ${PROJECT_SOURCE_DIR}/cmake/liburlparserConfig.cmake
    ${PROJECT_BINARY_DIR}/liburlparserPaths.cmake
    ${PROJECT_BINARY_DIR}/liburlparserConfigVersion.cmake
    DESTINATION ${_urlparser_cmake_dest})

if(DEFINED SKBUILD)
    RETURN()
endif()

add_executable(example examples/main.cpp)
target_link_libraries(example PRIVATE urlparser::urlparser)

enable_testing()
find_package(Threads REQUIRED)

# Only build tests/docs when liburlparser is the top-level project. When
# it's pulled in as a dependency (e.g. FetchContent from benchmarks/ or a
# consumer's own build), building its entire test suite and documentation
# pipeline as a side effect is wasted work - and tests/docs assume they can
# write into this source tree (public_suffix_list.dat, docs/out, coverage),
# which isn't guaranteed when embedded read-only inside another build.
if(CMAKE_SOURCE_DIR STREQUAL CMAKE_CURRENT_SOURCE_DIR)
    add_subdirectory(${CMAKE_CURRENT_SOURCE_DIR}/tests)
    add_subdirectory(${CMAKE_CURRENT_SOURCE_DIR}/docs)
endif()



