cmake_minimum_required(VERSION 3.25)

project(MetasequoiaImeEngine VERSION 0.9.0 LANGUAGES CXX) # x-release-please-version

include(CTest)

if(POLICY CMP0167)
    cmake_policy(SET CMP0167 NEW)
endif()

set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_CXX_EXTENSIONS OFF)

find_package(Boost REQUIRED)
find_package(fmt CONFIG REQUIRED)
find_package(spdlog CONFIG REQUIRED)
find_package(SQLite3 REQUIRED)
find_package(Threads REQUIRED)

if(TARGET SQLite3::SQLite3)
    set(METASEQUOIA_IME_SQLITE_TARGET SQLite3::SQLite3)
else()
    set(METASEQUOIA_IME_SQLITE_TARGET SQLite::SQLite3)
endif()

get_target_property(METASEQUOIA_IME_SPDLOG_INCLUDE_DIRS spdlog::spdlog_header_only INTERFACE_INCLUDE_DIRECTORIES)

set(METASEQUOIA_IME_ENGINE_SOURCES
    common/helpcode_utils.cpp
    common/string_utils.cpp
    core/session.cpp
    core/candidate_queries.cpp
    core/punctuation_policy.cpp
    core/runtime_paths.cpp
    core/pinyin_decoder.cpp
    core/ime_session.cpp
    core/input_session.cpp
    core/input_session_composition.cpp
    core/input_session_editing.cpp
    core/input_session_candidates.cpp
    direct_helpcode/direct_lattice.cpp
    direct_helpcode/direct_resolver.cpp
    direct_helpcode/spelling_graph.cpp
    english/english_dictionary.cpp
    japanese/japanese_matrix_search.cpp
    japanese/japanese_sentence_decoder.cpp
    japanese/romaji_converter.cpp
    local_modes/date_time_query.cpp
    local_modes/emoji_query.cpp
    local_modes/jianpin_query.cpp
    local_modes/kaomoji_query.cpp
    local_modes/local_database.cpp
    local_modes/quick_phrase_query.cpp
    local_modes/radical_query.cpp
    local_modes/symbol_query.cpp
    local_modes/v_mode_query.cpp
    neural/sentence_model.cpp
    neural/neural_decoder.cpp
    neural/rescore_worker.cpp
    providers/japanese_candidate_provider.cpp
    providers/pinyin_candidate_provider.cpp
    providers/provider_registry.cpp
    providers/wubi_candidate_provider.cpp
    quanpin/engine.cpp
    quanpin/quanpin_dictionary.cpp
    quanpin/quanpin_query.cpp
    quanpin/quanpin_utils.cpp
    quanpin/lattice_rerank.cpp
    quanpin/word_lattice.cpp
    schemes/japanese_romaji_scheme.cpp
    schemes/quanpin_scheme.cpp
    schemes/shuangpin_scheme.cpp
    schemes/wubi_scheme.cpp
    shuangpin/engine.cpp
    shuangpin/shuangpin_custom_profile.cpp
    shuangpin/shuangpin_dictionary.cpp
    shuangpin/shuangpin_profile.cpp
    shuangpin/shuangpin_query.cpp
    shuangpin/shuangpin_utils.cpp
    user_dictionary/user_dictionary_journal.cpp
    user_dictionary/personal_dictionary.cpp
    user_dictionary/dictionary_state.cpp
    googlepinyinime-rev/src/share/dictbuilder.cpp
    googlepinyinime-rev/src/share/dictlist.cpp
    googlepinyinime-rev/src/share/dicttrie.cpp
    googlepinyinime-rev/src/share/lpicache.cpp
    googlepinyinime-rev/src/share/matrixsearch.cpp
    googlepinyinime-rev/src/share/mystdlib.cpp
    googlepinyinime-rev/src/share/ngram.cpp
    googlepinyinime-rev/src/share/pinyinime.cpp
    googlepinyinime-rev/src/share/searchutility.cpp
    googlepinyinime-rev/src/share/spellingtable.cpp
    googlepinyinime-rev/src/share/spellingtrie.cpp
    googlepinyinime-rev/src/share/splparser.cpp
    googlepinyinime-rev/src/share/sync.cpp
    googlepinyinime-rev/src/share/userdict.cpp
    googlepinyinime-rev/src/share/utf16char.cpp
    googlepinyinime-rev/src/share/utf16reader.cpp
)

