# Copyright (C) 2023 The Qt Company Ltd.
# SPDX-License-Identifier: BSD-3-Clause

cmake_minimum_required(VERSION 3.22)
cmake_policy(VERSION 3.22)

# Enable policy to run automoc on generated files.
if(POLICY CMP0071)
  cmake_policy(SET CMP0071 NEW)
endif()

project(scriptableapplication)

# Set CPP standard to C++17 minimum.
set(CMAKE_CXX_STANDARD 17)

# Find required Qt packages.
find_package(Qt6 COMPONENTS Core Gui Widgets)

# Use provided python interpreter if given.
if(NOT python_interpreter)
    if(WIN32 AND "${CMAKE_BUILD_TYPE}" STREQUAL "Debug")
        find_program(python_interpreter "python_d")
        if(NOT python_interpreter)
            message(FATAL_ERROR
                "A debug Python interpreter could not be found, which is a requirement when "
                "building this example in a debug configuration. Make sure python_d.exe is in "
                "PATH.")
        endif()
    else()
        find_program(python_interpreter "python")
        if(NOT python_interpreter)
            message(FATAL_ERROR
                "No Python interpreter could be found. Make sure python is in PATH.")
        endif()
    endif()
endif()
message(STATUS "Using python interpreter: ${python_interpreter}")

# Macro to get various pyside / python include / link flags.
macro(pyside_config option output_var)
    if(${ARGC} GREATER 2)
        set(is_list ${ARGV2})
    else()
        set(is_list "")
    endif()

    execute_process(
      COMMAND ${python_interpreter} "${CMAKE_SOURCE_DIR}/../utils/pyside_config.py"
              ${option}
      OUTPUT_VARIABLE ${output_var}
      OUTPUT_STRIP_TRAILING_WHITESPACE)

    if ("${${output_var}}" STREQUAL "")
        message(FATAL_ERROR "Error: Calling pyside_config.py ${option} returned no output.")
    endif()
    if(is_list)
        string (REPLACE " " ";" ${output_var} "${${output_var}}")
    endif()
endmacro()

# Query for the shiboken6-generator path, PySide6 path, Python path, include paths and linker flags.
pyside_config(--shiboken-module-path SHIBOKEN_MODULE_PATH)
pyside_config(--shiboken-generator-path SHIBOKEN_GENERATOR_PATH)
pyside_config(--pyside-path PYSIDE_PATH)

pyside_config(--python-include-path PYTHON_INCLUDE_DIR)
pyside_config(--shiboken-include-path SHIBOKEN_INCLUDE_DIR 1)
pyside_config(--pyside-include-path PYSIDE_INCLUDE_DIR 1)

pyside_config(--python-link-flags-cmake PYTHON_LINKING_DATA 0)
pyside_config(--shiboken-module-shared-libraries-cmake SHIBOKEN_MODULE_SHARED_LIBRARIES 0)
pyside_config(--pyside-shared-libraries-cmake PYSIDE_SHARED_LIBRARIES 0)

# Print the computed variables
message(STATUS "Shiboken module path: ${SHIBOKEN_MODULE_PATH}")
message(STATUS "Shiboken generator path: ${SHIBOKEN_GENERATOR_PATH}")
message(STATUS "PySide path: ${PYSIDE_PATH}")
message(STATUS "Python include path: ${PYTHON_INCLUDE_DIR}")
message(STATUS "Shiboken include path: ${SHIBOKEN_INCLUDE_DIR}")
message(STATUS "PySide include path: ${PYSIDE_INCLUDE_DIR}")
message(STATUS "Python linking data: ${PYTHON_LINKING_DATA}")
message(STATUS "Shiboken module shared libraries: ${SHIBOKEN_MODULE_SHARED_LIBRARIES}")
message(STATUS "PySide shared libraries: ${PYSIDE_SHARED_LIBRARIES}")

set(SHIBOKEN_PATH "${SHIBOKEN_GENERATOR_PATH}/shiboken6${CMAKE_EXECUTABLE_SUFFIX}")

if(NOT EXISTS ${SHIBOKEN_PATH})
    message(FATAL_ERROR "Shiboken executable not found at path: ${SHIBOKEN_PATH}")
endif()


