#-----------------------------------------------------------------------------
# Copyright (c) 2005-2023, PyInstaller Development Team.
#
# Distributed under the terms of the GNU General Public License (version 2
# or later) with exception for distributing the bootloader.
#
# The full license is in the file COPYING.txt, distributed with this software.
#
# SPDX-License-Identifier: (GPL-2.0-or-later WITH Bootloader-exception)
#-----------------------------------------------------------------------------

import glob
import os
import shutil
import subprocess

from PyInstaller import compat
from PyInstaller.config import CONF  # workpath
from PyInstaller.utils.hooks import get_hook_config, logger
from PyInstaller.utils.hooks.gi import GiModuleInfo, collect_glib_translations

LOADERS_PATH = os.path.join('gdk-pixbuf-2.0', '2.10.0', 'loaders')
LOADER_MODULE_DEST_PATH = "lib/gdk-pixbuf/loaders"
LOADER_CACHE_DEST_PATH = "lib/gdk-pixbuf"  # NOTE: some search & replace code depends on / being used on all platforms.


def _find_gdk_pixbuf_query_loaders_executable(libdir):
    # Distributions either package gdk-pixbuf-query-loaders in the GI libs directory (not on the path), or on the path
    # with or without a -x64 suffix, depending on the architecture.
    cmds = [
        os.path.join(libdir, 'gdk-pixbuf-2.0', 'gdk-pixbuf-query-loaders'),
        'gdk-pixbuf-query-loaders-64',
        'gdk-pixbuf-query-loaders',
    ]

    for cmd in cmds:
        cmd_fullpath = shutil.which(cmd)
        if cmd_fullpath is not None:
            return cmd_fullpath

    return None


def _collect_loaders(libdir):
    # Assume loader plugins have .so library suffix on all non-Windows platforms
    lib_ext = "*.dll" if compat.is_win else "*.so"

    # Find all loaders
    loader_libs = []
    pattern = os.path.join(libdir, LOADERS_PATH, lib_ext)
    for f in glob.glob(pattern):
        loader_libs.append(f)

    # Sometimes the loaders are stored in a different directory from the library (msys2)
    if not loader_libs:
        pattern = os.path.abspath(os.path.join(libdir, '..', 'lib', LOADERS_PATH, lib_ext))
        for f in glob.glob(pattern):
            loader_libs.append(f)

    return loader_libs


def _generate_loader_cache(gdk_pixbuf_query_loaders, libdir, loader_libs):
    # Run the "gdk-pixbuf-query-loaders" command and capture its standard output providing an updated loader
    # cache; then write this output to the loader cache bundled with this frozen application. On all platforms,
    # we also move the package structure to point to lib/gdk-pixbuf instead of lib/gdk-pixbuf-2.0/2.10.0 in
    # order to make compatible with macOS .app bundle signing.
    #
    # On macOS we use @executable_path to specify a path relative to the generated bundle. However, on
    # non-Windows, we need to rewrite the loader cache because it is not relocatable by default. See
    # https://bugzilla.gnome.org/show_bug.cgi?id=737523
    #
    # To make it easier to rewrite, we just always write @executable_path, since its significantly easier to
    # find/replace at runtime. :)
    #
    # To permit string munging, decode the encoded bytes output by this command (i.e., enable the
    # "universal_newlines" option).
    #
    # On Fedora, the default loaders cache is /usr/lib64, but the libdir is actually /lib64. To get around this,
    # we pass the path to the loader command, and it will create a cache with the right path.
    #
    # On Windows, the loaders lib directory is relative, starts with 'lib', and uses \\ as path separators
    # (escaped \).
    cachedata = subprocess.run([gdk_pixbuf_query_loaders, *loader_libs],
                               check=True,
                               stdout=subprocess.PIPE,
                               encoding='utf-8').stdout

    output_lines = []
    prefix = '"' + os.path.join(libdir, 'gdk-pixbuf-2.0', '2.10.0')
    plen = len(prefix)

    win_prefix = '"' + '\\\\'.join(['lib', 'gdk-pixbuf-2.0', '2.10.0'])
    win_plen = len(win_prefix)

    msys2_prefix = '"' + os.path.abspath(os.path.join(libdir, '..', 'lib', 'gdk-pixbuf-2.0', '2.10.0'))
    msys2_plen = len(msys2_prefix)

    # For each line in the updated loader cache...
    for line in cachedata.splitlines():
        if line.startswith('#'):
            continue
        if line.startswith(prefix):
            line = '"@executable_path/' + LOADER_CACHE_DEST_PATH + line[plen:]
        elif line.startswith(win_prefix):
            line = '"' + LOADER_CACHE_DEST_PATH.replace('/', '\\\\') + line[win_plen:]
        elif line.startswith(msys2_prefix):
            line = ('"' + LOADER_CACHE_DEST_PATH + line[msys2_plen:]).replace('/', '\\\\')
        output_lines.append(line)

    return '\n'.join(output_lines)


def hook(hook_api):
    module_info = GiModuleInfo('GdkPixbuf', '2.0')
    if not module_info.available:
        return

    binaries, datas, hiddenimports = module_info.collect_typelib_data()

    libdir = module_info.get_libdir()

    # Collect GdkPixbuf loaders and generate loader cache file
    gdk_pixbuf_query_loaders = _find_gdk_pixbuf_query_loaders_executable(libdir)
    logger.debug("gdk-pixbuf-query-loaders executable: %s", gdk_pixbuf_query_loaders)
    if not gdk_pixbuf_query_loaders:
        logger.warning("gdk-pixbuf-query-loaders executable not found in GI library directory or in PATH!")
    else:
        # Find all GdkPixbuf loader modules
        loader_libs = _collect_loaders(libdir)

        # Collect discovered loaders
        for lib in loader_libs:
            binaries.append((lib, LOADER_MODULE_DEST_PATH))

        # Generate loader cache; we need to store it to CONF['workpath'] so we can collect it as a data file.
        cachedata = _generate_loader_cache(gdk_pixbuf_query_loaders, libdir, loader_libs)
        cachefile = os.path.join(CONF['workpath'], 'loaders.cache')
        with open(cachefile, 'w', encoding='utf-8') as fp:
            fp.write(cachedata)
        datas.append((cachefile, LOADER_CACHE_DEST_PATH))

    # Collect translations
    lang_list = get_hook_config(hook_api, "gi", "languages")
    if gdk_pixbuf_query_loaders:
        datas += collect_glib_translations('gdk-pixbuf', lang_list)

    hook_api.add_datas(datas)
    hook_api.add_binaries(binaries)
    hook_api.add_imports(*hiddenimports)
