"""Generate and work with PEP 425 Compatibility Tags."""
from __future__ import absolute_import

import distutils.util
import logging
import platform
import re
import sys
import sysconfig
from collections import OrderedDict

import pip._internal.utils.glibc
from pip._internal.utils.typing import MYPY_CHECK_RUNNING

if MYPY_CHECK_RUNNING:
    from typing import (
        Callable, Dict, Iterable, Iterator, List, Optional, Set, Tuple, Union
    )

    Pep425Tag = Tuple[str, str, str]

logger = logging.getLogger(__name__)

_osx_arch_pat = re.compile(r'(.+)_(\d+)_(\d+)_(.+)')


def format_tag(file_tag):
    # type: (Tuple[str, ...]) -> str
    """Format three tags in the form "<python_tag>-<abi_tag>-<platform_tag>".

    :param file_tag: A 3-tuple of tags (python_tag, abi_tag, platform_tag).
    """
    return '-'.join(file_tag)


def get_config_var(var):
    # type: (str) -> Optional[str]
    return sysconfig.get_config_var(var)


def get_abbr_impl():
    # type: () -> str
    """Return abbreviated implementation name."""
    if hasattr(sys, 'pypy_version_info'):
        pyimpl = 'pp'
    elif sys.platform.startswith('java'):
        pyimpl = 'jy'
    elif sys.platform == 'cli':
        pyimpl = 'ip'
    else:
        pyimpl = 'cp'
    return pyimpl


interpreter_name = get_abbr_impl


def get_impl_ver():
    # type: () -> str
    """Return implementation version."""
    impl_ver = get_config_var("py_version_nodot")
    if not impl_ver or get_abbr_impl() == 'pp':
        impl_ver = ''.join(map(str, get_impl_version_info()))
    return impl_ver


interpreter_version = get_impl_ver


def get_impl_version_info():
    # type: () -> Tuple[int, ...]
    """Return sys.version_info-like tuple for use in decrementing the minor
    version."""
    if get_abbr_impl() == 'pp':
        # as per https://github.com/pypa/pip/issues/2882
        # attrs exist only on pypy
        return (sys.version_info[0],
                sys.pypy_version_info.major,  # type: ignore
                sys.pypy_version_info.minor)  # type: ignore
    else:
        return sys.version_info[0], sys.version_info[1]


def get_flag(var, fallback, expected=True, warn=True):
    # type: (str, Callable[..., bool], Union[bool, int], bool) -> bool
    """Use a fallback method for determining SOABI flags if the needed config
    var is unset or unavailable."""
    val = get_config_var(var)
    if val is None:
        if warn:
            logger.debug("Config variable '%s' is unset, Python ABI tag may "
                         "be incorrect", var)
        return fallback()
    return val == expected


def get_abi_tag():
    # type: () -> Optional[str]
    """Return the ABI tag based on SOABI (if available) or emulate SOABI
    (CPython 2, PyPy)."""
    soabi = get_config_var('SOABI')
    impl = get_abbr_impl()
    abi = None  # type: Optional[str]

    if not soabi and impl in {'cp', 'pp'} and hasattr(sys, 'maxunicode'):
        d = ''
        m = ''
        u = ''
        is_cpython = (impl == 'cp')
        if get_flag(
                'Py_DEBUG', lambda: hasattr(sys, 'gettotalrefcount'),
                warn=is_cpython):
            d = 'd'
        if sys.version_info < (3, 8) and get_flag(
                'WITH_PYMALLOC', lambda: is_cpython, warn=is_cpython):
            m = 'm'
        if sys.version_info < (3, 3) and get_flag(
                'Py_UNICODE_SIZE', lambda: sys.maxunicode == 0x10ffff,
                expected=4, warn=is_cpython):
            u = 'u'
        abi = '%s%s%s%s%s' % (impl, get_impl_ver(), d, m, u)
    elif soabi and soabi.startswith('cpython-'):
        abi = 'cp' + soabi.split('-')[1]
    elif soabi:
        abi = soabi.replace('.', '_').replace('-', '_')

    return abi


def _is_running_32bit():
    # type: () -> bool
    return sys.maxsize == 2147483647