string(TOLOWER "${CMAKE_SYSTEM_PROCESSOR}" MSIME_NEURAL_PROCESSOR)
if(MSIME_NEURAL_PROCESSOR MATCHES "^(amd64|x86_64|x86|i[3-6]86)$")
    if(MSVC)
        list(APPEND METASEQUOIA_IME_ENGINE_SOURCES neural/int8_linear_avx2.cpp)
        set_property(SOURCE neural/int8_linear_avx2.cpp APPEND PROPERTY COMPILE_OPTIONS /arch:AVX2)
        set(MSIME_NEURAL_AVX2 1)
    elseif(CMAKE_CXX_COMPILER_ID MATCHES "^(GNU|Clang|AppleClang)$")
        list(APPEND METASEQUOIA_IME_ENGINE_SOURCES neural/int8_linear_avx2.cpp)
        set_property(SOURCE neural/int8_linear_avx2.cpp APPEND PROPERTY COMPILE_OPTIONS -mavx2 -mfma)
        set(MSIME_NEURAL_AVX2 1)
    endif()
endif()

# The vendored decoder accepts UTF-8 paths. Adapt its namespace-local file IO to native
# Windows paths without editing googlepinyinime-rev or changing the process locale.
if(WIN32)
    if(MSVC)
        set(METASEQUOIA_PINYIN_FILE_IO_OPTIONS "/FI${CMAKE_CURRENT_SOURCE_DIR}/core/pinyin_file_io.h")
    else()
        set(METASEQUOIA_PINYIN_FILE_IO_OPTIONS -include "${CMAKE_CURRENT_SOURCE_DIR}/core/pinyin_file_io.h")
    endif()
    set_property(SOURCE googlepinyinime-rev/src/share/dicttrie.cpp googlepinyinime-rev/src/share/userdict.cpp
        APPEND PROPERTY COMPILE_OPTIONS ${METASEQUOIA_PINYIN_FILE_IO_OPTIONS})
endif()

add_subdirectory(ngram)

add_library(MetasequoiaImeEngine STATIC ${METASEQUOIA_IME_ENGINE_SOURCES})
add_library(MetasequoiaIme::Engine ALIAS MetasequoiaImeEngine)

target_include_directories(MetasequoiaImeEngine
    PUBLIC
        ${CMAKE_CURRENT_SOURCE_DIR}
        ${CMAKE_CURRENT_SOURCE_DIR}/include
    PRIVATE
        ${CMAKE_CURRENT_SOURCE_DIR}/utfcpp/source
    SYSTEM PRIVATE
        ${METASEQUOIA_IME_SPDLOG_INCLUDE_DIRS}
)

target_compile_definitions(MetasequoiaImeEngine PRIVATE FMT_HEADER_ONLY=1 SPDLOG_FMT_EXTERNAL)
if(MSIME_NEURAL_AVX2)
    target_compile_definitions(MetasequoiaImeEngine PRIVATE MSIME_NEURAL_AVX2=1)
endif()

target_link_libraries(MetasequoiaImeEngine
    PUBLIC
        Boost::headers
        fmt::fmt-header-only
        ${METASEQUOIA_IME_SQLITE_TARGET}
        Threads::Threads
        metasequoia_ime_ngram
)

if(WIN32)
    target_link_libraries(MetasequoiaImeEngine PUBLIC ole32 shell32)
endif()

