#    Formatter.py
#
#    ------------------------------------------------------------
#    Copyright 2002, 2004 by Samuel Reynolds. All rights reserved.
#
#    Permission to use, copy, modify, and distribute this software and its
#    documentation for any purpose and without fee is hereby granted,
#    provided that the above copyright notice appear in all copies and that
#    both that copyright notice and this permission notice appear in
#    supporting documentation, and that the name of Samuel Reynolds
#    not be used in advertising or publicity pertaining to distribution
#    of the software without specific, written prior permission.
#
#    SAMUEL REYNOLDS DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE,
#    INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO
#    EVENT SHALL SAMUEL REYNOLDS BE LIABLE FOR ANY SPECIAL, INDIRECT, OR
#    CONSEQUENTIAL DAMAGES, OR FOR ANY DAMAGES WHATSOEVER RESULTING FROM
#    LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT,
#    NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION
#    WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
#    ------------------------------------------------------------


"""
Formatters for converting and validating data values.
"""

import copy, re, time
import six

class Formatter(object):
    """
    Formatter/validator for data values.
    """
    def __init__(self, *args, **kwargs):
        pass

    # Default (dummy) validate routine
    def validate(self, value):
        """
        Return true if value is valid for the field.
        value is a string from the UI.
        """
        return True

    # Default (dummy) format routine
    def format(self, value):
        """Format a value for presentation in the UI."""
        if value == None:
            return ''
        return str(value)

    # Default (dummy) coerce routine
    def coerce(self, value):
        """Convert a string from the UI into a storable value."""
        return value


class EnumFormatter(Formatter):
    """
    Formatter for enumerated (EnumType) data values.
    """
    def __init__(self, enumeration, *args, **kwargs):

        super(EnumFormatter,self).__init__(*args, **kwargs)

        self.enumeration = enumeration


    def validValues(self):
        """
        Return list of valid value (id,label) pairs.
        """
        return copy.copy(self.enumeration.items())


    def validate(self, value):
        """
        Return true if value is valid for the field.
        value is a string from the UI.
        """
        vv = [ s for i, s in self.validValues() ]
        return (value in vv)


    def format(self, value):
        """Format a value for presentation in the UI."""
        return self.enumeration[value]


    def coerce(self, value):
        """Convert a string from the UI into a storable value."""
        return getattr(self.enumeration, value)


class FormatterMeta(type):
    """
    Metaclass for subclasses of Formatter.

    Each instance class MUST define either validate(self, value) method
    or re_validation regular expression string.

    If the latter, the validate method will be autogenerated.

    If re_validation is defined, validate method is overridden to
    validate the string value against the regular expression.

    If re_validation_flags is defined, the flags will be
    used when the re_validation regular expression string is compiled.

    Each instance class MAY define:
    -    format(self, value) method.
    -    coerce(self, value) method.
    """
    def __new__(cls, classname, bases, classdict):
        newdict = copy.copy(classdict)

        # Generate __init__ method
        # Direct descendants of Formatter automatically get __init__.
        # Indirect descendants don't automatically get one.
        if Formatter in bases:
            def __init__(self, *args, **kwargs):
                Formatter.__init__(self, *args, **kwargs)
                initialize = getattr(self, 'initialize', None)
                if initialize:
                    initialize()
            newdict['__init__'] = __init__
        else:
            def __init__(self, *args, **kwargs):
                super(self.__class__,self).__init__(*args, **kwargs)
                initialize = getattr(self, 'initialize', None)
                if initialize:
                    initialize()
            newdict['__init__'] = __init__

        # Generate validate-by-RE method if specified
        re_validation = newdict.get('re_validation', None)
        if re_validation:
            # Override validate method
            re_validation_flags = newdict.get('re_validation_flags', 0)
            newdict['_re_validation'] = re.compile(re_validation, re_validation_flags)
            def validate(self, value):
                return (self._re_validation.match(value) != None)
            newdict['validate'] = validate

        # Delegate class creation to the expert
        return type.__new__(cls, classname, bases, newdict)


class ObjectIdFormatter(six.with_metaclass(FormatterMeta, Formatter)):
    """
    Object ID is assumed to be a large (32 bit?) unsigned integer.
    """
    re_validation = '^[0-9]+$'
    def coerce(self, value):
        if value: return long(value)
        return value

class StringFormatter(six.with_metaclass(FormatterMeta, Formatter)):
    pass

class AlphaFormatter(StringFormatter):
    """Alphabetic characters only."""
    re_validation = '^[a-zA-Z]*$'

class AlphaNumericFormatter(StringFormatter):
    """Alphanumeric characters only."""
    re_validation = '^[a-zA-Z0-9]*$'

