cmake_minimum_required(VERSION 3.16)

set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED ON)

if(APPLE)
  option(BUILD_UNIVERSAL "Build a Universal binary on macOS" OFF)
  if(BUILD_UNIVERSAL)
    # Build a Universal binary on macOS
    # This requires that the found Qt library is compiled as Universal binaries.
    set(CMAKE_OSX_ARCHITECTURES "arm64;x86_64" CACHE STRING "" FORCE)
  else()
    # Build for the host architecture on macOS
    set(CMAKE_OSX_ARCHITECTURES "${CMAKE_HOST_SYSTEM_PROCESSOR}" CACHE STRING "" FORCE)
  endif()
endif()

set(APP_VERSION_MAJOR 3)
set(APP_VERSION_MINOR 2)
set(APP_VERSION_PATCH 1)
set(APP_VERSION_BASE "${APP_VERSION_MAJOR}.${APP_VERSION_MINOR}.${APP_VERSION_PATCH}")
set(APP_VERSION "${APP_VERSION_BASE}")

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

# Include the binary directory for the generated header file
include_directories("${CMAKE_CURRENT_BINARY_DIR}")

project(gpt4all VERSION ${APP_VERSION_BASE} LANGUAGES CXX C)

set(CMAKE_AUTOMOC ON)
set(CMAKE_AUTORCC ON)

option(GPT4ALL_LOCALHOST "Build installer for localhost repo" OFF)
option(GPT4ALL_OFFLINE_INSTALLER "Build an offline installer" OFF)
option(GPT4ALL_SIGN_INSTALL "Sign installed binaries and installers (requires signing identities)" OFF)

# Generate a header file with the version number
configure_file(
  "${CMAKE_CURRENT_SOURCE_DIR}/cmake/config.h.in"
  "${CMAKE_CURRENT_BINARY_DIR}/config.h"
)

if(LINUX)
  find_package(Qt6 6.4 COMPONENTS Core Quick WaylandCompositor QuickDialogs2 Svg HttpServer Sql Pdf LinguistTools REQUIRED)
else()
  find_package(Qt6 6.4 COMPONENTS Core Quick QuickDialogs2 Svg HttpServer Sql Pdf LinguistTools REQUIRED)
endif()

# Get the Qt6Core target properties
get_target_property(Qt6Core_INCLUDE_DIRS Qt6::Core INTERFACE_INCLUDE_DIRECTORIES)
get_target_property(Qt6Core_LIBRARY_RELEASE Qt6::Core LOCATION_RELEASE)

# Find the qmake binary
find_program(QMAKE_EXECUTABLE NAMES qmake qmake6 PATHS ${Qt6Core_INCLUDE_DIRS}/../.. NO_DEFAULT_PATH)

# Get the Qt 6 root directory
get_filename_component(Qt6_ROOT_DIR "${Qt6Core_LIBRARY_RELEASE}" DIRECTORY)
get_filename_component(Qt6_ROOT_DIR "${Qt6_ROOT_DIR}/.." ABSOLUTE)

message(STATUS "qmake binary: ${QMAKE_EXECUTABLE}")
message(STATUS "Qt 6 root directory: ${Qt6_ROOT_DIR}")

set (CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin)

add_subdirectory(../gpt4all-backend llmodel)

set(CHAT_EXE_RESOURCES)

# Metal shader library
if (APPLE)
    list(APPEND CHAT_EXE_RESOURCES "${CMAKE_RUNTIME_OUTPUT_DIRECTORY}/default.metallib")
endif()

# App icon
if (WIN32)
    list(APPEND CHAT_EXE_RESOURCES "${CMAKE_CURRENT_SOURCE_DIR}/resources/gpt4all.rc")
elseif (APPLE)
    # The MACOSX_BUNDLE_ICON_FILE variable is added to the Info.plist
    # generated by CMake. This variable contains the .icns file name,
    # without the path.
    set(MACOSX_BUNDLE_ICON_FILE gpt4all.icns)

    # And the following tells CMake where to find and install the file itself.
    set(APP_ICON_RESOURCE "${CMAKE_CURRENT_SOURCE_DIR}/resources/gpt4all.icns")
    set_source_files_properties(${APP_ICON_RESOURCE} PROPERTIES
        MACOSX_PACKAGE_LOCATION "Resources")
    list(APPEND CHAT_EXE_RESOURCES "${APP_ICON_RESOURCE}")
