"""
Parse wheel filenames

``wheel-filename`` lets you verify `wheel
<https://www.python.org/dev/peps/pep-0427/>`_ filenames and parse them into
their component fields.

This package adheres strictly to the relevant PEPs, with the following
exceptions:

- Unlike other filename components, version components may contain the
  characters ``!`` and ``+`` for full PEP 440 support.

- Version components may be any sequence of the relevant set of characters;
  they are not verified for PEP 440 compliance.

- The ``.whl`` file extension is matched case-insensitively.

Visit <https://github.com/jwodder/wheel-filename> for more information.
"""

__version__      = '1.2.0'
__author__       = 'John Thorvald Wodder II'
__author_email__ = 'wheel-filename@varonathe.org'
__license__      = 'MIT'
__url__          = 'https://github.com/jwodder/wheel-filename'

__all__ = [
    'InvalidFilenameError',
    'ParsedWheelFilename',
    'parse_wheel_filename',
]

import os
import os.path
import re
from   typing import Iterable, List, NamedTuple, Optional, Union

# These patterns are interpreted with re.UNICODE in effect, so there's probably
# some character that matches \d but not \w that needs to be included
PYTHON_TAG_RGX   = r'[\w\d]+'
ABI_TAG_RGX      = r'[\w\d]+'
PLATFORM_TAG_RGX = r'[\w\d]+'

WHEEL_FILENAME_CRGX = re.compile(
    r'(?P<project>[A-Za-z0-9](?:[A-Za-z0-9._]*[A-Za-z0-9])?)'
    r'-(?P<version>[A-Za-z0-9_.!+]+)'
    r'(?:-(?P<build>[0-9][\w\d.]*))?'
    r'-(?P<python_tags>{0}(?:\.{0})*)'
    r'-(?P<abi_tags>{1}(?:\.{1})*)'
    r'-(?P<platform_tags>{2}(?:\.{2})*)'
    r'\.[Ww][Hh][Ll]'
    .format(PYTHON_TAG_RGX, ABI_TAG_RGX, PLATFORM_TAG_RGX)
)

class ParsedWheelFilename(NamedTuple):
    project: str
    version: str
    build: Optional[str]
    python_tags: List[str]
    abi_tags: List[str]
    platform_tags: List[str]

    def __str__(self) -> str:
        if self.build:
            fmt = '{0.project}-{0.version}-{0.build}-{1}-{2}-{3}.whl'
        else:
            fmt = '{0.project}-{0.version}-{1}-{2}-{3}.whl'
        return fmt.format(
            self,
            '.'.join(self.python_tags),
            '.'.join(self.abi_tags),
            '.'.join(self.platform_tags),
        )

    def tag_triples(self) -> Iterable[str]:
        """
        Returns a generator of all simple tag triples formed from the tags in
        the filename
        """
        for py in self.python_tags:
            for abi in self.abi_tags:
                for plat in self.platform_tags:
                    yield '-'.join([py, abi, plat])


def parse_wheel_filename(filename: Union[str, os.PathLike]) -> ParsedWheelFilename:
    """
    Parse a wheel filename into its components

    :param str filename: a wheel path or filename
    :rtype: ParsedWheelFilename
    :raises InvalidFilenameError: if the filename is invalid
    """
    basename = os.path.basename(filename)
    m = WHEEL_FILENAME_CRGX.fullmatch(basename)
    if not m:
        raise InvalidFilenameError(basename)
    return ParsedWheelFilename(
        project       = m.group('project'),
        version       = m.group('version'),
        build         = m.group('build'),
        python_tags   = m.group('python_tags').split('.'),
        abi_tags      = m.group('abi_tags').split('.'),
        platform_tags = m.group('platform_tags').split('.'),
    )


class InvalidFilenameError(ValueError):
    """ Raised when an invalid wheel filename is encountered """

    filename: str

    def __init__(self, filename: str) -> None:
        #: The invalid filename
        self.filename = filename

    def __str__(self) -> str:
        return 'Invalid wheel filename: ' + repr(self.filename)
