"""
Python-PDI - Library for Personal Data Interchange.
Copyright (C) 2002-2003 Peter Gebauer

This is the core of Python-PDI.

This program is free software; you can redistribute it and/or
modify it under the terms of the GNU General Public License
version 2 as published by the Free Software Foundation.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY 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 this program; if not, write to the Free Software
Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.

@var CRLF: Contains the type of linebreaking to apply when serializing components.
@var RULE_MUST: Rule for mandatory components and properties.
If one of these are missing a L{pdi.core.MissingComponentError} or L{pdi.core.MissingPropertyError} is raised when parsing.
@var RULE_MAY: Rule for components and properties that are allowed, but not mandatory.
If a component or property that has no must, recommended nor may rule is found you get a
L{pdi.core.InvalidComponentWarning} or L{pdi.core.InvalidPropertyWarning} type warning.
@var RULE_RECOMMENDED: Rule for components and properties that are recommended.
If one of these are missing you will be warned with L{pdi.core.MissingComponentWarning} or L{pdi.core.MissingPropertyWarning}.
@var RULE_NOT: Any component or property in this category is disallowed and will
raise L{pdi.core.InvalidComponentError} or L{pdi.core.InvalidPropertyError}.
"""

import warnings

CRLF = "\n"
RULE_MUST = 10
RULE_MAY = 20
RULE_RECOMMENDED = 30
RULE_NOT = 40

class ParseError(Exception):
    """If a parse error occurs there's a real problem in the data."""

    def __init__(self, compName, message, lineNumber):
        """
        @type   compName: string
        @param  compName: A string describing in what component this error occured.
        @type   message: string
        @param  message: A brief message explaining what went wrong.
        @type   lineNumber: number
        @param  lineNumber: On what line number the problem occured. This value can be fetched from a L{pdi.core.ParseError}.
        """
        Exception.__init__(self, "Parse error, %s, in component '%s' starting on line %s"%(message, compName, lineNumber))
        self.lineNumber = lineNumber

class ComponentError(Exception):
    """General component error class."""

    def __init__(self, compName, message, lineNumber):
        """
        @type   compName: string
        @param  compName: A string describing in what component this error occured.
        @type   message: string
        @param  message: A brief message explaining what went wrong.
        @type   lineNumber: number
        @param  lineNumber: On what line number the problem occured. This value can be fetched from a L{pdi.core.ParseError}.
        """
        Exception.__init__(self, "%s '%s' on line %s"%(message, compName, lineNumber))
        self.lineNumber = lineNumber

class MissingComponentError(ComponentError):
    """A mandatory component is missing."""
    def __init__(self, compName, subCompName, lineStart, lineEnd):
        """
        @type   compName: string
        @param  compName: A string describing in what component this error occured.
        @param  lineStart: The component that raised this exception starts on this line number.
        @param  lineEnd: The component that raised this exception ends on this line number.
        """
        Exception.__init__(self, "Mandatory component '%s' is missing in component '%s' on line %s to %s"
                           %(subCompName, compName, lineStart, lineEnd)
                           )
        self.lineNUmber = lineStart

class InvalidComponentError(ComponentError):
    """A component has been placed where it's not allowed to be."""
    def __init__(self, compName, lineNumber):
        """
        @type   compName: string
        @param  compName: A string describing in what component this error occured.
        @type   lineNumber: number
        @param  lineNumber: On what line number the problem occured. This value can be fetched from a L{pdi.core.ParseError}.
        """
        ComponentError.__init__(self, compName, "Invalid component", lineNumber)

class PropertyError(Exception):
    """All property related errrors should inherit from this class."""
    def __init__(self, message):
        """
        @type   message: string
        @param  message: A brief message describing what went wrong.
        """
        Exception.__init__(self, message)

class MissingPropertyError(PropertyError):
    """A mandatory property is missing."""
    def __init__(self, propertyName, compName, lineStart, lineEnd):
        """
        @type   propertyName: string
        @param  propertyName: A string describing what property is missing.
        @type   compName: string
        @param  compName: A string describing in what component this error occured.
        """
        PropertyError.__init__(self, "Missing mandatory property '%s' in component '%s' on lines %s to %s"
                               %(propertyName, compName, lineStart, lineEnd))

class PropertyValueError(PropertyError):
    """The value of the property is malformed or of unknown type. Either way, the value is invalid."""
    def __init__(self, property):
        """
        @type   property: L{pdi.core.Property}
        @param  property: The property that raised this exception.
        """
        PropertyError.__init__(self, "Invalid value for property '%s' on line %s"%(property.name, property.lineNumber))