if(MSVC)
    # NOMINMAX and the UNICODE pair stay PUBLIC: they change what the Windows headers declare and
    # what the TCHAR-flavoured entry points resolve to, so a consumer that disagrees with the engine
    # would be compiling against a different API surface. WIN32_LEAN_AND_MEAN is only an
    # include-time trim of Windows.h, and forcing it on consumers breaks any of them that go on to
    # include a header needing the fuller surface - COM-based SDK headers, for instance, rely on the
    # `interface` macro that combaseapi.h provides.
    target_compile_definitions(MetasequoiaImeEngine PUBLIC NOMINMAX UNICODE _UNICODE)
    target_compile_definitions(MetasequoiaImeEngine PRIVATE WIN32_LEAN_AND_MEAN)
    target_compile_options(MetasequoiaImeEngine PRIVATE /W4 /permissive- /utf-8 /Zc:__cplusplus)
else()
    target_compile_options(MetasequoiaImeEngine PRIVATE -Wall -Wextra -Wpedantic)
endif()

if(BUILD_TESTING)
    add_executable(test_segmentation_contract tests/src/test_segmentation_contract.cpp)
    target_link_libraries(test_segmentation_contract PRIVATE MetasequoiaIme::Engine)
    add_test(NAME segmentation_contract COMMAND test_segmentation_contract)

    # 候选结果的对照测试（R6 护栏）：同一（输入序列 + 前文 + 配置）在新建会话 / 重打 /
    # 清缓存 / 插过别的输入之后必须给出逐字相同的候选集合、顺序与预编辑。词库由用例自建，
    # 不依赖 product-lock 或本机安装的数据目录，也不依赖 sc.lm / .gram 是否存在。
    add_executable(test_candidate_consistency tests/src/test_candidate_consistency.cpp)
    if(MSVC)
        target_compile_options(test_candidate_consistency PRIVATE /utf-8)
    endif()
    target_link_libraries(test_candidate_consistency PRIVATE MetasequoiaIme::Engine)
    add_test(NAME candidate_consistency COMMAND test_candidate_consistency)

    add_executable(test_windows_ipc_contract contracts/tests/windows_ipc_contract.cpp)
    if(MSVC)
        target_compile_options(test_windows_ipc_contract PRIVATE /utf-8)
    endif()
    add_test(NAME windows_ipc_contract COMMAND test_windows_ipc_contract)

    add_executable(test_punctuation_contract contracts/tests/punctuation_contract.cpp)
    if(MSVC)
        target_compile_options(test_punctuation_contract PRIVATE /utf-8)
    endif()
    add_test(NAME punctuation_contract COMMAND test_punctuation_contract)

    add_executable(test_japanese_romaji tests/src/test_japanese_romaji.cpp)
    if(MSVC)
        target_compile_options(test_japanese_romaji PRIVATE /utf-8)
    endif()
    target_link_libraries(test_japanese_romaji PRIVATE MetasequoiaIme::Engine)
    add_test(NAME japanese_romaji COMMAND test_japanese_romaji)

    add_executable(test_platform_api tests/src/test_platform_api.cpp tests/src/test_public_session.cpp)
    target_link_libraries(test_platform_api PRIVATE MetasequoiaIme::Engine)
    add_test(NAME platform_api COMMAND test_platform_api)

    add_executable(test_data_path tests/src/test_data_path.cpp)
    target_link_libraries(test_data_path PRIVATE MetasequoiaIme::Engine)
    add_test(NAME data_path COMMAND test_data_path)

    add_executable(test_runtime_isolation tests/src/test_runtime_isolation.cpp)
    target_compile_definitions(test_runtime_isolation PRIVATE METASEQUOIA_TEST_PINYIN_MODEL="${CMAKE_CURRENT_SOURCE_DIR}/googlepinyinime-rev/data/dict_pinyin.dat")
    target_link_libraries(test_runtime_isolation PRIVATE MetasequoiaIme::Engine)
    add_test(NAME runtime_isolation COMMAND test_runtime_isolation)

    add_executable(test_dictionary_state tests/src/test_dictionary_state.cpp)
    target_link_libraries(test_dictionary_state PRIVATE MetasequoiaImeEngine)
    add_test(NAME dictionary_state COMMAND test_dictionary_state)
    add_test(NAME dictionary_state_capacity COMMAND test_dictionary_state --capacity)

    add_executable(test_personal_dictionary tests/src/test_personal_dictionary.cpp)
    target_link_libraries(test_personal_dictionary PRIVATE MetasequoiaImeEngine)
    add_test(NAME personal_dictionary COMMAND test_personal_dictionary)

    add_executable(test_candidate_removal tests/src/test_candidate_removal.cpp)
    target_link_libraries(test_candidate_removal PRIVATE MetasequoiaIme::Engine)
    add_test(NAME candidate_removal COMMAND test_candidate_removal)

    # These two are the disk-bound outliers. Both spend their time building, staging and tearing
    # down dictionaries on disk rather than in the decoder, and Windows runs them 15-25x slower than
    # the other platforms: runtime_isolation takes 10.6s there against 0.70s on macOS, and
    # candidate_removal 7.6s against 0.30s. That leaves them alone in having single-digit headroom
    # under the Windows job's 60s default, and a runner that lost disk throughput has already pushed
    # runtime_isolation past it while the CPU-bound tests barely moved. A timeout is here to catch a
    # hang, which is unbounded, so it is set against a hang rather than against the runner's best
    # day; the other twenty tests keep the strict default.
    set_tests_properties(runtime_isolation candidate_removal PROPERTIES TIMEOUT 300)

    add_executable(test_fuzzy_pinyin tests/src/test_fuzzy_pinyin.cpp)
    target_link_libraries(test_fuzzy_pinyin PRIVATE MetasequoiaIme::Engine)
    add_test(NAME fuzzy_pinyin COMMAND test_fuzzy_pinyin)

    add_executable(test_engine_smoke tests/src/test_engine_smoke.cpp)
    target_link_libraries(test_engine_smoke PRIVATE MetasequoiaIme::Engine)
    add_test(NAME engine_smoke COMMAND test_engine_smoke)

    # 神经整句排序集成测试：需数据目录里备好 sentence-model*.safetensors，缺文件时优雅跳过。
    add_executable(test_neural_sentence tests/src/test_neural_sentence.cpp)
    target_link_libraries(test_neural_sentence PRIVATE MetasequoiaIme::Engine)
    add_test(NAME neural_sentence COMMAND test_neural_sentence)

    if(MSIME_NEURAL_AVX2)
        add_executable(test_neural_int8_linear tests/src/test_neural_int8_linear.cpp)
        target_link_libraries(test_neural_int8_linear PRIVATE MetasequoiaIme::Engine)
        add_test(NAME neural_int8_linear COMMAND test_neural_int8_linear)
    endif()

    add_executable(test_input_session tests/src/test_input_session.cpp)
    target_compile_definitions(test_input_session PRIVATE METASEQUOIA_SKIP_FREQUENCY_TESTS)
    target_link_libraries(test_input_session PRIVATE MetasequoiaIme::Engine)
    add_test(NAME input_session COMMAND test_input_session)

    add_executable(test_frequency_input_session tests/src/test_input_session.cpp)
    target_compile_definitions(test_frequency_input_session PRIVATE METASEQUOIA_FREQUENCY_TESTS_ONLY)
    target_link_libraries(test_frequency_input_session PRIVATE MetasequoiaIme::Engine)
    add_test(NAME frequency_input_session COMMAND test_frequency_input_session)

    add_executable(test_direct_helpcode tests/src/test_direct_helpcode.cpp)
    target_link_libraries(test_direct_helpcode PRIVATE MetasequoiaIme::Engine)
    add_test(NAME direct_helpcode COMMAND test_direct_helpcode)

    add_executable(test_english_input_session tests/src/test_english_input_session.cpp)
    target_link_libraries(test_english_input_session PRIVATE MetasequoiaIme::Engine)
    add_test(NAME english_input_session COMMAND test_english_input_session)

    add_executable(test_quick_phrase_input_session tests/src/test_quick_phrase_input_session.cpp)
    target_link_libraries(test_quick_phrase_input_session PRIVATE MetasequoiaIme::Engine)
    add_test(NAME quick_phrase_input_session COMMAND test_quick_phrase_input_session)

    add_executable(test_mixed_expressive_input_session tests/src/test_mixed_expressive_input_session.cpp)
    target_link_libraries(test_mixed_expressive_input_session PRIVATE MetasequoiaIme::Engine)
    add_test(NAME mixed_expressive_input_session COMMAND test_mixed_expressive_input_session)

    add_executable(test_wubi_mixed_input_session tests/src/test_wubi_mixed_input_session.cpp)
    target_link_libraries(test_wubi_mixed_input_session PRIVATE MetasequoiaIme::Engine)
    add_test(NAME wubi_mixed_input_session COMMAND test_wubi_mixed_input_session)

    add_executable(test_jianpin_input_session tests/src/test_jianpin_input_session.cpp)
    target_link_libraries(test_jianpin_input_session PRIVATE MetasequoiaIme::Engine)
    add_test(NAME jianpin_input_session COMMAND test_jianpin_input_session)

    add_executable(test_temporary_input_session tests/src/test_temporary_input_session.cpp)
    target_link_libraries(test_temporary_input_session PRIVATE MetasequoiaIme::Engine)
    add_test(NAME temporary_input_session COMMAND test_temporary_input_session)

    add_executable(test_japanese_candidates tests/src/test_japanese_candidates.cpp)
    if(MSVC)
        target_compile_options(test_japanese_candidates PRIVATE /utf-8)
    endif()
    target_link_libraries(test_japanese_candidates PRIVATE MetasequoiaIme::Engine)
    add_test(NAME japanese_candidates COMMAND test_japanese_candidates)

    add_executable(test_local_modes tests/src/test_local_modes.cpp)
    target_link_libraries(test_local_modes PRIVATE MetasequoiaIme::Engine)
    add_test(NAME local_modes COMMAND test_local_modes)

    # Per-keystroke latency against the real dictionaries; run by hand, not by ctest.
    add_executable(bench_input_session tests/src/bench_input_session.cpp)
    target_link_libraries(bench_input_session PRIVATE MetasequoiaIme::Engine)

    # Per-key latency with CPU / page-fault / disk-read attribution and the pressure modes that
    # reproduce a paging spike; run by hand, not by ctest. Win32-only: it reads the process's
    # page-fault and IO counters. The block above is otherwise portable, so it stays guarded.
    if(WIN32)
        add_executable(bench_key_latency_diag tests/src/bench_key_latency_diag.cpp)
        target_link_libraries(bench_key_latency_diag PRIVATE MetasequoiaIme::Engine)
    endif()

    # Sentence-level segmentation evaluation (tests/data/quanpin_sentence_cases.tsv); run by hand, not by ctest.
    add_executable(eval_quanpin_sentence tests/src/eval_quanpin_sentence.cpp)
    target_link_libraries(eval_quanpin_sentence PRIVATE MetasequoiaIme::Engine)

    # Run this benchmark by hand with the released dictionaries and neural models.
    add_executable(bench_neural_rescore tests/src/bench_neural_rescore.cpp)
    target_link_libraries(bench_neural_rescore PRIVATE MetasequoiaIme::Engine)

    add_executable(test_online_input_session tests/src/test_online_input_session.cpp)
    target_link_libraries(test_online_input_session PRIVATE MetasequoiaIme::Engine)
    add_test(NAME online_input_session COMMAND test_online_input_session)
endif()

option(METASEQUOIA_IME_BUILD_VOICE "Build shared voice input capabilities" OFF)
if(METASEQUOIA_IME_BUILD_VOICE)
    add_subdirectory(voice)
endif()