# Get all relevant Qt include dirs, to pass them on to shiboken.
get_property(QT_WIDGETS_INCLUDE_DIRS TARGET Qt6::Widgets PROPERTY INTERFACE_INCLUDE_DIRECTORIES)
set(INCLUDES "")
foreach(INCLUDE_DIR ${QT_WIDGETS_INCLUDE_DIRS})
    list(APPEND INCLUDES "-I${INCLUDE_DIR}")
endforeach()

# On macOS, check if Qt is a framework build. This affects how include paths should be handled.
get_target_property(QtCore_is_framework Qt6::Core FRAMEWORK)
if (QtCore_is_framework)
    get_target_property(qt_core_library_location Qt6::Core LOCATION)
    # PYSIDE-623: We move up until the directory contains all the frameworks.
    #             This is "lib" in ".../lib/QtCore.framework/Versions/A/QtCore".
    get_filename_component(lib_dir "${qt_core_library_location}/../../../.." ABSOLUTE)
    list(APPEND INCLUDES "--framework-include-paths=${lib_dir}")
endif()

# Set up the options to pass to shiboken.
set(WRAPPED_HEADER ${CMAKE_SOURCE_DIR}/wrappedclasses.h)
set(TYPESYSTEM_FILE ${CMAKE_SOURCE_DIR}/scriptableapplication.xml)

set(SHIBOKEN_OPTIONS --generator-set=shiboken --enable-parent-ctor-heuristic
    --enable-pyside-extensions --enable-return-value-heuristic --use-isnull-as-nb-bool
    --avoid-protected-hack
    ${INCLUDES}
    -I${CMAKE_SOURCE_DIR}
    -T${CMAKE_SOURCE_DIR}
    -T${PYSIDE_PATH}/typesystems
    --output-directory=${CMAKE_CURRENT_BINARY_DIR}
    )

# Specify which sources will be generated by shiboken, and their dependencies.
set(GENERATED_SOURCES
    ${CMAKE_CURRENT_BINARY_DIR}/AppLib/applib_module_wrapper.cpp
    ${CMAKE_CURRENT_BINARY_DIR}/AppLib/mainwindow_wrapper.cpp)

set(GENERATED_SOURCES_DEPENDENCIES
    ${WRAPPED_HEADER}
    ${TYPESYSTEM_FILE}
    )

# Add custom target to run shiboken.
add_custom_command(OUTPUT ${GENERATED_SOURCES}
                    COMMAND ${SHIBOKEN_PATH}
                    ${SHIBOKEN_OPTIONS} ${WRAPPED_HEADER} ${TYPESYSTEM_FILE}
                    DEPENDS ${GENERATED_SOURCES_DEPENDENCIES}
                    WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}
                    COMMENT "Running generator for ${TYPESYSTEM_FILE}.")

# Set the CPP files.
set(SOURCES
    mainwindow.cpp
    pythonutils.cpp
    ${GENERATED_SOURCES}
    )

# We need to include the headers for the module bindings that we use.
set(PYSIDE_ADDITIONAL_INCLUDES "")
foreach(INCLUDE_DIR ${PYSIDE_INCLUDE_DIR})
    list(APPEND PYSIDE_ADDITIONAL_INCLUDES "${INCLUDE_DIR}/QtCore")
    list(APPEND PYSIDE_ADDITIONAL_INCLUDES "${INCLUDE_DIR}/QtGui")
    list(APPEND PYSIDE_ADDITIONAL_INCLUDES "${INCLUDE_DIR}/QtWidgets")
endforeach()

# =============================================================================================
# !!! (The section below is deployment related, so in a real world application you will want to
# take care of this properly with some custom script or tool).
# =============================================================================================
# Enable rpaths so that the example can be executed from the build dir.
set(CMAKE_SKIP_BUILD_RPATH FALSE)
set(CMAKE_BUILD_WITH_INSTALL_RPATH TRUE)
set(CMAKE_INSTALL_RPATH ${PYSIDE_PATH} ${SHIBOKEN_MODULE_PATH})
set(CMAKE_INSTALL_RPATH_USE_LINK_PATH TRUE)
# =============================================================================================
# !!! End of dubious section.
# =============================================================================================

# Declare executable so we can enable automoc.
add_executable(${PROJECT_NAME} main.cpp)

# Enable automoc.
set_property(TARGET ${PROJECT_NAME} PROPERTY AUTOMOC 1)