def get_platform():
    # type: () -> str
    """Return our platform name 'win32', 'linux_x86_64'"""
    if sys.platform == 'darwin':
        # distutils.util.get_platform() returns the release based on the value
        # of MACOSX_DEPLOYMENT_TARGET on which Python was built, which may
        # be significantly older than the user's current machine.
        release, _, machine = platform.mac_ver()
        split_ver = release.split('.')

        if machine == "x86_64" and _is_running_32bit():
            machine = "i386"
        elif machine == "ppc64" and _is_running_32bit():
            machine = "ppc"

        return 'macosx_{}_{}_{}'.format(split_ver[0], split_ver[1], machine)

    # XXX remove distutils dependency
    result = distutils.util.get_platform().replace('.', '_').replace('-', '_')
    if result == "linux_x86_64" and _is_running_32bit():
        # 32 bit Python program (running on a 64 bit Linux): pip should only
        # install and run 32 bit compiled extensions in that case.
        result = "linux_i686"

    return result


def is_linux_armhf():
    # type: () -> bool
    if get_platform() != "linux_armv7l":
        return False
    # hard-float ABI can be detected from the ELF header of the running
    # process
    try:
        with open(sys.executable, 'rb') as f:
            elf_header_raw = f.read(40)  # read 40 first bytes of ELF header
    except (IOError, OSError, TypeError):
        return False
    if elf_header_raw is None or len(elf_header_raw) < 40:
        return False
    if isinstance(elf_header_raw, str):
        elf_header = [ord(c) for c in elf_header_raw]
    else:
        elf_header = [b for b in elf_header_raw]
    result = elf_header[0:4] == [0x7f, 0x45, 0x4c, 0x46]  # ELF magic number
    result &= elf_header[4:5] == [1]  # 32-bit ELF
    result &= elf_header[5:6] == [1]  # little-endian
    result &= elf_header[18:20] == [0x28, 0]  # ARM machine
    result &= elf_header[39:40] == [5]  # ARM EABIv5
    result &= (elf_header[37:38][0] & 4) == 4  # EF_ARM_ABI_FLOAT_HARD
    return result


def is_manylinux1_compatible():
    # type: () -> bool
    # Only Linux, and only x86-64 / i686
    if get_platform() not in {"linux_x86_64", "linux_i686"}:
        return False

    # Check for presence of _manylinux module
    try:
        import _manylinux
        return bool(_manylinux.manylinux1_compatible)
    except (ImportError, AttributeError):
        # Fall through to heuristic check below
        pass

    # Check glibc version. CentOS 5 uses glibc 2.5.
    return pip._internal.utils.glibc.have_compatible_glibc(2, 5)


def is_manylinux2010_compatible():
    # type: () -> bool
    # Only Linux, and only x86-64 / i686
    if get_platform() not in {"linux_x86_64", "linux_i686"}:
        return False

    # Check for presence of _manylinux module
    try:
        import _manylinux
        return bool(_manylinux.manylinux2010_compatible)
    except (ImportError, AttributeError):
        # Fall through to heuristic check below
        pass

    # Check glibc version. CentOS 6 uses glibc 2.12.
    return pip._internal.utils.glibc.have_compatible_glibc(2, 12)


def is_manylinux2014_compatible():
    # type: () -> bool
    # Only Linux, and only supported architectures
    platform = get_platform()
    if platform not in {"linux_x86_64", "linux_i686", "linux_aarch64",
                        "linux_armv7l", "linux_ppc64", "linux_ppc64le",
                        "linux_s390x"}:
        return False

    # check for hard-float ABI in case we're running linux_armv7l not to
    # install hard-float ABI wheel in a soft-float ABI environment
    if platform == "linux_armv7l" and not is_linux_armhf():
        return False

    # Check for presence of _manylinux module
    try:
        import _manylinux
        return bool(_manylinux.manylinux2014_compatible)
    except (ImportError, AttributeError):
        # Fall through to heuristic check below
        pass

    # Check glibc version. CentOS 7 uses glibc 2.17.
    return pip._internal.utils.glibc.have_compatible_glibc(2, 17)


