# Copyright (c) 2023 Rackslab
#
# This file is part of RFL.
#
# SPDX-License-Identifier: LGPL-3.0-or-later

"""
This setup.py loads parameter defined in pyproject.toml and calls setuptools.setup()
with these parameters. This script is especially useful to build modern PEP 518 Python
projects that provide pyproject.toml with old versions of pip/python (eg. python 3.6)
without PEP 518 support.
"""

import glob
import os
import sys

from setuptools import setup, find_packages

try:
    import tomllib
except ImportError:
    import tomli as tomllib

header = "rfl-build"

_GLOB_CHARACTERS = {"*", "?", "[", "]", "{", "}"}


def _expand_glob_patterns(patterns, root_dir="."):
    """Expand glob patterns to explicit file paths relative to root_dir.

    Each entry in patterns is either a literal path or a glob. Literal paths are
    returned unchanged apart from normalisation to a forward-slash path relative to
    root_dir. Glob patterns are expanded with glob.iglob; matching paths are sorted
    and returned with the same normalisation.

    Returns a list of source file paths suitable for setuptools setup(data_files=…).
    """
    expanded = []
    for value in patterns:
        if any(char in value for char in _GLOB_CHARACTERS):
            glob_path = os.path.abspath(os.path.join(root_dir, value))
            expanded.extend(
                sorted(
                    os.path.relpath(path, root_dir).replace(os.sep, "/")
                    for path in glob.iglob(glob_path)
                )
            )
        else:
            expanded.append(os.path.relpath(value, root_dir).replace(os.sep, "/"))
    return expanded


with open("pyproject.toml", "rb") as fh:
    pyproject = tomllib.load(fh)

# Nothing much can be done by the script.
if "project" not in pyproject:
    print(f"{header}: project section not found, leaving.")
    sys.exit(0)

# Initialize dict of additional options
kwargs = {}

if "scripts" in pyproject["project"]:
    kwargs["entry_points"] = {
        "console_scripts": [
            f"{executable}={caller}"
            for executable, caller in pyproject["project"]["scripts"].items()
        ]
    }

# Python Packaging User Guide suggests using find_namespace_packages(…) to support
# namespace packages:
#
# https://packaging.python.org/en/latest/guides/packaging-namespace-packages/#native-namespace-packages
#
# Unfortunately, this function is not supported in setuptools < v40.1.0 that is provided
# on systems with Python 3.6 where this setup.py script is required (el8). The
# alternative solution is to explicitely list the packages to install. Then the
# following logic detects if there is a native namespace (when one topfolder declared in
# [tool.setuptools.packages.find] include directive does not contain an __init__.py). In
# this case, the subpackages are explicity declared (without wildcard) in setup(). If no
# namespace is detected, the packages are automatically detected with find_packages().

if "tool" in pyproject and "setuptools" in pyproject["tool"]:
    if "packages" in pyproject["tool"]["setuptools"]:
        if "find" in pyproject["tool"]["setuptools"]["packages"]:
            find_config = pyproject["tool"]["setuptools"]["packages"]["find"]
            # Same defaults as setuptools when keys are omitted from pyproject.toml.
            where = find_config.get("where", ["."])
            # where is a list in pyproject.toml; find_packages() accepts one directory.
            where_dir = where[0] if isinstance(where, list) else where
            include = find_config.get("include", ["*"])
            exclude = find_config.get("exclude", [])

            autofind = True
            packages = []
            for pattern in include:
                if "." not in pattern:
                    continue
                # Namespace check must run under where_dir (eg. src/rfl, not rfl).
                topfolder = os.path.join(where_dir, pattern.split(".", 1)[0])
                if "__init__.py" not in os.listdir(topfolder):
                    autofind = False
                packages.append(pattern.replace("*", ""))
            if autofind:
                if where_dir != ".":
                    # Src layout: honour where/include/exclude like a PEP 517 backend.
                    kwargs["packages"] = find_packages(
                        where=where_dir,
                        include=tuple(include),
                        exclude=tuple(exclude),
                    )
                    # Setuptools 39 needs this; it looks at the project root otherwise.
                    kwargs["package_dir"] = {"": where_dir}
                else:
                    # Flat layout: include is for namespace detection only; do not
                    # filter find_packages().
                    kwargs["packages"] = find_packages()
            else:
                kwargs["packages"] = packages
                if where_dir != ".":
                    # Explicit package names still live under where_dir on src layout.
                    kwargs["package_dir"] = {"": where_dir}
        else:
            # Explicit packages listing without find
            kwargs["packages"] = pyproject["tool"]["setuptools"]["packages"]

        print(f"{header}: packages: {kwargs['packages']}")

    if "package-data" in pyproject["tool"]["setuptools"]:
        kwargs["package_data"] = {
            package: data
            for package, data in pyproject["tool"]["setuptools"]["package-data"].items()
        }
        kwargs["include_package_data"] = True
        print(f"{header}: package data {kwargs['package_data']}")

    if "data-files" in pyproject["tool"]["setuptools"]:
        # Modern setuptools expands globs in [tool.setuptools.data-files] over PEP
        # 517, but setuptools 39 setup(data_files=…) expects an explicit file list.
        # Expand patterns here so legacy pip install matches PEP 517 behaviour.
        kwargs["data_files"] = [
            (dest, _expand_glob_patterns(patterns))
            for dest, patterns in pyproject["tool"]["setuptools"]["data-files"].items()
        ]
        print(f"{header}: data files {kwargs['data_files']}")

if "dependencies" in pyproject["project"]:
    kwargs["install_requires"] = pyproject["project"]["dependencies"]

if "optional-dependencies" in pyproject["project"]:
    kwargs["extras_require"] = {
        extra: deps
        for extra, deps in pyproject["project"]["optional-dependencies"].items()
    }
    print(f"{header}: extras require {kwargs['extras_require']}")

if "text" in pyproject["project"]["license"]:
    kwargs["license"] = pyproject["project"]["license"]["text"]
elif "file" in pyproject["project"]["license"]:
    kwargs["license_files"] = [pyproject["project"]["license"]["file"]]

if "urls" in pyproject["project"]:
    kwargs["url"] = pyproject["project"]["urls"]["Homepage"]

authors = pyproject["project"].get("authors") or []
if authors:
    if "name" in authors[0]:
        kwargs["author"] = authors[0]["name"]
    if "email" in authors[0]:
        kwargs["author_email"] = authors[0]["email"]

setup(
    name=pyproject["project"]["name"],
    version=pyproject["project"]["version"],
    platforms=["GNU/Linux"],
    **kwargs,
)
