#!/usr/bin/env python3

# SPDX-FileCopyrightText: 2024 Open Mobile Platform LLC <community@omp.ru>
# SPDX-License-Identifier: BSD-3-Clause

# Script copies files from conan package directory to the specified directory.
# It copies the following components:
# * qmlplugins for use inside the application
# * dynamically loaded libraries by executable and qmlplugins

import argparse
import glob
import os
import os.path
import shutil
import subprocess

CONAN_RUN_FILE="conanrun.sh"
QML_PLUGIN_DIRECTORY="qmlplugins"


def main():
    arguments = parse_arguments()
    conan_packages_data = list_conan_packages_data(arguments.conan_install_folder)
    packages_by_category = separate_qml_plugins(conan_packages_data)
    destination_folder = os.path.abspath(arguments.destination_folder)
    create_destination_folder(destination_folder)
    copy_qml_plugins(packages_by_category["qml_plugins"], destination_folder)
    plugin_dynamic_libraries = find_dynamic_libraries(destination_folder)
    executables = [arguments.executable, *plugin_dynamic_libraries]
    copy_libraries(
        executables,
        arguments.conan_install_folder,
        packages_by_category["libraries"],
        destination_folder,
    )
    set_aurora_directory_permissions(destination_folder)

def parse_arguments() -> argparse.Namespace:
    parser = argparse.ArgumentParser(description="Copy contents of package, deduplicate libraries")
    parser.add_argument(
        "executable",
        type=executable,
        help="Executable that dynamically loads libraries",
    )
    parser.add_argument(
        "conan_install_folder",
        type=conan_install_folder,
        help="Path to the output folder of the conan install command",
    )
    parser.add_argument(
        "destination_folder",
        help="Path to the folder in which data should be copied to",
    )
    return parser.parse_args()


def executable(string) -> str:
    if (not os.path.exists(string)) or (not os.path.isfile(string)):
        raise argparse.ArgumentTypeError("The specified file as executable does not exist")
    return os.path.abspath(string)


def conan_install_folder(string) -> str:
    conan_run_file = path_to_conan_run(string)
    if not os.path.exists(conan_run_file):
        raise argparse.ArgumentTypeError(f"The specified folder does not contain {CONAN_RUN_FILE} file")
    return os.path.abspath(string)


def path_to_conan_run(folder) -> str:
    return os.path.join(folder, CONAN_RUN_FILE)


def list_conan_packages_data(conan_install_folder) -> list:
    pkg_config_files = glob.glob(f"{conan_install_folder}/*.pc")
    return list(map(get_conan_package_data, pkg_config_files))


def get_conan_package_data(pkg_config_file) -> dict:
    with open(pkg_config_file, "r") as file:
        pkg_config_lines = file.read().splitlines()
        pkg_config_dirs = {}
        for line in pkg_config_lines:
            if "=" in line:
                elements = line.split("=")
                pkg_config_dirs[elements[0]] = elements[1]
        for key in pkg_config_dirs:
            pkg_config_dirs[key] = pkg_config_dirs[key].replace("${prefix}", pkg_config_dirs["prefix"])
        pkg_config_dirs[QML_PLUGIN_DIRECTORY] = os.path.join(pkg_config_dirs["prefix"], QML_PLUGIN_DIRECTORY)
        return pkg_config_dirs
    return {}


def separate_qml_plugins(conan_packages_data) -> dict:
    libraries = []
    qml_plugins = []
    for pkg_config_dirs in conan_packages_data:
        if os.path.isdir(pkg_config_dirs[QML_PLUGIN_DIRECTORY]):
            qml_plugins.append(pkg_config_dirs)
        else:
            libraries.append(pkg_config_dirs)
    return {
        "libraries": libraries,
        "qml_plugins": qml_plugins,
    }


def create_destination_folder(destination_folder):
    os.makedirs(destination_folder, exist_ok=True)