# Add the rest of the sources.
target_sources(${PROJECT_NAME} PUBLIC ${SOURCES})

# Apply relevant include and link flags.
target_include_directories(${PROJECT_NAME} PRIVATE ${PYTHON_INCLUDE_DIR})
target_include_directories(${PROJECT_NAME} PRIVATE ${SHIBOKEN_INCLUDE_DIR})
target_include_directories(${PROJECT_NAME} PRIVATE ${PYSIDE_INCLUDE_DIR})
target_include_directories(${PROJECT_NAME} PRIVATE ${PYSIDE_ADDITIONAL_INCLUDES})
target_include_directories(${PROJECT_NAME} PRIVATE ${CMAKE_SOURCE_DIR})

target_link_libraries(${PROJECT_NAME} PRIVATE Qt6::Widgets)
target_link_libraries(${PROJECT_NAME} PRIVATE ${SHIBOKEN_MODULE_SHARED_LIBRARIES})
target_link_libraries(${PROJECT_NAME} PRIVATE ${PYSIDE_SHARED_LIBRARIES})

# Find and link to the python library.
list(GET PYTHON_LINKING_DATA 0 PYTHON_LIBDIR)
list(GET PYTHON_LINKING_DATA 1 PYTHON_LIB)
find_library(PYTHON_LINK_FLAGS ${PYTHON_LIB} PATHS ${PYTHON_LIBDIR} HINTS ${PYTHON_LIBDIR})
target_link_libraries(${PROJECT_NAME} PRIVATE ${PYTHON_LINK_FLAGS})

# Same as CONFIG += no_keywords to avoid syntax errors in object.h due to the usage of the word Slot
target_compile_definitions(${PROJECT_NAME} PRIVATE QT_NO_KEYWORDS)

if(WIN32)
    # =============================================================================================
    # !!! (The section below is deployment related, so in a real world application you will want to
    # take care of this properly (this is simply to eliminate errors that users usually encounter.
    # =============================================================================================
    # Circumvent some "#pragma comment(lib)"s in "include/pyconfig.h" which might force to link
    # against a wrong python shared library.

    set(PYTHON_VERSIONS_LIST 3 36 37 38 39)
    set(PYTHON_ADDITIONAL_LINK_FLAGS "")
    foreach(VER ${PYTHON_VERSIONS_LIST})
        set(PYTHON_ADDITIONAL_LINK_FLAGS
            "${PYTHON_ADDITIONAL_LINK_FLAGS} /NODEFAULTLIB:\"python${VER}_d.lib\"")
        set(PYTHON_ADDITIONAL_LINK_FLAGS
            "${PYTHON_ADDITIONAL_LINK_FLAGS} /NODEFAULTLIB:\"python${VER}.lib\"")
    endforeach()

    set_target_properties(${PROJECT_NAME} PROPERTIES LINK_FLAGS "${PYTHON_ADDITIONAL_LINK_FLAGS}")

    # Add custom target to hard link PySide6 shared libraries (just like in qmake example), so you
    # don't have to set PATH manually to point to the PySide6 package.
    set(shared_libraries ${SHIBOKEN_MODULE_SHARED_LIBRARIES} ${PYSIDE_SHARED_LIBRARIES})
    foreach(LIBRARY_PATH ${shared_libraries})
        string(REGEX REPLACE ".lib$" ".dll" LIBRARY_PATH ${LIBRARY_PATH})
        get_filename_component(BASE_NAME ${LIBRARY_PATH} NAME)
        file(TO_NATIVE_PATH ${LIBRARY_PATH} SOURCE_PATH)
        file(TO_NATIVE_PATH "${CMAKE_CURRENT_BINARY_DIR}/${BASE_NAME}" DEST_PATH)
        add_custom_command(OUTPUT "${BASE_NAME}"
                            COMMAND mklink /H "${DEST_PATH}" "${SOURCE_PATH}"
                            DEPENDS ${LIBRARY_PATH}
                            WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}
                            COMMENT "Creating hardlink to PySide6 shared library ${BASE_NAME}")

        # Fake target that depends on the previous one, but has special ALL keyword, which means
        # it will always be executed.
        add_custom_target("fake_${BASE_NAME}" ALL DEPENDS ${BASE_NAME})
    endforeach()
    # =============================================================================================
    # !!! End of dubious section.
    # =============================================================================================
endif()
