# pylint: disable=E0611,E0401
from typing import (
    Any,
    Dict,
    Generator,
    List,
    NamedTuple,
    Optional,
    Tuple,
    Union,
    cast,
)

from distutils.version import StrictVersion


__all__ = ['bump_version']


_BUMP_VERSION_MAJOR: int = 0
_BUMP_VERSION_MINOR: int = 1
_BUMP_VERSION_PATCH: int = 2
_BUMP_VERSION_MINOR_ALPHA: int = 3
_BUMP_VERSION_MINOR_BETA: int = 4
_BUMP_VERSION_MINORS: Tuple[int, ...] = (
    _BUMP_VERSION_MINOR,
    _BUMP_VERSION_MINOR_ALPHA,
    _BUMP_VERSION_MINOR_BETA,
)
_BUMP_VERSION_PATCH_ALPHA: int = 5
_BUMP_VERSION_PATCH_BETA: int = 6
_BUMP_VERSION_PATCHES: Tuple[int, ...] = (
    _BUMP_VERSION_PATCH,
    _BUMP_VERSION_PATCH_ALPHA,
    _BUMP_VERSION_PATCH_BETA,
)
_BUMP_VERSION_POSITION_NAMES: Dict[int, str] = {
    _BUMP_VERSION_MAJOR: 'major',
    _BUMP_VERSION_MINOR: 'minor',
    _BUMP_VERSION_PATCH: 'patch',
}


class _VersionPart(NamedTuple):
    pos: int
    txt: str
    num: int
    pre_txt: str
    pre_num: int
    name: str


def _each_version_part(
        ver_obj: StrictVersion,
) -> Generator[_VersionPart, None, None]:
    version: Tuple[int, int, int] = ver_obj.version
    prerelease: Union[Tuple[str, int], None] = ver_obj.prerelease
    prerelease_built = False
    for pos, num in enumerate(version):
        txt = '%s' % num
        if pos == 2 and num == 0:
            txt = ''
        kwargs: Dict[str, Any] = {
            'pos': pos,
            'txt': txt,
            'num': num,
            'pre_txt': '',
            'pre_num': -1,
            'name': _BUMP_VERSION_POSITION_NAMES[pos]
        }
        if (prerelease_built is False and
                pos > 0 and
                prerelease is not None):
            prerelease = cast(Tuple[str, int], prerelease)
            should_add = True
            if pos == 1 and version[2] != 0:
                should_add = False
            if should_add is True:
                kwargs['txt'] = '%s%s%s' % (
                    kwargs['txt'],
                    prerelease[0],
                    prerelease[1]
                )
                kwargs['pre_txt'] = prerelease[0]
                kwargs['pre_num'] = prerelease[1]
                prerelease_built = True
        yield _VersionPart(**kwargs)


class _VersionInfo(NamedTuple):
    version: str
    major: _VersionPart
    minor: _VersionPart
    patch: _VersionPart
    pre_pos: int  # The pre-release position. -1 means no pre-release


def _build_version_info(
        version: str
) -> _VersionInfo:
    ver_obj = StrictVersion(version)
    pre_pos = -1
    args: List[Any] = [version]
    for part in _each_version_part(ver_obj):
        if part.pre_txt:
            pre_pos = part.pos
        args.append(part)
    args.append(pre_pos)
    return _VersionInfo(*args)


def _build_version_bump_position(
        position: int
) -> int:
    pos_min = -3
    pos_max = 2

    if (pos_min <= position <= pos_max) is False:
        raise ValueError(
            "The given value for 'position', %r, must be an 'int' "
            "between (%r) and (%r)." % (position, pos_min, pos_max)
        )
    # Turn position into a positive number
    if position < 0:
        pos_max += 1
        return pos_max + position
    return position


