# -*- 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 classes that represent rows of data retrieved from a data
#  source.

from Library.Base.Classwork import AnObject
from Library.Base.Events import ALoggingEventfulObject
from Library.IO.Logging.Logs import AMessageWithTimeStamp
from Library.String.Utilities import String, \
    sDefaultItemsDelimiter, sDefaultKeyDelimiter
    
from UserDict import DictMixin

## Events generated by ADataRow
drEvents = {
    # Before an item is retrieved from the data row
    "getItem": None,
    # Before an item is set within the data row
    "setItem": None,
    # Before an item is deleted from the data row
    "deleteItem": None,
    # Before the data row is flushed to the data source
    "flush": None,
    # Before the data row is fetched from the data source
    "fetch": None,
    # Before the data row is inserted into the data source
    "insert": None,
    # Before the data row is deleted from the data source
    "delete": None
}

## The default delimiters used when creating a string representation of ADataRow
drDefaultKeyDelimiter = "="
drDefaultPairsDelimiter = ", "

## The error message used when an attempt is made to retrieve a non-existent
# item from a data row
drErrorRetrievingItem = ("failed to retrieve the item \"{itemname}\""
                         " from {classname}")
## The error message used when failing to update a data row
drUpdateError = "failed to update record {} from \"{}\" in {}"
## The error message used when failing to fetch a data row
drFetchError = "failed to fetch data from \"{}\" in {} because: {}"

## A data row error
class ADataRowError(AMessageWithTimeStamp):
    pass
    
