cmake_minimum_required(VERSION 3.20)
project(mikrojs LANGUAGES C CXX)

set(CMAKE_C_STANDARD 11)
set(CMAKE_CXX_STANDARD 23)
set(CMAKE_C_STANDARD_REQUIRED ON)
set(CMAKE_CXX_STANDARD_REQUIRED ON)

# ── Resolve @mikrojs/quickjs via Node.js ─────────────────────────────
execute_process(
    COMMAND node -e "import('@mikrojs/quickjs').then(m=>process.stdout.write(m.cmakePath))"
    WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}
    OUTPUT_VARIABLE QUICKJS_CMAKE_PATH
    OUTPUT_STRIP_TRAILING_WHITESPACE
    RESULT_VARIABLE _QUICKJS_RESOLVE_RESULT
)
if(NOT _QUICKJS_RESOLVE_RESULT EQUAL 0)
    message(FATAL_ERROR "Failed to resolve @mikrojs/quickjs. Run pnpm install first.")
endif()

include("${QUICKJS_CMAKE_PATH}")

# qjsc is built by @mikrojs/quickjs postinstall during pnpm install
if(NOT EXISTS "${QJSC_EXECUTABLE}")
    message(FATAL_ERROR "qjsc not found at ${QJSC_EXECUTABLE}. Run 'pnpm install' first.")
endif()

# ── mikrojs static library ──────────────────────────────────────────
# Core source files shared between standalone lib and ESP-IDF builds.
# platform_posix.cpp is only used in the standalone build (ESP-IDF uses platform_esp32.cpp).
set(MIKROJS_CORE_SOURCES
    src/cutils_compat.c
    src/mikrojs.cpp
    src/modules.cpp
    src/builtins.cpp
    src/timers.cpp
    src/fs.cpp
    src/mem.cpp
    src/utils.cpp
    src/eval_bytecode.cpp
    src/mik_inspect.cpp
    src/mik_stdio.cpp
    src/mik_text_encoding.cpp
    src/mik_color.cpp
    src/mik_abort.cpp
    src/mik_cbor.cpp
    src/mik_result.cpp
    src/mik_console.cpp
    src/mik_sys.cpp
    src/mik_repl.cpp
    src/mik_app_config.cpp
    src/mik_udp.cpp
)

add_library(mikrojs STATIC
    ${MIKROJS_CORE_SOURCES}
    src/platform_posix.cpp
    deps/nanocbor/src/encoder.c
    deps/nanocbor/src/decoder.c
)
set_target_properties(mikrojs PROPERTIES POSITION_INDEPENDENT_CODE ON)

target_include_directories(mikrojs PUBLIC
    "${CMAKE_CURRENT_SOURCE_DIR}/include"
)
target_include_directories(mikrojs PRIVATE
    "${CMAKE_CURRENT_SOURCE_DIR}/deps/nanocbor/include"
)

# nanocbor defaults to <endian.h> (glibc). Apple SDKs and MSVC don't ship
# it, so route them to per-platform shims that map htobe*/be*toh onto
# whatever native intrinsics the toolchain provides.
if(APPLE)
    target_compile_definitions(mikrojs PRIVATE "NANOCBOR_BYTEORDER_HEADER=\"byteorder_apple.h\"")
elseif(WIN32)
    target_compile_definitions(mikrojs PRIVATE "NANOCBOR_BYTEORDER_HEADER=\"byteorder_windows.h\"")
endif()

# ── Generated bytecode headers ────────────────────────────────────
include(cmake/mikrojs_bytecode.cmake)
mikrojs_generate_bytecode(
    RUNTIME_DIR "${CMAKE_CURRENT_SOURCE_DIR}/runtime"
    MODULES cbor env result schema fs http/helpers http/request i2c kv/nvs kv/rtc kv/shared neopixel pin pwm reader sleep spi sntp stdio stream sys test uart udp wifi
    MODULE_PREFIX "mikrojs"
    SYMBOL_PREFIX "mikrojs"
    TARGET gen_bytecode
)
add_dependencies(mikrojs gen_bytecode)
target_include_directories(mikrojs PRIVATE "${gen_bytecode_INCLUDE_DIR}")

target_link_libraries(mikrojs PUBLIC quickjs)

# Note: self-registering native modules need force-include linker flags (-u symbol)
# to prevent stripping from static libraries. These are added by consumers:
# - ESP-IDF firmware: mikrojs_force_include_modules() in firmware CMakeLists
# - Host tests: target_link_options on mikrojs_tests below

# Suppress warnings from QuickJS headers in our code. MSVC uses /W3 by default
# and doesn't understand the -W flags. The dead-stripping flags below are
# similarly GCC/Clang-only; MSVC's linker dead-strips by default with /OPT:REF.
if(NOT MSVC)
    target_compile_options(mikrojs PRIVATE -Wall -Wextra -Wno-unused-parameter)
    # Emit per-function/data sections so the linker can dead-strip unreferenced
    # symbols at executable link time. This matches ESP-IDF's default toolchain
    # flags and lets us measure flash-size effects of source-level changes
    # (e.g. intrinsic stripping) on the host build.
    target_compile_options(mikrojs PRIVATE -ffunction-sections -fdata-sections)
