# -*- coding: utf-8 -*-
# This file is part of the bumpver project
# https://github.com/mbarkhau/bumpver
#
# Copyright (c) 2018-2024 Manuel Barkhau (mbarkhau@gmail.com) - MIT License
# SPDX-License-Identifier: MIT
"""Functions related to version string manipulation."""
from __future__ import absolute_import
from __future__ import division
from __future__ import print_function
from __future__ import unicode_literals
import typing as typ
import logging
import datetime as dt
try:
    import builtins
except ImportError:
    import __builtin__ as builtins
import lexid
str = getattr(builtins, 'unicode', str)
from . import version
from . import v1patterns
logger = logging.getLogger('bumpver.v1version')
CalInfo = typ.Union[version.V1CalendarInfo, version.V1VersionInfo]


def _is_cal_gt(left, right):
    """Is left > right for non-None fields."""
    lvals = []
    rvals = []
    for field in version.V1CalendarInfo._fields:
        lval = getattr(left, field)
        rval = getattr(right, field)
        if not (lval is None or rval is None):
            lvals.append(lval)
            rvals.append(rval)
    return lvals > rvals


def _ver_to_cal_info(vnfo):
    return version.V1CalendarInfo(vnfo.year, vnfo.quarter, vnfo.month, vnfo
        .dom, vnfo.doy, vnfo.iso_week, vnfo.us_week)


def cal_info(date=None):
    """Generate calendar components for current date.

    >>> from datetime import date

    >>> c = cal_info(date(2019, 1, 5))
    >>> (c.year, c.quarter, c.month, c.dom, c.doy, c.iso_week, c.us_week)
    (2019, 1, 1, 5, 5, 0, 0)

    >>> c = cal_info(date(2019, 1, 6))
    >>> (c.year, c.quarter, c.month, c.dom, c.doy, c.iso_week, c.us_week)
    (2019, 1, 1, 6, 6, 0, 1)

    >>> c = cal_info(date(2019, 1, 7))
    >>> (c.year, c.quarter, c.month, c.dom, c.doy, c.iso_week, c.us_week)
    (2019, 1, 1, 7, 7, 1, 1)

    >>> c = cal_info(date(2019, 4, 7))
    >>> (c.year, c.quarter, c.month, c.dom, c.doy, c.iso_week, c.us_week)
    (2019, 2, 4, 7, 97, 13, 14)
    """
    if date is None:
        date = version.TODAY
    kwargs = {'year': date.year, 'quarter': version.quarter_from_month(date
        .month), 'month': date.month, 'dom': date.day, 'doy': int(date.
        strftime('%j'), base=10), 'iso_week': int(date.strftime('%W'), base
        =10), 'us_week': int(date.strftime('%U'), base=10)}
    return version.V1CalendarInfo(**kwargs)


FieldKey = str
MatchGroupKey = str
MatchGroupStr = str
PatternGroups = typ.Dict[MatchGroupKey, MatchGroupStr]
FieldValues = typ.Dict[FieldKey, MatchGroupStr]


def _parse_field_values(field_values):
    fvals = field_values
    tag = fvals.get('tag')
    if tag is None:
        tag = 'final'
    tag = version.TAG_BY_PEP440_TAG.get(tag, tag)
    assert tag is not None
    bid = fvals['bid'] if 'bid' in fvals else '0001'
    year = int(fvals['year']) if 'year' in fvals else None
    if year is not None and year < 100:
        year += 2000
    doy = int(fvals['doy']) if 'doy' in fvals else None
    month = None
    dom = None
    if year and doy:
        date = version.date_from_doy(year, doy)
        month = date.month
        dom = date.day
    else:
        month = int(fvals['month']) if 'month' in fvals else None
        dom = int(fvals['dom']) if 'dom' in fvals else None
    iso_week = None
    us_week = None
    if year and month and dom:
        date = dt.date(year, month, dom)
        doy = int(date.strftime('%j'), base=10)
        iso_week = int(date.strftime('%W'), base=10)
        us_week = int(date.strftime('%U'), base=10)
    else:
        iso_week = None
        us_week = None
    quarter = int(fvals['quarter']) if 'quarter' in fvals else None
    if quarter is None and month:
        quarter = version.quarter_from_month(month)
    major = int(fvals['major']) if 'major' in fvals else 0
    minor = int(fvals['minor']) if 'minor' in fvals else 0
    patch = int(fvals['patch']) if 'patch' in fvals else 0
    return version.V1VersionInfo(year=year, quarter=quarter, month=month,
        dom=dom, doy=doy, iso_week=iso_week, us_week=us_week, major=major,
        minor=minor, patch=patch, bid=bid, tag=tag)


def _is_calver(cinfo):
    """Check pattern for any calendar based parts.

    >>> _is_calver(cal_info())
    True

    >>> vnfo = _parse_version_info({'year': "2018", 'month': "11", 'bid': "0018"})
    >>> _is_calver(vnfo)
    True

    >>> vnfo = _parse_version_info({'MAJOR': "1", 'MINOR': "023", 'PATCH': "45"})
    >>> _is_calver(vnfo)
    False
    """
    for field in version.V1CalendarInfo._fields:
        maybe_val = getattr(cinfo, field, None)
        if isinstance(maybe_val, int):
            return True
    return False


