# -*- coding: UTF-8 -*-
#    This file is part of arena.
#   
#    arena is free software: you can redistribute it and/or modify it
#    under the terms of the GNU General Public License as published by the
#    Free Software Foundation; either version 3 of the License or, at your
#    option, any later version.
#
#    arena is distributed in the hope that it will be useful, but
#    WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABLITY
#    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 arena.  If not, see <http://www.gnu.org/licenses/>.

## @file
#  This file defines a utility class that can be used to work with Python
#  strings.  It also defines an interface that is adopted by those classes
#  which are capable of producing text representations of their data.

import math, random, sys, time
from Library.IO.Streams import AStream
from Library.Memory.collections import OrderedDict

## Characters that are considered numeric
sNumericCharacters = "0123456789."
## The default delimiter to use between key/value pairs when exploding or 
#  imploding a string
sDefaultItemsDelimiter = ","
## The default delimiter to use between a key and a value when exploding or
#  imploding a string
sDefaultKeyDelimiter = "="

## A utility class that provides routines to handle time.
class Time:
    ## Ways to format Greenwich Mean Time
    formatGMT = {
        ## The format used to create a representation of GMT (RFC 2822)
        "rfc2822": "%a, %d %b %Y %H:%M:%S GMT",
        ## The format used to create a representation of GMT (RFC 3339)
        "rfc3339": "%Y-%m-%dT%H:%M:%SZ",
        ## The format used to create a readable representation of GMT
        "readable": "%A, %d %B %Y %H:%M:%S GMT"
    }
    
    ## Ways to format local time
    formatLocal = {
        ## The format used to create a representation of local time (RFC 2822)
        "rfc2822": "%a, %d %b %Y %H:%M:%S %Z",
        ## The format used to create a representation of local time (RFC 3339)
        "rfc3339": "%Y-%m-%dT%H:%M:%S",
        ## The format used to create a readable representation of local time
        "readable": "%A, %d %B %Y %I:%M:%S %p %Z"
    }
    
    ## The format used to create a representation of the time zone offset
    formatTZOffset = "{hours}:{minutes}"
        
    ## The format used to create a representation of the passage of time
    formatMeasurements = {
        "singular": {
            "year": "year",
            "day": "day",
            "hour": "hour",
            "minute": "minute",
            "second": "second"
        },
        
        "plural": {
            "year": "years",
            "day": "days",
            "hour": "hours",
            "minute": "minutes",
            "second": "seconds"
        }
    }
    
    ## The order in which to measure time
    measuringOrder = {
        "year": 60 * 60 * 24 * 365.2425,
        "day": 60 * 60 * 24,
        "hour": 60 * 60,
        "minute": 60,
        "second": 1
    }
    
    ## Return the current local time as the number of seconds since the Unix
    #  epoch.
    @classmethod
    def now(Self):
        return time.time()
        

    ## Return the current GMT time
    @classmethod
    def gmt(Self):
        return time.gmtime()
        
    ## Return the current local time zone offset expressed as: `+/-(HH:MM)`
    @classmethod
    def zoneOffset(Self):
        hours = str(math.trunc(time.timezone / (-60 * 60)))
        if len(hours) < 3:
            hours = hours[0] + "0" + hours[1:]
        
        # Python increases time zone offsets to the WEST instead of to the EAST    
        if time.timezone > 0:
            hours = "-" + hours[1:]
            
        else:
            hours = "+" + hours[1:]
            
        minutes = str(math.trunc(math.fmod(time.timezone, 60)))
        if len(minutes) < 2:
            minutes = "0" + minutes
        
        return Self.formatTZOffset.format(hours=hours, minutes=minutes)
        
    ## Return the current local time zone
    @classmethod
    def zone(Self):
        return time.strftime("%Z")
    
    ## Format the given local time according to the specified format.
    #
    #  The \p fmt parameter can specify a format string to pass 
    #  to \p strftime(), or it can name one of the pre-defined formats
    #  specified in Time.formatLocal.  If \p fmt is not given, a "readable" 
    #  version of local time is returned.
    #
    #  \p when should be time specified must be a tuple or struct_time that 
    #  represents a time as returned by \p gmtime() or \p localtime().  If 
    #  no time is given, the current system time is used.
    @classmethod
    def localString(Self, when=None, fmt=""):
        if isinstance(when, float):
            when = time.localtime(when)
                
        elif isinstance(when, int):
            when = time.localtime(when)
                
        elif isinstance(when, long):
            when = time.localtime(when)
            
        else:
            when = time.localtime()
            
        # Check to see if `fmt` specifies an actual format to pass to 
        # strftime(), or if it names one of our pre-defined formats
        if fmt in Self.formatLocal:
            fmt = Self.formatLocal[fmt]
        
        # Otherwise, default to the human-readable format    
        elif len(fmt) == 0:
            fmt = Self.formatLocal["readable"]
            
        return time.strftime(fmt, when)
        
    ## Formats the given GMT time according to the specified format.
    #
    #  The \p fmt parameter can specify a format string to pass 
    #  to \p strftime(), or it can name one of the pre-defined formats
    #  specified in Time.formatGMT.  If \p fmt is not given, a "readable" 
    #  version of GMT time is returned.
    #
    #  \p when should be time specified must be a tuple or struct_time that 
    #  represents a time as returned by \p gmtime() or \p localtime().  If 
    #  no time is given, the current system time is used.
    @classmethod
    def gmtString(Self, when=None, fmt=""):
        if isinstance(when, float):
            when = time.gmtime(when)
                
        elif isinstance(when, int):
            when = time.gmtime(when)
                
        elif isinstance(when, long):
            when = time.gmtime(when)
            
        else:
            when = time.gmtime()
            
        # Check to see if `fmt` specifies an actual format to pass to 
        # strftime(), or if it names one of our pre-defined formats
        if fmt in Self.formatGMT:
            fmt = Self.formatGMT[fmt]
        
        # Otherwise, default to the human-readable format    
        elif len(fmt) == 0:
            fmt = Self.formatGMT["readable"]
            
        return time.strftime(fmt, when) 
        
    ## Return the number of seconds specified as a string of the form:
    #  <tt>XX years, XXX days, XX hours, XX minutes and XX seconds</tt>
    @classmethod
    def measureFromSeconds(Self, when):
        result = OrderedDict()
        value = 0
        measurements = OrderedDict(sorted(Self.measuringOrder.items(), 
            key=lambda t: t[1], reverse=True))
        
        # Loop through each measurement of time, as determined by the
        # measuring order specified above.
        #
        # 'when' is decremented by the value stored in each span
        for span in measurements.iterkeys():
            if when >= measurements[span]:
                value = math.trunc(when / measurements[span])
                when -= (value * measurements[span])                
                
            # Determine the appropriate word to use to describe the measurement
            # (singular or plural usage)
            if value != 1:
                result[Self.formatMeasurements["plural"][span]] = value
                
            else:
                result[Self.formatMeasurements["singular"][span]] = value
                
            value = 0
            
        # Construct a string representation of the result.  The key specifies
        # what to call the measurement (i.e., day, years, etc.); the value
        # specifies how much of that measurement exists.
        prettyString = ""        
        for key, value in result.iteritems():
            if value == 0:
                continue
                
            if len(prettyString) > 0:
                prettyString += ", "
                
            prettyString += str(value) + " " + key
            
        return prettyString

