"""
Python helper for Semantic Versioning (http://semver.org/)

Original source: https://github.com/k-bx/python-semver
We copy to make sure this simple library is and maintain in our core.

Changed by Tom Valk: Added function to detect if the release is a pre-release.
"""

import collections
import re


__version__ = '2.7.7'
__author__ = 'Kostiantyn Rybnikov'
__author_email__ = 'k-bx@k-bx.com'

_REGEX = re.compile(
	r"""
	^
	(?P<major>(?:0|[1-9][0-9]*))
	\.
	(?P<minor>(?:0|[1-9][0-9]*))
	\.
	(?P<patch>(?:0|[1-9][0-9]*))
	(\-*(?P<prerelease>
		(?:0|[1-9A-Za-z-][0-9A-Za-z-]*)
		(\.(?:0|[1-9A-Za-z-][0-9A-Za-z-]*))*
	))?
	(\+(?P<build>
		[0-9A-Za-z-]+
		(\.[0-9A-Za-z-]+)*
	))?
	$
	""", re.VERBOSE)

_LAST_NUMBER = re.compile(r'(?:[^\d]*(\d+)[^\d]*)+')

if not hasattr(__builtins__, 'cmp'):
	def cmp(a, b):
		return (a > b) - (a < b)


def parse(version):
	"""Parse version to major, minor, patch, pre-release, build parts.

	:param version: version string
	:return: dictionary with the keys 'build', 'major', 'minor', 'patch',
			 and 'prerelease'. The prerelease or build keys can be None
			 if not provided
	:rtype: dict
	"""
	match = _REGEX.match(version)
	if match is None:
		raise ValueError('%s is not valid SemVer string' % version)

	version_parts = match.groupdict()

	version_parts['major'] = int(version_parts['major'])
	version_parts['minor'] = int(version_parts['minor'])
	version_parts['patch'] = int(version_parts['patch'])

	return version_parts


class VersionInfo(collections.namedtuple(
	'VersionInfo', 'major minor patch prerelease build')):
	"""
	:param int major: version when you make incompatible API changes.
	:param int minor: version when you add functionality in
					  a backwards-compatible manner.
	:param int patch: version when you make backwards-compatible bug fixes.
	:param str prerelease: an optional prerelease string
	:param str build: an optional build string

	>>> import semver
	>>> ver = semver.parse('3.4.5-pre.2+build.4')
	>>> ver
	{'build': 'build.4', 'major': 3, 'minor': 4, 'patch': 5,
	'prerelease': 'pre.2'}
	"""
	__slots__ = ()

	def __eq__(self, other):
		if not isinstance(other, (VersionInfo, dict)):
			return NotImplemented
		return _compare_by_keys(self._asdict(), _to_dict(other)) == 0

	def __ne__(self, other):
		if not isinstance(other, (VersionInfo, dict)):
			return NotImplemented
		return _compare_by_keys(self._asdict(), _to_dict(other)) != 0

	def __lt__(self, other):
		if not isinstance(other, (VersionInfo, dict)):
			return NotImplemented
		return _compare_by_keys(self._asdict(), _to_dict(other)) < 0

	def __le__(self, other):
		if not isinstance(other, (VersionInfo, dict)):
			return NotImplemented
		return _compare_by_keys(self._asdict(), _to_dict(other)) <= 0

	def __gt__(self, other):
		if not isinstance(other, (VersionInfo, dict)):
			return NotImplemented
		return _compare_by_keys(self._asdict(), _to_dict(other)) > 0

	def __ge__(self, other):
		if not isinstance(other, (VersionInfo, dict)):
			return NotImplemented
		return _compare_by_keys(self._asdict(), _to_dict(other)) >= 0


def _to_dict(obj):
	if isinstance(obj, VersionInfo):
		return obj._asdict()
	return obj


def parse_version_info(version):
	"""Parse version string to a VersionInfo instance.

	:param version: version string
	:return: a :class:`VersionInfo` instance
	:rtype: :class:`VersionInfo`
	"""
	parts = parse(version)
	version_info = VersionInfo(
		parts['major'], parts['minor'], parts['patch'],
		parts['prerelease'], parts['build'])

	return version_info


def _nat_cmp(a, b):
	def convert(text):
		return int(text) if re.match('[0-9]+', text) else text

	def split_key(key):
		return [convert(c) for c in key.split('.')]

	def cmp_prerelease_tag(a, b):
		if isinstance(a, int) and isinstance(b, int):
			return cmp(a, b)
		elif isinstance(a, int):
			return -1
		elif isinstance(b, int):
			return 1
		else:
			return cmp(a, b)

	a, b = a or '', b or ''
	a_parts, b_parts = split_key(a), split_key(b)
	for sub_a, sub_b in zip(a_parts, b_parts):
		cmp_result = cmp_prerelease_tag(sub_a, sub_b)
		if cmp_result != 0:
			return cmp_result
	else:
		return cmp(len(a), len(b))


def _compare_by_keys(d1, d2):
	for key in ['major', 'minor', 'patch']:
		v = cmp(d1.get(key), d2.get(key))
		if v:
			return v

	rc1, rc2 = d1.get('prerelease'), d2.get('prerelease')
	rccmp = _nat_cmp(rc1, rc2)

	if not rccmp:
		return 0
	if not rc1:
		return 1
	elif not rc2:
		return -1

	return rccmp


