#!/usr/bin/python3
"""
Library  handling for Fortran  dh_fortran_lib

Copyright (C) 2025 Alastair McKinstry <mckinstry@debian.org>
Released under the GPL-3 Gnu Public License.
"""

import os
import click
from subprocess import check_output
from os.path import exists, basename, islink
import dhfortran.compilers as cmplrs
import dhfortran.debhelper as dh
import dhfortran.cli as cli
import magic
import fnmatch


def get_soname(libfile: str) -> str:
    """Helper: get soname from library
    shared libs in Debian _should_ have a SONAME, SOVER
    """
    res = check_output(["objdump", "-p", libfile]).decode("utf-8")
    for line in res.split("\n"):
        lk = line.split()
        if len(lk) == 2 and lk[0] == "SONAME":
            bits = lk[1].split(".")
            major, soname = bits[2], lk[1]
            ext = libfile[libfile.rfind(".so") + 3 :]
            if soname.endswith(".so"):
                # Format libxxx-2.0.0.so
                major = ""
            return (major, soname, ext)
    return None


##
## Library-file -specific interface for debhelper
##
class LibFileHelper(dh.DhFortranHelper):

    def __init__(self, options):
        super().__init__(options, "fortran-lib", "dh_fortran_lib")
        if self.needs_orig_libs():
            self.options.include_orig_libs = True

        self.remapped_libs = {}

    def needs_orig_libs(self) -> bool:
        """Do we need to ship the original libs, ie are any executables/libs linked against them ?"""
        # TODO
        return False

    def autogenerate_pkg_filelist(
        self, searchdirs: list[str], dest=None
    ) -> list[tuple[str, str, str]]:
        """For automatic mode, find  lib files to install"""

        worklist = []
        libs_pkg, libdevel_pkg = self.libs_pkgs[0], self.libdevel_pkgs[0]
        for d in searchdirs:
            if d is None:
                continue
            for root, dir, files in os.walk(d):
                for item in fnmatch.filter(files, "*.a"):
                    f = f"{root}/{item}"
                    if "ar archive" in magic.from_file(f):
                        worklist.append((libdevel_pkg, f, dest))
                for item in fnmatch.filter(files, "*.so*"):
                    if "shared object" in magic.from_file(f):
                        worklist.append((libs_pkg, f, dest))
                    if item.endswith(".so") and "symbolic link" in magic.from_file(f):
                        worklist.append((libdevel_pkg, f, dest))
        return worklist

    def can_work_automatically(self, *cmdline_files: list[str]) -> bool:
        """Should we automatically automagically detect lib files, or work on specified packages?
        Called by Debhelper.process_and_move_files()

        Run if we haven't run already, haven't been passed cmdline files,
        """
        if self.options.o__buildsystem != "fortran":
            cli.debug_print(
                "dh_fortran_lib : buildsystem not fortran; not running automatically"
            )
            return False
        if len(cmdline_files) > 0:
            cli.debug_print("dh_fortran_lib not automatic: files passed on cmdlline")
            return False
        for p in self.packages:
            if self.has_acted_on_package(p):
                cli.debug_print(
                    "dh_fortran_lib not automatic: has acted on pkgs already"
                )
                return False
        if len(self.config_file_pkgs) != 0:
            cli.verbose_print(
                "dh_fortran won't run automatically: user has specified fortran-lib files"
            )
            return False
        if len(self.libdevel_pkgs) != 1 and len(self.libs_pkgs) != 1:
            cli.debug_print(
                "dh_fortran_lib: Need 1 lib, 1 libdevel pkg to run automatically"
            )
            return False
        cli.verbose_print(
            "No fortran-lib files found, 1 libdevel package, 1 libs pkg: may install libs automatically"
        )
        return True

    def compute_dest(self, libfile, target_dest=None):
        """Where does lib go ? Should be called by base DebHelper() to move files"""

        abi = cmplrs.get_abi_vendor(self.options.flavor)
        flibdir = f"/usr/lib/{cmplrs.multiarch}/fortran/{abi}"
        if target_dest is None:
            return flibdir
        if target_dest.startswith("/"):
            return target_dest
        else:
            return f"{flibdir}/{target_dest}"

    def process_static_lib(self, orig_libname, libfile, target_pkg_dir, target_dest):
        dest = f"{target_pkg_dir}/{target_dest}/{libfile}"
        if exists(dest):
            if self.options.flavor == self.options.preferred:
                cli.verbose_print(f"{dest} exists but will be overwritten (preferred)")
            else:
                cli.verbose_print(f"{dest} exists and will not be overwritten")
                return

        self.install_dir(f"{target_pkg_dir}/{target_dest}")
        self.doit(["cp", "--reflink=auto", "-a", orig_libname, dest])
        if (
            self.options.include_orig_libs
            and self.options.flavor == cmplrs.get_fc_default()
        ):
            dest = f"{target_pkg_dir}//usr/lib/{cmplrs.multiarch}"
            self.install_dir(dest)
            self.doit(["cp", "--reflink=auto", "-a", orig_libname, dest])

    def process_shared_lib(self, orig_libname, libfile, target_pkg_dir, destdir):
        # TODO. This will need to be updated for cross-compile
        (major, soname, ext) = get_soname(orig_libname)
        abi = cmplrs.get_abi_vendor(self.options.flavor)
        soname_prefix = soname[: soname.rfind(".so")]
        if major == "":
            new_soname = f"{soname_prefix}-{abi}.so"
        else:
            new_soname = f"{soname_prefix}-{abi}.so.{major}"
        libname_prefix = libfile[: libfile.rfind(".so")]
        new_libname = f"{libname_prefix}-{abi}.so{ext}"
        new_soname_dest = f"/usr/lib/{cmplrs.multiarch}/{new_soname}"
        new_libname_dest = f"/usr/lib/{cmplrs.multiarch}/{new_libname}"

        if (
            self.options.include_orig_libs
            and self.options.flavor == cmplrs.get_fc_default()
        ):
            keep_orig = True
            orig_libname_dest = f"/usr/lib/{cmplrs.multiarch}/{libname_prefix}.so{ext}"
        else:
            keep_orig = False

        if islink(orig_libname):
            if orig_libname.endswith(".so"):
                self.install_dir(f"{target_pkg_dir}/{destdir}")
                self.install_dir(f"{target_pkg_dir}/usr/lib/{cmplrs.multiarch}")
                self.make_symlink(
                    f"usr/lib/{cmplrs.multiarch}/{libname_prefix}-{abi}.so",
                    new_soname_dest,
                    target_pkg_dir,
                )
                self.make_symlink(
                    f"{destdir}/{libname_prefix}.so", new_soname_dest, target_pkg_dir
                )
                if keep_orig:
                    self.make_symlink(
                        orig_libname_dest,
                        f"/usr/lib/{cmplrs.multiarch}/{soname}",
                        target_pkg_dir,
                    )
        else:
            if not magic.from_file(orig_libname).startswith("ELF "):
                cli.warning(f"{orig_libname} is not an ELF file; ignoring")
                return
            if exists(new_libname_dest):
                if self.options.flavor == self.options.preferred:
                    cli.verbose_print(
                        f"{new_libname_dest} exists but will be overwritten (preferred)"
                    )
                else:
                    cli.verbose_print(
                        f"{new_libname_dest} exists and will not be overwritten"
                    )
                    return
            self.install_dir(f"{target_pkg_dir}/usr/lib/{cmplrs.multiarch}")
            self.doit(
                [
                    "patchelf",
                    "--set-soname",
                    new_soname,
                    "--output",
                    f"{target_pkg_dir}/{new_libname_dest}",
                    orig_libname,
                ]
            )
            #  SONAME link, eg libfoo.so.1 -> libfoo.so.1.2
            self.install_dir(f"{target_pkg_dir}/usr/lib/{cmplrs.multiarch}")
            self.make_symlink(
                f"/usr/lib/{cmplrs.multiarch}/{new_soname}",
                new_libname_dest,
                target_pkg_dir,
            )
            if keep_orig:
                self.doit(
                    ["cp", "-f", orig_libname, f"{target_pkg_dir}/{orig_libname_dest}"]
                )
                self.make_symlink(
                    f"/usr/lib/{cmplrs.multiarch}/{soname}",
                    orig_libname_dest,
                    target_pkg_dir,
                )

        if orig_libname.endswith(".so"):
            # For cmake, pkgconf
            self.remapped_libs[basename(orig_libname), abi] = f"{destdir}/{new_libname}"

    def process_file(
        self,
        pkg: str,
        libfile: str,
        target_pkg_dir: str,
        target_dest: str | None = None,
    ):
        """Called by Debhelper.process_and_move_files()"""
        cli.debug_print(
            f"process_file: name {libfile} target_pkg_dir {target_pkg_dir}  dest {target_dest}"
        )
        base = basename(libfile)
        destdir = self.compute_dest(libfile, target_dest)

        if libfile.endswith(".a"):
            self.process_static_lib(libfile, base, target_pkg_dir, destdir)
            return
        if libfile.find(".so") == -1:
            cli.warning(f"file {libfile} is not an ELF file; ignoring")
            return
        self.process_shared_lib(libfile, base, target_pkg_dir, destdir)