## A row of data retrieved from a data source.
#
#  This class is designed to represent a single record from a data source.
#  It records changes that are made to its fields, so that updates can be made 
#  by calling ADataRow.flush(). 
class ADataRow(ALoggingEventfulObject, DictMixin):
    ## Construct a new instance of the class.
    #
    #  If fields and values are specified in \p fieldsValues, they are stored
    #  for later use by the data row.
    #  
    #  The constructor expects that \p params will contain the following items:
    #  - \p DataSource -- A reference to an instance of ADataSource, from which
    #                     the data for the row is retrieved.
    #  - \p category -- The name of the category (or table) from which the
    #                   record was retrieved
    def __init__(Self, fieldsValues=None, **params):
        ALoggingEventfulObject.__init__(Self, **params)
        
        if "DataSource" in Self._params:
            Self._Source = Self._params["DataSource"]
            del Self._params["DataSource"]
            
        if "category" in Self._params:
            Self._category = Self._params["category"]
            del Self._params["category"]
        
        # Transfer any fields provided to the constructor
        if fieldsValues is not None:
            for key, value in fieldsValues.iteritems():
                Self._fieldsValues[key] = value

    ## Initializer
    def init(Self):
        result = ALoggingEventfulObject.init(Self)

        if result == True:
            # Establish private attributes
            Self._Source = None
            Self._category = None
            Self._fieldsValues = {}
            Self._changes = []

            # Establish public attributes
            # The delimiter used to separate field names from field values when
            # creating a string representation of the data row
            Self.keyDelimiter = drDefaultKeyDelimiter
            # The delimiter used to separate the field name/field value pairs
            # when creating a string representation of the data row
            Self.pairsDelimiter = drDefaultPairsDelimiter
            # Whether or not the row can be modified
            Self.readOnly = True
            
            # Update events generated by the class
            Self._events.update(drEvents)

        return result

    ## Flush any changes made to the data row back to the data source.
    #
    #  This routine should be implemented by descendants of ADataRow.
    #
    #  This method generates the \p flush event.
    def flush(Self, *criteria):
        Event = Self.generate("flush")
        
        if Event.handled():
            del Event
            return
            
        del Event
        
        if Self.readOnly:
            return
            
        if len(Self._changes) == 0:
            return
            
        fields = {}

        # Update the fields that have been modified
        for fieldName in Self._changes:
            fields[fieldName] = Self._fieldsValues[fieldName]
                
        # Update the record
        if Self._Source.update(Self._category,
            fieldsValues=fields,
            criteria=criteria) < 0:
                Self.error(ADataRowError(drUpdateError, 
                    Self["id"], Self._category,
                    Self._Source.displayName()))
            
        Self._changes = []            
            
    ## Fetch the data row from the data source.
    def fetch(Self, **params):
        result = tuple()
        
        Event = Self.generate("fetch", **params)
        if Event.handled():
            result = Event.result()
            del Event
            return result
            
        try:
            result = Self._Source.fetch(Self._category, **params)
        
            # Retrieve the record
            for row in result:
                Self._fieldsValues.update(row)
            
        except Exception, Err:
            Self.error(ADataRowError(drFetchError, Self._category,
                Self._Source.name(), str(Err)))
                
        return result
    
    ## Insert the data row into the data source.
    def insert(Self):
        result = -1
        
        Event = Self.generate("insert")
        if Event.handled():
            result = Event.result()
            del Event
            return result
            
        del Event
        
        result = Self._Source.insert(Self._category,
            fieldsValues = Self._fieldsValues)
            
        if result > 0:
            Self._fieldsValues["id"] = result
            Self._changes = []
            
        return result
            
    ## Delete the data row from the data source
    def delete(Self):
        result = -1
        
        Event = Self.generate("delete")
        if Event.handled():
            result = Event.result()
            del Event
            return result
            
        del Event
        
        return Self._Source.delete(Self._category,
            criteria = [
                ("{field:id} = {}", Self._fieldsValues["id"])
            ]
        )               

    # IsPrintable implementation -----------------------------------------------
    ## Construct a string representation of the data row.
    #
    #  This representation is formatted as a list of the field names and field
    #  values in the data row.  The list is delimited using the string
    #  specified by ADataRow.pairsDelimiter and the field names are separated
    #  from the field values using the string specified by
    #  ADataRow.keyDelimiter.
    def toString(Self):
        return String.implode(Self._fieldsValues, Self.pairsDelimiter, 
          Self.keyDelimiter)

    # Properties ---------------------------------------------------------------
    ## Determine whether or not changes have been made to the data row
    #  since it was initially loaded from the data source.
    def isDirty(Self):
        return len(Self._changes) > 0

    ## Retrieve the list of changed fields.
    def changes(Self):
        return Self._changes
    
    ## Retrieve a reference to the data source used by the data row.
    def Source(Self):
        return Self._Source

    ## Get value of the specified field from the data row.
    #
    #  This routine is provided for convenience, so that access to the fields
    #  and values of the data row can be programmed as though the row was a
    #  dictionary object.  This method accepts either a string key or an
    #  integer index into the data row (with index zero representing the first
    #  field).
    #
    #  This method generates the \p getItem event.
    def __getitem__(Self, key):
        Event = Self.generate("getItem", key=key)
        
        if Event.handled():
            del Event
            return
            
        del Event
        
        if key in Self._fieldsValues:
            return Self._fieldsValues[key]

        elif isinstance(key, int):
            values = Self._fieldsValues.values()
            if key < len(values):
              return values[key]

        else:
            raise KeyError(drErrorRetrievingItem.format(itemname=key,
              classname=Self.classname()))

    ## Set the value of the specified field.
    #
    #  This routine is provided for convenience, so that access to the fields
    #  and values of the data row can be programmed as though the row was a
    #  dictionary object.  This method accepts either a string key or an
    #  integer index into the data row (with the first field at index 0).
    #
    #  This method generates the \p setItem event.
    def __setitem__(Self, key, value):
        Event = Self.generate("setItem", key=key, value=value)
        
        if Event.handled():
            del Event
            return
            
        del Event
        
        if isinstance(key, int):
            keys = Self._fieldsValues.keys()
            if key < len(keys):
                key = keys[key]

        if key in Self._fieldsValues:
            # Update the field value
            Self._fieldsValues[key] = value
            # Indicate that the field was changed
            if not key in Self._changes:
                Self._changes.append(key)

    ## Delete the specified field.
    #
    #  This routine is provided for convenience, so that access to the fields
    #  and values of the data row can be programmed as though the row was a
    #  dictionary object.
    #
    #  This method generates the \p deleteItem event.
    def __delitem__(Self, key):
        Event = Self.generate("deleteItem", key=key)
        
        if Event.handled():
            del Event
            return
            
        del Event
        
        if key in Self._fieldsValues:
            del Self._fieldsValues[key]

    ## Retrieve the keys (field names) in the data row.
    #
    #  This routine is provided for convenience, so that access to the fields
    #  and values of the data row can be programmed as though the row was a
    #  dictionary object.
    def keys(Self):
        return Self._fieldsValues.keys()


