"""
$URL: svn+ssh://svn.mems-exchange.org/repos/trunk/dulcinea/lib/property/property_type.py $
$Id: property_type.py 31174 2008-09-24 17:59:59Z rmasse $
"""
from dulcinea.base import DulcineaPersistent
from dulcinea.material import Material, get_material_db
from dulcinea.physical_unit import get_standard_units
from dulcinea.physical_unit import PhysicalUnit, get_standard_unit
from dulcinea.physical_value import PhysicalValue
from dulcinea.property import get_property_db
from dulcinea.property.errors import PropertyTypeError, ConstraintError
from dulcinea.range_value import RangeValue
from dulcinea.set_utils import in_set
from durus.persistent import PersistentObject
from durus.persistent_dict import PersistentDict
from qp.lib.spec import mapping, anything, instance, spec, require, string
from qp.lib.spec import Specified
from qpy import xml, xml_quote, stringify
import dulcinea.property.property_template


class PropertyType (DulcineaPersistent):

    INT = 1
    STRING = 2
    PHYSICAL_VALUE = 3
    MATERIAL = 4
    BOOLEAN = 5

    ATOMIC_TYPES = ((INT, "integer"),
                    (STRING, "string"),
                    (PHYSICAL_VALUE, "physical value"),
                    (MATERIAL, "material"),
                    (BOOLEAN, 'boolean'))

    ATOMIC = 10
    LIST = 20
    TABLE = 30

    AGGREGATE_TYPES = ((ATOMIC, "atomic"),
                       (LIST, "list"),
                       (TABLE, "table"))

    def is_atomic (self):
        return 0

    def is_int (self):
        return 0

    def is_string (self):
        return 0

    def is_physical_value (self):
        return 0

    def is_material (self):
        return 0

    def is_boolean (self):
        return 0

    def is_list (self):
        return 0

    def is_table (self):
        return 0

    def is_discrete (self, constraint):
        raise NotImplementedError

    def allow_tolerance (self):
        return False

    def get_unit (self):
        return None

    def get_compatible_units (self):
        return None

    def check_value (self, value):
        """Checks that the *type* of value is compatible
        with this type of property.
        Raise PropertyTypeError if problems are discovered.
        """
        raise NotImplementedError

    def supports_simplify (self):
        return True

    def supports_constraints (self):
        "Returns true if setting constraints makes sense for this type."
        return True

    def check_constraint (self, constraint):
        """Checks that constraint is a sequence of
        items whose types are compatible with this type
        of property.
        Raise PropertyTypeError if problems are discovered.
        """
        if constraint is None:
            return
        if type(constraint) is list:
            for item in constraint:
                self.check_value(item)
        else:
            raise PropertyTypeError(
                'invalid constraint %r for %r' % (constraint, self))

    def check_value_in_constraint (self, value, constraint):
        if not in_set(value, constraint):
            self.value_in_constraint_error(value, constraint)

    def check_set_in_constraint (self, set, constraint):
        self.check_constraint(set)
        if constraint is None or set == constraint:
            return
        if set is None:
            raise ConstraintError
        for element in set:
            if not in_set(element, constraint):
                raise ConstraintError(
                    '%s not within constraint %s' % (element, constraint))


    def value_in_constraint_error (self, bad_value, constraint):
        raise ConstraintError(
            '%s not allowed: %s' % (bad_value,
                                    self.explain_constraint(constraint)))

    def explain_constraint (self, constraint, html=0):
        if html:
            texttype = xml
        else:
            texttype = str
        if not constraint:
            return None
        else:
            set = self.format_set(constraint, html=html)
            if set is None:
                return None
            elif len(set) <= 2:
                return texttype("must be ") + texttype(" or ").join(set)
            else:
                return texttype("must be one of ") + texttype(", ").join(set)

    def value_error (self, value):
        raise PropertyTypeError(
            'invalid value %r for %r' % (value, self))

    def format_value (self, value, html=0):
        if html:
            return xml_quote(value)
        else:
            return str(value)

    def format_set (self, set, html=False):
        """(set:[any], html:bool=False) -> [str|xml]
        Return a list of nice string (or xml) renderings of the elements
        of set, which is a constraint for this property type.

        Implemented in subclasses.
        """
        raise NotImplementedError

    def format_constraint (self, set, html=0):
        if set is not None:
            formatted_set = self.format_set(set, html=html)
            if formatted_set is not None:
                if html:
                    comma = xml(", ")
                else:
                    comma = ", "
                return comma.join(formatted_set)
        if html:
            return xml('<em>unconstrained</em>')
        else:
            return '(unconstrained)'