endif()

# Embedding model
set(LOCAL_EMBEDDING_MODEL "nomic-embed-text-v1.5.f16.gguf")
set(LOCAL_EMBEDDING_MODEL_MD5 "a5401e7f7e46ed9fcaed5b60a281d547")
set(LOCAL_EMBEDDING_MODEL_PATH "${CMAKE_BINARY_DIR}/resources/${LOCAL_EMBEDDING_MODEL}")
set(LOCAL_EMBEDDING_MODEL_URL "https://gpt4all.io/models/gguf/${LOCAL_EMBEDDING_MODEL}")
message(STATUS "Downloading embedding model from ${LOCAL_EMBEDDING_MODEL_URL} ...")
file(DOWNLOAD
    "${LOCAL_EMBEDDING_MODEL_URL}"
    "${LOCAL_EMBEDDING_MODEL_PATH}"
    EXPECTED_HASH "MD5=${LOCAL_EMBEDDING_MODEL_MD5}"
)
message(STATUS "Embedding model downloaded to ${LOCAL_EMBEDDING_MODEL_PATH}")
if (APPLE)
    list(APPEND CHAT_EXE_RESOURCES "${LOCAL_EMBEDDING_MODEL_PATH}")
endif()

qt_add_executable(chat
    main.cpp
    chat.h chat.cpp
    chatllm.h chatllm.cpp
    chatmodel.h chatlistmodel.h chatlistmodel.cpp
    chatapi.h chatapi.cpp
    chatviewtextprocessor.h chatviewtextprocessor.cpp
    database.h database.cpp
    download.h download.cpp
    embllm.cpp embllm.h
    localdocs.h localdocs.cpp localdocsmodel.h localdocsmodel.cpp
    llm.h llm.cpp
    modellist.h modellist.cpp
    mysettings.h mysettings.cpp
    network.h network.cpp
    server.h server.cpp
    logger.h logger.cpp
    ${APP_ICON_RESOURCE}
    ${CHAT_EXE_RESOURCES}
)

qt_add_qml_module(chat
    URI gpt4all
    VERSION 1.0
    NO_CACHEGEN
    QML_FILES
      main.qml
      qml/AddCollectionView.qml
      qml/AddModelView.qml
      qml/ApplicationSettings.qml
      qml/ChatDrawer.qml
      qml/ChatView.qml
      qml/CollectionsDrawer.qml
      qml/HomeView.qml
      qml/LocalDocsSettings.qml
      qml/LocalDocsView.qml
      qml/ModelSettings.qml
      qml/ModelsView.qml
      qml/NetworkDialog.qml
      qml/NewVersionDialog.qml
      qml/PopupDialog.qml
      qml/SettingsView.qml
      qml/StartupDialog.qml
      qml/SwitchModelDialog.qml
      qml/Theme.qml
      qml/ThumbsDownDialog.qml
      qml/Toast.qml
      qml/ToastManager.qml
      qml/MyBusyIndicator.qml
      qml/MyButton.qml
      qml/MyCheckBox.qml
      qml/MyComboBox.qml
      qml/MyDialog.qml
      qml/MyDirectoryField.qml
      qml/MyFancyLink.qml
      qml/MyMenu.qml
      qml/MyMenuItem.qml
      qml/MyMiniButton.qml
      qml/MySettingsButton.qml
      qml/MySettingsDestructiveButton.qml
      qml/MySettingsLabel.qml
      qml/MySettingsStack.qml
      qml/MySettingsTab.qml
      qml/MySlug.qml
      qml/MyTextArea.qml
      qml/MyTextButton.qml
      qml/MyTextField.qml
      qml/MyToolButton.qml
      qml/MyWelcomeButton.qml
    RESOURCES
      icons/antenna_1.svg
      icons/antenna_2.svg
      icons/antenna_3.svg
      icons/caret_down.svg
      icons/caret_right.svg
      icons/changelog.svg
      icons/chat.svg
      icons/check.svg
      icons/close.svg
      icons/copy.svg
      icons/db.svg
      icons/discord.svg
      icons/download.svg
      icons/edit.svg
      icons/eject.svg
      icons/email.svg
      icons/file-md.svg
      icons/file-pdf.svg
      icons/file-txt.svg
      icons/file.svg
      icons/github.svg
      icons/globe.svg
      icons/gpt4all-32.png
      icons/gpt4all-48.png
      icons/gpt4all.svg
      icons/gpt4all_transparent.svg
      icons/home.svg
      icons/image.svg
      icons/info.svg
      icons/left_panel_closed.svg
      icons/left_panel_open.svg
      icons/local-docs.svg
      icons/models.svg
      icons/network.svg
      icons/nomic_logo.svg
      icons/notes.svg
      icons/plus.svg
      icons/recycle.svg
      icons/regenerate.svg
      icons/search.svg
      icons/send_message.svg
      icons/settings.svg
      icons/stack.svg
      icons/stop_generating.svg
      icons/thumbs_down.svg
      icons/thumbs_up.svg
      icons/trash.svg
      icons/twitter.svg
      icons/up_down.svg
      icons/you.svg
)