def _build_version_bump_type(
        position_positive: int,
        pre_release: Union[str, None]
) -> int:
    if pre_release is None:
        prerelease = ''
    else:
        pre_release = cast(str, pre_release)
        prerelease = pre_release.strip().lower()

    if prerelease == '':
        if position_positive == 0:
            return _BUMP_VERSION_MAJOR
        if position_positive == 1:
            return _BUMP_VERSION_MINOR
        return _BUMP_VERSION_PATCH
    if prerelease in ('a', 'alpha', 'b', 'beta'):
        is_alpha = False
        if prerelease in ('a', 'alpha'):
            is_alpha = True

        if position_positive == 0:
            raise ValueError(
                "Only the 'minor' or 'patch' parts of the version number "
                "can get a prerelease bump."
            )
        if position_positive == 1:
            if is_alpha is True:
                return _BUMP_VERSION_MINOR_ALPHA
            return _BUMP_VERSION_MINOR_BETA
        if is_alpha is True:
            return _BUMP_VERSION_PATCH_ALPHA
        return _BUMP_VERSION_PATCH_BETA
    raise ValueError(
        "The given value for 'pre_release', %r, can only be one of: "
        "'a', 'alpha', 'b', 'beta', None."
    )


def bump_version(
        version: str,
        position: int = 2,
        pre_release: Optional[str] = None
) -> str:
    """Increase the version number from a version number string.

    *New in version 0.3*

    Args:
        version (str): The version number to be bumped.
        position (int, optional): The position (starting with zero) of the
            version number component to be increased.  Defaults to: ``2``
        pre_release (str, Optional): A value of ``a`` or ``alpha`` will
            create or increase an alpha version number.  A value of ``b`` or
            ``beta`` will create or increase a beta version number.

    Raises:
        ValueError: if the given ``version`` is an invalid version number.
        ValueError: if the given ``position`` does not exist.
        ValueError: if the given ``prerelease`` is not in:
            ``a, alpha, b, beta``
        ValueError: if trying to 'major' part, of a version number, to
            a pre-release version.

    :rtype:
        :obj:`str`

        * The increased version number.

    Examples:
        >>> from flutils.packages import bump_version
        >>> bump_version('1.2.2')
        '1.2.3'
        >>> bump_version('1.2.3', position=1)
        '1.3'
        >>> bump_version('1.3.4', position=0)
        '2.0'
        >>> bump_version('1.2.3', prerelease='a')
        '1.2.4a0'
        >>> bump_version('1.2.4a0', pre_release='a')
        '1.2.4a1'
        >>> bump_version('1.2.4a1', pre_release='b')
        '1.2.4b0'
        >>> bump_version('1.2.4a1')
        '1.2.4'
        >>> bump_version('1.2.4b0')
        '1.2.4'
        >>> bump_version('2.1.3', position=1, pre_release='a')
        '2.2a0'
        >>> bump_version('1.2b0', position=2)
        '1.2.1'

    """
    ver_info = _build_version_info(version)
    position = _build_version_bump_position(position)
    bump_type = _build_version_bump_type(position, pre_release)
    # noinspection PyUnusedLocal
    hold: List[Union[int, str]] = []
    if bump_type == _BUMP_VERSION_MAJOR:
        hold = [ver_info.major.num + 1, 0]
    elif bump_type in _BUMP_VERSION_MINORS:
        if bump_type == _BUMP_VERSION_MINOR:
            if ver_info.minor.pre_txt:
                hold = [ver_info.major.num, ver_info.minor.num]
            else:
                hold = [ver_info.major.num, ver_info.minor.num + 1]
        else:
            if bump_type == _BUMP_VERSION_MINOR_ALPHA:
                if ver_info.minor.pre_txt == 'a':
                    part = '%sa%s' % (
                        ver_info.minor.num,
                        ver_info.minor.pre_num + 1
                    )
                else:
                    part = '{}a0'.format(ver_info.minor.num + 1)
            else:
                if ver_info.minor.pre_txt == 'a':
                    part = '{}b0'.format(ver_info.minor.num)
                elif ver_info.minor.pre_txt == 'b':
                    part = '%sb%s' % (
                        ver_info.minor.num,
                        ver_info.minor.pre_num + 1
                    )
                else:
                    part = '{}b0'.format(ver_info.minor.num + 1)
            hold = [ver_info.major.num, part]
    else:
        if bump_type == _BUMP_VERSION_PATCH:
            if ver_info.patch.pre_txt:
                hold = [
                    ver_info.major.num,
                    ver_info.minor.num,
                    ver_info.patch.num
                ]
            else:
                hold = [
                    ver_info.major.num,
                    ver_info.minor.num,
                    ver_info.patch.num + 1
                ]
        else:
            if bump_type == _BUMP_VERSION_PATCH_ALPHA:
                if ver_info.patch.pre_txt == 'a':
                    part = '%sa%s' % (
                        ver_info.patch.num,
                        ver_info.patch.pre_num + 1
                    )
                else:
                    part = '{}a0'.format(ver_info.patch.num + 1)
            else:
                if ver_info.patch.pre_txt == 'a':
                    part = '{}b0'.format(ver_info.patch.num)

                elif ver_info.patch.pre_txt == 'b':
                    part = '%sb%s' % (
                        ver_info.patch.num,
                        ver_info.patch.pre_num + 1
                    )
                else:
                    part = '{}b0'.format(ver_info.patch.num + 1)
            hold = [ver_info.major.num, ver_info.minor.num, part]
    out = '.'.join(map(str, hold))
    return out