## A utility class for working with strings
class String:
    ## Escape special characters within a string.
    #
    #  /p specialCharacters should be a dictionary object whose keys specify the
    #  character to match and whose values specify the replacement value for
    #  that character.
    @classmethod
    def escape(Self, thisString, specialCharacters):
        result = ""
        if (len(thisString) > 0) and (len(specialCharacters) > 0):
            for char in thisString:
                # Escape special characters found in the string
                if char in specialCharacters:
                    result += specialCharacters[char]

                else:
                    result += char

        return result

    ## Return the quoted form of the specified string, using the specified 
    #  beginning and end markers.
    @classmethod
    def quote(Self, thisString, beginMark="'", endMark=None):
        if endMark == None:
            endMark = beginMark

        return beginMark + thisString + endMark

    ## Implode the list or dictionary object using the specified
    #  delimiters.
    #
    #  This routine returns a string that contains each item or key/value pair
    #  separated using the specified delimiters. 
    @classmethod
    def implode(Self, theseItems, itemsDelimiter=sDefaultItemsDelimiter, 
        keyDelimiter=sDefaultKeyDelimiter):
        
        result = ""

        if isinstance(theseItems, dict):
            index = 0

            for key, value in theseItems.iteritems():
                if index > 0:
                    result += itemsDelimiter

                result += str(key) + keyDelimiter + str(value)
                index += 1

        else:
            index = 0

            for value in theseItems:
                if index > 0:
                    result += itemsDelimiter

                result += str(value)
                index += 1

        return result
        
    ## Expand the specified string into a list or dictionary object.
    #
    #  The string is assumed to contain one or more items separated by the
    #  specified delimiters.
    @classmethod
    def explode(Self, thisString, resultType=list, 
        itemDelimiter=sDefaultItemsDelimiter, 
        keyDelimiter=sDefaultKeyDelimiter):
        
        result = resultType()
        
        if len(thisString) > 0:
            if isinstance(result, dict):
                # Expand the items in the string
                items = Self.explode(thisString, list, itemDelimiter)
                if len(items) > 0:
                    # Expand each item into a key/value pair
                    for item in items:
                        key, sep, value = item.partition(keyDelimiter)
                        result[key.lstrip()] = value
                        
            else:
                # Split the string at the item delimiter
                item, sep, remainder = thisString.partition(itemDelimiter)
        
                # Build the list
                while len(remainder) > 0:
                    result.append(item)
                    item, sep, remainder = remainder.partition(itemDelimiter)
                
                result.append(item)                        

        return result

    ## Perform a first pass of a format string, collecting the names (or
    #  indices) of fields specified in the string.
    #
    #  The field specifiers are assumed to conform to the type used by
    #  str.format(), and can be either keyword specifiers or positional
    #  specifiers (or a mix of both).
    #  
    #  @return This routine returns a dictonary object with two keys, 
    #  <tt>fields</tt> and <tt>sequence</tt>.  /p fields is a list that contains
    #  the names or positions of fields specified within the format 
    #  string.  \p sequence specifies the non-field substrings and field
    #  placeholders, in the order in which they are declared within the format
    #  string.
    @classmethod
    def formatPrescan(Self, fmt):
        result = {}
        formatLength = len(fmt)
        
        if formatLength == 0:
            return result
            
        result["fields"] = []
        result["sequence"] = []
        position = 0
        i = 0
        fieldName = ""
        substring = ""

        # Loop through the format string
        while i < formatLength:
            # Check for a field specifier
            if fmt[i] != "{":
                substring += fmt[i]

            elif fmt[i+1] != "{":
                result["sequence"].append(substring)
                substring = ""

                i = i + 1
                # Accumulate the field name
                while (fmt[i] != "}") and (i < formatLength):
                    fieldName += fmt[i]
                    i += 1

                # If a field name was specified, store it
                if len(fieldName) > 0:
                    result["fields"].append(fieldName)
                    result["sequence"].append(fieldName)
                    fieldName = ""

                else:
                    result["fields"].append(position)
                    result["sequence"].append(position)
                    position += 1

            i += 1

        else:
            result["sequence"].append(substring)

        return result

    ## Format a string using a helper function.
    #
    #  This routine is necessary because str.format() removes single-quote
    #  characters from string values as it processes them, and these characters
    #  are needed when specifying field names as part of an SQL query passed to
    #  a database server.
    @classmethod
    def formatUsing(Self, callback, fmt, *posValues, **kwValues):
        result = ""
        formatLength = len(fmt)

        if formatLength == 0:
            return fmt

        formatting = Self.formatPrescan(fmt)
        if len(formatting) > 0:
            for value in formatting["sequence"]:
                #if Instance is None:
                result += callback(value, posValues, kwValues)

                #else:
                #result += callback(Instance, value, posValues, kwValues)

        return result
        
    ## The default behavior for formatting a string.
    @classmethod
    def handleFormatField(Self, field, posValues, kwValues):
        result = ""
        
        # A string value in the sequence can be a sub-string or field name
        if isinstance(field, basestring):
            # If the value is a key in the keyed values list, then we have a
            # field name
            if field in kwValues:
                result  = kwValues[field]

            else:
                result = field

        # Otherwise, we have a positional index
        else:
            result = posValues[field]
            
        return str(result)
        
    ## Format a string.
    #
    #  This routine is necessary to provide Python 2.7 functionality in
    #  the event that Python 2.6 is being used, as is the case with my
    #  hosting provider.
    @classmethod
    def format(Self, s, *posValues, **kwValues):
        return Self.formatUsing(Self.handleFormatField, s, *posValues, 
            **kwValues)
        
    ## Determine whether the specified string consists of only numeric
    #  characters.
    @classmethod
    def isNumeric(Self, thisString):
        if len(thisString) == 0:
            return False
            
        for char in thisString:
            if char not in sNumericCharacters:
                return False
                
        else:
            return True
            
    ## Construct a hash value for the specified string.
    #    
    #  This method simply returns the value of the hash() function as applied
    #  to the specified string.
    @classmethod
    def hash(Self, thisString):
        #result = 0L
        
        #if len(thisString) > 0:
        #  result = 5381L
        #  c = 0L
          
        #  for ch in thisString:
        #      c = ord(ch)
        #      result = (result * 33) + c
            
        #return result
        return long(hash(thisString))
        
    ## Attempt to convert the specified string to an integer value.
    #    
    #  If the attempt fails, no exception is raised, and the routine returns
    #  the default value specified.
    @classmethod
    def toInteger(Self, thisString, defaultValue=0):
        result = defaultValue
        
        if len(thisString) > 0:
            try:
                result = int(thisString)
                
            except ValueError:
                pass
                
        return result
        
    ## Attempt to convert the specified string to a long integer value.
    #    
    #  If the attempt fails, no exception is raised, and the routine returns
    #  the default value specified.
    @classmethod
    def toLong(Self, thisString, defaultValue=0):
        result = defaultValue
        
        if len(thisString) > 0:
            try:
                result = long(thisString)
                
            except ValueError:
                pass
                
        return result

    ## Attempt to convert the specified string to an floating-point value.
    #    
    #  If the attempt fails, no exception is raised, and the routine returns
    #  the default value specified.
    @classmethod
    def toFloat(Self, thisString, defaultValue=0.0):
        result = defaultValue
        
        if len(thisString) > 0:
            try:
                result = float(thisString)
                
            except ValueError:
                pass
                
        return result
                                
    ## Obfuscate the specified string.
    #
    #  \p thisString is obfuscated by constructing a string that contains the
    #  ASCII character code for each character in the string.  The character
    #  code is prefixed by \p prefix and has \p postfix appended to it.
    #
    #  The previous actions only occur if a random number generated by the
    #  Python \p random.random() function is greater than or equal to \p odds.
    #  Setting \p odds to a value of 1.0 will effectively prevent obfuscation
    #  (since \p random.random() only returns values between 0 and 1), while
    #  setting \p odds to a value of 0.0 will ensure that every character is 
    #  obfuscated.  Otherwise, a mix of obfuscated and regular characters from
    #  \p thisString is returned.
    #
    #  In its default state, this method is designed to obfuscate e-mail 
    #  addresses and other text that might appear on a Web page.
    @staticmethod
    def obfuscate(thisString, prefix="&#", postfix=";", odds=0.25):
        result = ""
        
        if len(thisString) > 0:
            for ch in thisString:
                if random.random() >= odds:
                    # Do we use hex values or not?
                    if random.random() >= odds:
                        result += prefix + "x{0:x}".format(ord(ch)) + postfix
                        
                    else:
                        result += prefix + str(ord(ch)) + postfix
                    
                else:
                    result += ch
                    
        return result    
        
    ## Format the specified value as a human-readable file size.  This code
    #  is borrowed from jinja2 code, and includes a modification that I had to
    #  make in order to get the jinja2 code to work with my file size values.
    @classmethod
    def fileSize(Self, value, binary=False):
        """Format the value like a 'human-readable' file size (i.e. 13 kB,
        4.1 MB, 102 Bytes, etc).  Per default decimal prefixes are used (Mega,
        Giga, etc.), if the second parameter is set to `True` the binary
        prefixes are used (Mebi, Gibi).
        """
        bytes = float(value)
        base = binary and 1024 or 1000
        prefixes = [
            (binary and "KiB" or "kB"),
            (binary and "MiB" or "MB"),
            (binary and "GiB" or "GB"),
            (binary and "TiB" or "TB"),
            (binary and "PiB" or "PB"),
            (binary and "EiB" or "EB"),
            (binary and "ZiB" or "ZB"),
            (binary and "YiB" or "YB")
        ]
        
        if bytes == 1:
            return "1 Byte"
    
        elif bytes < base:
            return "%d Bytes" % bytes
        else:
            for i, prefix in enumerate(prefixes):
                # I modified the below lines -- MM
                unit = base ** (i + 1)
                unity = bytes / unit
                if unity < base:
                    return "%.1f %s" % (unity, prefix)
            
            return "%.1f %s" % (unity, prefix) 
            
    ## Determine whether the specified string value is empty
    @classmethod
    def isEmpty(Self, s):
        result = True
        
        if isinstance(s, basestring):
            if (len(s) != 0) and (s.isspace() == False):
                result = False
                
        return result
    