qt_add_translations(chat
    TS_FILES
    ${CMAKE_SOURCE_DIR}/translations/gpt4all_en_US.ts
    ${CMAKE_SOURCE_DIR}/translations/gpt4all_es_MX.ts
    ${CMAKE_SOURCE_DIR}/translations/gpt4all_zh_CN.ts
    ${CMAKE_SOURCE_DIR}/translations/gpt4all_zh_TW.ts
    ${CMAKE_SOURCE_DIR}/translations/gpt4all_ro_RO.ts
    ${CMAKE_SOURCE_DIR}/translations/gpt4all_it_IT.ts
    ${CMAKE_SOURCE_DIR}/translations/gpt4all_pt_BR.ts
)

set_target_properties(chat PROPERTIES
    WIN32_EXECUTABLE TRUE
)

macro(REPORT_MISSING_SIGNING_CONTEXT)
    message(FATAL_ERROR [=[
        Signing requested but no identity configured.
        Please set the correct env variable or provide the MAC_SIGNING_IDENTITY argument on the command line
        ]=])
endmacro()

if (APPLE)
    set_target_properties(chat PROPERTIES
        MACOSX_BUNDLE TRUE
        MACOSX_BUNDLE_GUI_IDENTIFIER gpt4all
        MACOSX_BUNDLE_BUNDLE_VERSION ${PROJECT_VERSION}
        MACOSX_BUNDLE_SHORT_VERSION_STRING ${PROJECT_VERSION_MAJOR}.${PROJECT_VERSION_MINOR}
        RESOURCE "${CHAT_EXE_RESOURCES}"
        OUTPUT_NAME gpt4all
    )
    add_dependencies(chat ggml-metal)

    if(NOT MAC_SIGNING_IDENTITY)
        if(NOT DEFINED ENV{MAC_SIGNING_CERT_NAME} AND GPT4ALL_SIGN_INSTALL)
            REPORT_MISSING_SIGNING_CONTEXT()
        endif()
        set(MAC_SIGNING_IDENTITY $ENV{MAC_SIGNING_CERT_NAME})
    endif()
    if(NOT MAC_SIGNING_TID)
        if(NOT DEFINED ENV{MAC_NOTARIZATION_TID} AND GPT4ALL_SIGN_INSTALL)
            REPORT_MISSING_SIGNING_CONTEXT()
        endif()
        set(MAC_SIGNING_TID $ENV{MAC_NOTARIZATION_TID})
    endif()

    # Setup MacOS signing for individual binaries
    set_target_properties(chat PROPERTIES
        XCODE_ATTRIBUTE_CODE_SIGN_STYLE "Manual"
        XCODE_ATTRIBUTE_DEVELOPMENT_TEAM ${MAC_SIGNING_TID}
        XCODE_ATTRIBUTE_CODE_SIGN_IDENTITY ${MAC_SIGNING_IDENTITY}
        XCODE_ATTRIBUTE_CODE_SIGNING_REQUIRED True
        XCODE_ATTRIBUTE_OTHER_CODE_SIGN_FLAGS "--timestamp=http://timestamp.apple.com/ts01  --options=runtime,library"
    )