# pylint: disable=E0611,E0401
from typing import (
    Any,
    Dict,
    Generator,
    List,
    NamedTuple,
    Optional,
    Tuple,
    Union,
    cast,
)

from distutils.version import StrictVersion


__all__ = ['bump_version']


_BUMP_VERSION_MAJOR: int = 0
_BUMP_VERSION_MINOR: int = 1
_BUMP_VERSION_PATCH: int = 2
_BUMP_VERSION_MINOR_ALPHA: int = 3
_BUMP_VERSION_MINOR_BETA: int = 4
_BUMP_VERSION_MINORS: Tuple[int, ...] = (
    _BUMP_VERSION_MINOR,
    _BUMP_VERSION_MINOR_ALPHA,
    _BUMP_VERSION_MINOR_BETA,
)
_BUMP_VERSION_PATCH_ALPHA: int = 5
_BUMP_VERSION_PATCH_BETA: int = 6
_BUMP_VERSION_PATCHES: Tuple[int, ...] = (
    _BUMP_VERSION_PATCH,
    _BUMP_VERSION_PATCH_ALPHA,
    _BUMP_VERSION_PATCH_BETA,
)
_BUMP_VERSION_POSITION_NAMES: Dict[int, str] = {
    _BUMP_VERSION_MAJOR: 'major',
    _BUMP_VERSION_MINOR: 'minor',
    _BUMP_VERSION_PATCH: 'patch',
}


class _VersionPart(NamedTuple):
    pos: int
    txt: str
    num: int
    pre_txt: str
    pre_num: int
    name: str


def _each_version_part(
        ver_obj: StrictVersion,
) -> Generator[_VersionPart, None, None]:
    version: Tuple[int, int, int] = ver_obj.version
    prerelease: Union[Tuple[str, int], None] = ver_obj.prerelease
    prerelease_built = False
    for pos, num in enumerate(version):
        txt = '%s' % num
        if pos == 2 and num == 0:
            txt = ''
        kwargs: Dict[str, Any] = {
            'pos': pos,
            'txt': txt,
            'num': num,
            'pre_txt': '',
            'pre_num': -1,
            'name': _BUMP_VERSION_POSITION_NAMES[pos]
        }
        if (prerelease_built is False and
                pos > 0 and
                prerelease is not None):
            prerelease = cast(Tuple[str, int], prerelease)
            should_add = True
            if pos == 1 and version[2] != 0:
                should_add = False
            if should_add is True:
                kwargs['txt'] = '%s%s%s' % (
                    kwargs['txt'],
                    prerelease[0],
                    prerelease[1]
                )
                kwargs['pre_txt'] = prerelease[0]
                kwargs['pre_num'] = prerelease[1]
                prerelease_built = True
        yield _VersionPart(**kwargs)


