# Copyright (C) 2022 The Qt Company Ltd.
# SPDX-License-Identifier: LicenseRef-Qt-Commercial

import glob
import os
import sys
from typing import List, Optional, TextIO

from PySide6.QtCore import QDir

_SHIBOKEN_PACKAGE = None
_SHIBOKEN_GENERATOR_PACKAGE = None
_PYSIDE_PACKAGE = None
_QT_VERSION = 6
_PYSIDE_VERSION = 6 if _QT_VERSION > 5 else 2
_CXX_STANDARD = 17 if _QT_VERSION > 5 else 11


_SHIBOKEN_OPTIONS = """--generator-set=shiboken --enable-parent-ctor-heuristic
--enable-return-value-heuristic --use-isnull-as-nb_nonzero
--avoid-protected-hack
${includes}
-I${CMAKE_SOURCE_DIR}
-T${CMAKE_SOURCE_DIR}
--output-directory=${CMAKE_CURRENT_BINARY_DIR})
"""

_SHIBOKEN_TARGET = f"""
# Specify which sources will be generated by shiboken, and their dependencies.

set(generated_sources_dependencies ${{wrapped_headers}} ${{typesystem_file}})

# Add custom target to run shiboken.
add_custom_command(OUTPUT ${{generated_sources}}
                   COMMAND shiboken{_PYSIDE_VERSION}
                   ${{shiboken_options}} ${{wrapped_headers}} ${{typesystem_file}}
                   DEPENDS ${{generated_sources_dependencies}}
                   IMPLICIT_DEPENDS CXX ${{wrapped_headers}}
                   WORKING_DIRECTORY ${{CMAKE_CURRENT_BINARY_DIR}}
                   COMMENT "Running generator for ${{typesystem_file}}.")
"""

_GENERATED_SOURCES_COMMENT = """
# Specify which C++ files will be generated by shiboken. This includes the
# module wrapper and a '.cpp' file per C++ type. These are needed for
# generating the module shared library.
"""


_TYPESYSTEM_COMMENT = """
# The typesystem xml file which defines the relationships between the C++
# types / functions and the corresponding Python equivalents.
"""


_LIBRARY_HEADER = """
# ======================= CMake target - bindings_library =====================

# Define and build the bindings library.
add_library(${bindings_library} MODULE ${generated_sources})

# Needed mostly on Windows to export symbols, and create a .lib file, otherwise
# the binding library can't link to the sample library.
target_compile_definitions(${bindings_library} PRIVATE BINDINGS_BUILD)

"""


_LIBRARY_FOOTER = """
# Adjust the name of generated module.
set_property(TARGET ${bindings_library} PROPERTY PREFIX "")
set_property(TARGET ${bindings_library} PROPERTY OUTPUT_NAME
             "${bindings_library}${PYTHON_EXTENSION_SUFFIX}")
if(WIN32)
    set_property(TARGET ${bindings_library} PROPERTY SUFFIX ".pyd")
endif()
"""


def clean_path(path: str) -> str:
    return path if sys.platform != 'win32' else path.replace('\\', '/')


def find_package_path(dir_name: str) -> Optional[str]:
    """Find a Python package"""
    for p in sys.path:
        if 'site-' in p:
            package = os.path.join(p, dir_name)
            if os.path.exists(package):
                p = clean_path(os.path.realpath(package))
                return QDir.fromNativeSeparators(p)
    return None


def shared_library_suffix() -> str:
    if sys.platform == 'win32':
        return 'lib'
    if sys.platform == 'darwin':
        return 'dylib'
    return 'so.*'


def shared_library_glob_pattern() -> str:
    glob = '*.' + shared_library_suffix()
    return glob if sys.platform == 'win32' else 'lib' + glob


def shiboken_library() -> Optional[str]:
    """Find the shiboken library"""
    for candidate in glob.glob(os.path.join(_SHIBOKEN_PACKAGE,
                               shared_library_glob_pattern())):
        if 'shiboken' in os.path.basename(candidate):
            return QDir.fromNativeSeparators(candidate)
    return None