endif()

target_compile_definitions(chat
    PRIVATE $<$<OR:$<CONFIG:Debug>,$<CONFIG:RelWithDebInfo>>:QT_QML_DEBUG>)

# usearch uses the identifier 'slots' which conflicts with Qt's 'slots' keyword
target_compile_definitions(chat PRIVATE QT_NO_SIGNALS_SLOTS_KEYWORDS)

target_include_directories(chat PRIVATE usearch/include
                                        usearch/fp16/include)

if(LINUX)
  target_link_libraries(chat
      PRIVATE Qt6::Quick Qt6::Svg Qt6::HttpServer Qt6::Sql Qt6::Pdf Qt6::WaylandCompositor)
else()
  target_link_libraries(chat
    PRIVATE Qt6::Quick Qt6::Svg Qt6::HttpServer Qt6::Sql Qt6::Pdf)
endif()
target_link_libraries(chat
    PRIVATE llmodel)


# -- install --

set(COMPONENT_NAME_MAIN ${PROJECT_NAME})

if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT)
  set(CMAKE_INSTALL_PREFIX ${CMAKE_BINARY_DIR}/install CACHE PATH "..." FORCE)
endif()

install(TARGETS chat DESTINATION bin COMPONENT ${COMPONENT_NAME_MAIN})

install(
    TARGETS llmodel
    LIBRARY DESTINATION lib COMPONENT ${COMPONENT_NAME_MAIN}  # .so/.dylib
    RUNTIME DESTINATION bin COMPONENT ${COMPONENT_NAME_MAIN}  # .dll
)

# We should probably iterate through the list of the cmake for backend, but these need to be installed
# to the this component's dir for the finicky qt installer to work
if (LLMODEL_KOMPUTE)
    set(MODEL_IMPL_TARGETS
        llamamodel-mainline-kompute
        llamamodel-mainline-kompute-avxonly
    )
else()
    set(MODEL_IMPL_TARGETS
        llamamodel-mainline-cpu
        llamamodel-mainline-cpu-avxonly
    )
endif()

if (APPLE)
    list(APPEND MODEL_IMPL_TARGETS llamamodel-mainline-metal)
endif()

install(
    TARGETS ${MODEL_IMPL_TARGETS}
    LIBRARY DESTINATION lib COMPONENT ${COMPONENT_NAME_MAIN}  # .so/.dylib
    RUNTIME DESTINATION lib COMPONENT ${COMPONENT_NAME_MAIN}  # .dll
)

if(APPLE AND GPT4ALL_SIGN_INSTALL)
    include(SignMacOSBinaries)
    install_sign_osx(chat)
    install_sign_osx(llmodel)
    foreach(tgt ${MODEL_IMPL_TARGETS})
        install_sign_osx(${tgt})
    endforeach()
endif()

if(WIN32 AND GPT4ALL_SIGN_INSTALL)
    include(SignWindowsBinaries)
    sign_target_windows(chat)
    sign_target_windows(llmodel)
    foreach(tgt ${MODEL_IMPL_TARGETS})
        sign_target_windows(${tgt})
    endforeach()
endif()

if (LLMODEL_CUDA)
    set_property(TARGET llamamodel-mainline-cuda llamamodel-mainline-cuda-avxonly
                 APPEND PROPERTY INSTALL_RPATH "$ORIGIN")

    install(
        TARGETS llamamodel-mainline-cuda
                llamamodel-mainline-cuda-avxonly
        RUNTIME_DEPENDENCY_SET llama-cuda-deps
        LIBRARY DESTINATION lib COMPONENT ${COMPONENT_NAME_MAIN}  # .so/.dylib
        RUNTIME DESTINATION lib COMPONENT ${COMPONENT_NAME_MAIN}  # .dll
    )
    if (WIN32)
        install(
            RUNTIME_DEPENDENCY_SET llama-cuda-deps
            PRE_EXCLUDE_REGEXES "^(nvcuda|api-ms-.*)\\.dll$"
            POST_INCLUDE_REGEXES "(^|[/\\\\])(lib)?(cuda|cublas)" POST_EXCLUDE_REGEXES .
            DIRECTORIES "${CUDAToolkit_BIN_DIR}"
            DESTINATION lib COMPONENT ${COMPONENT_NAME_MAIN}
        )
    endif()
