# size.py
# Python module to represent storage sizes
#
# Copyright (C) 2010  Red Hat, Inc.
#
# This copyrighted material is made available to anyone wishing to use,
# modify, copy, or redistribute it subject to the terms and conditions of
# the GNU General Public License v.2, or (at your option) any later version.
# This program is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY expressed or implied, including the implied warranties of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General
# Public License for more details.  You should have received a copy of the
# GNU General Public License along with this program; if not, write to the
# Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
# 02110-1301, USA.  Any Red Hat trademarks that are incorporated in the
# source code or documentation are not subject to the GNU General Public
# License and may only be used or replicated with the express permission of
# Red Hat, Inc.
#
# Red Hat Author(s): David Cantrell <dcantrell@redhat.com>

import re

from decimal import Decimal
from decimal import InvalidOperation

from errors import *

import gettext
_ = lambda x: gettext.ldgettext("blivet", x)
P_ = lambda x, y, z: gettext.ldngettext("blivet", x, y, z)

# Decimal prefixes for different size increments, along with the name
# and accepted abbreviation for the prefix.  These prefixes are all
# for 'bytes'.
_decimalPrefix = [(1000, _("kilo"), _("k")),
                  (1000**2, _("mega"), _("M")),
                  (1000**3, _("giga"), _("G")),
                  (1000**4, _("tera"), _("T")),
                  (1000**5, _("peta"), _("P")),
                  (1000**6, _("exa"), _("E")),
                  (1000**7, _("zetta"), _("Z")),
                  (1000**8, _("yotta"), _("Y"))]

# Binary prefixes for the different size increments.  Same structure
# as the above list.
_binaryPrefix = [(1024, _("kibi"), _("Ki")),
                 (1024**2, _("mebi"), _("Mi")),
                 (1024**3, _("gibi"), _("Gi")),
                 (1024**4, _("tebi"), None),
                 (1024**5, _("pebi"), None),
                 (1024**6, _("ebi"), None),
                 (1024**7, _("zebi"), None),
                 (1024**8, _("yobi"), None)]

_bytes = [_('b'), _('byte'), _('bytes')]
_prefixes = _decimalPrefix + _binaryPrefix

def _makeSpecs(prefix, abbr):
    """ Internal method used to generate a list of specifiers. """
    specs = []

    if prefix:
        specs.append(prefix.lower() + _("byte"))
        specs.append(prefix.lower() + _("bytes"))

    if abbr:
        specs.append(abbr.lower() + _("b"))
        specs.append(abbr.lower())

    return specs

def _parseSpec(spec):
    """ Parse string representation of size. """
    if not spec:
        raise ValueError("invalid size specification", spec)

    m = re.match(r'([0-9.]+)\s*([A-Za-z]*)$', spec.strip())
    if not m:
        raise ValueError("invalid size specification", spec)

    try:
        size = Decimal(m.groups()[0])
    except InvalidOperation:
        raise ValueError("invalid size specification", spec)

    if size < 0:
        raise SizeNotPositiveError("spec= param must be >=0")

    specifier = m.groups()[1].lower()
    if specifier in _bytes or not specifier:
        return size

    for factor, prefix, abbr in _prefixes:
        check = _makeSpecs(prefix, abbr)

        if specifier in check:
            return size * factor

    raise ValueError("invalid size specification", spec)

class Size(Decimal):
    """ Common class to represent storage device and filesystem sizes.
        Can handle parsing strings such as 45MB or 6.7GB to initialize
        itself, or can be initialized with a numerical size in bytes.
        Also generates human readable strings to a specified number of
        decimal places.
    """

    def __new__(cls, bytes=None,  spec=None):
        """ Initialize a new Size object.  Must pass either bytes or spec,
            but not both.  The bytes parameter is a numerical value for
            the size this object represents, in bytes.  The spec parameter
            is a string specification of the size using any of the size
            specifiers in the _decimalPrefix or _binaryPrefix lists combined
            with a 'b' or 'B'.  For example, to specify 640 kilobytes, you
            could pass any of these parameter:

                spec="640kb"
                spec="640 kb"
                spec="640KB"
                spec="640 KB"
                spec="640 kilobytes"

            If you want to use spec to pass a bytes value, you can use the
            letter 'b' or 'B' or simply leave the specifier off and bytes
            will be assumed.
        """
        if bytes and spec:
            raise SizeParamsError("only specify one parameter")

        if bytes is not None:
            if type(bytes).__name__ in ["int", "long", "float", 'Decimal'] and bytes >= 0:
                self = Decimal.__new__(cls, value=bytes)
            else:
                raise SizeNotPositiveError("bytes= param must be >=0")
        elif spec:
            self = Decimal.__new__(cls, value=_parseSpec(spec))
        else:
            raise SizeParamsError("missing bytes= or spec=")

        return self

    def __str__(self, context=None):
        return self.humanReadable()

    def __repr__(self):
        return "Size('%s')" % self

    def __add__(self, other, context=None):
        return Size(bytes=Decimal.__add__(self, other, context=context))

    # needed to make sum() work with Size arguments
    def __radd__(self, other, context=None):
        return Size(bytes=Decimal.__radd__(self, other, context=context))

    def __sub__(self, other, context=None):
        # subtraction is implemented using __add__ and negation, so we'll
        # be getting passed a Size
        return Decimal.__sub__(self, other, context=context)

    def __mul__(self, other, context=None):
        return Size(bytes=Decimal.__mul__(self, other, context=context))

    def __div__(self, other, context=None):
        return Size(bytes=Decimal.__div__(self, other, context=context))

    def _trimEnd(self, val):
        """ Internal method to trim trailing zeros. """
        val = re.sub(r'(\.\d*?)0+$', '\\1', val)
        while val.endswith('.'):
            val = val[:-1]

        return val

    def convertTo(self, spec="b"):
        """ Return the size in the units indicated by the specifier.  The
            specifier can be prefixes from the _decimalPrefix and
            _binaryPrefix lists combined with 'b' or 'B' for abbreviations)
            or 'bytes' (for prefixes like kilo or mega).  The size is
            returned as a Decimal.
        """
        spec = spec.lower()

        if spec in _bytes:
            return self

        for factor, prefix, abbr in _prefixes:
            check = _makeSpecs(prefix, abbr)

            if spec in check:
                return Decimal(self / Decimal(factor))

        return None

    def humanReadable(self, places=None, max_places=2):
        """ Return a string representation of this size with appropriate
            size specifier and in the specified number of decimal places
            (default: auto with a maximum of 2 decimal places).
        """
        if places is not None and places < 0:
            raise SizePlacesError("places= must be >=0 or None")

        if max_places is not None and max_places < 0:
            raise SizePlacesError("max_places= must be >=0 or None")

        check = self._trimEnd("%d" % self)

        if Decimal(check) < 1000:
            return "%s B" % check

        for factor, prefix, abbr in _prefixes:
            newcheck = super(Size, self).__div__(Decimal(factor))

            if newcheck < 1000:
                # nice value, use this factor, prefix and abbr
                break

        if places is not None:
            fmt = "%%.%df" % places
            retval = fmt % newcheck
        else:
            retval = self._trimEnd("%f" % newcheck)

        if max_places is not None:
            (whole, point, fraction) = retval.partition(".")
            if point and len(fraction) > max_places:
                if max_places == 0:
                    retval = whole
                else:
                    retval = "%s.%s" % (whole, fraction[:max_places])

        if abbr:
            return retval + " " + abbr + _("B")
        else:
            return retval + " " + prefix + P_("byte", "bytes", newcheck)