## A class that represents an expanding list field
class AnExpandingListField(AnObject):
    ## Constructor
    def __init__(Self, itemsDelimiter=sDefaultItemsDelimiter, 
        uniqueItems=False, *data):
        
        AnObject.__init__(Self)
        
        Self._itemsDelimiter = itemsDelimiter
        Self._uniqueItems = uniqueItems
        
        Self.append(*data)
            
    ## Initializer
    def init(Self):
        result = AnObject.init(Self)
        
        if result:
            # Establish private attributes
            # The delimiter to use between items in the list
            Self._itemsDelimiter = ""
            # Whether or not items in the list must be unique values
            Self._uniqueItems = False
            # The items stored by the list
            Self._items = []
            # Whether the field has changed
            Self._changed = False
            
        return result
        
    ## Append an item to the list
    def append(Self, *items):
        for item in items:
            if (Self._uniqueItems) and (item in Self._items):
                return
            
            Self._items.append(item)
            Self._changed = True
            
    ## Remove an item from the list
    def remove(Self, key):
        if key in Self._items:
            Self._items.remove(key)
            Self._changed = True
            
    ## Clear the list
    def clear(Self):
        Self._items = []
        Self._changed = True            
        
    ## Condense the list
    def implode(Self):
        return String.implode(Self._items, Self._itemsDelimiter)
        
    ## Expand the list
    def explode(Self, s):
        if len(s) == 0:
            return
            
        Self.append(*String.explode(s, list, Self._itemsDelimiter))
        
    # Properties ---------------------------------------------------------------
    ## Return whether or not the field has been changed
    def changed(Self):
        return Self._changed
        
    ## Fetch the specified item from the list
    def __getitem__(Self, index):
        return Self._items[index]
        
    ## Set the specified item from the list
    def __setitem__(Self, index, value):
        if value not in Self._items:
            Self._items[index] = value
            
    ## Determine whether the specified item is in the list
    def __contains__(Self, value):
        return value in Self._items
        
    ## Retrieve the number of items in the list
    def __len__(Self):
        return len(Self._items)
        
    ## Iterate over the items in the list
    def __iter__(Self):
        return Self._items.__iter__()