## This class defines and implements a method to print the string representation 
#  of a class to the specified stream.
class IsPrintable:
    ## Return a string representation of the class conveniently.
    #    
    #  This method is provided for convenience, as it is much more convenient
    #  and easier on the eyes than calling the inherent __str__() method, with
    #  all of those unsightly underscores.  It can be overridden by descendant
    #  classes that wish to return something more informative than the name of
    #  their class. 
    def toString(Self):
        return Self.__class__.__name__
        
    ## Return a string representation of the class.
    #    
    #  This method simply calls IsPrintable.toString().  It is designed to 
    #  allow descendants of IsPrintable to implement the toString() method 
    #  rather than the __str__() method, as the former is easier to type (and 
    #  less ugly) than the latter.
    def __str__(Self):
        return Self.toString()
    
    ## Print a string representation of the class to the specified stream.
    #    
    #  Classes that adopt this interface will probably not need to override
    #  this routine; instead, they can define how their class data is 
    #  represented by overriding the __str__() (generic objects) or toString() 
    #  (descendants of AnObject) methods.
    #    
    #  The return value of this function indicates how much information was
    #  written to the destination stream (how many characters, in this case).
    def printTo(Self, Dest):
        if isinstance(Dest, AStream):
            return Dest.write(str(Self))
        
        return 0
        
