#!/usr/bin/python
########################################################################
# Copyright (c) 2010-2024 Broadcom. All Rights Reserved.
# Broadcom Confidential. The term "Broadcom" refers to Broadcom Inc.
# and/or its subsidiaries.
########################################################################

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

import re

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

VERSION_RE = re.compile(r'(\d*)(.*)')

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
      self.__versiontuple = Version.versiontuple(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)

   def __repr__(self):
      return '{}({!r})'.format(self.__class__.__name__,
                               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)

   @staticmethod
   def versiontuple(versionstring):
      def splitfield(field):
         m = VERSION_RE.match(field)
         # If no digit is found, use -1, which is lower
         # than any possible version number
         numeric = int(m.group(1)) if m.group(1) else -1
         return (numeric, m.group(2))

      return tuple(splitfield(f) for f in versionstring.split('.'))


   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.
      """
      # Use a precomputed tuple if comparing with a Version object
      theirs = other.__versiontuple if isinstance(other, Version) else \
                  Version.versiontuple(str(other))
      return self._compare(self.__versiontuple, 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 __repr__(self):
      return '{}({!r}, {!r}, {!r}, {!r})'.format(self.__class__.__name__,
                                                 self.majorversion,
                                                 self.minorversion,
                                                 self.revisionnumber,
                                                 self.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)

   def __repr__(self):
      return '{}({!r}, {!r})'.format(self.__class__.__name__,
                                     self.version.versionstring,
                                     self.release.versionstring)

   @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__)