@click.command(
    context_settings=dict(
        ignore_unknown_options=False,
    )
)
@click.option("--fc", help="Fortran compiler flavor (eg gfortran-14, flang-21)")
@click.option("--flavor", help="Fortran compiler flavor. DEPRECATED, use --fc instead")
@click.option(
    "--preferred", help="Preferred compiler flavor when installing, eg gfortran-15"
)
@click.option(
    "--include-orig-libs",
    help="Include original libs as well as moved versions",
    is_flag=True,
    default=True,
)
@click.argument("files", nargs=-1, type=click.UNPROCESSED)
@cli.debhelper_common_args
def dh_fortran_lib(files, *args, **kwargs):
    """
    *dh_fortran_lib* is a debhelper program that enables multiple compiler flavous of a Fortran library
    to be installed in parallel by mangling the library filename and SONAME.

    Fortran libraries compiled by different compilers are not expected to be ABI-compatible, and hence
    for multiple compilers to be supported simultaneously the libraries must be named differently,
    and shared libraries need to include the  compiler flavor in the SONAME.

    *dh_fortran_lib* makes this possible without changes being necessary to the upstream library code.

    It does this by renaming a library, for example:

            $(LIBDIR)/libfiat.so.1.2 => $(LIBDIR)/libfiat-flang.so.1.2
    =back

    Symlinks also get renamed:

            $(LIBDIR)/libfiat.so.1 => $(LIBDIR)/libfiat-flang.so.1

    A  compilation link is added per vendor /ABI :
            $(LIBDIR)/fortran/gfortran/libfiat.so -> $(LIBDIR)/libfiat-flang.so.1.2

    and the SONAME in the ELF file is changed:

            $ readelf -a $(LIBDIR)/libfiat.so.1.2 | grep SONAME
                    0x000000000000000e (SONAME)             Library soname: [libfiat.so.1]
            $ readelf -a $(LIBDIR)/libfiat-flang.so.1.2 | grep SONAME
                    0x000000000000000e (SONAME)             Library soname: [libfiat-flang.so.1]

    Note this is defined per ABI-vendor: if fc=gfortran-14, the link is named for 'gnu'.
    If the links are already defined, then the preferred flavor is examined. For each vendor, dh_fortran_lib
    has a preferred flavor: eg gfortran-14 or gfortran-15. If the flavor of the library provided is
    is the preferred flavor, the library is replaced if it exists. The option --preferred
    can be used to override the default choice.

    For static libraries, we place them in a ABI-specific directory:

            $(LIBDIR)/fortran/gnu/libfiat.a

    The consequence of this is that any library that builds against I<libfiat> with appropriate search paths
    set will use *libfiat-gnu* instead. This enables parallel builds with multiple compiler flavors to
    be installed simultaneously.

    == USAGE

    The expected usage is that this will be called in debian/rules as:

            dh_fortran_lib --fc=$(FLAVOR) [--preferred=$(PREFERRED)] $(BUILDDIR)/XXX/libfiat.so.1

    The files are installed in the tmpdir (usually debian/tmp) by default.

    When installing multiple flavors with the same ABI (eg gfortran-14, gfortran-15), you can use the
    --preferred option to state which library to install; in this case, if FLAVOR==PREFERRED, then
    this library is installed even if an existing library with the same ABI is present.
    """

    cli.debug_print(f"dh_fortran_lib called with files {files} kwargs {kwargs}")
    cli.validate_flavor(kwargs["flavor"])
    cli.validate_compiler(kwargs["fc"])
    fc, flavor = kwargs["fc"], kwargs["flavor"]
    if fc is not None:
        flavor = fc
    # Get defaults if not defined.
    flavor, arch = cmplrs.get_fc_flavor_arch(flavor)

    kwargs.update(
        {
            "flavor": flavor,
            "preferred": cmplrs.get_preferred(kwargs["preferred"], flavor),
        }
    )

    d = LibFileHelper(dh.build_options(**kwargs))

    searchdirs = [d.options.builddir, d.options.tmpdir, "."]
    d.process_and_move_files(searchdirs, *files)
    d.install_dir("debian/.debhelper")
    with open("debian/.debhelper/remappped_fortran_libs", "wt+") as f:
        print(d.remapped_libs, file=f)


if __name__ == "__main__":
    import pytest

    pytest.main(["tests/lib.py"])