class IntPropertyType (PropertyType):

    def get_key (self):
        return (PropertyType.ATOMIC, PropertyType.INT,
                None, None, None)

    def is_atomic (self):
        return 1

    def is_int (self):
        return 1

    def is_discrete(self, constraint):
        if constraint is None:
            return False
        for item in constraint:
            if isinstance(item, RangeValue):
                return False
        return True

    def __str__ (self):
        return "Int"

    def check_value (self, value):
        if value is None:
            return
        if type(value) is int:
            return
        if isinstance(value, RangeValue):
            if type(value.get_min()) is int:
                return
        self.value_error(value)

    def simplify_value (self, value):
        return value

    def reconstitute_value (self, value):
        return value

    def format_set (self, set, html=0):
        if html:
            return list(map(xml_quote, set))
        else:
            return list(map(str, set))

    def get_constraint_range (self, constraint):
        if constraint is None:
            return None

        def get_min_and_max (val):
            if isinstance(val, RangeValue):
                return val.get_min(), val.get_max()
            else:
                return val, val

        val_min, val_max = get_min_and_max(constraint[0])
        for value in constraint[1:]:
            next_min, next_max = get_min_and_max(value)
            val_min = min(val_min, next_min)
            val_max = max(val_max, next_max)
        return RangeValue(val_min, val_max)

class StringPropertyType (PropertyType):

    def get_key (self):
        return (PropertyType.ATOMIC, PropertyType.STRING,
                None, None, None)

    def is_atomic (self):
        return 1

    def is_string (self):
        return 1

    def is_discrete (self, constraint):
        if constraint is None:
            return False
        else:
            return True

    def __str__ (self):
        return "String"

    def check_value (self, value):
        if value is None:
            return
        if string(value):
            return
        self.value_error(value)

    def simplify_value (self, value):
        return value

    def reconstitute_value (self, value):
        return value

    def format_set (self, set, html=0):
        return set

class BooleanPropertyType (PropertyType):


    def get_key (self):
        return (PropertyType.ATOMIC, PropertyType.BOOLEAN,
                None, None, None)

    def is_atomic (self):
        return 1

    def is_boolean (self):
        return 1

    def is_discrete(self, constraint):
        return True

    def __str__ (self):
        return "Boolean"

    def check_value (self, value):
        if value in (True, False, None):
            return
        self.value_error(value)

    def simplify_value (self, value):
        return value

    def reconstitute_value (self, value):
        return value

    def supports_constraints (self):
        return False

    def format_value (self, value, html=0):
        if value:
            return 'yes'
        else:
            return 'no'

    def format_constraint (self, set, html=0):
        if html:
            return xml('<em>yes</em> or <em>no</em>')
        else:
            return '(yes or no)'


class PhysicalValuePropertyType (PropertyType):

    sub_type_is = (None, PhysicalUnit)

    def __init__ (self, sub_type):
        assert sub_type is None or isinstance(sub_type, PhysicalUnit), (
            'invalid sub_type %r for %r' % (sub_type, self))
        self.sub_type = sub_type

    def get_key (self):
        return (PropertyType.ATOMIC, PropertyType.PHYSICAL_VALUE,
                None, self.sub_type, None)

    def get_unit (self):
        return self.sub_type

    def get_compatible_units (self):
        unit = self.get_unit()
        if not unit:
            return []
        return get_standard_units().get_compatible_units(unit)

    def is_atomic (self):
        return 1

    def is_physical_value (self):
        return 1

    def is_discrete(self, constraint):
        if constraint is None:
            return False
        for item in constraint:
            if item.is_range():
                return False
        return True

    def allow_tolerance(self):
        return True

    def __str__ (self):
        if self.sub_type is None:
            return "PhysicalValue"
        else:
            return "PhysicalValue (%s)" % self.sub_type

    def check_value (self, value):
        if value is None:
            return
        if isinstance(value, PhysicalValue):
            if self.sub_type is value.get_unit():
                return
            if self.sub_type and self.sub_type.is_compatible(value.get_unit()):
                return
        self.value_error(value)

    def get_constraint_range (self, constraint):
        if constraint is None:
            return None

        val_min = constraint[0].get_min()
        val_max = constraint[0].get_max()
        for value in constraint[1:]:
            val_min = min(val_min, value.get_min())
            val_max = max(val_max, value.get_max())
        assert val_min.get_unit() is val_max.get_unit(), (
            'unmatched units %r and %r for %r' % (val_min.get_unit(),
                                                  val_max.get_unit(),
                                                  self))
        return PhysicalValue(RangeValue(val_min.get_value(),
                                        val_max.get_value()),
                             val_min.get_unit())

    def simplify_value (self, value):
        default_unit = self.get_unit()
        if value.unit is not default_unit:
            value = value.convert(default_unit)
        return value.value

    def reconstitute_value (self, value):
        if string(value) and value.find(' .. ') != -1:
            value = RangeValue(*list(map(float, value.split(' .. ', 1))))
        return PhysicalValue(value, self.get_unit())

    def format_value (self, value, html=0):
        if isinstance(value, PhysicalValue):
            return value.format(html=html)
        return stringify(value)

    def format_set (self, set, html=0):
        items = [value.format(show_unit=0, html=html)
                 for value in set[0:-1]]
        items.append(set[-1].format(show_unit=1, html=html))
        return items