def copy_qml_plugins(qml_plugins, destination_folder):
    qml_plugins_directory = os.path.join(destination_folder, QML_PLUGIN_DIRECTORY)
    for pkg_config_dirs in qml_plugins:
        shutil.copytree(pkg_config_dirs[QML_PLUGIN_DIRECTORY], qml_plugins_directory, dirs_exist_ok=True)


def find_dynamic_libraries(destination_folder) -> list:
    return glob.glob(f"{destination_folder}/**/*.so", recursive=True)


def copy_libraries(executables, conan_install_folder, conan_packages_data, destination_folder):
    executable_libraries = list_executable_libraries_via_readelf(
        executables,
        conan_install_folder,
    )
    if "not" in executable_libraries:
        print("Could not find required libraries via readelf. Copying all libraries.")
        copy_all_libraries(conan_packages_data, destination_folder)
    else:
        copy_required_libraries(executable_libraries, conan_packages_data, destination_folder)
    
    
def run_readelf(file_path):
    result = subprocess.run(
        ["readelf", "-d", file_path], capture_output=True, text=True
    )
    if result.returncode != 0:
        raise RuntimeError(f"Failed to run readelf on {file_path}: {result.stderr}")
    needed_libraries = set()
    for line in result.stdout.splitlines():
        if "(NEEDED)" in line:
            parts = line.split("[", 1)
            if len(parts) > 1 and "]" in parts[1]:
                library = parts[1].split("]")[0]
                needed_libraries.add(library)
    return needed_libraries
    
def resolve_library_path(library_name, search_paths):
    for path in search_paths:
        candidate = os.path.join(path, library_name)
        if os.path.isfile(candidate):
            return candidate
    return None

def gather_libraries_recursive(libraries, search_paths, processed):
    dependencies = set()
    for library in libraries:
        if library in processed:
            continue
        processed.add(library)
        library_path = resolve_library_path(library, search_paths)
        if library_path:
            dependencies.add(library_path)
            nested_libraries = run_readelf(library_path)
            dependencies.update(
                gather_libraries_recursive(nested_libraries, search_paths, processed)
            )
    return dependencies

def list_executable_libraries_via_readelf(executables, conan_install_folder) -> set:
    conan_run = path_to_conan_run(conan_install_folder)
    bash_command = f"source '{conan_run}'; echo $LD_LIBRARY_PATH"
    result = subprocess.run(["bash", "-c", bash_command], capture_output=True, text=True)
    if result.returncode != 0:
        raise RuntimeError(f"Unable to fetch library paths from conan: {result.stderr}")
    library_paths = result.stdout.strip().split(":")

    initial_libraries = set()
    for executable in executables:
        initial_libraries.update(run_readelf(executable))

    required_libraries = gather_libraries_recursive(initial_libraries, library_paths, set())

    return required_libraries


def copy_all_libraries(conan_packages_data, destination_folder):
    for pkg_config_dirs in conan_packages_data:
        if "libdir" in pkg_config_dirs:
            shutil.copytree(
                pkg_config_dirs["libdir"],
                destination_folder,
                ignore=allow_only_libraries,
                symlinks=True,
                dirs_exist_ok=True,
            )


def allow_only_libraries(directory, contents):
    filtered_contents = []
    for file in contents:
        if ".so" not in file:
            if not os.path.isdir(os.path.join(directory, file)):
                filtered_contents.append(file)
    return filtered_contents


def copy_required_libraries(libraries, conan_packages_data, destination_folder):
    for library in libraries:
        for pkg_config_dirs in conan_packages_data:
            if "libdir" in pkg_config_dirs and library.startswith(pkg_config_dirs["libdir"]):
                shutil.copy(library, destination_folder)
                break


def set_aurora_directory_permissions(destination_folder):
    for root, directories, files in os.walk(destination_folder):
        for directory in directories:
            os.chmod(os.path.join(root, directory), 0o755)


if __name__ == "__main__":
    main()