VersionInfoKW = typ.Dict[str, typ.Union[str, int, None]]


def _parse_pattern_groups(pattern_groups):
    for part_name in pattern_groups.keys():
        is_valid_part_name = (part_name in v1patterns.
            COMPOSITE_PART_PATTERNS or part_name in v1patterns.
            PATTERN_PART_FIELDS)
        if not is_valid_part_name:
            err_msg = "Invalid part '{0}'".format(part_name)
            raise version.PatternError(err_msg)
    field_value_items = [(field_name, pattern_groups[part_name]) for 
        part_name, field_name in v1patterns.PATTERN_PART_FIELDS.items() if 
        part_name in pattern_groups.keys()]
    all_fields = [field_name for field_name, _ in field_value_items]
    unique_fields = set(all_fields)
    duplicate_fields = [f for f in unique_fields if all_fields.count(f) > 1]
    if any(duplicate_fields):
        err_msg = 'Multiple parts for same field {0}.'.format(duplicate_fields)
        raise version.PatternError(err_msg)
    else:
        return dict(field_value_items)


def _parse_version_info(pattern_groups):
    """Parse normalized V1VersionInfo from groups of a matched pattern.

    >>> vnfo = _parse_version_info({'year': "2018", 'month': "11", 'bid': "0099"})
    >>> (vnfo.year, vnfo.month, vnfo.quarter, vnfo.bid, vnfo.tag)
    (2018, 11, 4, '0099', 'final')

    >>> vnfo = _parse_version_info({'year': "18", 'month': "11"})
    >>> (vnfo.year, vnfo.month, vnfo.quarter)
    (2018, 11, 4)

    >>> vnfo = _parse_version_info({'year': "2018", 'doy': "11", 'bid': "099", 'tag': "b"})
    >>> (vnfo.year, vnfo.month, vnfo.dom, vnfo.bid, vnfo.tag)
    (2018, 1, 11, '099', 'beta')

    >>> vnfo = _parse_version_info({'MAJOR': "1", 'MINOR': "23", 'PATCH': "45"})
    >>> (vnfo.major, vnfo.minor, vnfo.patch)
    (1, 23, 45)

    >>> vnfo = _parse_version_info({'MAJOR': "1", 'MMM': "023", 'PPPP': "0045"})
    >>> (vnfo.major, vnfo.minor, vnfo.patch)
    (1, 23, 45)
    """
    field_values = _parse_pattern_groups(pattern_groups)
    return _parse_field_values(field_values)


def parse_version_info(version_str, raw_pattern='{pycalver}'):
    """Parse normalized V1VersionInfo.

    >>> vnfo = parse_version_info("v201712.0033-beta", raw_pattern="{pycalver}")
    >>> assert vnfo == _parse_version_info({'year': 2017, 'month': 12, 'bid': "0033", 'tag': "beta"})

    >>> vnfo = parse_version_info("1.23.456", raw_pattern="{semver}")
    >>> assert vnfo == _parse_version_info({'MAJOR': "1", 'MINOR': "23", 'PATCH': "456"})
    """
    pattern = v1patterns.compile_pattern(raw_pattern)
    match = pattern.regexp.match(version_str)
    if match is None:
        err_msg = ("Invalid version string '{0}' for pattern '{1}'/'{2}'".
            format(version_str, raw_pattern, pattern.regexp.pattern))
        raise version.PatternError(err_msg)
    else:
        return _parse_version_info(match.groupdict())


def is_valid(version_str, raw_pattern='{pycalver}'):
    """Check if a version matches a pattern.

    >>> is_valid("v201712.0033-beta", raw_pattern="{pycalver}")
    True
    >>> is_valid("v201712.0033-beta", raw_pattern="{semver}")
    False
    >>> is_valid("1.2.3", raw_pattern="{semver}")
    True
    >>> is_valid("v201712.0033-beta", raw_pattern="{semver}")
    False
    """
    try:
        parse_version_info(version_str, raw_pattern)
        return True
    except version.PatternError:
        return False


ID_FIELDS_BY_PART = {'MAJOR': 'major', 'MINOR': 'minor', 'MM': 'minor',
    'MMM': 'minor', 'MMMM': 'minor', 'MMMMM': 'minor', 'MMMMMM': 'minor',
    'MMMMMMM': 'minor', 'PATCH': 'patch', 'PP': 'patch', 'PPP': 'patch',
    'PPPP': 'patch', 'PPPPP': 'patch', 'PPPPPP': 'patch', 'PPPPPPP':
    'patch', 'BID': 'bid', 'BB': 'bid', 'BBB': 'bid', 'BBBB': 'bid',
    'BBBBB': 'bid', 'BBBBBB': 'bid', 'BBBBBBB': 'bid'}