class _VersionInfo(NamedTuple):
    version: str
    major: _VersionPart
    minor: _VersionPart
    patch: _VersionPart
    pre_pos: int  # The pre-release position. -1 means no pre-release


def _build_version_info(
        version: str
) -> _VersionInfo:
    ver_obj = StrictVersion(version)
    pre_pos = -1
    args: List[Any] = [version]
    for part in _each_version_part(ver_obj):
        if part.pre_txt:
            pre_pos = part.pos
        args.append(part)
    args.append(pre_pos)
    return _VersionInfo(*args)


def _build_version_bump_position(
        position: int
) -> int:
    pos_min = -3
    pos_max = 2

    if (pos_min <= position <= pos_max) is False:
        raise ValueError(
            "The given value for 'position', %r, must be an 'int' "
            "between (%r) and (%r)." % (position, pos_min, pos_max)
        )
    # Turn position into a positive number
    if position < 0:
        pos_max += 1
        return pos_max + position
    return position


def _build_version_bump_type(
        position_positive: int,
        pre_release: Union[str, None]
) -> int:
    if pre_release is None:
        prerelease = ''
    else:
        pre_release = cast(str, pre_release)
        prerelease = pre_release.strip().lower()

    if prerelease == '':
        if position_positive == 0:
            return _BUMP_VERSION_MAJOR
        if position_positive == 1:
            return _BUMP_VERSION_MINOR
        return _BUMP_VERSION_PATCH
    if prerelease in ('a', 'alpha', 'b', 'beta'):
        is_alpha = False
        if prerelease in ('a', 'alpha'):
            is_alpha = True

        if position_positive == 0:
            raise ValueError(
                "Only the 'minor' or 'patch' parts of the version number "
                "can get a prerelease bump."
            )
        if position_positive == 1:
            if is_alpha is True:
                return _BUMP_VERSION_MINOR_ALPHA
            return _BUMP_VERSION_MINOR_BETA
        if is_alpha is True:
            return _BUMP_VERSION_PATCH_ALPHA
        return _BUMP_VERSION_PATCH_BETA
    raise ValueError(
        "The given value for 'pre_release', %r, can only be one of: "
        "'a', 'alpha', 'b', 'beta', None."
    )