class EmailFormatter(StringFormatter):
    """Internet email addresses (more or less)."""
    # This regex does not match all legal email addresses, but
    # it does a pretty good job.
    # Strangely enough, '/' is legal in email addresses.
    # However, I've never seen it used, so I prefer to leave it out.
    _re_subs = {
            'sub1' : r'[a-zA-Z~_-][a-zA-Z0-9_:~-]*',
            'sub2' : r'(\.[a-zA-Z0-9_:~-]+)*',
            'sfx'  : r'\.[a-zA-Z]{2,3}'
        }
    re_validation = '^%(sub1)s%(sub2)s[@]%(sub1)s%(sub2)s%(sfx)s$' % _re_subs

class MoneyFormatter(StringFormatter):
    """Assumes decimal money, but doesn't assume currency."""
    re_validation = '^(([0-9]+([.][0-9]{2})?)|([0-9]*[.][0-9]{2}))$'

class IntFormatter(six.with_metaclass(FormatterMeta, Formatter)):
    """Signed or unsigned integer."""
    #re_validation = '^[-+]?[0-9]+$'
    def validate(self, value):
        try:
            v = int(value)
            return True
        except:
            return False
    def coerce(self, value):
        if value: return int(value)
        return value

class Int8Formatter(IntFormatter):
    pass

class Int16Formatter(IntFormatter):
    pass

class Int24Formatter(IntFormatter):
    def coerce(self, value):
        if value: return long(value)
        return value

class Int32Formatter(IntFormatter):
    def coerce(self, value):
        if value: return long(value)
        return value

class Int64Formatter(IntFormatter):
    def coerce(self, value):
        if value: return long(value)
        return value

class UIntFormatter(six.with_metaclass(FormatterMeta, Formatter)):
    """Unsigned integer."""
    re_validation = '^[0-9]+$'
    def coerce(self, value):
        if value: return int(value)
        return value

class UInt8Formatter(UIntFormatter):
    pass

class UInt16Formatter(UIntFormatter):
    def coerce(self, value):
        if value: return long(value)
        return value

class UInt24Formatter(UIntFormatter):
    def coerce(self, value):
        if value: return long(value)
        return value

class UInt32Formatter(UIntFormatter):
    def coerce(self, value):
        if value: return long(value)
        return value

class FloatFormatter(six.with_metaclass(FormatterMeta, Formatter)):
    """Signed or unsigned floating-point number."""
    re_validation = '^[-+]?(([0-9]+[.]?[0-9]*)|([0-9]*[.]?[0-9]+))$'
    def coerce(self, value):
        if value: return float(value)
        return value

class DoubleFormatter(FloatFormatter):
    pass

class UFloatFormatter(six.with_metaclass(FormatterMeta, Formatter)):
    """Unsigned floating-point number."""
    re_validation = '^(([0-9]+[.]?[0-9]*)|([0-9]*[.]?[0-9]+))$'
    def coerce(self, value):
        if value: return float(value)
        return value

class UDoubleFormatter(UFloatFormatter):
    pass


class TextFormatter(six.with_metaclass(FormatterMeta, Formatter)):
    pass

class TimeElapsedFormatter(six.with_metaclass(FormatterMeta, Formatter)):
    """Elapsed time string (HH:MM:SS)."""
    re_validation = '^([1][0-2]|[0]?[0-9]):[0-5][0-9](:[0-5][0-9])?$'

class DateFormatter(six.with_metaclass(FormatterMeta, Formatter)):
    """
    Date string (YYYY-MM-DD).

    Storage format:      YYYY-MM-DD
    Presentation format: YYYY-MM-DD

    Accepts only YYYY-MM-DD format (allows variant separators '/' and '.').
    Accepts dates in range (1000-2999)-(01-12)-(01-31).
    Leading zeros optional in month and day.
    Does not enforce # of days in month.
    """
    re_validation = r'^[1-2][0-9]{3}([-/.])([0][1-9]|[1][0-2])\1([0][1-9]|[12][0-9]|[3][0-1])$'

    def coerce(self, value):
        """Convert alternate date separators to '-'."""
        return re.sub(r'[/.]', '-', value)

class DateFormatterMDY(DateFormatter):
    """Alternate date string (MM-DD-YYYY).

    Storage format:      YYYY-MM-DD
    Presentation format: MM-DD-YYYY

    Accepts only MM-DD-YYYY format  (allows variant separators '/' and '.').
    Accepts dates in range (01-12)-(01-31)-(1000-2999).
    Leading zeros optional in month and day.
    Does not enforce # of days in month.
    """
    re_validation = r'^([0][1-9]|[1][0-2])([-/.])([0][1-9]|[12][0-9]|[3][0-1])\1[12][0-9]{3}$'

    def format(self, value):
        dt = time.strptime(value, '%Y-%m-%d')
        return time.strftime('%m-%d-%Y', dt)

    def coerce(self, value):