def format_version(vinfo, raw_pattern):
    """Generate version string.

    >>> import datetime as dt
    >>> vinfo = parse_version_info("v201712.0033-beta", raw_pattern="{pycalver}")
    >>> vinfo_a = vinfo._replace(**cal_info(date=dt.date(2017, 1, 1))._asdict())
    >>> vinfo_b = vinfo._replace(**cal_info(date=dt.date(2017, 12, 31))._asdict())
    >>> vinfo_c = vinfo_b._replace(major=1, minor=2, patch=34, tag='final')

    >>> format_version(vinfo_a, raw_pattern="v{yy}.{BID}{release}")
    'v17.33-beta'
    >>> format_version(vinfo_a, raw_pattern="{pep440_version}")
    '201701.33b0'

    >>> format_version(vinfo_a, raw_pattern="{pycalver}")
    'v201701.0033-beta'
    >>> format_version(vinfo_b, raw_pattern="{pycalver}")
    'v201712.0033-beta'

    >>> format_version(vinfo_a, raw_pattern="v{year}w{iso_week}.{BID}{release}")
    'v2017w00.33-beta'
    >>> format_version(vinfo_b, raw_pattern="v{year}w{iso_week}.{BID}{release}")
    'v2017w52.33-beta'

    >>> format_version(vinfo_a, raw_pattern="v{year}d{doy}.{bid}{release}")
    'v2017d001.0033-beta'
    >>> format_version(vinfo_b, raw_pattern="v{year}d{doy}.{bid}{release}")
    'v2017d365.0033-beta'

    >>> format_version(vinfo_c, raw_pattern="v{year}w{iso_week}.{BID}-{tag}")
    'v2017w52.33-final'
    >>> format_version(vinfo_c, raw_pattern="v{year}w{iso_week}.{BID}{release}")
    'v2017w52.33'

    >>> format_version(vinfo_c, raw_pattern="v{MAJOR}.{MINOR}.{PATCH}")
    'v1.2.34'
    >>> format_version(vinfo_c, raw_pattern="v{MAJOR}.{MM}.{PPP}")
    'v1.02.034'
    """
    full_pattern = raw_pattern
    for part_name, full_part_format in v1patterns.FULL_PART_FORMATS.items():
        full_pattern = full_pattern.replace('{' + part_name + '}',
            full_part_format)
    kwargs = vinfo._asdict()
    release_tag = vinfo.tag
    if release_tag == 'final':
        kwargs['release'] = ''
        kwargs['pep440_tag'] = ''
    else:
        kwargs['release'] = '-' + release_tag
        kwargs['pep440_tag'] = version.PEP440_TAG_BY_TAG[release_tag] + '0'
    kwargs['release_tag'] = release_tag
    year = vinfo.year
    if year:
        kwargs['yy'] = str(year)[-2:]
        kwargs['yyyy'] = year
    kwargs['BID'] = int(vinfo.bid, 10)
    for part_name, field in ID_FIELDS_BY_PART.items():
        val = kwargs[field]
        if part_name.lower() == field.lower():
            if isinstance(val, str):
                kwargs[part_name] = int(val, base=10)
            else:
                kwargs[part_name] = val
        else:
            assert len(set(part_name)) == 1
            padded_len = len(part_name)
            kwargs[part_name] = str(val).zfill(padded_len)
    return full_pattern.format(**kwargs)


def incr(old_version, raw_pattern='{pycalver}', **kwargs):
    major = kwargs.get('major', False)
    minor = kwargs.get('minor', False)
    patch = kwargs.get('patch', False)
    tag = kwargs.get('tag', None)
    tag_num = kwargs.get('tag_num', False)
    pin_date = kwargs.get('pin_date', False)
    maybe_date = kwargs.get('maybe_date', None)
    """Increment version string.

    'old_version' is assumed to be a string that matches 'pattern'
    """
    try:
        old_vinfo = parse_version_info(old_version, raw_pattern)
    except version.PatternError as ex:
        logger.error(str(ex))
        return None
    date = version.TODAY if maybe_date is None else maybe_date
    cur_cinfo = _ver_to_cal_info(old_vinfo) if pin_date else cal_info(date)
    if _is_cal_gt(old_vinfo, cur_cinfo):
        logger.warning("Old version appears to be from the future '{0}'".
            format(old_version))
        cur_vinfo = old_vinfo
    else:
        cur_vinfo = old_vinfo._replace(**cur_cinfo._asdict())
    cur_vinfo = cur_vinfo._replace(bid=lexid.next_id(cur_vinfo.bid))
    if major:
        cur_vinfo = cur_vinfo._replace(major=cur_vinfo.major + 1, minor=0,
            patch=0)
    if minor:
        cur_vinfo = cur_vinfo._replace(minor=cur_vinfo.minor + 1, patch=0)
    if patch:
        cur_vinfo = cur_vinfo._replace(patch=cur_vinfo.patch + 1)
    if tag_num:
        raise NotImplementedError(
            '--tag-num not supported for old style patterns')
    if tag:
        cur_vinfo = cur_vinfo._replace(tag=tag)
    new_version = format_version(cur_vinfo, raw_pattern)
    if new_version == old_version:
        logger.error('Invalid arguments or pattern, version did not change.')
        return None
    else:
        return new_version