endif()

if (NOT APPLE)
    install(FILES "${CMAKE_BINARY_DIR}/resources/${LOCAL_EMBEDDING_MODEL}"
            DESTINATION resources
            COMPONENT ${COMPONENT_NAME_MAIN})
endif()

set(CPACK_GENERATOR "IFW")
set(CPACK_VERBATIM_VARIABLES YES)
set(CPACK_IFW_VERBOSE ON)

if(${CMAKE_SYSTEM_NAME} MATCHES Linux)
    find_program(LINUXDEPLOYQT linuxdeployqt HINTS "$ENV{HOME}/dev/linuxdeployqt/build/tools/linuxdeployqt" "$ENV{HOME}/project/linuxdeployqt/bin")
    configure_file("${CMAKE_CURRENT_SOURCE_DIR}/cmake/deploy-qt-linux.cmake.in"
                   "${CMAKE_BINARY_DIR}/cmake/deploy-qt-linux.cmake" @ONLY)
    set(CPACK_PRE_BUILD_SCRIPTS ${CMAKE_BINARY_DIR}/cmake/deploy-qt-linux.cmake)
    set(CPACK_IFW_ROOT "~/Qt/Tools/QtInstallerFramework/4.6")
    set(CPACK_PACKAGE_FILE_NAME "${COMPONENT_NAME_MAIN}-installer-linux")
    set(CPACK_IFW_TARGET_DIRECTORY "@HomeDir@/${COMPONENT_NAME_MAIN}")
elseif(${CMAKE_SYSTEM_NAME} MATCHES Windows)
    find_program(WINDEPLOYQT windeployqt HINTS ${_qt_bin_dir})
    configure_file("${CMAKE_CURRENT_SOURCE_DIR}/cmake/deploy-qt-windows.cmake.in"
                   "${CMAKE_BINARY_DIR}/cmake/deploy-qt-windows.cmake" @ONLY)
    set(CPACK_PRE_BUILD_SCRIPTS ${CMAKE_BINARY_DIR}/cmake/deploy-qt-windows.cmake)
    set(CPACK_IFW_ROOT "C:/Qt/Tools/QtInstallerFramework/4.6")
    set(CPACK_IFW_PACKAGE_ICON "${CMAKE_CURRENT_SOURCE_DIR}/resources/gpt4all.ico")
    set(CPACK_PACKAGE_FILE_NAME "${COMPONENT_NAME_MAIN}-installer-win64")
    set(CPACK_IFW_TARGET_DIRECTORY "@HomeDir@\\${COMPONENT_NAME_MAIN}")
elseif(${CMAKE_SYSTEM_NAME} MATCHES Darwin)
    find_program(MACDEPLOYQT macdeployqt HINTS ${_qt_bin_dir})
    configure_file("${CMAKE_CURRENT_SOURCE_DIR}/cmake/deploy-qt-mac.cmake.in"
                   "${CMAKE_BINARY_DIR}/cmake/deploy-qt-mac.cmake" @ONLY)
    set(CPACK_PRE_BUILD_SCRIPTS ${CMAKE_BINARY_DIR}/cmake/deploy-qt-mac.cmake)
    set(CPACK_IFW_ROOT "~/Qt/Tools/QtInstallerFramework/4.6")
    set(CPACK_IFW_PACKAGE_ICON "${CMAKE_CURRENT_SOURCE_DIR}/resources/gpt4all.icns")
    set(CPACK_PACKAGE_FILE_NAME "${COMPONENT_NAME_MAIN}-installer-darwin")
    set(CPACK_IFW_TARGET_DIRECTORY "@ApplicationsDir@/${COMPONENT_NAME_MAIN}")
    set(CPACK_BUNDLE_NAME ${COMPONENT_NAME_MAIN})
    set(CPACK_BUNDLE_ICON "${CMAKE_CURRENT_SOURCE_DIR}/resources/gpt4all.icns")