endif()

# Version metadata for sys.version()
execute_process(
    COMMAND node -e "import('../../../package.json',{with:{type:'json'}}).then(m=>process.stdout.write(m.default.version))"
    WORKING_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}"
    OUTPUT_VARIABLE _MIK_VERSION
    OUTPUT_STRIP_TRAILING_WHITESPACE
    RESULT_VARIABLE _MIK_VERSION_RESULT
)
if(NOT _MIK_VERSION_RESULT EQUAL 0 OR _MIK_VERSION STREQUAL "")
    message(FATAL_ERROR "Failed to resolve package version for MIK_FW_VERSION. Check that package.json exists at the workspace root.")
endif()
target_compile_definitions(mikrojs PRIVATE "MIK_FW_VERSION=\"${_MIK_VERSION}\"")

# UTC build timestamp. `string(TIMESTAMP ... UTC)` honors SOURCE_DATE_EPOCH
# when set (for reproducible builds). Refreshes on re-configure only, not
# every build — coarse enough for sys.firmware.date, which is a sanity
# fence for clock-synced checks, not a precise build identifier.
string(TIMESTAMP _MIK_BUILD_DATE_UTC "%Y-%m-%dT%H:%M:%SZ" UTC)
target_compile_definitions(mikrojs PRIVATE "MIK_BUILD_DATE_UTC=\"${_MIK_BUILD_DATE_UTC}\"")

# ── Exported variables for ESP-IDF consumers ─────────────────────────
# ESP-IDF components can't use CMake targets, so export source/include paths.
# Only set PARENT_SCOPE when included as a subdirectory (avoids warnings in standalone builds).
if(NOT CMAKE_SOURCE_DIR STREQUAL CMAKE_CURRENT_SOURCE_DIR)
# Prefix each core source with the full path for PARENT_SCOPE export
set(MIKROJS_SOURCES "")
foreach(_src ${MIKROJS_CORE_SOURCES})
    list(APPEND MIKROJS_SOURCES "${CMAKE_CURRENT_SOURCE_DIR}/${_src}")
endforeach()
set(MIKROJS_SOURCES ${MIKROJS_SOURCES} PARENT_SCOPE)
set(MIKROJS_NANOCBOR_DIR "${CMAKE_CURRENT_SOURCE_DIR}/deps/nanocbor" PARENT_SCOPE)
set(MIKROJS_INCLUDE_DIR "${CMAKE_CURRENT_SOURCE_DIR}/include" PARENT_SCOPE)
set(MIKROJS_SCRIPTS_DIR "${CMAKE_CURRENT_SOURCE_DIR}/scripts" PARENT_SCOPE)
set(MIKROJS_RUNTIME_DIR "${CMAKE_CURRENT_SOURCE_DIR}/runtime" PARENT_SCOPE)
endif()

# ── Tests ────────────────────────────────────────────────────────────
if(BUILD_TESTING)
    enable_testing()

    add_executable(mikrojs_tests
        test/main.cpp
        test/runtime_test.cpp
        test/modules_test.cpp
        test/text_encoding_test.cpp
        test/cbor_test.cpp
        test/abort_test.cpp
        test/repl_protocol_test.cpp
        test/app_config_test.cpp
        test/oom_test.cpp
        test/reader_test.cpp
        test/stream_test.cpp
        test/runtime_recycle_test.cpp
        test/udp_test.cpp
    )

    target_link_libraries(mikrojs_tests PRIVATE mikrojs)
    target_include_directories(mikrojs_tests PRIVATE
        "${CMAKE_CURRENT_SOURCE_DIR}/test"
        "${CMAKE_CURRENT_SOURCE_DIR}/deps/nanocbor/include"
    )

    include(CTest)
    add_test(NAME mikrojs_tests COMMAND mikrojs_tests)

    # native_stubs registers no-op `native:*` modules so the bench can load
    # ESP-only builtins (wifi, fetch, kv, …) on the host. Compiled into the
    # executable directly so its self-registering constructors aren't
    # dead-stripped (no -u flags needed).
    add_executable(memory_bench test/memory_bench.cpp test/native_stubs.cpp)
    target_link_libraries(memory_bench PRIVATE mikrojs)
    # Run the bench as a smoke test so module-load regressions (e.g. a
    # builtin that imports an unregistered native module on host) surface
    # in `pnpm test:lib` / `pnpm check` rather than only in the CI
    # memory-bench workflow. Output is discarded; we only assert it exits 0.
    add_test(NAME memory_bench COMMAND memory_bench)
    set_tests_properties(memory_bench PROPERTIES
        WORKING_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}")

    add_executable(bundle_compare test/bundle_compare.cpp)
    target_link_libraries(bundle_compare PRIVATE mikrojs)
    # Dead-strip unreferenced code at link time. Used for measuring the
    # binary-size impact of source-level changes (intrinsic drops, etc.).
    # Applied only to memory_bench to avoid interfering with doctest's
    # static-initializer registration in mikrojs_tests.
    if(APPLE)
        target_link_options(memory_bench PRIVATE -Wl,-dead_strip)
    else()
        target_link_options(memory_bench PRIVATE -Wl,--gc-sections)
    endif()
endif()