def get_darwin_arches(major, minor, machine):
    # type: (int, int, str) -> List[str]
    """Return a list of supported arches (including group arches) for
    the given major, minor and machine architecture of an macOS machine.
    """
    arches = []

    def _supports_arch(major, minor, arch):
        # type: (int, int, str) -> bool
        # Looking at the application support for macOS versions in the chart
        # provided by https://en.wikipedia.org/wiki/OS_X#Versions it appears
        # our timeline looks roughly like:
        #
        # 10.0 - Introduces ppc support.
        # 10.4 - Introduces ppc64, i386, and x86_64 support, however the ppc64
        #        and x86_64 support is CLI only, and cannot be used for GUI
        #        applications.
        # 10.5 - Extends ppc64 and x86_64 support to cover GUI applications.
        # 10.6 - Drops support for ppc64
        # 10.7 - Drops support for ppc
        #
        # Given that we do not know if we're installing a CLI or a GUI
        # application, we must be conservative and assume it might be a GUI
        # application and behave as if ppc64 and x86_64 support did not occur
        # until 10.5.
        #
        # Note: The above information is taken from the "Application support"
        #       column in the chart not the "Processor support" since I believe
        #       that we care about what instruction sets an application can use
        #       not which processors the OS supports.
        if arch == 'ppc':
            return (major, minor) <= (10, 5)
        if arch == 'ppc64':
            return (major, minor) == (10, 5)
        if arch == 'i386':
            return (major, minor) >= (10, 4)
        if arch == 'x86_64':
            return (major, minor) >= (10, 5)
        if arch in groups:
            for garch in groups[arch]:
                if _supports_arch(major, minor, garch):
                    return True
        return False

    groups = OrderedDict([
        ("fat", ("i386", "ppc")),
        ("intel", ("x86_64", "i386")),
        ("fat64", ("x86_64", "ppc64")),
        ("fat32", ("x86_64", "i386", "ppc")),
    ])  # type: Dict[str, Tuple[str, ...]]

    if _supports_arch(major, minor, machine):
        arches.append(machine)

    for garch in groups:
        if machine in groups[garch] and _supports_arch(major, minor, garch):
            arches.append(garch)

    arches.append('universal')

    return arches


def get_all_minor_versions_as_strings(impl, version_info):
    # type: (str, Tuple[int, ...]) -> List[str]
    if len(version_info) == 1:
        return [str(version_info[0])]

    # PyPy is handled specially, see: get_impl_version_info.
    if impl != 'pp':
        version_info = version_info[:2]

    versions = []
    major = version_info[:-1]
    # Support all previous minor Python versions.
    for minor in range(version_info[-1], -1, -1):
        versions.append(''.join(map(str, major + (minor,))))
    return versions


def _mac_platforms(arch):
    # type: (str) -> List[str]
    match = _osx_arch_pat.match(arch)
    if match:
        name, major, minor, actual_arch = match.groups()
        tpl = '{}_{}_%i_%s'.format(name, major)
        arches = []
        for m in reversed(range(int(minor) + 1)):
            for a in get_darwin_arches(int(major), m, actual_arch):
                arches.append(tpl % (m, a))
    else:
        # arch pattern didn't match (?!)
        arches = [arch]
    return arches


def _custom_manylinux_platforms(arch):
    # type: (str) -> List[str]
    arches = [arch]
    arch_prefix, arch_sep, arch_suffix = arch.partition('_')
    if arch_prefix == 'manylinux2014':
        # manylinux1/manylinux2010 wheels run on most manylinux2014 systems
        # with the exception of wheels depending on ncurses. PEP 599 states
        # manylinux1/manylinux2010 wheels should be considered
        # manylinux2014 wheels:
        # https://www.python.org/dev/peps/pep-0599/#backwards-compatibility-with-manylinux2010-wheels
        if arch_suffix in {'i686', 'x86_64'}:
            arches.append('manylinux2010' + arch_sep + arch_suffix)
            arches.append('manylinux1' + arch_sep + arch_suffix)
    elif arch_prefix == 'manylinux2010':
        # manylinux1 wheels run on most manylinux2010 systems with the
        # exception of wheels depending on ncurses. PEP 571 states
        # manylinux1 wheels should be considered manylinux2010 wheels:
        # https://www.python.org/dev/peps/pep-0571/#backwards-compatibility-with-manylinux1-wheels
        arches.append('manylinux1' + arch_sep + arch_suffix)
    return arches


