# Copyright 2019-2021 Collabora Ltd.
# SPDX-License-Identifier: BSD-3-Clause
# (See debian/copyright)

import fnmatch
import os
try:
    from typing import (
        Any,
        List,
        Mapping,
    )
    # placate pyflakes
    Any, List, Mapping
except ImportError:
    pass

import json


class Library:
    """
    A shared library.
    """

    def __init__(
        self,
        data        # type: Any
    ) -> None:
        """
        Construct from the data found in steam-runtime-abi.yaml or
        steam-runtime-abi.json, which is one of:

        - a string, the SONAME of the shared library
        - a dict with exactly one key, the SONAME. The corresponding
            value is a dict which may contain these optional keys:
            - deb: Name of the Debian package containing this shared
                library. The default is as per Debian Policy §8.1,
                e.g. libattr.so.1 -> libattr1, libX11.so.6 -> libx11-6.
            - aliases: Alternative names for this library
            - public_symbol_versions: List of patterns to match public
                symbol versions. Prepend the pattern with '!' to
                indicate private symbol versions. The list should end
                with a wildcard "everything else is public '*'" or a
                wildcard "everything else is private '!*'".
            - public_symbols: List of patterns to match public symbols.
                Prepend the pattern with '!' to indicate private
                symbols. The list should end with a wildcard
                "everything else is public '*'" or a wildcard
                "everything else is private '!*'".
        """
        if isinstance(data, str):
            data = {data: {}}

        assert isinstance(data, dict)
        assert len(data) == 1
        for k, v in data.items():
            self.soname = k                         # type: str
            self.deb = v.get('deb', '')             # type: str
            self.aliases = v.get('aliases', [])     # type: List[str]
            self.check_loadable = v.get(
                'check_loadable', True)             # type: bool
            self.compare_by = v.get(
                'compare_by', []
            )                                       # type: List[str]
            self.from_host_system = v.get(
                'from_host_system', False)          # type: bool
            self.generate_dependency = v.get(
                'generate_dependency', True)        # type: bool
            self.multiarch = v.get(
                'multiarch', False,
            )                                       # type: bool
            self.not_architectures = v.get(
                'not_architectures', [])            # type: List[str]
            self.only_architectures = v.get(
                'only_architectures', [])           # type: List[str]
            self.public_symbol_versions = v.get(
                'public_symbol_versions', []
            )                                       # type: List[str]
            self.public_symbols = v.get(
                'public_symbols', []
            )                                       # type: List[str]
            self.track_abi = v.get(
                'track_abi',
                ('public_symbols' in v or 'public_symbol_versions' in v),
            )                                       # type: bool

        if not self.deb:
            # Derive the canonical package name from the SONAME according
            # to the rules in Debian Policy §8.1 [3]
            if '.so.' in self.soname:
                head, tail = self.soname.rsplit('.so.', 1)

                if head[-1].isdecimal():
                    self.deb = head + '-' + tail
                else:
                    self.deb = head + tail

            elif self.soname.endswith('.so'):
                self.deb = self.soname[:-3]

            else:
                raise ValueError('Unexpected SONAME {}'.format(self.soname))

            self.deb = self.deb.lower().replace('_', '-')

    def __is_public(
        self,
        s,          # type: str
        v,          # type: str
        patterns,   # type: List[str]
    ) -> bool:
        for pattern in patterns:
            if pattern == '!':
                # This indicates that all patterns after this point
                # are guesswork, so we don't allow them here, only at
                # runtime
                break

            if pattern.startswith('!'):
                result = False
                pattern = pattern[1:]
            else:
                result = True

            if fnmatch.fnmatchcase(s + '@' + v, pattern):
                return result

            if fnmatch.fnmatchcase(s, pattern):
                return result

        raise RuntimeError(
            'Cannot decide whether {} in {} is public or private'.format(
                s,
                self.soname,
            )
        )

    def symbol_version_is_public(self, version: str) -> bool:
        return self.__is_public(version, version, self.public_symbol_versions)

    def symbol_is_public(self, symbol: str, version: str) -> bool:
        return self.__is_public(symbol, version, self.public_symbols)


class Architecture:
    """
    An architecture.
    """
    def __init__(
        self,
        multiarch_tuple: str,
        data                    # type: Any
    ):
        assert isinstance(data, dict)

        self.multiarch_tuple = multiarch_tuple
        self.dpkg_name = data.get(
            'dpkg_name',
            multiarch_tuple.split('-')[0]
        )   # type: str
        self.subset = bool(data.get('subset', False))


class AbiData:
    """
    The ABI of the Steam Runtime.
    """
    def __init__(
        self,
        filename: str = '',
        native_architecture: str = ''
    ) -> None:
        self.shared_libraries = {}      # type: Mapping[str, Library]
        self.architectures = {}         # type: Mapping[str, Architecture]

        if not filename:
            filename = os.path.join(
                os.path.dirname(__file__),
                'steam-runtime-abi.json',
            )

        with open(filename, 'r') as reader:
            if filename.endswith('.yaml'):
                import yaml
                self.data = yaml.safe_load(reader)      # type: Any
            else:
                self.data = json.load(reader)

        for arch, data in self.data['architectures'].items():
            self.architectures[arch] = Architecture(arch, data)

        if (
            native_architecture
            and native_architecture not in self.architectures
        ):
            self.architectures[native_architecture] = Architecture(
                native_architecture,
                dict(multiarch_tuple=native_architecture),
            )

        for item in self.data['shared_libraries']:
            lib = Library(item)
            assert lib.soname not in self.shared_libraries
            self.shared_libraries[lib.soname] = lib
