#!/usr/bin/python
########################################################################
# Copyright (C) 2010-2020 VMWare, Inc.                                 #
# All Rights Reserved                                                  #
########################################################################

"""This module contain a class to express and compare version information."""

import re

__all__ = ['Version', 'FWVersion', 'VibVersion']

class Version(object):
   """A generic class for expressing a version.
         Attributes:
            * versionstring - A string containing the version.
   """
   def __init__(self, versionstring=''):
      """Class constructor.
            Parameters:
               * versionstring - The string containing version information.
            Returns: A new Version object.
      """
      self.versionstring = versionstring

   __str__ = lambda self: self.versionstring
   __nonzero__ = lambda self: bool(self.versionstring)
   __bool__ = __nonzero__

   # Needed for Py3
   def __format__(self, spec):
      return '{1:{0}}'.format(spec, self.versionstring)

   @staticmethod
   def _compare(x, y):
      return (x > y) - (x < y)

   @classmethod
   def fromstring(cls, versionstring):
      """Parses versionstring into appropriate fields, returning a Version
         object. (This method is more useful in child classes."""
      return cls(versionstring)

   def __cmp__(self, other):
      """Provides a comparison function between two Version objects.
         Comparison is based solely on the 'versionstring' attribute, but the
         string is divided into fields by splitting at '.' characters. Each
         field is then further divided into a leading numeric portion and a
         trailing portion. Fields from each versionstring are compared left-
         to-right, by first doing a comparison between the numeric portions,
         then comparing the string portions of the numeric parts are equal.
         Comparison ends when one of the fields is greater or less than the
         corresponding field in the other version, or when we run out of
         fields to compare. This algorithm provides the least-surprising
         results.  For example:
             * 4.2 is less than 4.10. (a stringwise comparision would provide
               the opposite result.)
             * 4.0 is less than 4.0a. (a numeric comparison could not account
               for the 'a'.)
             * 4 is less than 4.0.
             * 4.a is less than 4.0a.
             * 4.0 is less than 4a.
         Returns: -1, 0, or 1, if other is less than, equal to or greater
                  than self.
      """
      def splitfield(field):
         m = re.match(r'(\d*)(.*)', field)
         # NoneType cannot compare to Int in Python 3
         # use -1 since None < 0 is true for Python 2
         numeric = int(m.group(1)) if m.group(1) else -1
         return (numeric, m.group(2))

      mine = tuple(splitfield(f) for f in self.versionstring.split('.'))
      theirs = tuple(splitfield(f) for f in str(other).split('.'))
      return self._compare(mine, theirs)

   __lt__ = lambda self, other: self.__cmp__(other) < 0
   __le__ = lambda self, other: self.__cmp__(other) <= 0
   __eq__ = lambda self, other: self.__cmp__(other) == 0
   __ne__ = lambda self, other: self.__cmp__(other) != 0
   __ge__ = lambda self, other: self.__cmp__(other) >= 0
   __gt__ = lambda self, other: self.__cmp__(other) > 0

class FWVersion(Version):
   """A class for representing a firmware version.
         Attributes:
            * majorversion   - A positive integer expressing the major
                               firmware version. May be None if all
                               other version fields are None.
            * minorversion   - A positive integer expressing the minor
                               firmware version. May be None if
                               revisionnumber and buildnumber are None.
            * revisionnumber - A positive integer expressing the
                               firmware revision number. May be None if
                               buildnumber is None.
            * buildnumber    - A positive integer expressing the
                               firmware build number. May be None.
         Properties:
            * versionstring - A read-only property expressing the
                              version fields as a string.
   """
   def __init__(self, majorversion=None, minorversion=None,
                revisionnumber=None, buildnumber=None):
      """Class constructor.
            Parameters:
               * majorversion   - Sets attribute of same name.
               * minorversion   - Sets attribute of same name.
               * revisionnumber - Sets attribute of same name.
               * buildnumber    - Sets attribute of same name.
      """
      self.majorversion    = majorversion
      self.minorversion    = minorversion
      self.revisionnumber  = revisionnumber
      self.buildnumber     = buildnumber

   def __cmp__(self, other):
      if isinstance(other, FWVersion):
         mine   = (self.majorversion, self.minorversion,
                   self.revisionnumber, self.buildnumber)
         theirs = (other.majorversion, other.minorversion,
                   other.revisionnumber, other.buildnumber)
         mine = tuple([f for f in mine if f is not None])
         theirs = tuple([f for f in theirs if f is not None])
         return self._compare(mine, theirs)
      else:
         return Version.__cmp__(self, other)

   @property
   def versionstring(self):
      """Return firmware version as a string. All numeric fields are
         concatenated with a '.', and None values are not included.
         Returns the empty string if all values are None.
      """
      attrs = (self.majorversion, self.minorversion,
               self.revisionnumber, self.buildnumber)
      return '.'.join([str(f) for f in attrs if f is not None])

   @classmethod
   def fromstring(cls, versionstring):
      """Create a new FWVersion object from a string.
            Parameters:
               * versionstring - A properly formatted string consisting of
                                 zero to four decimal integers separated by
                                 '.' characters.
            Returns: A new FWVersion object.
            Raises:
               * ValueError - If string is not in the proper format.
      """
      try:
         parameters = tuple(int(field) for field in versionstring.split('.'))
         return cls(*parameters)
      except Exception:
         raise ValueError("Could not parse '%s' into version fields." %
                          versionstring)

class VibVersion(Version):
   """A class to express a VIB version.
        Attributes:
           * version - A string giving the version.
           * release - A string giving the release.
        Properties:
           * versionstring - Represents VibVersion as a string.
   """
   def __init__(self, version, release=''):
      """Class constructor.
            Parameters:
               * version - A string to assign to the version attribute.
               * release - A string to assign to the release attribute.
            Returns: A new VibVersion object.
      """
      self.version = Version(version)
      self.release = Version(release)

   @property
   def versionstring(self):
      """Returns VibVersion information as a string."""
      if self.release.versionstring:
         return "%s-%s" % (self.version.versionstring,
                           self.release.versionstring)
      else:
         return self.version.versionstring

   @classmethod
   def fromstring(cls, versionstring):
      """Create an VibVersion object from a string.
            Parameters:
               * versionstring - Must be a string in either version-release
                                 or epoch:version-release format.
            Returns: A new VibVersion object.
            Raises:
               * ValueError - If string cannot be parsed into a Vib version.
      """
      if versionstring.count('-'):
         try:
            (version, release) = versionstring.split('-')
         except Exception:
            msg = "Could not parse '%s' to a Vib version." % versionstring
            raise ValueError(msg)
      else:
         version = versionstring
         release = ''

      return cls(version, release)

   def __cmp__(self, other):
      # We don't universally apply the same condition to Version.
      if isinstance(other, VibVersion) or other.__class__.__name__ == 'VibVersion':
         mine   = (self.version, self.release)
         theirs = (other.version, other.release)
         return self._compare(mine, theirs)
      elif isinstance(other, Version):
         return Version.__cmp__(self.version, other)
      else:
         raise TypeError("Type '%s' not valid for comparisons with VibVersion."
                         % other.__class__.__name__)