#        value = re.sub(r'[/.]', '-', value)
#        dt = time.strptime(value, '%m-%d-%Y')
#        return time.strftime('%Y-%m-%d', dt)
        m, d, y = re.split('[-/.]', value)
        return '%04d-%02d-%02d' % (int(y), int(m), int(d))

class TimeFormatter(six.with_metaclass(FormatterMeta, Formatter)):
    """
    Time string (12-hour or 24-hour format, with or without seconds or am/pm).

    Storage format:      HH:MM:SS -- 24-hour format.
    Presentation format: HH:MM    -- 24-hour format.

    Accepts 12-hour or 24-hour format, with or without seconds or am/pm.
    """

    reTime24 = r'(([0]?[0-9]|[1][0-9]|[2][0-3]):[0-5][0-9](:[0-5][0-9])?)'
    reTimeAP = r'(([1][0-2]|[0]?[0-9]):[0-5][0-9](:[0-5][0-9])?[ ]*([aApP][mM])?)'
    re_validation = r'^(%s|%s)$' % (reTime24, reTimeAP)

    def format(self, value):
        return ':'.join(value.split(':')[:2])

    def coerce(self, value):
        # TODO pass validate() may still fail here, e.g., 10:15am
        for fmt in ('%H:%M:%S', '%H:%M', '%I:%M:%S %p', '%I:%M %p','%I:%M:%S'):
            try:
                dt = time.strptime(value, fmt)
                break
            except ValueError:
                pass
        return time.strftime('%H:%M:%S', dt)

class TimeFormatter12H(TimeFormatter):
    """
    Alternate time string (12-hour format, without seconds).

    Storage format:      HH:MM(:SS) -- 24-hour format.
    Presentation format: HH:MM(aa) -- 12-hour format.
                         (aa may be 'am' or 'pm')
    """

    def format(self, value):
        dt = time.strptime(value, '%H:%M:%S')
        return time.strftime('%I:%M %p', dt)

class DateTimeFormatter(six.with_metaclass(FormatterMeta, Formatter)):
    """
    Date/time string.

    Uses a DateFormatter and a TimeFormatter.
    """

    # Storage format: YYYY-MM-DD HH:MM:SS -- 24-hour format.
    # Presentation format: same as storage format.

    def validate(self , value):
        datef = DateFormatter()
        timef = TimeFormatter()
        date, time = re.split(r'[ ]+', value)
        return (datef.validate(date) and timef.validate(time))


if __name__ == '__main__':

    from EnumType import EnumType

#    from prs.PRSObject import PRSObject
#
#    print 'dir(ObjectIdField) =', dir(ObjectIdField)
#    print 'ObjectIdField.__init__ =', ObjectIdField.__init__
#    print 'ObjectIdField.__init__.func_doc =', ObjectIdField.__init__.func_doc
#    anIdField = ObjectIdFormatter(False)
#    print 'dir(anIdField) =', dir(anIdField)
#    print 'anIdField.__class__ =', anIdField.__class__
#
#    print
#    print 'dir(DateFormatterMDY) =', dir(DateFormatterMDY)
#    print

    # ========== EnumFormatter ==========
    print('EnumFormatter')
    TestEnum = EnumType('A', 'B', 'C', 'D')
    formatter = EnumFormatter(TestEnum)
    passCount = 0
    failCount = 0

    # formatter.validValues
    expectedValidValues = [ (0,'A'), (1,'B'), (2,'C'), (3,'D') ]
    temp = formatter.validValues()
    if temp == expectedValidValues:
        passCount += 1
    else:
        failCount += 1
        print('formatter.validValues() = %r -- WRONG -- expected %r' % (temp, expectedValidValues))

    # formatter.format and formatter.coerce
    for id, name in TestEnum.items():
        # formatter.format
        temp = formatter.format(id)
        if temp == name:
            passCount += 1
        else:
            failCount += 1
            print('formatter.format(%d) = %r -- WRONG -- expected %r' % (id, temp, name))
        # formatter.coerce
        temp = formatter.coerce(name)
        if temp == id:
            passCount += 1
        else:
            failCount += 1
            print('formatter.coerce(%r) = %r -- WRONG -- expected %r' % (name, temp, id))
    if failCount == 0:
        print('\tPASS (%d tests)' % passCount)


    # ========== EmailFormatter ==========
    print()
    print('EmailFormatter')

    tests = [
            ('a@b.com', True),
            ('a@b', False),
            ('@b', False),
            ('@b.c', False),
            ('@b.us', False),
            ('a@b.us', True),
            ('~joe_public-73@bozo.net', True),
        ]
    formatter = EmailFormatter()
    passCount = 0
    failCount = 0

    for email, expect in tests:
        actual = formatter.validate(email)
        if actual == expect:
            passCount += 1
        else:
            print(email, actual, expect)
            failCount += 1
    print('\tPASS (%d tests)' % passCount)
    print('\tFAIL (%d tests)' % failCount)