def compare(ver1, ver2):
	"""Compare two versions

	:param ver1: version string 1
	:param ver2: version string 2
	:return: The return value is negative if ver1 < ver2,
			 zero if ver1 == ver2 and strictly positive if ver1 > ver2
	:rtype: int
	"""

	v1, v2 = parse(ver1), parse(ver2)

	return _compare_by_keys(v1, v2)


def match(version, match_expr):
	"""Compare two versions through a comparison

	:param str version: a version string
	:param str match_expr: operator and version; valid operators are
		  <   smaller than
		  >   greater than
		  >=  greator or equal than
		  <=  smaller or equal than
		  ==  equal
		  !=  not equal
	:return: True if the expression matches the version, otherwise False
	:rtype: bool
	"""
	prefix = match_expr[:2]
	if prefix in ('>=', '<=', '==', '!='):
		match_version = match_expr[2:]
	elif prefix and prefix[0] in ('>', '<'):
		prefix = prefix[0]
		match_version = match_expr[1:]
	else:
		raise ValueError("match_expr parameter should be in format <op><ver>, "
						 "where <op> is one of "
						 "['<', '>', '==', '<=', '>=', '!=']. "
						 "You provided: %r" % match_expr)

	possibilities_dict = {
		'>': (1,),
		'<': (-1,),
		'==': (0,),
		'!=': (-1, 1),
		'>=': (0, 1),
		'<=': (-1, 0)
	}

	possibilities = possibilities_dict[prefix]
	cmp_res = compare(version, match_version)

	return cmp_res in possibilities


def max_ver(ver1, ver2):
	"""Returns the greater version of two versions

	:param ver1: version string 1
	:param ver2: version string 2
	:return: the greater version of the two
	:rtype: :class:`VersionInfo`
	"""
	cmp_res = compare(ver1, ver2)
	if cmp_res == 0 or cmp_res == 1:
		return ver1
	else:
		return ver2


def min_ver(ver1, ver2):
	"""Returns the smaller version of two versions

	:param ver1: version string 1
	:param ver2: version string 2
	:return: the smaller version of the two
	:rtype: :class:`VersionInfo`
	"""
	cmp_res = compare(ver1, ver2)
	if cmp_res == 0 or cmp_res == -1:
		return ver1
	else:
		return ver2


def format_version(major, minor, patch, prerelease=None, build=None):
	"""Format a version according to the Semantic Versioning specification

	:param str major: the required major part of a version
	:param str minor: the required minor part of a version
	:param str patch: the required patch part of a version
	:param str prerelease: the optional prerelease part of a version
	:param str build: the optional build part of a version
	:return: the formatted string
	:rtype: str
	"""
	version = "%d.%d.%d" % (major, minor, patch)
	if prerelease is not None:
		version = version + "-%s" % prerelease

	if build is not None:
		version = version + "+%s" % build

	return version


def _increment_string(string):
	"""
	Look for the last sequence of number(s) in a string and increment, from:
	http://code.activestate.com/recipes/442460-increment-numbers-in-a-string/#c1
	"""
	match = _LAST_NUMBER.search(string)
	if match:
		next_ = str(int(match.group(1)) + 1)
		start, end = match.span(1)
		string = string[:max(end - len(next_), start)] + next_ + string[end:]
	return string


def bump_major(version):
	"""Raise the major part of the version

	:param: version string
	:return: the raised version string
	:rtype: str
	"""
	verinfo = parse(version)
	return format_version(verinfo['major'] + 1, 0, 0)


def bump_minor(version):
	"""Raise the minor part of the version

	:param: version string
	:return: the raised version string
	:rtype: str
	"""
	verinfo = parse(version)
	return format_version(verinfo['major'], verinfo['minor'] + 1, 0)


def bump_patch(version):
	"""Raise the patch part of the version

	:param: version string
	:return: the raised version string
	:rtype: str
	"""
	verinfo = parse(version)
	return format_version(verinfo['major'], verinfo['minor'],
						  verinfo['patch'] + 1)


def bump_prerelease(version):
	"""Raise the prerelease part of the version

	:param: version string
	:return: the raised version string
	:rtype: str
	"""
	verinfo = parse(version)
	verinfo['prerelease'] = _increment_string(verinfo['prerelease'] or 'rc.0')
	return format_version(verinfo['major'], verinfo['minor'], verinfo['patch'],
						  verinfo['prerelease'])


def bump_build(version):
	"""Raise the build part of the version

	:param: version string
	:return: the raised version string
	:rtype: str
	"""
	verinfo = parse(version)
	verinfo['build'] = _increment_string(verinfo['build'] or 'build.0')
	return format_version(verinfo['major'], verinfo['minor'], verinfo['patch'],
						  verinfo['prerelease'], verinfo['build'])


def is_prerelease(version):
	"""
	Detect if the version given is a pre-release.

	:param version: version string
	:return: if the version is a pre-release
	:rtype: bool
	"""
	verinfo = parse(version)
	return 'prerelease' in verinfo and verinfo['prerelease'] is not None