class InvalidPropertyError(PropertyError):
    """The property is not allowed in that component."""
    def __init__(self, property):
        """
        @type   property: L{pdi.core.Property}
        @param  property: The property that raised this exception.
        """
        PropertyError.__init__(self, "Invalid property '%s' on line %s"%(property.name, property.lineNumber))

class InvalidPropertyValueError(PropertyError):
    """You tried to set an invalid value for the property."""
    def __init__(self, property, value):
        """
        @type   property: L{pdi.core.Property}
        @param  property: The property that raised this exception.
        @type   value: string
        @param  value: The invalid value.
        """
        PropertyError.__init__(self, "Invalid value '%s' for property '%s' on line %s"%(value, property.name, property.lineNumber))

class InvalidComponentWarning(UserWarning):
    """The component is not mandatory, recommended nor presumed (may)."""

class MissingComponentWarning(UserWarning):
    """The component is when recommended components are not found."""

class MissingPropertyWarning(UserWarning):
    """A recommended property is missing."""

class InvalidPropertyWarning(UserWarning):
    """A property that is not mandatory, recommended nor presumed (may) has been found or it has invalid syntax."""

class InvalidPropertyTypeWarning(UserWarning):
    """A property has a defined type that isn't registered."""

class InvalidPropertyContentWarning(UserWarning):
    """The content of the property is invalid according to the property's validation method."""

class Property(object):
    """All properties must inherit from this class. An instance of this class should be treated as an unknown property."""

    def __init__(self, name, content, value, encoding, type, lineNumber = None):
        """
        @type   name: string
        @param  name: The name of this property (not type).
        @type   content: string
        @param  content: The raw content fetched directly from the input data.
        @type   encoding: string
        @param  encoding: The encoding used to store the raw content.
        @type   type: string
        @param  type: Type of content.
        @type   lineNumber: number
        @param  lineNumber: The line on which this property was found.
        """
        self.name = name
        self.content = content
        self.encoding = encoding
        self.type = type
        self.value = value
        self.lineNumber = lineNumber
        self.validate()

    def _validate(self, content):
        """
        Internal method for validation. Override this for your own validation.
        You should issue warnings in this method. Exceptions are disliked, if your value is really out
        of whack you may set it to None.

        @type   content: string
        @param  content: The raw content fetched from data.
        @rtype: boolean
        @return: None or zero if invalid value.
        """
        return 1

    def validate(self):
        """
        Validates the property content. Issues warnings.

        @rtype: boolean
        @return: None or zero if invalid content.
        """
        return self._validate(self.content)

    def invalidContent(self, message, content):
        """
        Issue warning.
        @type   message: string
        @param  message: Explaining why the content is invalid.
        @type   content: string
        @param  content: The invalid content.
        """
        warnings.warn("Invalid content '%s' for type '%s' in property '%s' on line %s: %s"
                      %(content, self.getType(), self.name, self.lineNumber, message),
                      InvalidPropertyContentWarning)

    def getContent(self):
        """
        Use this to fetch the content.
        @return: The content in it's correct form and type.
        """
        return self.content

    def setContent(self, content):
        """
        Use this to set the content. The content will be validated.
        @type   content: string
        @param  content: The data string content.
        """
        if not self._validate(content):
            raise InvalidPropertyContentError(property, content)
        self.content = content

    def __str__(self):
        """Serializes a property back to raw data."""
        ret = self.name
        if self.encoding:
            ret += ";ENCODING=" + self.encoding
        if self.type:
            ret += ";TYPE=" + self.type
        if self.value:
            ret += ";VALUE=" + self.value
        ret += ":" + self.content
        return ret

class UnknownProperty(Property):
    """Unknown type. All properties that doesn't specify a VALUE=TYPE are instansiated as this object."""

class TextProperty(Property):
    """A text-type property."""

class DateProperty(Property):
    """A date-type property. It should be an 8 digit number."""

    def _validate(self, content):
        """Makes sure this is readable UTF-8 text."""
        if len(content) != 8:
            self.invalidContent("must be 8 digit number", content)
            return None
        try:
            timestamp = int(self.content)
        except ContentError:
            self.invalidContent("may only contain numbers", content)
            return None
        return 1