def init_generator() -> None:
    """Init the generator, locate the required packages (throws)"""
    global _PYSIDE_PACKAGE
    global _SHIBOKEN_PACKAGE
    global _SHIBOKEN_GENERATOR_PACKAGE
    global _PYSIDE_VERSION
    if _SHIBOKEN_PACKAGE:
        return
    name = f"shiboken{_PYSIDE_VERSION}"
    _SHIBOKEN_PACKAGE = find_package_path(name)
    if not _SHIBOKEN_PACKAGE:
        raise ImportError(f'Unable to locate package "{name}"')
    name = f"shiboken{_PYSIDE_VERSION}_generator"
    _SHIBOKEN_GENERATOR_PACKAGE = find_package_path(name)
    if not _SHIBOKEN_GENERATOR_PACKAGE:
        raise ImportError(f'Unable to locate package "{name}"')
    name = f"PySide{_PYSIDE_VERSION}"
    _PYSIDE_PACKAGE = find_package_path(name)
    if not _PYSIDE_PACKAGE:
        raise ImportError(f'Unable to locate package "{name}"')


def _link_argument(library_path: str) -> str:
    """Return the link argument, that is, /lib/libfoo.so -> foo.so"""
    p = library_path
    components = os.path.splitext(p)
    suffix = components[1]
    if suffix in ['.so', '.lib', '.a']:
        p = components[0]
    base = os.path.basename(p)
    if sys.platform != 'win32' and base.startswith('lib'):
        return base[3:]
    return base


def _write_cmake_set_var(file: TextIO, name: str, value: str) -> None:
    file.write(f'set({name} {value})\n\n')


def _write_cmake_set_string_var(file: TextIO, name: str, value: str) -> None:
    file.write(f'set({name} "{value}")\n\n')


def _write_cmake_set_list_var(file: TextIO, name: str, values: List[str],
                              separator: str = '\n    ') -> None:
    file.write(f'set({name}')
    for v in values:
        file.write(separator)
        file.write(v)
    file.write(')\n\n')


def _write_cmake_find_package(file: TextIO, package: str,
                              component: str) -> None:
    file.write(f'find_package ({package} COMPONENTS {component} REQUIRED)\n')


def _wrapper_file_name(gen_dir: str, class_name: str) -> str:
    file_part = class_name.lower().replace('.', '_')
    return f'{gen_dir}/{file_part}_wrapper.cpp'


def _write_qt_include_var(file: TextIO, qt_modules: List[str]) -> List[str]:
    """Create a list of variables with Qt include paths and
       return the list of expanded variables"""
    file.write('# Get all relevant Qt include dirs, to pass them on to shiboken.\n')
    result = []
    for qt_module in qt_modules:
        qt_inc_list_var = f'Qt{qt_module}_INCLUDE_DIRS'
        file.write(f'get_property({qt_inc_list_var} TARGET Qt{_QT_VERSION}::Core '
                   f'PROPERTY INTERFACE_INCLUDE_DIRECTORIES)\n')
        result.append(f'${{{qt_inc_list_var}}}')
    return result


