# -*- python -*-

# @HEADER
# ***********************************************************************
#
#          PyTrilinos: Python Interfaces to Trilinos Packages
#                 Copyright (2014) Sandia Corporation
#
# Under the terms of Contract DE-AC04-94AL85000 with Sandia
# Corporation, the U.S. Government retains certain rights in this
# software.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are
# met:
#
# 1. Redistributions of source code must retain the above copyright
# notice, this list of conditions and the following disclaimer.
#
# 2. Redistributions in binary form must reproduce the above copyright
# notice, this list of conditions and the following disclaimer in the
# documentation and/or other materials provided with the distribution.
#
# 3. Neither the name of the Corporation nor the names of the
# contributors may be used to endorse or promote products derived from
# this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY SANDIA CORPORATION "AS IS" AND ANY
# EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL SANDIA CORPORATION OR THE
# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
# PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
# LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
# NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
#
# Questions? Contact William F. Spotz (wfspotz@sandia.gov)
#
# ***********************************************************************
# @HEADER

class PropertyBase(object):
    """
    PropertyBase is a base class for simple property classes that have a
    restricted set of property names and type-checked property values.  To
    define a property class, do the following:

      1. Define a class that inherits from PropertyBase
      2. Add a static data member named 'props', which is a dictionary.  Its
         keys should be valid property names, and its values should specify the
         type of the corresponding property.  This type specification can be a
         type object, or a sequence of objects which define an enumeration.
      3. Optionally define a static data member named 'defaults', with all or a
         subset of the 'props' keys, and default values for those properties.
         Keys in 'defaults' but not in 'props' will cause an AttributeError
         exception to be raised, and values in 'defaults' that are of the wrong
         type will cause a TypeError exception to be raised.  Properties not
         specified in 'defaults' will default to None. 
      4. Define the derived constructor such that it calls the base constructor:
           def __init__(self, **kwargs):
               PropertyBase.__init__(self, **kwargs)

    And that is it.  As an example, if we wanted a class that kept track of the
    name, author, number of pages and price of a book:

      >>> class BookProperties(PropertyBase):
      ...     props = {'name':str, 'author':str, 'pages':int, 'price':float}
      ...     defaults = {'price' : 9.99 }
      ...     def __init__(self, **kwargs):
      ...         PropertyBase.__init__(self, **kwargs)
      ... 
      >>> bookProps = BookProperties(name='A Tale of Two Cities')
      >>> bookProps.author = 'Charles Dickens'
      >>> print bookProps
      price = 9.9900000000000002
      pages = None
      name = 'A Tale of Two Cities'
      author = 'Charles Dickens'
      >>>
    """
    def __init__(self, **kwargs):
        # Check for existence of self.props
        if not hasattr(self, 'props'):
            raise AttributeError, \
                  "\n'%s', derived from PropertyBase does not have a props\n" \
                  "attribute, which should be implimented as a static member\n" \
                  "of '%s', whose keys are attribute names and\n" \
                  "whose values are types." % (self.__class__.__name__,
                                               self.__class__.__name__)
        # Initialize the props values
        self.initializePropValues()
        # Fill properties with intitial values of None
        for name in self.props:
            self.__dict__[name] = None
        # If defaults is an attribute, use it to set the default values
        if hasattr(self, 'defaults'):
            for name in self.defaults:
                self.__setattr__(name, self.defaults[name])
        # Fill properties specified by keyword arguments, while performing type
        # checks
        for name in kwargs:
            self.__setattr__(name, kwargs[name])

    def initializePropValues(self):
        # Loop over the property specifications and convert each scalar to a
        # one-tuple
        for name in self.props:
            propSpec = self.props[name]
            if isinstance(propSpec, (tuple, list)):
                if len(propSpec) == 0:
                    self.props[name] = (propSpec,)
            else:
                self.props[name] = (propSpec,)

    def __setattr__(self, name, value):
        # For illegal property names, raise an exception
        if name not in self.props:
            raise AttributeError, \
                  "'%s' is not a valid attribute name" % name
        # For legal value types, assign the property value to the named class
        # attribute
        propSpecs = self.props[name]
        for propSpec in propSpecs:
            if isinstance(propSpec, type):
                if isinstance(value, propSpec):
                    self.__dict__[name] = value
                    break
            elif value == propSpec:
                self.__dict__[name] = value
                break
        # For illegal value types, raise an exception
        else:
            if len(propSpecs) == 1 and isinstance(propSpecs[0], type):
                raise TypeError, \
                      "For attribute '%s', value should be of %s.\n" \
                      "Got attribute of %s" % (name, propSpecs[0], type(value))
            else:
                raise ValueError, \
                      "For attribute '%s', value '%s'\n" \
                      "and type %s is not among the enumerated values:\n" \
                      "%s" % (name, str(value), type(value), str(propSpecs))

    def __getattr__(self, name):
        # For illegal property names, raise an exception
        if name not in self.props:
            raise AttributeError, \
                  "'%s' is not a valid attribute name" % name
        # Return the named property value
        return self.__dict__[name]

    def __str__(self):
        # Build a human readable string representation
        result = ''
        for name in self.props:
            attribute = self.__getattr__(name)
            if attribute is self: attribute = 'self reference'
            result += name + ' = ' + repr(attribute) + "\n"
        if len(result): result = result[:-1]
        return result

    def __repr__(self):
        # Build an evaluatable string representation
        result = ''
        for name in self.props:
            attribute = self.__getattr__(name)
            if attribute is self: attribute = 'self reference'
            if attribute is not None:
                result += name + '=' + repr(attribute) + ", "
        if len(result) > 2: result = result[:-2]
        return self.__class__.__name__ + "(" + result + ")"