class Component(object):
    """
    The mama of all components. Inherit from this class only if you intend to create a completely new standard.
    @ivar   begin: The line number where this component started. None if it never was started == very bad!
    @ivar   end: The line number where this component ended. None if it never was ended == very bad, will raise an exception.
    @ivar   properties: A dictionary of all properties where the uppercased name is the key and an instance of the property is the value.
    @ivar   propertiesMay: Internal list for keeping track of properties that may occur.
    @ivar   propertiesMust: Internal list for keeping track of mandatory properties.
    @ivar   propertiesNot: Internal list for keeping track of disallowed properties.
    @ivar   components: A list of all subcomponents under this component.
    @ivar   componentsMust: Internal list for keeping track of mandatory components.
    @ivar   componentsMay: Internal list for keeping track components that may occur.
    @ivar   componentsRecommended: Internal list for keeping track of recommended compoennts.
    @ivar   componentsNot: Internal list for keeping track of disallowed components.
    @ivar   parent: Internal instance of a L{pdi.core.Component} for keeping track of the parent.
    @ivar   ignoreWarnings: Set this baby to true to supress warnings.
    @ivar   classes: Internal dictionary for keeping track valid classes for components and properties.
    @ivar   componentTracker: Internal dictionary for keeping track of what subcomponent classes has been added.
    @ivar   propertiesRecommended: Internal list for keeping track of recommended properties.
    """

    def __init__(self, parent = None):
        """
        @type   parent: L{pdi.core.Component}
        @param  parent: The parent component, if any. May be omitted or None if it is a top-level component.
        """
        self.begin = None
        self.end = None
        self.properties = {}
        self.propertiesMay = []
        self.propertiesMust = []
        self.propertiesRecommended = []
        self.propertiesNot = []
        self.components = []
        self.componentsMust = []
        self.componentsMay = []
        self.componentsRecommended = []
        self.componentsNot = []
        self.parent = parent
        self.ignoreWarnings = None
        self.classes = {}
        self.componentTracker = []
        self.registerPropertyTypes({'UNKNOWN' : UnknownProperty,
                                    'DATE' : DateProperty,
                                    'TEXT' : TextProperty})

    def getName(self):
        """
        Return the name of this component. This is the uppercased class name unless an unknown component.
        @rtype: string
        @return: The components name (usually the uppercased class name, but not always). It has to be uppercased.
        """
        return self.__class__.__name__.upper()

    def parseLine(self, data, lineNumber):
        """
        This parses one line of data.
        @type   data: string
        @param  data: A line of data.
        @type   lineNumber: number
        @param  lineNumber: The line we are currently parsing. Needed for exceptions, warnings and debugging in general.
        @rtype: L{pdi.core.Component}        
        @return: The next component that needs parsing. Can be a child, parent or self.
        @raise  ParseError: Raised if parsed data is whack!
        @raise  ComponentError: Raised if a component is invalid. This probably indicates an internal error.
        @raise  InvalidComponentError: Raised if a disallowed component is found.
        @raise  MissingComponentError: Raised if a mandatory component is not found.
        @raise  InvalidPropertyError: Raised if a disallowed property is found.
        @raise  MissingPropertyError: Raised if a mandatory property is not found.
        @raise  PropertyValueError: Raised if a property fails to validate itself.
        """
        if data[0] in ' \t':
           if self.lastProperty:
               if self.properties[self.lastProperty.name].value != None:
                  self.properties[self.lastProperty.name].setContent(self.properties[self.lastProperty.name].getContent() + CRLF + data[0:-1])
               else:
                 self.properties[self.lastProperty.name].setContent(data[0:-1])
               return self
           else:
               raise ParseError(self.getName(), "displaced content line", lineNumber)
        unpack = data.split(":")
        if len(unpack) != 2:
            value=unpack[1]
            unpack.remove(value)
            while len(unpack) > 1:
              next_part=unpack[1]
              value = value + ":" + next_part
              unpack.remove(next_part)

            key= unpack.pop()
        else:
            key, value = unpack
        return self.interpret(key.upper().strip(), value.strip(), lineNumber)

    def interpret(self, key, value, lineNumber = -1):
        """Interprets a symbol and value."""
        if key == "BEGIN":
            if value.upper() == self.getName() and not self.begin:
                self.begin = lineNumber
                return self
            subcomponent = None
            for available in self.componentsMay + self.componentsMust + self.componentsRecommended:
                if value.upper() == available:
                    if self.classes.has_key(available):
                        subcomponent = self.classes[available](self)
                    else:
                        self.ComponentError("Internal error, component not found in class dictionary", available, lineNumber)
            if not subcomponent:
                subcomponent = VUnknown(value, self)
                if not self.ignoreWarnings:
                    warnings.warn("Presumably invalid component '%s' on line %s"%(value, lineNumber))
            subcomponent.interpret(key, value)
            self.addComponent(subcomponent, lineNumber)
            return subcomponent
        elif key == "END":
            if value.upper() == self.getName():
                self.end = lineNumber
                self._validate(None, lineNumber)
                return self.parent
            raise ParseError(self.getName(), "expected 'END:" + self.getName() + "', but found 'END:" + value + "'", lineNumber)
        else:
            for prop in self.propertiesRecommended:
                if not self.properties.has_key(prop.upper()) and not self.ignoreWarnings:
                    warnings.warn("Recommended property '%s' not found"%(prop), MissingPropertyWarning)
            unpack = key.split(";")
            propertyClass = UnknownProperty
            propertyName = key.upper()
            propertyType = None
            propertyValue = None
            propertyEncoding = None
            if len(unpack) > 1:
                propertyName = unpack[0].upper()
                del unpack[0]
                for unpacked in unpack:
                    unpack2 = unpacked.split("=")
                    if len(unpack2) == 2:
                        if unpack2[0].upper() == "VALUE":
                            if self.classes.has_key("TYPE_" + unpack2[1].upper()):
                                propertyClass = self.classes["TYPE_" + unpack2[1].upper()]
                                propertyValue = unpack2[1].upper()
                            else:
                                warnings.warn("Unknown property value '%s' for property '%s' on line %s"
                                              %(unpack2[1], unpack[0], lineNumber), InvalidPropertyTypeWarning)
                        elif unpack2[0].upper() == "ENCODING":
                            propertyEncoding = unpack2[1]
                        elif unpack2[0].upper() == "TYPE":
                            propertyType = unpack2[1]
                    else:
                        propertyName += ";" + unpacked.upper()
            if not propertyName in self.propertiesMay + self.propertiesMust + self.propertiesRecommended and not self.ignoreWarnings:
                warnings.warn("Presumably invalid property '%s' on line %s"
                              %(propertyName, lineNumber), InvalidPropertyWarning)
            self.addProperty(propertyClass(propertyName, value, propertyValue, propertyEncoding, propertyType, lineNumber), lineNumber)
        if self.begin:
            return self
        return self.parent

    def registerComponents(self, componentList, rule = RULE_MAY):
        """
        Register all valid (or invalid, depending on rule) components.
        All components that are supposed to be valid should be registered with this method, otherwise they
        become instances of UnknownComponent.
        
        @type   componentList: list
        @param  componentList: A list with L{pdi.core.Component} derived classes that are valid (or invalid).
        @type   rule: number
        @param  rule: A pdi.core.RULE_MUST, pdi.core.RULE_MAY, pdi.core.RULE_RECOMMEND or pdi.core.RULE_NOT.
        """
        for component in componentList:
            if rule != RULE_NOT:
                self.classes[component.__name__.upper()] = component
            if rule == RULE_MUST:
                self.componentsMust.append(component.__name__.upper())
            elif rule == RULE_MAY:
                self.componentsMay.append(component.__name__.upper())
            elif rule == RULE_RECOMMENDED:
                self.componentsRecommended.append(component.__name__.upper())
            elif rule == RULE_NOT:
                self.componentsNot.append(component.__name__.upper())
            else:
                raise ValueError("Second argument must be RULE_<?> value")

    def registerProperties(self, propertyList, rule = RULE_MAY):
        """
        Register valid (or invalid, depending on rule) properties.

        @type   propertyList: list
        @param  propertyList: A list of strings containing the uppercased names of the properties.
        @type   rule: number
        @param  rule: A pdi.core.RULE_MUST, pdi.core.RULE_MAY, pdi.core.RULE_RECOMMEND or pdi.core.RULE_NOT.
        """
        for property in propertyList:
            if rule == RULE_MUST:
                self.propertiesMust.append(property.upper())
            elif rule == RULE_MAY:
                self.propertiesMay.append(property.upper())
            elif rule == RULE_RECOMMENDED:
                self.propertiesRecommended.append(property.upper())
            elif rule == RULE_NOT:
                self.propertiesNot.append(property.upper())
            else:
                raise ValueError("Second argument must be RULE_<?> value")

    def registerPropertyTypes(self, propertyTypes):
        """
        Register available property types. Properties without a type or a type that has not been registered will
        be instansiated as UnknownProperty.

        @type   propertyTypes: list
        @param  propertyTypes: A list of L{pdi.core.Property} derived classes.
        """
        for propertyType in propertyTypes.keys():
            self.classes["TYPE_" + propertyType] = propertyTypes[propertyType]

    def addComponents(self, componentList):
        """
        Candy method for adding several components at one time.

        @type   componentList: list
        @param  componentList: A list of L{pdi.core.Component} instances.
        @raise  InvalidComponentError: If any subcomponent you tried to add is not valid for the component.
        """
        for comp in componentList:
            self.addComponent(comp)

    def addProperties(self, propertyList):
        """
        Candy method for adding several properties at one time.

        @type   propertyList: list
        @param  propertyList: A list of L{pdi.core.Property} instances.
        @raise  InvalidPropertyError: If any property you tried to add is not valid for the component.
        """
        for prop in propertyList:
            self.addProperty(prop)

    def addComponent(self, component, lineNumber = None):
        """
        Add a subcomponent to this component.

        @type   component: L{pdi.core.Component}
        @param  component: The subcomponent to add.
        @type   lineNumber: number
        @param  lineNumber: The line currently parsed. Used internally when parsing files. May be omitted or None.
        @rtype:  L{pdi.core.Component}
        @return: The subcomponent you just added.
        @raise  InvalidComponentError: If the subcomponent you tried to add is not valid for the component.
        """
        self.components.append(component)
        if not component.getName() in self.componentTracker:
            self.componentTracker.append(component.getName())
        for comp in self.componentsNot:
            if comp in self.componentTracker:
                raise InvalidComponentError(prop, lineNumber)
        return component

    def addProperty(self, property, lineNumber = None):
        """
        Add a property to this component. The property will also be validated and warnings issued
        as the property implementation sees fit.

        @type   property: L{pdi.core.Property}
        @param  property: The property to add.
        @type   lineNumber: number
        @param  lineNumber: The line currently parsed. Used internally when parsing files. May be omitted or None.
        @rtype:  L{pdi.core.Property}
        @return: The property you just added.
        @raise  InvalidPropertyError: If the property you tried to add is not valid for the component.
        """
        self.properties[property.name.upper()] = property
        property.validate()
        self.lastProperty = property
        for prop in self.properties.keys():
            if prop in self.propertiesNot:
                raise InvalidPropertyError(prop, lineNumber)
        return property

    def validate(self, lineNumber = None):
        """
        This will make sure that all mandatory components and properties are present.
        The validation is recursive, so you only need to call it for the top component.

        @type   lineNumber: number
        @param  lineNumber: The line currently parsed. Used internally when parsing files. May be omitted or None.
        @raise  MissingComponentError: A mandatory component is missing.
        @raise  MissingPropertyError: A mandatory property is missing.
        """
        self._validate(1, lineNumber)

    def _validate(self, recursive, lineNumber = None):
        """
        Internal method for validating components.

        @type   recursive: boolean
        @param  recursive: If true, itterate recursevly over subcomponents as well.
        @type   lineNumber: number
        @param  lineNumber: The line currently parsed. Used internally when parsing files. May be omitted or None.
        @raise  MissingComponentError: A mandatory component is missing.
        @raise  MissingPropertyError: A mandatory property is missing.
        """
        for comp in self.componentsMust:
            if not comp in self.componentTracker:
                raise MissingComponentError(self.getName(), comp, self.begin, self.end)
        for prop in self.propertiesMust:
            if not self.properties.has_key(prop):
                raise MissingPropertyError(prop, self.getName(), self.begin, self.end)
        if recursive:
            for comp in self.components:
                comp._validate(recursive, lineNumber)
        
    def __str__(self):
        """Serialize this component as well as it's properties and subcomponents."""
        ret = "BEGIN:" + self.getName() + CRLF
        for key in self.properties.keys():
            ret = ret + self.properties[key].__str__() + CRLF
        for child in self.components:
            ret = ret + child.__str__()
        ret = ret + "END:" + self.getName() + CRLF
        return ret

class VUnknown(Component):
    """
    This class is used for all components that do not have their own classes registered.
    """

    def __init__(self, name, parent = None):
        """
        @type   name: string
        @param  name: The name of the component is required since it doesn't provide it through the class name.
        @type   parent: L{pdi.core.Component}
        @param  parent: The parent component, if any. May be omitted or None if it is a top-level component.
        """
        super(VUnknown, self).__init__(parent)
        self.name = name        

    def getName(self):
        """
        Returns the name provided on instansiation rather than the uppercased class name due to the fact
        that the component is of unknown type.
        @rtype: string
        @return: The components name (usually the uppercased class name, but not always).
        """
        return self.name