class MaterialPropertyType (PropertyType):

    def get_key (self):
        return (PropertyType.ATOMIC, PropertyType.MATERIAL,
                None, None, None)

    def is_atomic (self):
        return 1

    def is_material (self):
        return 1

    def is_discrete(self, constraint):
        return True

    def __str__ (self):
        return "Material"

    def check_value (self, value):
        if (value is None or
            isinstance(value, Material)):
            return
        self.value_error(value)

    def check_value_in_constraint (self, value, constraint):
        for material in constraint:
            if value.is_descendant_of(material):
                break
        else:
            self.value_in_constraint_error(value, constraint)

    def simplify_value (self, value):
        return value.get_name()

    def reconstitute_value (self, value):
        material = get_material_db().get_material(value)
        if material is None:
            raise PropertyTypeError('unknown material %r' % value)
        return material


    def format_set (self, set, html=0):
        if len(set) == len(get_material_db().get_materials()):
            return None
        else:
            return [value.get_label()
                    for value in set]


class ListPropertyType (PropertyType):

    element_type_is = PropertyType

    def __init__ (self, element_type):
        assert isinstance(element_type, PropertyType), (
            'invalid element_type %r for %r' % (element_type, self))
        self.element_type = element_type

    def get_key (self):
        element_key = self.element_type.get_key()
        return (PropertyType.LIST, element_key[1],
                None, element_key[3], None)

    def is_int (self):
        return self.element_type.is_int()

    def is_string (self):
        return self.element_type.is_string()

    def is_physical_value (self):
        return self.element_type.is_physical_value()

    def get_compatible_units (self):
        return self.element_type.get_compatible_units()

    def get_unit (self):
        return self.element_type.get_unit()

    def is_material (self):
        return self.element_type.is_material()

    def is_list (self):
        return 1

    def is_discrete(self, constraint):
        return self.element_type.is_discrete(constraint)

    def __str__ (self):
        return "List of %s" % self.element_type

    def check_value (self, value):
        if value is None:
            return
        if type(value) is list:
            for val in value:
                self.element_type.check_value(val)
            return
        self.value_error(value)

    def supports_simplify (self):
        return False

    def supports_constraints (self):
        return self.element_type.supports_constraints()

    def check_value_in_constraint (self, values, constraint):
        for value in values:
            self.element_type.check_value_in_constraint(value, constraint)

    def check_constraint (self, constraint):
        self.element_type.check_constraint(constraint)

    def format_value (self, value, html=0):
        fv = self.element_type.format_value
        values_format = [fv(val, html=html) for val in value]
        if html:
            comma = xml(", ")
        else:
            comma = ", "
        return comma.join(values_format)

    def format_set (self, set, html=0):
        return self.element_type.format_set(set, html=0)