endif()

set(CPACK_PACKAGE_INSTALL_DIRECTORY ${COMPONENT_NAME_MAIN})
set(CPACK_PACKAGE_VERSION_MAJOR ${PROJECT_VERSION_MAJOR})
set(CPACK_PACKAGE_VERSION_MINOR ${PROJECT_VERSION_MINOR})
SET(CPACK_PACKAGE_VERSION_PATCH ${PROJECT_VERSION_PATCH})
set(CPACK_PACKAGE_HOMEPAGE_URL "https://gpt4all.io")
set(CPACK_PACKAGE_ICON "${CMAKE_CURRENT_SOURCE_DIR}/icons/gpt4all-48.png")
set(CPACK_RESOURCE_FILE_LICENSE ${CMAKE_CURRENT_SOURCE_DIR}/LICENSE)
set(CPACK_RESOURCE_FILE_README ${CMAKE_CURRENT_SOURCE_DIR}/README.md)
set(CPACK_PACKAGE_EXECUTABLES "GPT4All")
set(CPACK_CREATE_DESKTOP_LINKS "GPT4All")
set(CPACK_IFW_PACKAGE_NAME "GPT4All")
set(CPACK_IFW_PACKAGE_TITLE "GPT4All Installer")
set(CPACK_IFW_PACKAGE_PUBLISHER "Nomic, Inc.")
set(CPACK_IFW_PRODUCT_URL "https://gpt4all.io")
set(CPACK_IFW_PACKAGE_WIZARD_STYLE "Aero")
set(CPACK_IFW_PACKAGE_LOGO "${CMAKE_CURRENT_SOURCE_DIR}/icons/gpt4all-48.png")
set(CPACK_IFW_PACKAGE_WINDOW_ICON "${CMAKE_CURRENT_SOURCE_DIR}/icons/gpt4all-32.png")
set(CPACK_IFW_PACKAGE_WIZARD_SHOW_PAGE_LIST OFF)

include(InstallRequiredSystemLibraries)
include(CPack)
include(CPackIFW)
if(GPT4ALL_OFFLINE_INSTALLER)
  cpack_add_component(${COMPONENT_NAME_MAIN})
else()
  cpack_add_component(${COMPONENT_NAME_MAIN} DOWNLOADED)
endif()
cpack_ifw_configure_component(${COMPONENT_NAME_MAIN} ESSENTIAL FORCED_INSTALLATION)
cpack_ifw_configure_component(${COMPONENT_NAME_MAIN} VERSION ${APP_VERSION})
cpack_ifw_configure_component(${COMPONENT_NAME_MAIN} LICENSES "MIT LICENSE" ${CPACK_RESOURCE_FILE_LICENSE})
cpack_ifw_configure_component(${COMPONENT_NAME_MAIN} SCRIPT "${CMAKE_CURRENT_SOURCE_DIR}/cmake/installerscript.qs")
cpack_ifw_configure_component(${COMPONENT_NAME_MAIN} REPLACES "gpt4all-chat") #Was used in very earliest prototypes

if (GPT4ALL_LOCALHOST)
    cpack_ifw_add_repository("GPT4AllRepository" URL "http://localhost/repository")
elseif(GPT4ALL_OFFLINE_INSTALLER)
  add_compile_definitions(GPT4ALL_OFFLINE_INSTALLER)
else()
  if(${CMAKE_SYSTEM_NAME} MATCHES Linux)
      cpack_ifw_add_repository("GPT4AllRepository" URL "https://gpt4all.io/installer_repos/linux/repository")
  elseif(${CMAKE_SYSTEM_NAME} MATCHES Windows)
      #To sign the target on windows have to create a batch script add use it as a custom target and then use CPACK_IFW_EXTRA_TARGETS to set this extra target
      cpack_ifw_add_repository("GPT4AllRepository" URL "https://gpt4all.io/installer_repos/windows/repository")
  elseif(${CMAKE_SYSTEM_NAME} MATCHES Darwin)
      cpack_ifw_add_repository("GPT4AllRepository" URL "https://gpt4all.io/installer_repos/mac/repository")
  endif()
endif()
