#!/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 sys
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


def get_soname(libfile: str) -> str:
    """Helper: get soname from library"""
    res = check_output(["objdump", "-p", libfile]).decode("utf-8")
    for line in res.split("\n"):
        l = line.split()
        if len(l) == 2 and l[0] == "SONAME":
            bits = l[1].split(".")
            major, soname = bits[2], l[1]
            ext = libfile[libfile.rfind(".so") + 3 :]
            return (major, soname, ext)


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

    def __init__(self, options):
        super().__init__(options, "fortran-lib", "dh_fortran_lib")
        self.remapped_libs = {}

    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, target_dest):
        dest = f"{target_pkg}/{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}/{target_dest}")
        self.doit(["cp", "--reflink=auto", "-a", orig_libname, dest])

    
    def process_shared_lib(self, orig_libname, libfile, target_pkg, destdir):
        (major, soname, ext) = get_soname(orig_libname)
        base = basename(orig_libname)
        abi = cmplrs.get_abi_vendor(self.options.flavor)
        libname_prefix = libfile[: libfile.rfind(".so")]
        new_libname = f"{libname_prefix}-{abi}.so{ext}"
        soname_prefix = soname[: soname.rfind(".so")]
        new_soname = f"{soname_prefix}-{abi}.so.{major}"
        soname_dest = f"/usr/lib/{cmplrs.multiarch}/{new_soname}"
        libname_dest = f"/usr/lib/{cmplrs.multiarch}/{new_libname}"

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

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

    def process_file(self, pkg, libfile, target_pkg, target_dest=None):
        cli.verbose_print(
            f"process_file: name {libfile} target_pkg {target_pkg}  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, 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, destdir)


@click.command(
    context_settings=dict(
        ignore_unknown_options=True,
    )
)
@click.option("--flavor", help="Fortran flavor (eg gfortran-14, flang-21)")
@click.option("--preferred", help="Preferred flavor when installing, eg gfortran-15")
@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-gfortran.so.1.2
    =back

    Symlinks also get renamed:

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

    A  compilation link is added per vendor /ABI :
            $(LIBDIR)/fortran/gfortran/libfiat.so -> $(LIBDIR)/libfiat-gfortran.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-gfortran.so.1.2 | grep SONAME
                    0x000000000000000e (SONAME)             Library soname: [libfiat-gfortran.so.1]

    Note this is defined per ABI-vendor: if flavor=gfortran-14, the link is named for 'gfortran'.
    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/gfortran/libfiat.a

    The consequence of this is that any library that builds against I<libfiat> with appropriate search paths
    set will use *libfiat-gfortran* 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 --flavor=$(FLAVOR) [--preferred=$(PREFERRED)] $(BUILDDIR)/XXX/libfiat.so.1

    The files are installed in the sourcedir (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.verbose_print(f"dh_fortran_lib called with files {files} kwargs {kwargs}")
    cli.validate_flavor(kwargs["flavor"])
    cli.validate_flavor(kwargs["preferred"])

    # Get defaults if not defined
    flavor = cmplrs.get_flavor(kwargs["flavor"])
    kwargs.update(
        {
            "flavor": flavor,
            "preferred": cmplrs.get_preferred(kwargs["preferred"], flavor),
        }
    )
    d = LibFileHelper(dh.build_options(**kwargs))
    d.process_and_move_files(*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"])