def _write_cmake_file(cf: TextIO, name: str, library: str,
                      typesystem_file: str, headers: List[str],
                      include_paths: List[str], shiboken_options: List[str],
                      qt_modules: List[str], selected_classes: List[str]) -> None:
    """Write cmake file to file cf"""
    # Add headers to include paths
    global _PYSIDE_VERSION
    all_include_paths = include_paths
    for i in headers:
        directory = os.path.dirname(i)
        if directory not in all_include_paths:
            all_include_paths.append(directory)

    cf.write('cmake_minimum_required(VERSION 3.16)\n\n')

    cf.write(f'set(CMAKE_CXX_STANDARD {_CXX_STANDARD})\n\n')

    cf.write(f'project({name})\n\n')

    _write_cmake_find_package(cf, 'Python3', 'Development')
    for qt_module in qt_modules:
        _write_cmake_find_package(cf, f'Qt{_QT_VERSION}', qt_module)
    cf.write('\n')

    cf.write('# The name of the generated bindings module (as imported in Python).\n')
    _write_cmake_set_string_var(cf, 'bindings_library', name)

    cf.write(_GENERATED_SOURCES_COMMENT)
    cf.write('set(generated_sources\n')
    gen_dir = f'${{CMAKE_CURRENT_BINARY_DIR}}/{name}'
    for c in selected_classes:
        cf.write('    ')
        cf.write(_wrapper_file_name(gen_dir, c))
        cf.write('\n')
    cf.write('    # module is always needed\n')
    cf.write('    ')
    cf.write(_wrapper_file_name(gen_dir, f'{name}_module'))
    cf.write(')\n\n')

    cf.write(_TYPESYSTEM_COMMENT)
    cmake_tf = '${CMAKE_SOURCE_DIR}/' + os.path.basename(typesystem_file)
    _write_cmake_set_var(cf, 'typesystem_file', cmake_tf)

    cf.write('# The header file with all the types and functions for which bindings will be generated.\n')  # noqa: E501
    _write_cmake_set_list_var(cf, 'wrapped_headers', headers)

    cf.write(f'# The Include paths for shiboken{_PYSIDE_VERSION}.\n')

    if qt_modules:
        qt_includes = _write_qt_include_var(cf, qt_modules)
        qt_includes_list = ' '.join(qt_includes)
        cf.write(f'set(QT_INCLUDE_DIRS {qt_includes_list})\n')
        all_include_paths.append('${QT_INCLUDE_DIRS}')
    # Join include path by -I for shiboken
    cf.write('set(includes "")\n')
    cf.write('foreach(include_dir ')
    cf.write(' '.join(all_include_paths))
    cf.write(')\n    list(APPEND includes "-I${include_dir}")\n')
    cf.write('endforeach()\n')

    cf.write('\n# ====================== Shiboken target for generating binding C++ files  ====================\n\n')  # noqa: E501

    cf.write('# Set up the options to pass to shiboken.\nset(shiboken_options\n')
    if shiboken_options:
        cf.write(' '.join(shiboken_options))
        cf.write('\n')
    cf.write(_SHIBOKEN_OPTIONS)
    cf.write('\n')
    cf.write(_SHIBOKEN_TARGET)

    cf.write(_LIBRARY_HEADER)
    cf.write('target_include_directories(${bindings_library} PRIVATE\n')
    cf.write('    ${Python3_INCLUDE_DIRS}\n')
    for i in all_include_paths:
        cf.write(f'    {i}\n')
    cf.write(f'    {_SHIBOKEN_GENERATOR_PACKAGE}/include\n')
    if qt_modules:
        cf.write(f'    {_PYSIDE_PACKAGE}/include\n')
    cf.write(')\n\n')
    # Libraries
    library_dirs = [os.path.dirname(library), '${Python3_LIBRARY_DIRS}']
    libraries = ['${Python3_LIBRARIES}', _link_argument(library),
                 shiboken_library()]
    cf.write('target_link_directories(${bindings_library} PRIVATE\n')
    for ld in library_dirs:
        cf.write(f'    {ld}\n')
    cf.write(')\n\ntarget_link_libraries(${bindings_library} PRIVATE\n')
    for lib in libraries:
        cf.write(f'    {lib}\n')
    cf.write(')\n\n')
    cf.write(_LIBRARY_FOOTER)


def write_cmake_file(name: str, library: str, cmake_file: str,
                     typesystem_file: str, headers: List[str],
                     include_paths: List[str], shiboken_options: List[str],
                     qt_modules: List[str], selected_classes: List[str]) -> None:
    """Write cmake file"""
    with open(cmake_file, 'w') as cf:
        _write_cmake_file(cf, name, library, typesystem_file, headers,
                          include_paths, shiboken_options, qt_modules,
                          selected_classes)


def write_typesystem_file(typesystem_file: str, dom_doc: str) -> None:
    """Write typesystem file"""
    with open(typesystem_file, 'w') as tf:
        tf.write(dom_doc.toString())


def code_model_dumper() -> str:
    """Return path of the code model dump tool."""
    result = os.path.join(_SHIBOKEN_GENERATOR_PACKAGE, 'dumpcodemodel')
    if sys.platform == 'win32':
        result += '.exe'
    return result
