# SPDX-License-Identifier: BSD-2-Clause

# Copyright (c) 2026 Phil Thompson <phil@riverbankcomputing.com>


import os

from .exceptions import UserException
from .installable import Installable
from .py_versions import OLDEST_SUPPORTED_MINOR
from .version import SIP_VERSION_STR


class Buildable:
    """ Encapsulate the components used to build something that can be
    installed.
    """

    def __init__(self, project, name):
        """ Initialise the buildable. """

        self.project = project
        self.name = name
        self.builder_settings = []
        self.installables = []

        self.build_dir = os.path.join(project.build_dir, name)
        os.makedirs(self.build_dir, exist_ok=True)


class BuildableFromSources(Buildable):
    """ Encapsulate the sources used to build an extension module, executable
    etc.
    """

    def __init__(self, project, name, target, *, uses_limited_api=False,
            gil_disabled=False):
        """ Initialise the buildable. """

        super().__init__(project, name)

        if uses_limited_api and not project.sip_module:
            raise UserException(
                    "{0} cannot use the limited API without using a shared "
                    "'sip' module".format(name))

        self.target = target
        self.uses_limited_api = uses_limited_api
        self.gil_disabled = gil_disabled

        self.define_macros = []
        self.sources = []
        self.headers = []
        self.include_dirs = []
        self.libraries = []
        self.library_dirs = []
        self.extra_compile_args = []
        self.extra_link_args = []
        self.extra_objects = []
        self.debug = False
        self.cpp_standard = None

        if self.uses_limited_api:
            major, minor, micro = project.limited_abi_version
            hex_version = '0x{0:02x}{1:02x}{2:02x}00'.format(major, minor,
                    micro)

            self.define_macros.append('Py_LIMITED_API=' + hex_version)

            if self.gil_disabled and (major, minor) >= (3, 15):
                self.define_macros.append('Py_TARGET_ABI3T=' + hex_version)

    def make_names_relative(self):
        """ Make all file and directory names relative to the build directory.
        """

        # Make the file names relative to the build directory.
        self.include_dirs = self._relative_names(self.include_dirs)
        self.headers = self._relative_names(self.headers)
        self.sources = self._relative_names(self.sources)
        self.library_dirs = self._relative_names(self.library_dirs)

    def _relative_names(self, names):
        """ Return a list of times made relative to build directory.  Note that
        we only really do this for cosmetic reasons to simplify what the user
        might see.
        """

        rel_names = []

        for fn in names:
            try:
                common = os.path.commonpath([fn, self.build_dir])
                _, common = os.path.splitdrive(common)

                if len(common) > 1:
                    # Only convert to a relative name if there is at least one
                    # parent directory in common.
                    fn = os.path.relpath(fn, self.build_dir)
            except ValueError:
                # This is most likely to happen if the build directory is on a
                # different Windows drive.
                pass

            rel_names.append(fn)

        return rel_names


class BuildableExecutable(BuildableFromSources):
    """ Encapsulate the sources used to build an executable. """


class BuildableModule(BuildableFromSources):
    """ Encapsulate the sources used to build an extension module. """

    def __init__(self, project, name, fq_name, *, uses_limited_api=False,
            gil_disabled=False):
        """ Initialise the sources. """

        super().__init__(project, name, fq_name.split('.')[-1],
                uses_limited_api=uses_limited_api, gil_disabled=gil_disabled)

        self.fq_name = fq_name

        self.exceptions = False
        self.static = False

    def get_install_subdir(self):
        """ Return the sub-directory the extension module should be installed
        in relative to the eventual target directory.
        """

        return os.sep.join(self.fq_name.split('.')[:-1])

    def get_module_extension(self):
        """ Return the filename extension that a module should have. """

        if self.project.py_platform == 'win32':
            return '.pyd'

        from importlib.machinery import EXTENSION_SUFFIXES

        if self.uses_limited_api:
            # Prefer an arch-specific extension if present (added in Python
            # v3.15).
            abi3 = abi3t = None

            for es in EXTENSION_SUFFIXES:
                if es.startswith('.abi3t-'):
                    abi3t = es
                elif abi3t is None and es.startswith('.abi3t'):
                    abi3t = es
                elif es.startswith('.abi3-'):
                    abi3 = es
                elif abi3 is None and es.startswith('.abi3'):
                    abi3 = es

            if self.gil_disabled and abi3t is not None:
                return abi3t

            if abi3 is not None:
                return abi3

        return EXTENSION_SUFFIXES[0]


class BuildableBindings(BuildableModule):
    """ Encapsulate the sources used to build the extension module for a set of
    bindings.
    """

    def __init__(self, bindings, fq_name, *, uses_limited_api=False,
            gil_disabled=False):
        """ Initialise the sources. """

        super().__init__(bindings.project, fq_name.split('.')[-1], fq_name,
                uses_limited_api=uses_limited_api, gil_disabled=gil_disabled)

        self.bindings = bindings

        self.sip_module_configuration = None

    def get_bindings_installable(self, name):
        """ Return an installable for the buildable's bindings directory. """

        target_subdir = os.path.join(self.project.get_bindings_dir(),
                self.name)

        return Installable(name, target_subdir=target_subdir)

    def write_configuration(self, bindings_dir):
        """ Write the configuration of the bindings and add it as an
        installable.
        """

        # Make sure the bindings directory exists.
        bindings_dir = os.path.join(bindings_dir, self.name)
        os.makedirs(bindings_dir, exist_ok=True)

        config_path = os.path.join(bindings_dir, self.name + '.toml')

        # Create an installable for the configuration file.
        installable = self.get_bindings_installable('config')
        installable.files.append(config_path)
        self.installables.append(installable)

        # Write the configuration file.
        bindings = self.bindings

        with open(config_path, 'w') as cf:
            sip_version_str = SIP_VERSION_STR if self.project.version_info else ''
            abi_major, abi_minor = self.project.abi_version
            tags = ', '.join(['"{}"'.format(t) for t in bindings.tags])
            disabled = ', '.join(
                    ['"{}"'.format(f) for f in bindings.disabled_features])

            cf.write(f"# Automatically generated configuration for {self.fq_name}.\n")
            cf.write(f'''
sip-version = "{sip_version_str}"
sip-abi-version = "{abi_major}.{abi_minor}"
module-tags = [{tags}]
module-disabled-features = [{disabled}]
''')

            if self.project.abi_version[0] >= 14:
                cf.write(f'sip-module-configuration = {self.sip_module_configuration}\n')