## A class that represents an expanding dictionary field
class AnExpandingDictionaryField(AnObject, DictMixin):
    ## Constructor
    def __init__(Self, keyDelimiter=sDefaultKeyDelimiter, 
        pairsDelimiter=sDefaultItemsDelimiter, **data):
        
        AnObject.__init__(Self)
        
        Self._keyDelimiter = keyDelimiter
        Self._pairsDelimiter = pairsDelimiter
        
        Self._data.update(data)
        
    ## Initializer
    def init(Self):
        result = AnObject.init(Self)
        
        if result:
            # Establish private attributes
            # The delimiter to use between keys and values
            Self._keyDelimiter = ""
            # The delimiter to use between key/value pairs
            Self._pairsDelimiter = ""
            # The data stored in the field
            Self._data = {}
            # Indicates whether or not the field has changed
            Self._changed = False
            
        return result
        
    ## Condense the field data into a delimited string
    def implode(Self):
        return String.implode(Self._data, Self._pairsDelimiter, 
            Self._keyDelimiter)
            
    ## Expand the field data from a delimited string
    def explode(Self, s):
        if len(s) == 0:
            return
            
        Self._data.update(String.explode(s, dict, Self._pairsDelimiter,
            Self._keyDelimiter))
        
    # Properties ---------------------------------------------------------------
    ## Return the actual dictionary used to manage the items in the field
    def data(Self):
        return Self._data
        
    ## Get or set the key delimiter
    def keyDelimiter(Self, *newDelimiter):
        result = Self._keyDelimiter
        
        if len(newDelimiter) > 0:
            Self._keyDelimiter = newDelimiter[0]
            
        return result
        
    ## Get or set the pairs delimiter
    def pairsDelimiter(Self, *newDelimiter):
        result = Self._pairsDelimiter
        
        if len(newDelimiter) > 0:
            Self._pairsDelimiter = newDelimiter[0]
            
        return result
        
    ## Determine whether the field has changed
    def changed(Self):
        return Self._changed
        
    ## Get the value of the named item from the field data
    def __getitem__(Self, key):
        if key in Self._data:
            return Self._data[key]
            
        return None
        
    ## Set the value of the named item in the field data
    def __setitem__(Self, key, value):
        Self._data[key] = value
        
        Self._changed = True
        
    ## Delete the named item from the field data
    def __delitem__(Self, key):
        if key in Self._data:
            del Self._data[key]
            
            Self._changed = True
            
    ## Determine whether the specified key exists in the field data
    def __contains__(Self, key):
        return key in Self._data
            
    ## Retrieve a list of keys from the field data
    def keys(Self):
        return Self._data.keys()

## A data row that includes expandable fields
class ADataRowWithExpandingFields(ADataRow):
    ## Constructor
    def __init__(Self, fieldsValues=None, **params):
        ADataRow.__init__(Self, fieldsValues, **params)
        
        if "expand" in Self._params:
            for key in Self._params["expand"].iterkeys():
                value = Self._params["expand"][key]
                
                if issubclass(value[0], dict):
                    if len(value) > 1:
                        Self._expand[key] = \
                            AnExpandingDictionaryField(value[1], value[2])
                    
                    else:
                        Self._expand[key] = AnExpandingDictionaryField()
                        
                else:
                    if len(value) > 1:
                        Self._expand[key] = \
                            AnExpandingListField(value[1], value[2])
                            
                    else:
                        Self._expand[key] = AnExpandingListField()
                    
            # Check for fields and values to expand
            if len(Self._fieldsValues) > 0:
                for key, value in Self._fieldsValues.iteritems():
                    if key in Self._expand:
                        Self._expand[key].explode(value)
                
            del Self._params["expand"]
            
    ## Initializer
    def init(Self):
        result = ADataRow.init(Self)
        
        if result:
            # Establish private attributes
            # The list of field names to expand and condense
            Self._expand = {}
            
        return result
        
    ## Flush the data row back to the source
    def flush(Self, *params):
        if len(Self._changes) == 0:
            return
            
        if Self.readOnly:
            return
            
        # Check for expanding fields that have changed
        for fieldName in Self._expand.iterkeys():
            if Self._expand[fieldName].changed():
                if fieldName not in Self._changes:
                    Self._changes.append(fieldName)
                    
                # Condense the data
                Self._fieldsValues[fieldName] = \
                    Self._expand[fieldName].implode()
                    
        ADataRow.flush(Self, *params)
                    
    ## Fetch the data row from the data source
    def fetch(Self, **params):
        result = ADataRow.fetch(Self, **params)
                   
        # Check for expanding fields
        for key, value in Self._fieldsValues.iteritems():
            if key in Self._expand:
                Self._expand[key].explode(value)
            
        return result    
        
    ## Insert the data row into the data source.
    def insert(Self):
        # Condense fields
        for key in Self._expand.iterkeys():
            Self._fieldsValues[key] = Self._expand[key].implode()
            
        return ADataRow.insert(Self)
                    
    # Properties ---------------------------------------------------------------
    ## Get the specified item from the data row
    def __getitem__(Self, key):
        # Check to see if the key is one of our expanding fields
        if key in Self._expand:
            return Self._expand[key]
            
        return ADataRow.__getitem__(Self, key)