def bump_version(
        version: str,
        position: int = 2,
        pre_release: Optional[str] = None
) -> str:
    """Increase the version number from a version number string.

    *New in version 0.3*

    Args:
        version (str): The version number to be bumped.
        position (int, optional): The position (starting with zero) of the
            version number component to be increased.  Defaults to: ``2``
        pre_release (str, Optional): A value of ``a`` or ``alpha`` will
            create or increase an alpha version number.  A value of ``b`` or
            ``beta`` will create or increase a beta version number.

    Raises:
        ValueError: if the given ``version`` is an invalid version number.
        ValueError: if the given ``position`` does not exist.
        ValueError: if the given ``prerelease`` is not in:
            ``a, alpha, b, beta``
        ValueError: if trying to 'major' part, of a version number, to
            a pre-release version.

    :rtype:
        :obj:`str`

        * The increased version number.

    Examples:
        >>> from flutils.packages import bump_version
        >>> bump_version('1.2.2')
        '1.2.3'
        >>> bump_version('1.2.3', position=1)
        '1.3'
        >>> bump_version('1.3.4', position=0)
        '2.0'
        >>> bump_version('1.2.3', prerelease='a')
        '1.2.4a0'
        >>> bump_version('1.2.4a0', pre_release='a')
        '1.2.4a1'
        >>> bump_version('1.2.4a1', pre_release='b')
        '1.2.4b0'
        >>> bump_version('1.2.4a1')
        '1.2.4'
        >>> bump_version('1.2.4b0')
        '1.2.4'
        >>> bump_version('2.1.3', position=1, pre_release='a')
        '2.2a0'
        >>> bump_version('1.2b0', position=2)
        '1.2.1'

    """
    ver_info = _build_version_info(version)
    position = _build_version_bump_position(position)
    bump_type = _build_version_bump_type(position, pre_release)
    # noinspection PyUnusedLocal
    hold: List[Union[int, str]] = []
    if bump_type == _BUMP_VERSION_MAJOR:
        hold = [ver_info.major.num + 1, 0]
    elif bump_type in _BUMP_VERSION_MINORS:
        if bump_type == _BUMP_VERSION_MINOR:
            if ver_info.minor.pre_txt:
                hold = [ver_info.major.num, ver_info.minor.num]
            else:
                hold = [ver_info.major.num, ver_info.minor.num + 1]
        else:
            if bump_type == _BUMP_VERSION_MINOR_ALPHA:
                if ver_info.minor.pre_txt == 'a':
                    part = '%sa%s' % (
                        ver_info.minor.num,
                        ver_info.minor.pre_num + 1
                    )
                else:
                    part = '{}a0'.format(ver_info.minor.num + 1)
            else:
                if ver_info.minor.pre_txt == 'a':
                    part = '{}b0'.format(ver_info.minor.num)
                elif ver_info.minor.pre_txt == 'b':
                    part = '%sb%s' % (
                        ver_info.minor.num,
                        ver_info.minor.pre_num + 1
                    )
                else:
                    part = '{}b0'.format(ver_info.minor.num + 1)
            hold = [ver_info.major.num, part]
    else:
        if bump_type == _BUMP_VERSION_PATCH:
            if ver_info.patch.pre_txt:
                hold = [
                    ver_info.major.num,
                    ver_info.minor.num,
                    ver_info.patch.num
                ]
            else:
                hold = [
                    ver_info.major.num,
                    ver_info.minor.num,
                    ver_info.patch.num + 1
                ]
        else:
            if bump_type == _BUMP_VERSION_PATCH_ALPHA:
                if ver_info.patch.pre_txt == 'a':
                    part = '%sa%s' % (
                        ver_info.patch.num,
                        ver_info.patch.pre_num + 1
                    )
                else:
                    part = '{}a0'.format(ver_info.patch.num + 1)
            else:
                if ver_info.patch.pre_txt == 'a':
                    part = '{}b0'.format(ver_info.patch.num)

                elif ver_info.patch.pre_txt == 'b':
                    part = '%sb%s' % (
                        ver_info.patch.num,
                        ver_info.patch.pre_num + 1
                    )
                else:
                    part = '{}b0'.format(ver_info.patch.num + 1)
            hold = [ver_info.major.num, ver_info.minor.num, part]
    out = '.'.join(map(str, hold))
    return out

# pylint: disable=E0611,E0401 # The pre-release position. -1 means no pre-release # Turn position into a positive number Increase the version number from a version number string.      *New in version 0.3*      Args:         version (str): The version number to be bumped.         position (int, optional): The position (starting with zero) of the             version number component to be increased.  Defaults to: ``2``         pre_release (str, Optional): A value of ``a`` or ``alpha`` will             create or increase an alpha version number.  A value of ``b`` or             ``beta`` will create or increase a beta version number.      Raises:         ValueError: if the given ``version`` is an invalid version number.         ValueError: if the given ``position`` does not exist.         ValueError: if the given ``prerelease`` is not in:             ``a, alpha, b, beta``         ValueError: if trying to 'major' part, of a version number, to             a pre-release version.      :rtype:         :obj:`str`          * The increased version number.      Examples:         >>> from flutils.packages import bump_version         >>> bump_version('1.2.2')         '1.2.3'         >>> bump_version('1.2.3', position=1)         '1.3'         >>> bump_version('1.3.4', position=0)         '2.0'         >>> bump_version('1.2.3', prerelease='a')         '1.2.4a0'         >>> bump_version('1.2.4a0', pre_release='a')         '1.2.4a1'         >>> bump_version('1.2.4a1', pre_release='b')         '1.2.4b0'         >>> bump_version('1.2.4a1')         '1.2.4'         >>> bump_version('1.2.4b0')         '1.2.4'         >>> bump_version('2.1.3', position=1, pre_release='a')         '2.2a0'         >>> bump_version('1.2b0', position=2)         '1.2.1' # noinspection PyUnusedLocal