class TablePropertyType (PropertyType):

    key_type_is = PropertyType
    element_type_is = PropertyType

    def __init__ (self, element_type, key_type):
        assert isinstance(element_type, PropertyType), (
            'invalid element_type %r for %r' % (element_type, self))
        assert isinstance(key_type, PropertyType), (
            'invalid key_type %r for %r' % (key_type, self))
        self.element_type = element_type
        self.key_type = key_type

    def get_key (self):
        element_key = self.element_type.get_key()
        key_key = self.key_type.get_key()
        return (PropertyType.TABLE, element_key[1],
                key_key[1], element_key[3], key_key[3])

    def is_int (self):
        return self.element_type.is_int()

    def is_string (self):
        return self.element_type.is_string()

    def is_physical_value (self):
        return self.element_type.is_physical_value()

    def get_compatible_units (self):
        return (self.key_type.get_compatible_units() or
                self.element_type.get_compatible_units())

    def get_unit (self):
        return self.element_type.get_unit()

    def is_material (self):
        return self.element_type.is_material()

    def is_key_int (self):
        return self.key_type.is_int()

    def is_key_string (self):
        return self.key_type.is_string()

    def is_key_physical_value (self):
        return self.key_type.is_physical_value()

    def is_key_material (self):
        return self.key_type.is_material()

    def is_table (self):
        return 1

    def is_discrete(self, constraint):
        assert constraint is None, 'cannot constrain %r' % self
        return False

    def get_key_type (self):
        return self.key_type

    def get_element_type (self):
        return self.element_type

    def __str__ (self):
        return "Table (%s to %s)" % (self.key_type, self.element_type)

    def check_value (self, value):
        if value is None:
            return
        if type(value) is dict:
            for key, val in value.items():
                self.key_type.check_value(key)
                self.element_type.check_value(val)
            return
        self.value_error(value)

    def supports_simplify (self):
        return False

    def supports_constraints (self):
        "Returns true if setting constraints makes sense for this type."
        return False

    def check_value_in_constraint (self, value, constraint):
        assert constraint is None, 'cannot constrain %r' % self

    def check_constraint (self, constraint):
        assert constraint is None, 'cannot constrain %r' % self

    def format_value (self, value, html=0):
        if html:
            texttype = xml
        else:
            texttype = str
        rows = [texttype("%s: %s") % (
                    self.key_type.format_value(key, html=html),
                    self.element_type.format_value(value, html=html))
                for key, value in value.items()]
        return texttype(", ").join(sorted(rows))


def create_atomic_type (element_type, element_sub_type=None):
    if element_type == PropertyType.INT:
        property_type = IntPropertyType()
    elif element_type == PropertyType.STRING:
        property_type = StringPropertyType()
    elif element_type == PropertyType.PHYSICAL_VALUE:
        property_type = PhysicalValuePropertyType(element_sub_type)
    elif element_type == PropertyType.MATERIAL:
        property_type = MaterialPropertyType()
    elif element_type == PropertyType.BOOLEAN:
        property_type = BooleanPropertyType()
    else:
        assert False, 'unknown atomic type %r' % element_type
    return property_type


def get_property_type (*args, **kw):
    return get_property_db().get_property_type(*args, **kw)

def get_physical_value_property_type (unit_string=None):
    unit = unit_string and get_standard_unit(unit_string)
    return get_property_type(PropertyType.ATOMIC,
                              PropertyType.PHYSICAL_VALUE,
                              element_sub_type=unit)

def get_list_of_physical_value_property_type (unit_string=None):
    unit = unit_string and get_standard_unit(unit_string)
    return get_property_type(PropertyType.LIST,
                              PropertyType.PHYSICAL_VALUE,
                              element_sub_type=unit)

def get_int_property_type ():
    return get_property_type(PropertyType.ATOMIC,
                              PropertyType.INT)

def get_list_of_int_property_type ():
    return get_property_type(PropertyType.LIST,
                              PropertyType.INT)

def get_string_property_type ():
    return get_property_type(PropertyType.ATOMIC,
                              PropertyType.STRING)

def get_list_of_string_property_type ():
    return get_property_type(PropertyType.LIST,
                              PropertyType.STRING)

def get_boolean_property_type ():
    return get_property_type(PropertyType.ATOMIC,
                              PropertyType.BOOLEAN)

def get_material_property_type ():
    return get_property_type(PropertyType.ATOMIC,
                              PropertyType.MATERIAL)

def get_list_of_material_property_type ():
    return get_property_type(PropertyType.LIST,
                              PropertyType.MATERIAL)

def get_string_int_table_property_type ():
    return get_property_type(PropertyType.TABLE,
                              PropertyType.INT,
                              key_type=PropertyType.STRING)