class _UniquedStrSequence(object):
    def __init__(
        self,
        items=None,  # type: Optional[List[str]]
    ):
        # type: (...) -> None
        self._seen = set()  # type: Set[str]
        self._sequence = []  # type: List[str]
        if items:
            self.extend(items)

    def append(self, item):
        # type: (str) -> None
        if item not in self._seen:
            self._seen.add(item)
            self._sequence.append(item)

    def extend(self, items):
        # type: (Iterable[str]) -> None
        for item in items:
            self.append(item)

    def __iter__(self):
        # type: () -> Iterator[str]
        return iter(self._sequence)


def get_supported(
    version_info=None,  # type: Optional[Tuple[int, ...]]
    platforms=None,  # type: Optional[List[str]]
    impl=None,  # type: Optional[str]
    abi=None  # type: Optional[str]
):
    # type: (...) -> List[Pep425Tag]
    """Return a list of supported tags for the given constraints.

    :param version_info: a version tuple or None. If None, the local
        interpreter version. The version will be assumed to support
        our ABI.
    :param platforms: specify the exact platforms you want valid
        tags for, or None. If None, use the local system platform.
    :param impl: specify the exact implementation you want valid
        tags for, or None. If None, use the local interpreter impl.
    :param abi: specify the exact abi you want valid
        tags for, or None. If None, use the local interpreter abi.
    """
    supported = []

    impl = impl or get_abbr_impl()

    # Versions must be given with respect to the preference
    version_info = version_info or get_impl_version_info()
    versions = get_all_minor_versions_as_strings(impl, version_info)
    current_version = versions[0]
    other_versions = versions[1:]

    abis = []  # type: List[str]

    abi = abi or get_abi_tag()
    if abi:
        abis[0:0] = [abi]

    # abi3 was introduced in Python 3.2
    supports_abi3 = version_info[:2] >= (3, 2) and impl == "cp"

    if supports_abi3:
        abis.append("abi3")

    abis.append('none')

    arches = _UniquedStrSequence()
    for arch in _UniquedStrSequence(platforms or [get_platform()]):
        arch_prefix, arch_sep, arch_suffix = arch.partition('_')
        if arch.startswith('macosx'):
            arches.extend(_mac_platforms(arch))
        elif arch_prefix in ['manylinux2014', 'manylinux2010']:
            arches.extend(_custom_manylinux_platforms(arch))
        elif not platforms:
            if is_manylinux2014_compatible():
                arches.append('manylinux2014' + arch_sep + arch_suffix)
            if is_manylinux2010_compatible():
                arches.append('manylinux2010' + arch_sep + arch_suffix)
            if is_manylinux1_compatible():
                arches.append('manylinux1' + arch_sep + arch_suffix)
            arches.append(arch)
        else:
            arches.append(arch)

    # Current version, current API (built specifically for our Python):
    for abi in abis:
        for arch in arches:
            supported.append(('%s%s' % (impl, current_version), abi, arch))

    # abi3 modules compatible with older version of Python
    if supports_abi3:
        for version in other_versions:
            # abi3 was introduced in Python 3.2
            if version in {'31', '30'}:
                break
            for arch in arches:
                supported.append(("%s%s" % (impl, version), "abi3", arch))

    # Has binaries, does not use the Python API:
    for arch in arches:
        supported.append(('py%s' % (current_version[0]), 'none', arch))

    # No abi / arch, but requires our implementation:
    supported.append(('%s%s' % (impl, current_version), 'none', 'any'))

    # No abi / arch, generic Python
    supported.append(('py%s' % (current_version,), 'none', 'any'))
    supported.append(('py%s' % (current_version[0]), 'none', 'any'))
    for version in other_versions:
        supported.append(('py%s' % (version,), 'none', 'any'))

    return supported