def get_physical_value_int_table_property_type (unit_string=None):
    unit = unit_string and get_standard_unit(unit_string)
    return get_property_type(PropertyType.TABLE,
                              element_type=PropertyType.INT,
                              key_type=PropertyType.PHYSICAL_VALUE,
                              key_sub_type=unit)



class PropertyDatabase (PersistentObject, Specified):

    property_types_is = spec(
        mapping({(int, int, (None, int), anything, anything): PropertyType},
                PersistentDict),
        "The first three elements of the tuple are constants, defined "
        "in PropertyType that identify the aggregate type, the key "
        "type, and the element type of this particular PropertyType. "
        "The last two elements of the tuple identify subtypes of "
        "the key and the element types.  These subtypes are "
        "usually instances of PhysicalUnit.")
    master_templates_is = spec(
        mapping({string:instance('MasterTemplate')}, PersistentDict),
        "master templates by name")

    def __init__(self):
        self.property_types = PersistentDict()
        self.master_templates = PersistentDict()


    def add_master_template(self, master):
        require(master, dulcinea.property.property_template.MasterTemplate)
        if self.master_templates.has_key(master.get_name()):
            raise ValueError(
                "MasterTemplate '%s' is already defined." % master.get_name())
        self.master_templates[master.get_name()] = master

    def rename_master_template(self, master, new_name):
        """(master : MasterTemplate, new_name : string)

        This should only be called by MasterTemplate.set_name()
        """
        if not self.master_templates.has_key(master.get_name()):
            raise ValueError(
                "MasterTemplate '%s' is already defined." % master.get_name())
        if self.master_templates.has_key(new_name):
            raise ValueError(
                "MasterTemplate '%s' is already defined." % master.get_name())
        old_name = master.get_name()
        master.set_name(new_name)
        del self.master_templates[old_name]
        self.master_templates[new_name] = master

    def get_master_template(self, name):
        return self.master_templates.get(name)

    def get_master_templates(self):
        return list(self.master_templates.values())

    def _create_property_type(self,
                               aggregate_type,
                               element_type,
                               key_type=None,
                               element_sub_type=None,
                               key_sub_type=None):
        assert aggregate_type is PropertyType.TABLE or (
            key_type is None and key_sub_type is None), (
            "invalid type creation: key_type %r or key_sub_type %r "
            "defined for non table type" % (key_type, key_sub_type))
        assert element_type is PropertyType.PHYSICAL_VALUE or (
            element_sub_type is None), (
            "invalid type creation: element_type %r does not take "
            "sub_type argument %r" % (element_type, element_sub_type))

        if aggregate_type is PropertyType.ATOMIC:
            property_type = create_atomic_type(element_type,
                                                element_sub_type)

        elif aggregate_type is PropertyType.LIST:
            element_inst = self.get_property_type(
                PropertyType.ATOMIC, element_type,
                element_sub_type=element_sub_type)
            property_type = ListPropertyType(element_inst)

        elif aggregate_type is PropertyType.TABLE:
            assert key_type is PropertyType.PHYSICAL_VALUE or (
                key_sub_type is None), (
                "invalid type creation: key_type %r does not take "
                "sub_type argument %r" % (key_type, key_sub_type))
            element_inst = self.get_property_type(
                PropertyType.ATOMIC, element_type,
                element_sub_type=element_sub_type)
            key_inst = self.get_property_type(
                PropertyType.ATOMIC, key_type, element_sub_type=key_sub_type)
            property_type = TablePropertyType(element_inst, key_inst)

        else:
            assert False, (
                "unknown aggregate type: %r" % aggregate_type)

        self.property_types[(aggregate_type,
                              element_type,
                              key_type,
                              element_sub_type,
                              key_sub_type)] = property_type

        return property_type


    def get_property_type(self,
                           aggregate_type,
                           element_type,
                           key_type=None,
                           element_sub_type=None,
                           key_sub_type=None):
        property_type = self.property_types.get((aggregate_type,
                                                   element_type,
                                                   key_type,
                                                   element_sub_type,
                                                   key_sub_type))
        if property_type is None:
            property_type = self._create_property_type(aggregate_type,
                                                         element_type,
                                                         key_type,
                                                         element_sub_type,
                                                         key_sub_type)
        return property_type

