# -*- test-case-name: twistedcaldav.test.test_icalendar -*-
##
# Copyright (c) 2005-2017 Apple Inc. All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
##

"""
iCalendar Utilities
"""

__all__ = [
    "InvalidICalendarDataError",
    "iCalendarProductID",
    "allowedComponents",
    "Property",
    "Component",
    "tzexpand",
]

import codecs
import collections
from difflib import unified_diff
import heapq
import itertools
import uuid

from twisted.internet.defer import inlineCallbacks, returnValue
from twext.python.log import Logger
from txweb2.stream import IStream
from txweb2.dav.util import allDataFromStream

from twistedcaldav.accounting import accountingEnabledForCategory, \
    emitAccounting
from twistedcaldav.config import config
from twistedcaldav.dateops import timeRangesOverlap, normalizeForIndex, differenceDateTime, \
    normalizeForExpand
from twistedcaldav.instance import InstanceList, InvalidOverriddenInstanceError
from twistedcaldav.timezones import hasTZ

from txdav.caldav.datastore.scheduling.utils import normalizeCUAddr

from pycalendar.icalendar import definitions
from pycalendar.parameter import Parameter
from pycalendar.icalendar.calendar import Calendar
from pycalendar.icalendar.component import Component as PyComponent
from pycalendar.componentbase import ComponentBase
from pycalendar.datetime import DateTime
from pycalendar.duration import Duration
from pycalendar.exceptions import ErrorBase
from pycalendar.period import Period
from pycalendar.icalendar.property import Property as PyProperty
from pycalendar.timezone import Timezone
from pycalendar.utcoffsetvalue import UTCOffsetValue

log = Logger()

iCalendarProductID = "-//CALENDARSERVER.ORG//NONSGML Version 1//EN"

allowedStoreComponents = ("VEVENT", "VTODO", "VPOLL",)
allowedSchedulingComponents = allowedStoreComponents + ("VFREEBUSY",)
allowedComponents = allowedSchedulingComponents + ("VTIMEZONE",)


def _updateAllowedComponents(allowed):
    global allowedStoreComponents, allowedSchedulingComponents, allowedComponents
    allowedStoreComponents = allowed
    allowedSchedulingComponents = allowedStoreComponents + ("VFREEBUSY",)
    allowedComponents = allowedSchedulingComponents + ("VTIMEZONE",)


# Additional per-user data components - see datafilters.peruserdata.py for details
PERUSER_COMPONENT = "X-CALENDARSERVER-PERUSER"
PERUSER_UID = "X-CALENDARSERVER-PERUSER-UID"
PERINSTANCE_COMPONENT = "X-CALENDARSERVER-PERINSTANCE"

PRIVATE_COMMENT = "X-CALENDARSERVER-PRIVATE-COMMENT"
ATTENDEE_COMMENT = "X-CALENDARSERVER-ATTENDEE-COMMENT"
ATTENDEE_COMMENT_REF = "X-CALENDARSERVER-ATTENDEE-REF"
DTSTAMP_PARAM = "X-CALENDARSERVER-DTSTAMP"

# 2445 default values and parameters
# Structure: propname: (<default value>, <parameter defaults dict>)

normalizeProps = {
    "CALSCALE": ("GREGORIAN", {"VALUE": "TEXT"}),
    "METHOD": (None, {"VALUE": "TEXT"}),
    "PRODID": (None, {"VALUE": "TEXT"}),
    "VERSION": (None, {"VALUE": "TEXT"}),
    "ATTACH": (None, {"VALUE": "URI"}),
    "CATEGORIES": (None, {"VALUE": "TEXT"}),
    "CLASS": (None, {"VALUE": "TEXT"}),
    "COMMENT": (None, {"VALUE": "TEXT"}),
    "DESCRIPTION": (None, {"VALUE": "TEXT"}),
    "GEO": (None, {"VALUE": "FLOAT"}),
    "LOCATION": (None, {"VALUE": "TEXT"}),
    "PERCENT-COMPLETE": (None, {"VALUE": "INTEGER"}),
    "PRIORITY": (0, {"VALUE": "INTEGER"}),
    "RESOURCES": (None, {"VALUE": "TEXT"}),
    "STATUS": (None, {"VALUE": "TEXT"}),
    "SUMMARY": (None, {"VALUE": "TEXT"}),
    "COMPLETED": (None, {"VALUE": "DATE-TIME"}),
    "DTEND": (None, {"VALUE": "DATE-TIME"}),
    "DUE": (None, {"VALUE": "DATE-TIME"}),
    "DTSTART": (None, {"VALUE": "DATE-TIME"}),
    "DURATION": (None, {"VALUE": "DURATION"}),
    "FREEBUSY": (None, {"VALUE": "PERIOD"}),
    "TRANSP": ("OPAQUE", {"VALUE": "TEXT"}),
    "TZID": (None, {"VALUE": "TEXT"}),
    "TZNAME": (None, {"VALUE": "TEXT"}),
    "TZOFFSETFROM": (None, {"VALUE": "UTC-OFFSET"}),
    "TZOFFSETTO": (None, {"VALUE": "UTC-OFFSET"}),
    "TZURL": (None, {"VALUE": "URI"}),
    "ATTENDEE": (None, {
        "VALUE": "CAL-ADDRESS",
        "CUTYPE": "INDIVIDUAL",
        "ROLE": "REQ-PARTICIPANT",
        "PARTSTAT": "NEEDS-ACTION",
        "RSVP": "FALSE",
        "SCHEDULE-AGENT": "SERVER",
    }),
    "CONTACT": (None, {"VALUE": "TEXT"}),
    "ORGANIZER": (None, {"VALUE": "CAL-ADDRESS"}),
    "RECURRENCE-ID": (None, {"VALUE": "DATE-TIME"}),
    "RELATED-TO": (None, {"VALUE": "TEXT"}),
    "URL": (None, {"VALUE": "URI"}),
    "UID": (None, {"VALUE": "TEXT"}),
    "EXDATE": (None, {"VALUE": "DATE-TIME"}),
    "EXRULE": (None, {"VALUE": "RECUR"}),
    "RDATE": (None, {"VALUE": "DATE-TIME"}),
    "RRULE": (None, {"VALUE": "RECUR"}),
    "ACTION": (None, {"VALUE": "TEXT"}),
    "REPEAT": (0, {"VALUE": "INTEGER"}),
    "TRIGGER": (None, {"VALUE": "DURATION"}),
    "CREATED": (None, {"VALUE": "DATE-TIME"}),
    "DTSTAMP": (None, {"VALUE": "DATE-TIME"}),
    "LAST-MODIFIED": (None, {"VALUE": "DATE-TIME"}),
    "SEQUENCE": (0, {"VALUE": "INTEGER"}),
    "REQUEST-STATUS": (None, {"VALUE": "TEXT"}),

    "VOTER": (None, {
        "VALUE": "CAL-ADDRESS",
        "CUTYPE": "INDIVIDUAL",
        "ROLE": "REQ-PARTICIPANT",
        "RSVP": "FALSE",
        "SCHEDULE-AGENT": "SERVER",
    }),
}

# transformations to apply to property values
normalizePropsValue = {
    "ATTENDEE": normalizeCUAddr,
    "ORGANIZER": normalizeCUAddr,
    "VOTER": normalizeCUAddr,
}

ignoredComponents = ("VTIMEZONE", PERUSER_COMPONENT,)

# Used for min/max time-range query limits
minDateTime = DateTime(1900, 1, 1, 0, 0, 0, tzid=Timezone.UTCTimezone)
maxDateTime = DateTime(2100, 1, 1, 0, 0, 0, tzid=Timezone.UTCTimezone)


class InvalidICalendarDataError(ValueError):
    pass


class Property (object):
    """
    iCalendar Property
    """

    def __init__(self, name, value, params={}, parent=None, **kwargs):
        """
        @param name: the property's name
        @param value: the property's value
        @param params: a dictionary of parameters, where keys are parameter names and
            values are (possibly empty) lists of parameter values.
        """
        if name is None:
            assert value is None
            assert params is None

            pyobj = kwargs["pycalendar"]

            if not isinstance(pyobj, PyProperty):
                raise TypeError("Not a Property: {0!r}".format(property,))

            self._pycalendar = pyobj
        else:
            # Convert params dictionary to list of lists format used by pycalendar
            valuetype = kwargs.get("valuetype")
            self._pycalendar = PyProperty(name, value, valuetype=valuetype)
            for attrname, attrvalue in params.items():
                self._pycalendar.addParameter(Parameter(attrname, attrvalue))

        self._parent = parent

    def __str__(self):
        return str(self._pycalendar)

    def __repr__(self):
        return (
            "<{self.__class__.__name__}: {name!r}: {value!r}>"
            .format(self=self, name=self.name(), value=self.value())
        )

    def __hash__(self):
        return hash(str(self))

    def __ne__(self, other):
        return not self.__eq__(other)

    def __eq__(self, other):
        if not isinstance(other, Property):
            return False
        return self._pycalendar == other._pycalendar

    def __gt__(self, other):
        return not (self.__eq__(other) or self.__lt__(other))

    def __lt__(self, other):
        my_name = self.name()
        other_name = other.name()

        if my_name < other_name:
            return True
        if my_name > other_name:
            return False

        return self.value() < other.value()

    def __ge__(self, other):
        return self.__eq__(other) or self.__gt__(other)

    def __le__(self, other):
        return self.__eq__(other) or self.__lt__(other)

    def duplicate(self):
        """
        Duplicate this object and all its contents.
        @return: the duplicated calendar.
        """
        # FIXME: does the parent need to be set in this case?
        return Property(None, None, None, pycalendar=self._pycalendar.duplicate())

    def name(self):
        return self._pycalendar.getName()

    def value(self):
        return self._pycalendar.getValue().getValue()

    def strvalue(self):
        return str(self._pycalendar.getValue())

    def _markAsDirty(self):
        parent = getattr(self, "_parent", None)
        if parent is not None:
            parent._markAsDirty()

    def setValue(self, value):
        self._pycalendar.setValue(value)
        self._markAsDirty()

    def parameterNames(self):
        """
        Returns a set containing parameter names for this property.
        """
        result = set()
        for pyattrlist in self._pycalendar.getParameters().values():
            for pyattr in pyattrlist:
                result.add(pyattr.getName())
        return result

    def parameterValue(self, name, default=None):
        """
        Returns a single value for the given parameter.
        """
        try:
            return self._pycalendar.getParameterValue(name)
        except KeyError:
            return default

    def parameterValues(self, name, default=None):
        """
        Returns a multi-value C{list} for the given parameter.
        """
        try:
            return self._pycalendar.getParameterValues(name)
        except KeyError:
            return default

    def hasParameter(self, paramname):
        return self._pycalendar.hasParameter(paramname)

    def setParameter(self, paramname, paramvalue):
        self._pycalendar.replaceParameter(Parameter(paramname, paramvalue))
        self._markAsDirty()

    def removeParameter(self, paramname):
        self._pycalendar.removeParameters(paramname)
        self._markAsDirty()

    def removeAllParameters(self):
        self._pycalendar.setParameters({})
        self._markAsDirty()

    def removeParameterValue(self, paramname, paramvalue):

        paramname = paramname.upper()
        for attrName in self.parameterNames():
            if attrName.upper() == paramname:
                for attr in tuple(self._pycalendar.getParameters()[attrName]):
                    for value in attr.getValues():
                        if value == paramvalue:
                            if not attr.removeValue(value):
                                self._pycalendar.removeParameters(paramname)
        self._markAsDirty()

    def containsTimeRange(self, start, end, defaulttz=None):
        """
        Determines whether this property contains a date/date-time within the specified
        start/end period.
        The only properties allowed for this query are: COMPLETED, CREATED, DTSTAMP and
        LAST-MODIFIED (caldav -09).
        @param start: a L{DateTime} specifying the beginning of the given time span.
        @param end: a L{DateTime} specifying the end of the given time span.
            C{end} may be None, indicating that there is no end date.
        @param defaulttz: the default L{PyTimezone} to use in datetime comparisons.
        @return: True if the property's date/date-time value is within the given time range,
                 False if not, or the property is not an appropriate date/date-time value.
        """

        # Verify that property name matches the ones allowed
        allowedNames = ["COMPLETED", "CREATED", "DTSTAMP", "LAST-MODIFIED"]
        if self.name() not in allowedNames:
            return False

        # get date/date-time value
        dt = self._pycalendar.getValue().getValue()
        assert isinstance(dt, DateTime), "Not a date/date-time value: {0!r}".format(self,)

        return timeRangesOverlap(dt, None, start, end, defaulttz)


class Component (object):
    """
    X{iCalendar} component.
    """

    # Private Event access levels.
    ACCESS_PROPERTY = "X-CALENDARSERVER-ACCESS"
    ACCESS_PUBLIC = "PUBLIC"
    ACCESS_PRIVATE = "PRIVATE"
    ACCESS_CONFIDENTIAL = "CONFIDENTIAL"
    ACCESS_RESTRICTED = "RESTRICTED"

    accessMap = {
        "PUBLIC": ACCESS_PUBLIC,
        "PRIVATE": ACCESS_PRIVATE,
        "CONFIDENTIAL": ACCESS_CONFIDENTIAL,
        "RESTRICTED": ACCESS_RESTRICTED,
    }

    confidentialPropertiesMap = {
        "VCALENDAR": ("PRODID", "VERSION", "CALSCALE", ACCESS_PROPERTY),
        "VEVENT": ("UID", "RECURRENCE-ID", "SEQUENCE", "DTSTAMP", "STATUS", "TRANSP", "DTSTART", "DTEND", "DURATION", "RRULE", "RDATE", "EXRULE", "EXDATE",),
        "VTODO": ("UID", "RECURRENCE-ID", "SEQUENCE", "DTSTAMP", "STATUS", "DTSTART", "COMPLETED", "DUE", "DURATION", "RRULE", "RDATE", "EXRULE", "EXDATE",),
        "VJOURNAL": ("UID", "RECURRENCE-ID", "SEQUENCE", "DTSTAMP", "STATUS", "DTSTART", "RRULE", "RDATE", "EXRULE", "EXDATE",),
        "VFREEBUSY": ("UID", "DTSTAMP", "DTSTART", "DTEND", "DURATION", "FREEBUSY",),
        "VTIMEZONE": None,
    }
    extraRestrictedProperties = ("SUMMARY", "LOCATION",)

    # Hidden instance.
    HIDDEN_INSTANCE_PROPERTY = "X-CALENDARSERVER-HIDDEN-INSTANCE"

    allowedTypesList = None

    @classmethod
    def allowedTypes(cls):
        if cls.allowedTypesList is None:
            cls.allowedTypesList = ["text/calendar"]
            if config.EnableJSONData:
                cls.allowedTypesList.append("application/calendar+json")
        return cls.allowedTypesList

    @classmethod
    def allFromString(clazz, string, format=None):
        """
        Just default to reading a single VCALENDAR
        """
        return clazz.fromString(string, format)

    @classmethod
    def allFromStream(clazz, stream, format=None):
        """
        Just default to reading a single VCALENDAR
        """
        return clazz.fromStream(stream, format)

    @classmethod
    def fromString(clazz, string, format=None):
        """
        Construct a L{Component} from a string.
        @param string: a string containing iCalendar data.
        @return: a L{Component} representing the first component described by
            C{string}.
        """
        return clazz._fromData(string, False, format)

    @classmethod
    def fromStream(clazz, stream, format=None):
        """
        Construct a L{Component} from a stream.
        @param stream: a C{read()}able stream containing iCalendar data.
        @return: a L{Component} representing the first component described by
            C{stream}.
        """
        return clazz._fromData(stream, True, format)

    @classmethod
    def _fromData(clazz, data, isstream, format=None):
        """
        Construct a L{Component} from a stream.
        @param stream: a C{read()}able stream containing iCalendar data.
        @param format: a C{str} indicating whether the data is iCalendar or jCal
        @return: a L{Component} representing the first component described by
            C{stream}.
        """

        if isstream:
            pass
        else:
            if type(data) is unicode:
                data = data.encode("utf-8")
            else:
                # Valid utf-8 please
                data.decode("utf-8")

            # No BOMs please
            if data[:3] == codecs.BOM_UTF8:
                data = data[3:]

        errmsg = "Unknown"
        try:
            result = Calendar.parseData(data, format)
        except ErrorBase, e:
            errmsg = "{0}: {1}".format(e.mReason, e.mData,)
            result = None
        if not result:
            if isstream:
                data.seek(0)
                data = data.read()
            raise InvalidICalendarDataError("{0}\n{1}".format(errmsg, data,))
        return clazz(None, pycalendar=result)

    @classmethod
    def fromIStream(clazz, stream, format=None):
        """
        Construct a L{Component} from a stream.
        @param stream: an L{IStream} containing iCalendar data.
        @return: a deferred returning a L{Component} representing the first
            component described by C{stream}.
        """
        #
        # FIXME:
        #   This reads the request body into a string and then parses it.
        #   A better solution would parse directly and incrementally from the
        #   request stream.
        #
        def parse(data):
            return clazz.fromString(data, format)
        return allDataFromStream(IStream(stream), parse)

    @classmethod
    def componentsFromData(cls, data, format):
        """
        Need to split a single VCALENDAR in text form into separate ones based
        on UID with the appropriate VTIEMZONES included.
        """

        # Split into components by UID and TZID
        try:
            vcal = cls.fromString(data, format)
        except InvalidICalendarDataError:
            return None

        return cls.componentsFromComponent(vcal)

    @classmethod
    def componentsFromComponent(cls, sourceComponent):
        """
        Need to split a single VCALENDAR in Component form into separate ones
        based on UID with the appropriate VTIEMZONES included.
        """

        results = []

        by_uid = collections.OrderedDict()
        by_tzid = {}
        for subcomponent in sourceComponent.subcomponents():
            if subcomponent.name() == "VTIMEZONE":
                by_tzid[subcomponent.propertyValue("TZID")] = subcomponent
            else:
                by_uid.setdefault(subcomponent.propertyValue("UID"), []).append(subcomponent)

        # Re-constitute as separate VCALENDAR objects
        for components in by_uid.values():

            newvcal = cls("VCALENDAR")
            newvcal.addProperty(Property("VERSION", "2.0"))
            newvcal.addProperty(Property("PRODID", sourceComponent.propertyValue("PRODID")))

            # Get the set of TZIDs and include them
            tzids = set()
            for component in components:
                tzids.update(component.timezoneIDs())
            for tzid in tzids:
                try:
                    tz = by_tzid[tzid]
                    newvcal.addComponent(tz.duplicate())
                except KeyError:
                    # We ignore the error and generate invalid ics which someone will
                    # complain about at some point
                    pass

            # Now add each component
            for component in components:
                newvcal.addComponent(component.duplicate())

            results.append(newvcal)

        return results

    @classmethod
    def newCalendar(cls):
        """
        Create and return an empty C{VCALENDAR} component.

        @return: a new C{VCALENDAR} component with appropriate metadata
            properties already set (version, product ID).
        @rtype: an instance of this class
        """
        self = cls("VCALENDAR")
        self.addProperty(Property("VERSION", "2.0"))
        self.addProperty(Property("PRODID", iCalendarProductID))
        return self

    def __init__(self, name, **kwargs):
        """
        Use this constructor to initialize an empty L{Component}.
        To create a new L{Component} from X{iCalendar} data, don't use this
        constructor directly.  Use one of the factory methods instead.
        @param name: the name (L{str}) of the X{iCalendar} component type for the
            component.
        """
        if name is None:
            if "pycalendar" in kwargs:
                pyobj = kwargs["pycalendar"]

                if pyobj is not None:
                    if not isinstance(pyobj, ComponentBase):
                        raise TypeError("Not a ComponentBase: {0!r}".format(pyobj,))

                self._pycalendar = pyobj
            else:
                raise AssertionError("name may not be None")

            if "parent" in kwargs:
                parent = kwargs["parent"]

                if parent is not None:
                    if not isinstance(parent, Component):
                        raise TypeError("Not a Component: {0!r}".format(parent,))

                self._parent = parent
            else:
                self._parent = None
        else:
            # FIXME: figure out creating an arbitrary component
            self._pycalendar = Calendar(add_defaults=False) if name == "VCALENDAR" else PyComponent.makeComponent(name, None)
            self._parent = None

    def __str__(self):
        """
        NB This does not automatically include timezones in VCALENDAR objects.
        """
        cachedCopy = getattr(self, "_cachedCopy", None)
        if cachedCopy is not None:
            return cachedCopy
        self._cachedCopy = str(self._pycalendar)
        return self._cachedCopy

    def _markAsDirty(self):
        """
        Invalidate the cached copy of serialized icalendar data
        """
        self._cachedCopy = None
        parent = getattr(self, "_parent", None)
        if parent is not None:
            parent._markAsDirty()

    def __repr__(self):
        return (
            "<{self.__class__.__name__}: {pycal!r}>"
            .format(self=self, pycal=str(self._pycalendar))
        )

    def __hash__(self):
        return hash(str(self))

    def __ne__(self, other):
        return not self.__eq__(other)

    def __eq__(self, other):
        if not isinstance(other, Component):
            return False
        return self._pycalendar == other._pycalendar

    def getText(self, format=None):
        """
        Return text representation and include non-standard timezones.
        """
        return self._getTextWithTimezones(includeTimezones=Calendar.NONSTD_TIMEZONES, format=format)

    def getTextWithTimezones(self, includeTimezones, format=None):
        """
        Return text representation and include timezones if the option is on.
        """
        includeTimezones = Calendar.ALL_TIMEZONES if includeTimezones else Calendar.NONSTD_TIMEZONES
        return self._getTextWithTimezones(includeTimezones=includeTimezones, format=format)

    def getTextWithoutTimezones(self, format=None):
        """
        Return text representation without including timezones.
        """
        return self._getTextWithTimezones(includeTimezones=Calendar.NO_TIMEZONES, format=format)

    def _getTextWithTimezones(self, includeTimezones, format=None):
        """
        Return text representation and include timezones if the option is on.
        """
        assert self.name() == "VCALENDAR", "Must be a VCALENDAR: {0!r}".format(self,)

        result = self._pycalendar.getText(includeTimezones=includeTimezones, format=format)
        if result is None:
            raise ValueError("Unknown format requested for calendar data.")
        return result

    # FIXME: Should this not be in __eq__?
    def same(self, other):
        return self._pycalendar == other._pycalendar

    def name(self):
        """
        @return: the name of the iCalendar type of this component.
        """
        return self._pycalendar.getType()

    def ignored(self):
        """
        @return: where this component is one of the L{ignoredComponents}.
        @rtype: L{bool}
        """
        return self.name() in ignoredComponents

    def mainType(self):
        """
        Determine the primary type of iCal component in this calendar.
        @return: the name of the primary type.
        @raise: L{InvalidICalendarDataError} if there is more than one primary type.
        """
        assert self.name() == "VCALENDAR", "Must be a VCALENDAR: {0!r}".format(self,)

        mtype = None
        for component in self.subcomponents(ignore=True):
            if mtype and (mtype != component.name()):
                raise InvalidICalendarDataError("Component contains more than one type of primary type: {0!r}".format(self,))
            else:
                mtype = component.name()

        return mtype

    def mainComponent(self):
        """
        Return the primary iCal component in this calendar. If a master component exists, use that,
        otherwise use the first override.

        @return: the L{Component} of the primary type.
        """
        assert self.name() == "VCALENDAR", "Must be a VCALENDAR: {0!r}".format(self,)

        result = None
        for component in self.subcomponents(ignore=True):
            if not component.hasProperty("RECURRENCE-ID"):
                return component
            elif result is None:
                result = component

        return result

    def masterComponent(self):
        """
        Return the master iCal component in this calendar.
        @return: the L{Component} for the master component,
            or C{None} if there isn't one.
        """
        assert self.name() == "VCALENDAR", "Must be a VCALENDAR: {0!r}".format(self,)

        for component in self.subcomponents(ignore=True):
            if not component.hasProperty("RECURRENCE-ID"):
                return component

        return None

    def overriddenComponent(self, recurrence_id):
        """
        Return the overridden iCal component in this calendar matching the supplied RECURRENCE-ID property.
        This also returns the matching master component if recurrence_id is C{None}.

        @param recurrence_id: The RECURRENCE-ID property value to match.
        @type recurrence_id: L{DateTime}
        @return: the L{Component} for the overridden component,
            or C{None} if there isn't one.
        """
        assert self.name() == "VCALENDAR", "Must be a VCALENDAR: {0!r}".format(self,)

        if isinstance(recurrence_id, str):
            recurrence_id = DateTime.parseText(recurrence_id) if recurrence_id else None

        for component in self.subcomponents(ignore=True):
            rid = component.getRecurrenceIDUTC()
            if rid and recurrence_id and rid == recurrence_id:
                return component
            elif rid is None and recurrence_id is None:
                return component

        return None

    def accessLevel(self, default=ACCESS_PUBLIC):
        """
        Return the access level for this component.
        @return: the access level for the calendar data.
        """
        assert self.name() == "VCALENDAR", "Must be a VCALENDAR: {0!r}".format(self,)

        access = self.propertyValue(Component.ACCESS_PROPERTY)
        if access:
            access = access.upper()
        return Component.accessMap.get(access, default)

    def duplicate(self):
        """
        Duplicate this object and all its contents.
        @return: the duplicated calendar.
        """
        result = Component(None, pycalendar=self._pycalendar.duplicate())
        if hasattr(self, "noInstanceIndexing"):
            result.noInstanceIndexing = self.noInstanceIndexing
        return result

    def subcomponents(self, ignore=False):
        """
        @return: an iterable of L{Component} objects, one for each subcomponent
            of this component.
        """
        return (
            Component(None, pycalendar=c, parent=self)
            for c in self._pycalendar.getComponents()
            if not ignore or (c.getType() not in ignoredComponents)
        )

    def addComponent(self, component):
        """
        Adds a subcomponent to this component.
        @param component: the L{Component} to add as a subcomponent of this
            component.
        """
        self._pycalendar.addComponent(component._pycalendar)
        component._parent = self
        self._markAsDirty()

    def removeComponent(self, component):
        """
        Removes a subcomponent from this component.
        @param component: the L{Component} to remove.
        """
        self._pycalendar.removeComponent(component._pycalendar)
        component._parent = None
        self._markAsDirty()

    def hasProperty(self, name):
        """
        @param name: the name of the property whose existence is being tested.
        @return: True if the named property exists, False otherwise.
        """
        return self._pycalendar.hasProperty(name)

    def getProperty(self, name):
        """
        Get one property from the property list.
        @param name: the name of the property to get.
        @return: the L{Property} found or None.
        @raise: L{InvalidICalendarDataError} if there is more than one property of the given name.
        """
        properties = tuple(self.properties(name))
        if len(properties) == 1:
            return properties[0]
        if len(properties) > 1:
            raise InvalidICalendarDataError("More than one {0} property in component {1!r}".format(name, self))
        return None

    def properties(self, name=None):
        """
        @param name: if given and not C{None}, restricts the returned properties
            to those with the given C{name}.
        @return: an iterable of L{Property} objects, one for each property of
            this component.
        """
        properties = []
        if name is None:
            [properties.extend(i) for i in self._pycalendar.getProperties().values()]
        elif self._pycalendar.countProperty(name) > 0:
            properties = self._pycalendar.getProperties(name)

        return (
            Property(None, None, None, parent=self, pycalendar=p)
            for p in properties
        )

    def propertyValue(self, name, default=None):
        properties = tuple(self.properties(name))
        if len(properties) == 1:
            return properties[0].value()
        if len(properties) > 1:
            raise InvalidICalendarDataError("More than one {0} property in component {1!r}".format(name, self))
        return default

    def getStartDateUTC(self):
        """
        Return the start date or date-time for the specified component
        converted to UTC.
        @param component: the Component whose start should be returned.
        @return: the L{DateTime} for the start.
        """
        dtstart = self.propertyValue("DTSTART")
        return dtstart.duplicateAsUTC() if dtstart is not None else None

    def getEndDateUTC(self):
        """
        Return the end date or date-time for the specified component,
        taking into account the presence or absence of DTEND/DURATION properties.
        The returned date-time is converted to UTC.
        @param component: the Component whose end should be returned.
        @return: the L{DateTime} for the end.
        """
        dtend = self.propertyValue("DTEND")
        if dtend is None:
            dtstart = self.propertyValue("DTSTART")
            duration = self.propertyValue("DURATION")
            if duration is not None:
                dtend = dtstart + duration

        return dtend.duplicateAsUTC() if dtend is not None else None

    def getDueDateUTC(self):
        """
        Return the due date or date-time for the specified component
        converted to UTC. Use DTSTART/DURATION if no DUE property.
        @param component: the Component whose start should be returned.
        @return: the L{DateTime} for the start.
        """
        due = self.propertyValue("DUE")
        if due is None:
            dtstart = self.propertyValue("DTSTART")
            duration = self.propertyValue("DURATION")
            if dtstart is not None and duration is not None:
                due = dtstart + duration

        return due.duplicateAsUTC() if due is not None else None

    def getCompletedDateUTC(self):
        """
        Return the completed date or date-time for the specified component
        converted to UTC.
        @param component: the Component whose start should be returned.
        @return: the datetime.date or datetime.datetime for the start.
        """
        completed = self.propertyValue("COMPLETED")
        return completed.duplicateAsUTC() if completed is not None else None

    def getCreatedDateUTC(self):
        """
        Return the created date or date-time for the specified component
        converted to UTC.
        @param component: the Component whose start should be returned.
        @return: the datetime.date or datetime.datetime for the start.
        """
        created = self.propertyValue("CREATED")
        return created.duplicateAsUTC() if created is not None else None

    def getRecurrenceIDUTC(self):
        """
        Return the recurrence-id for the specified component.
        @param component: the Component whose r-id should be returned.
        @return: the L{DateTime} for the r-id.
        """
        rid = self.propertyValue("RECURRENCE-ID")
        return rid.duplicateAsUTC() if rid is not None else None

    def getRange(self):
        """
        Determine whether a RANGE=THISANDFUTURE parameter is present
        on any RECURRENCE-ID property.
        @return: True if the parameter is present, False otherwise.
        """
        ridprop = self.getProperty("RECURRENCE-ID")
        if ridprop is not None:
            range = ridprop.parameterValue("RANGE")
            if range is not None:
                return (range == "THISANDFUTURE")

        return False

    def getExdates(self):
        """
        Get the set of all EXDATEs in this (master) component.
        """
        exdates = set()
        for property in self.properties("EXDATE"):
            for exdate in property.value():
                exdates.add(exdate.getValue())
        return exdates

    def getTriggerDetails(self):
        """
        Return the trigger information for the specified alarm component.
        @param component: the Component whose start should be returned.
        @return: a tuple consisting of:
            trigger : the 'native' trigger value
            related : either True (for START) or False (for END)
            repeat : an integer for the REPEAT count
            duration: the repeat duration if present, otherwise None
        """
        assert self.name() == "VALARM", "Component is not a VAlARM: {0!r}".format(self,)

        # The trigger value
        trigger = self.propertyValue("TRIGGER")
        if trigger is None:
            raise InvalidICalendarDataError("VALARM has no TRIGGER property: {0!r}".format(self,))

        # The related parameter
        related = self.getProperty("TRIGGER").parameterValue("RELATED")
        if related is None:
            related = True
        else:
            related = (related == "START")

        # Repeat property
        repeat = self.propertyValue("REPEAT")
        if repeat is None:
            repeat = 0
        else:
            repeat = int(repeat)

        # Duration property
        duration = self.propertyValue("DURATION")

        if repeat > 0 and duration is None:
            raise InvalidICalendarDataError("VALARM has invalid REPEAT/DURATIOn properties: {0!r}".format(self,))

        return (trigger, related, repeat, duration)

    def getRecurrenceSet(self):
        return self._pycalendar.getRecurrenceSet()

    def getEffectiveStartEnd(self):
        # Get the start/end range needed for instance comparisons

        if self.name() in ("VEVENT", "VJOURNAL",):
            return self.getStartDateUTC(), self.getEndDateUTC()
        elif self.name() == "VTODO":
            start = self.getStartDateUTC()
            due = self.getDueDateUTC()
            if start is None and due is not None:
                return due, due
            else:
                return start, due
        else:
            return None, None

    def getFBType(self):

        # Only VEVENTs block time
        if self.name() not in ("VEVENT",):
            return "FREE"

        # Handle status
        status = self.propertyValue("STATUS")
        if status == "CANCELLED":
            return "FREE"
        elif status == "TENTATIVE":
            return "BUSY-TENTATIVE"
        else:
            return "BUSY"

    def addProperty(self, property):
        """
        Adds a property to this component.
        @param property: the L{Property} to add to this component.
        """
        self._pycalendar.addProperty(property._pycalendar)
        self._pycalendar.finalise()
        property._parent = self
        self._markAsDirty()

    def removeProperty(self, property):
        """
        Remove a property from this component.
        @param property: the L{Property} to remove from this component.
        """

        if isinstance(property, str):
            for property in tuple(self.properties(property)):
                self.removeProperty(property)
        else:
            self._pycalendar.removeProperty(property._pycalendar)
            self._pycalendar.finalise()
            property._parent = None
            self._markAsDirty()

    def removeProperties(self, name):
        """
        remove all properties with name
        @param name: the name of the properties to remove.
        """
        self._pycalendar.removeProperties(name)
        self._pycalendar.finalise()
        self._markAsDirty()

    def removeAllPropertiesWithName(self, pname):
        """
        Remove all properties with the given name from all components.

        @param pname: the property name to remove from all components.
        @type pname: C{str}
        """

        for property in tuple(self.properties(pname)):
            self.removeProperty(property)

        for component in self.subcomponents():
            component.removeAllPropertiesWithName(pname)

    def replaceProperty(self, property):
        """
        Add or replace a property in this component.
        @param property: the L{Property} to add or replace in this component.
        """

        # Remove all existing ones first
        self._pycalendar.removeProperties(property.name())
        self.addProperty(property)
        self._markAsDirty()

    def timezoneIDs(self):
        """
        Returns the set of TZID parameter values appearing in any property in
        this component.
        @return: a set of strings, one for each unique TZID value.
        """
        result = set()

        if self.name() == "VCALENDAR":
            for component in self.subcomponents():
                if component.name() != "VTIMEZONE":
                    result.update(component.timezoneIDs())
        else:
            for property in self.properties():
                tzid = property.parameterValue("TZID")
                if tzid is not None:
                    result.add(tzid)
                    break

        return result

    def timezones(self):
        """
        Returns the set of TZID's for each VTIMEZONE component.

        @return: a set of strings, one for each unique TZID value.
        """

        assert self.name() == "VCALENDAR", "Not a calendar: {0!r}".format(self,)

        results = set()
        for component in self.subcomponents():
            if component.name() == "VTIMEZONE":
                results.add(component.propertyValue("TZID"))

        return results

    def truncateRecurrence(self, maximumCount):
        """
        Truncate RRULEs etc to make sure there are no more than the given number
        of instances.

        @param maximumCount: the maximum number of instances to allow
        @type maximumCount: C{int}
        @return: a C{bool} indicating whether a change was made or not
        """

        changed = False
        master = self.masterComponent()
        if master and master.isRecurring():
            rrules = master._pycalendar.getRecurrenceSet()
            if rrules:
                for rrule in rrules.getRules():
                    if rrule.getUseCount():
                        # Make sure COUNT is less than the limit
                        if rrule.getCount() > maximumCount:
                            rrule.setCount(maximumCount)
                            changed = True
                    elif rrule.getUseUntil():
                        # Need to figure out how to determine number of instances
                        # with this UNTIL and truncate if needed
                        start = master.getStartDateUTC()
                        diff = differenceDateTime(start, rrule.getUntil())
                        diff = diff.getDays() * 24 * 60 * 60 + diff.getSeconds()

                        period = {
                            definitions.eRecurrence_YEARLY: 365 * 24 * 60 * 60,
                            definitions.eRecurrence_MONTHLY: 30 * 24 * 60 * 60,
                            definitions.eRecurrence_WEEKLY: 7 * 24 * 60 * 60,
                            definitions.eRecurrence_DAILY: 1 * 24 * 60 * 60,
                            definitions.eRecurrence_HOURLY: 60 * 60,
                            definitions.eRecurrence_MINUTELY: 60,
                            definitions.eRecurrence_SECONDLY: 1
                        }[rrule.getFreq()] * rrule.getInterval()

                        if diff / period > maximumCount:
                            rrule.setUseUntil(False)
                            rrule.setUseCount(True)
                            rrule.setCount(maximumCount)
                            rrules.changed()
                            changed = True
                    else:
                        # For frequencies other than yearly we will truncate at our limit
                        if rrule.getFreq() != definitions.eRecurrence_YEARLY:
                            rrule.setUseCount(True)
                            rrule.setCount(maximumCount)
                            rrules.changed()
                            changed = True

        if changed:
            self._markAsDirty()
        return changed

    def onlyPastInstances(self, rid):
        """
        Remove all recurrence instances at or beyond the specified recurrence-id. Adjust the bounds of any RRULEs
        to match the new limit, remove RDATEs/EXDATEs and overridden components beyond the limit.

        @param rid: the recurrence-id limit
        @type rid: L{DateTime}
        """

        if not self.isRecurring():
            return
        master = self.masterComponent()
        if master:
            # Adjust any RRULE first
            rrules = master._pycalendar.getRecurrenceSet()
            if rrules:
                for rrule in rrules.getRules():
                    rrule.setUseUntil(True)
                    rrule.setUseCount(False)
                    until = rid.duplicate()
                    if until.isDateOnly():
                        until.offsetDay(-1)
                    else:
                        until.offsetSeconds(-1)
                    rrule.setUntil(until)

            # Remove any RDATEs or EXDATEs in the future
            for property in list(itertools.chain(
                master.properties("RDATE"),
                master.properties("EXDATE"),
            )):
                for value in list(property.value()):
                    if value.getValue() >= rid:
                        property.value().remove(value)
                if len(property.value()) == 0:
                    master.removeProperty(property)

        # Remove overrides in the future
        for component in list(self.subcomponents(ignore=True)):
            c_rid = component.getRecurrenceIDUTC()
            if c_rid is not None and c_rid >= rid:
                self.removeComponent(component)

        # Handle per-user data component by removing ones in the future
        for component in list(self.subcomponents()):
            if component.name() == PERUSER_COMPONENT:
                for subcomponent in list(component.subcomponents()):
                    c_rid = subcomponent.getRecurrenceIDUTC()
                    if c_rid is not None and c_rid >= rid:
                        component.removeComponent(subcomponent)
                if len(list(component.subcomponents())) == 0:
                    self.removeComponent(component)

        self._markAsDirty()

        # We changed the instance set so remove any instance cache
        # TODO: we could be smarter here and truncate the instance list
        if hasattr(self, "cachedInstances"):
            delattr(self, "cachedInstances")

    def onlyFutureInstances(self, rid):
        """
        Remove all recurrence instances from the specified recurrence-id into the past. Adjust the bounds of
        any RRULEs to match the new limit, remove RDATEs/EXDATEs and overridden components beyond the limit.
        This also requires "re-basing" the master component to the new first instance - but noting that has to
        match any RRULE pattern.

        @param rid: the recurrence-id limit
        @type rid: L{DateTime}
        """

        if not self.isRecurring():
            return
        master = self.masterComponent()
        if master:
            # Check if cut-off matches an RDATE
            adjusted_rid = rid
            adjust_rrule = None
            adjust_count = 0
            continuing_rrule = True

            # Need to detect the first valid RRULE instance after the cut-off as that needs to be the new DTSTART
            rrules = master._pycalendar.getRecurrenceSet()
            if rrules and len(rrules.getRules()) != 0:
                rrule = rrules.getRules()[0]
                upperlimit = rid.duplicate()
                upperlimit.offsetYear(1)
                rrule_expanded = []
                rrule.expand(
                    master.propertyValue("DTSTART"),
                    Period(DateTime(1900, 1, 1), upperlimit),
                    rrule_expanded,
                )
                for ctr, i in enumerate(sorted(rrule_expanded)):
                    if i >= rid:
                        adjusted_rid = i
                        adjust_rrule = rrule
                        adjust_count = ctr
                        break
                else:
                    # RRULE not needed in derived master
                    continuing_rrule = False

            # Adjust master to previously derived instance
            derived = self.deriveInstance(adjusted_rid, allowExcluded=True)
            if derived is None:
                return

            # Adjust any COUNT to exclude the earlier instances - note we do this after
            # deriving the instance otherwise it might truncate the instance we care about
            if adjust_rrule is not None and rrule.getUseCount():
                adjust_rrule.setCount(adjust_rrule.getCount() - adjust_count)

            # Fix up recurrence properties so the derived one looks like the master
            derived.removeProperty(derived.getProperty("RECURRENCE-ID"))
            for property in list(itertools.chain(
                master.properties("RRULE") if continuing_rrule else (),
                master.properties("RDATE"),
                master.properties("EXDATE"),
            )):
                derived.addProperty(property)

            # Now switch over to using the new "derived" master
            self.removeComponent(master)
            master = derived
            self.addComponent(master)

            # Remove any RDATEs or EXDATEs in the past
            for property in list(itertools.chain(
                master.properties("RDATE"),
                master.properties("EXDATE"),
            )):
                for value in list(property.value()):
                    # If the derived master was derived from an RDATE we remove the RDATE
                    if value.getValue() < rid or property.name() == "RDATE" and value.getValue() == adjusted_rid:
                        property.value().remove(value)
                if len(property.value()) == 0:
                    master.removeProperty(property)

        # Remove overrides in the past - but do not remove any override matching
        # the cut-off as that is still a valid override after "re-basing" the master.
        for component in list(self.subcomponents(ignore=True)):
            c_rid = component.getRecurrenceIDUTC()
            if c_rid is not None and c_rid < rid:
                self.removeComponent(component)

        # Handle per-user data component by removing ones in the past
        for component in list(self.subcomponents()):
            if component.name() == PERUSER_COMPONENT:
                for subcomponent in list(component.subcomponents()):
                    c_rid = subcomponent.getRecurrenceIDUTC()
                    if c_rid is not None and c_rid < rid:
                        component.removeComponent(subcomponent)
                if len(list(component.subcomponents())) == 0:
                    self.removeComponent(component)

        self._markAsDirty()

        # We changed the instance set so remove any instance cache
        # TODO: we could be smarter here and truncate the instance list
        if hasattr(self, "cachedInstances"):
            delattr(self, "cachedInstances")

    def expand(self, start, end, timezone=None):
        """
        Expand the components into a set of new components, one for each
        instance in the specified range. Date-times are converted to UTC. A
        new calendar object is returned.

        @param start: the L{DateTime} for the start of the range.
        @param end: the L{DateTime} for the end of the range.
        @param timezone: the L{Component} or L{Timezone} of the VTIMEZONE to use for floating/all-day.
        @return: the L{Component} for the new calendar with expanded instances.
        """

        if timezone is not None and isinstance(timezone, Component):
            pytz = Timezone(tzid=timezone.propertyValue("TZID"))
        else:
            pytz = timezone

        # Create new calendar object with same properties as the original, but
        # none of the originals sub-components
        calendar = Component("VCALENDAR")
        for property in calendar.properties():
            calendar.removeProperty(property)
        for property in self.properties():
            calendar.addProperty(property)

        # Expand the instances and add each one - use the normalizeForExpand date/time normalization method here
        # so that all-day date/times remain that way. However, when doing the timeRangesOverlap test below, we
        # Need to convert the all-days to floating (T000000) so that the timezone overlap calculation can be done
        # properly.
        instances = self.expandTimeRanges(end, lowerLimit=start, normalizeFunction=normalizeForExpand)
        first = True
        for key in instances:
            instance = instances[key]
            if timeRangesOverlap(normalizeForIndex(instance.start), normalizeForIndex(instance.end), start, end, pytz):
                calendar.addComponent(self.expandComponent(instance, first))
            first = False

        return calendar

    def expandComponent(self, instance, first):
        """
        Create an expanded component based on the instance provided.
        NB Expansion also requires UTC conversions.
        @param instance: an L{Instance} for the instance being expanded.
        @return: a new L{Component} for the expanded instance.
        """

        # Duplicate the component from the instance
        newcomp = instance.component.duplicate()

        # Strip out unwanted recurrence properties
        for property in tuple(newcomp.properties()):
            if property.name() in ["RRULE", "RDATE", "EXRULE", "EXDATE", "RECURRENCE-ID"]:
                newcomp.removeProperty(property)

        # Convert all datetime properties to UTC unless they are floating
        for property in newcomp.properties():
            value = property.value()
            if isinstance(value, DateTime) and value.local():
                property.removeParameter("TZID")
                property.setValue(value.duplicateAsUTC())

        # Now reset DTSTART, DTEND/DURATION
        for property in newcomp.properties("DTSTART"):
            property.setValue(instance.start)
        for property in newcomp.properties("DTEND"):
            property.setValue(instance.end)
        for property in newcomp.properties("DURATION"):
            property.setValue(instance.end - instance.start)

        # Add RECURRENCE-ID if not master instance
        if not instance.isMasterInstance():
            newcomp.addProperty(Property("RECURRENCE-ID", instance.rid))

        return newcomp

    def cacheExpandedTimeRanges(self, limit, ignoreInvalidInstances=False):
        """
        Expand instances up to the specified limit and cache the results in this object
        so we can return cached results in the future. The limit value is the actual value
        that the requester needs, but we will cache an addition 365-days worth to give us some
        breathing room to return results for future instances.

        @param limit: the max datetime to cache up to.
        @type limit: L{DateTime}
        """

        # Checked for cached values first
        if hasattr(self, "cachedInstances"):
            cachedLimit = self.cachedInstances.limit
            if cachedLimit is None or cachedLimit > limit:
                # We have already fully expanded, or cached up to the requested time,
                # so return cached instances
                return self.cachedInstances

        lookAheadLimit = limit + Duration(days=365)
        self.cachedInstances = self.expandTimeRanges(
            lookAheadLimit,
            ignoreInvalidInstances=ignoreInvalidInstances
        )
        return self.cachedInstances

    def expandTimeRanges(self, limit, lowerLimit=None, ignoreInvalidInstances=False, normalizeFunction=normalizeForIndex):
        """
        Expand the set of recurrence instances for the components
        contained within this VCALENDAR component. We will assume
        that this component has already been validated as a CalDAV resource
        (i.e. only one type of component, all with the same UID)
        @param limit: L{DateTime} value representing the end of the expansion.
        @param ignoreInvalidInstances: C{bool} whether to ignore instance errors.
        @return: a set of Instances for each recurrence in the set.
        """

        componentSet = self.subcomponents()
        return self.expandSetTimeRanges(componentSet, limit, lowerLimit=lowerLimit, ignoreInvalidInstances=ignoreInvalidInstances, normalizeFunction=normalizeFunction)

    def expandSetTimeRanges(self, componentSet, limit, lowerLimit=None, ignoreInvalidInstances=False, normalizeFunction=normalizeForIndex):
        """
        Expand the set of recurrence instances up to the specified date limit.
        What we do is first expand the master instance into the set of generate
        instances. Then we merge the overridden instances, taking into account
        THISANDFUTURE and THISANDPRIOR.

        @param componentSet: the set of components that are to make up the
                recurrence set. These MUST all be components with the same UID
                and type, forming a proper recurring set.
        @param limit: L{DateTime} value representing the end of the expansion.

        @param componentSet: the set of components that are to make up the recurrence set.
            These MUST all be components with the same UID and type, forming a proper
            recurring set.
        @type componentSet: C{list}
        @param limit: the end of the expansion
        @type limit: L{DateTime}
        @param ignoreInvalidInstances: whether or not invalid recurrences raise an exception
        @type ignoreInvalidInstances: C{bool}
        @param normalizeFunction: a function used to normalize date/time values in instances
        @type normalizeFunction: C{function}
        @return: L{InstanceList} containing expanded L{Instance} for each recurrence in the set.
        """

        # Set of instances to return
        instances = InstanceList(ignoreInvalidInstances=ignoreInvalidInstances, normalizeFunction=normalizeFunction)
        try:
            instances.expandTimeRanges(componentSet, limit, lowerLimit=lowerLimit)
        except InvalidOverriddenInstanceError as e:
            if accountingEnabledForCategory("Invalid Instance"):
                emitAccounting(
                    "Invalid Instance",
                    self.resourceUID().encode("base64")[:-1],
                    "{}\n\n{}".format(str(e), str(self)),
                )
            raise
        return instances

    def getComponentInstances(self):
        """
        Get the R-ID value for each component.

        @return: a tuple of recurrence-ids
        """

        # Extract appropriate sub-component if this is a VCALENDAR
        if self.name() == "VCALENDAR":
            result = ()
            for component in self.subcomponents(ignore=True):
                result += component.getComponentInstances()
            return result
        else:
            rid = self.getRecurrenceIDUTC()
            return (rid,)

    def isRecurring(self):
        """
        Check whether any recurrence properties are present in any component.
        """

        # Extract appropriate sub-component if this is a VCALENDAR
        if self.name() == "VCALENDAR":
            for component in self.subcomponents(ignore=True):
                if component.isRecurring():
                    return True
        else:
            for propname in ("RRULE", "RDATE", "EXDATE", "RECURRENCE-ID",):
                if self.hasProperty(propname):
                    return True
        return False

    def isRecurringUnbounded(self):
        """
        Check for unbounded recurrence.
        """

        master = self.masterComponent()
        if master:
            rrules = master.properties("RRULE")
            for rrule in rrules:
                if not rrule.value().getUseCount() and not rrule.value().getUseUntil():
                    return True
        return False

    def deriveInstance(self, rid, allowCancelled=False, newcomp=None, allowExcluded=False):
        """
        Derive an instance from the master component that has the provided RECURRENCE-ID, but
        with all other properties, components etc from the master. If the requested override is
        currently marked as an EXDATE in the existing master, allow an option whereby the override
        is added as STATUS:CANCELLED and the EXDATE removed.

        IMPORTANT: all callers of this method MUST check the return value for None. Never assume that
        a valid instance will be derived - no matter how much you think you understand iCalendar recurrence.
        There is always some new thing that will surprise you.

        @param rid: recurrence-id value
        @type rid: L{DateTime} or C{str}
        @param allowCancelled: whether to allow a STATUS:CANCELLED override
        @type allowCancelled: C{bool}
        @param allowExcluded: whether to derive an instance for an existing EXDATE
        @type allowExcluded: C{bool}

        @return: L{Component} for newly derived instance, or C{None} if not a valid override
        """

        if allowCancelled and newcomp is not None:
            raise ValueError("Cannot re-use master component with allowCancelled")

        # Must have a master component
        master = self.masterComponent()
        if master is None:
            return None

        if isinstance(rid, str):
            rid = DateTime.parseText(rid) if rid else None

        # TODO: Check that the recurrence-id is a valid instance
        # For now we just check that there is no matching EXDATE
        didCancel = False
        matchedExdate = False
        for exdate in tuple(master.properties("EXDATE")):
            for exdateValue in exdate.value():
                if exdateValue.getValue() == rid:
                    if allowCancelled:
                        exdate.value().remove(exdateValue)
                        if len(exdate.value()) == 0:
                            master.removeProperty(exdate)
                        didCancel = True

                        # We changed the instance set so remove any instance cache
                        if hasattr(self, "cachedInstances"):
                            delattr(self, "cachedInstances")
                        break
                    elif allowExcluded:
                        matchedExdate = True
                        break
                    else:
                        # Cannot derive from an existing EXDATE
                        return None

        if not matchedExdate:
            # Check whether recurrence-id matches an RDATE - if so it is OK
            rdates = set()
            for rdate in master.properties("RDATE"):
                rdates.update([item.getValue().duplicateAsUTC() for item in rdate.value()])
            if rid not in rdates:
                # Check whether we have a truncated RRULE
                rrules = master.properties("RRULE")
                if len(tuple(rrules)):
                    instances = self.cacheExpandedTimeRanges(rid)
                    instance_rid = normalizeForIndex(rid)
                    if str(instance_rid) not in instances.instances:
                        # No match to a valid RRULE instance
                        return None
                else:
                    # No RRULE and no match to an RDATE => error
                    return None

        # If we were fed an already derived component, use that, otherwise make a new one
        if newcomp is None:
            newcomp = self.masterDerived()

        # New DTSTART is the RECURRENCE-ID we are deriving but adjusted to the
        # original DTSTART's localtime
        dtstart = newcomp.getProperty("DTSTART")
        if newcomp.hasProperty("DTEND"):
            dtend = newcomp.getProperty("DTEND")
            oldduration = dtend.value() - dtstart.value()

        newdtstartValue = rid.duplicate()
        if not dtstart.value().isDateOnly():
            if dtstart.value().local():
                newdtstartValue.adjustTimezone(dtstart.value().getTimezone())
        else:
            newdtstartValue.setDateOnly(True)

        dtstart.setValue(newdtstartValue)
        if newcomp.hasProperty("DTEND"):
            dtend.setValue(newdtstartValue + oldduration)

        newcomp.replaceProperty(Property("RECURRENCE-ID", dtstart.value(), params={}))

        if didCancel:
            newcomp.replaceProperty(Property("STATUS", "CANCELLED"))

        # After creating/changing a component we need to do this to keep PyCalendar happy
        newcomp._pycalendar.finalise()

        return newcomp

    def masterDerived(self):
        """
        Generate a component from the master instance that can be fed repeatedly to
        deriveInstance in the case where the result of deriveInstance is not going
        to be inserted into the component. This provides an optimization for avoiding
        unnecessary .duplicate() calls on the master for each deriveInstance.
        """

        # Must have a master component
        master = self.masterComponent()
        if master is None:
            return None

        # Create the derived instance
        newcomp = master.duplicate()

        # Strip out unwanted recurrence properties
        for property in tuple(newcomp.properties()):
            if property.name() in ("RRULE", "RDATE", "EXRULE", "EXDATE", "RECURRENCE-ID",):
                newcomp.removeProperty(property)

        return newcomp

    def validInstances(self, rids, ignoreInvalidInstances=False):
        """
        Test whether the specified recurrence-ids are valid instances in this event.

        @param rid: recurrence-id values
        @type rid: iterable

        @return: C{set} of valid rids
        """

        valid = set()
        non_master_rids = [rid for rid in rids if rid is not None]
        if non_master_rids:
            # Pre-cache instance expansion up to the highest rid
            highest_rid = max(non_master_rids)
            self.cacheExpandedTimeRanges(
                highest_rid + Duration(days=1),
                ignoreInvalidInstances=ignoreInvalidInstances
            )
        for rid in rids:
            if self.validInstance(rid, clear_cache=False, ignoreInvalidInstances=ignoreInvalidInstances):
                valid.add(rid)
        return valid

    def validInstance(self, rid, clear_cache=True, ignoreInvalidInstances=False):
        """
        Test whether the specified recurrence-id is a valid instance in this event.

        @param rid: recurrence-id value
        @type rid: L{DateTime}

        @return: C{bool}
        """

        if self.masterComponent() is None:
            return rid in set(self.getComponentInstances())

        if rid is None:
            return True

        # Get expansion
        instances = self.cacheExpandedTimeRanges(
            rid,
            ignoreInvalidInstances=ignoreInvalidInstances
        )
        new_rids = set([instances[key].rid for key in instances])
        return rid in new_rids

    def addExdate(self, exdate):
        """
        Add an EXDATE to a master recurring component and ensure the value type, TZID
        etc match the DTSTART of the master. This method assumes that L{self} is the
        master component - no checking of that will be done.

        @param exdate: the exdate to add
        @type exdate: L{DateTime}
        """
        dtstart = self.getProperty("DTSTART")
        if dtstart is not None and not dtstart.value().isDateOnly() and dtstart.value().local():
            exdate.adjustTimezone(dtstart.value().getTimezone())
        self.addProperty(Property("EXDATE", [exdate, ]))

    def removeExdate(self, exdate):
        """
        Remove an EXDATE from a master recurring component if present. If not present,
        do nothing. This assumes L{self} is the master component

        @param exdate: the exdate to add
        @type exdate: L{DateTime}
        """
        for exdateProp in tuple(self.properties("EXDATE")):
            for exdateValue in exdateProp.value():
                if exdateValue.getValue() == exdate:
                    exdateProp.value().remove(exdateValue)
                    if len(exdateProp.value()) == 0:
                        self.removeProperty(exdateProp)

    def resourceUID(self):
        """
        @return: the UID of the subcomponents in this component.
        """
        assert self.name() == "VCALENDAR", "Not a calendar: {0!r}".format(self,)

        if not hasattr(self, "_resource_uid"):
            for subcomponent in self.subcomponents(ignore=True):
                self._resource_uid = subcomponent.propertyValue("UID")
                break
            else:
                self._resource_uid = None

        return self._resource_uid

    def newUID(self, newUID=None):
        """
        Generate a new UID for all components in this VCALENDAR
        """
        assert self.name() == "VCALENDAR", "Not a calendar: {0!r}".format(self,)

        newUID = str(uuid.uuid4()) if newUID is None else newUID
        self._pycalendar.changeUID(self.resourceUID(), newUID)
        self._resource_uid = newUID
        self._markAsDirty()
        return self._resource_uid

    def resourceType(self):
        """
        @return: the name of the iCalendar type of the subcomponents in this
            component.
        """
        assert self.name() == "VCALENDAR", "Not a calendar: {0!r}".format(self,)

        if not hasattr(self, "_resource_type"):
            has_timezone = False

            for subcomponent in self.subcomponents():
                name = subcomponent.name()
                if name == "VTIMEZONE":
                    has_timezone = True
                elif subcomponent.ignored():
                    continue
                else:
                    self._resource_type = name
                    break
            else:
                if has_timezone:
                    self._resource_type = "VTIMEZONE"
                else:
                    raise InvalidICalendarDataError("No component type found for calendar component: {0!r}".format(self,))

        return self._resource_type

    def stripStandardTimezones(self):
        """
        Remove timezones that this server knows about
        """
        return self._pycalendar.stripStandardTimezones()

    def validCalendarData(self, doFix=True, doRaise=True, validateRecurrences=False):
        """
        @return: tuple of fixed, unfixed issues
        @raise InvalidICalendarDataError: if the given calendar data is not valid and
            cannot be fixed.
        """
        if self.name() != "VCALENDAR":
            log.debug("Not a calendar: {s!r}", s=self)
            raise InvalidICalendarDataError("Not a calendar")
        if not self.resourceType():
            log.debug("Unknown resource type: {s!r}", s=self)
            raise InvalidICalendarDataError("Unknown resource type")

        # Do underlying iCalendar library validation with data fix
        fixed, unfixed = self._pycalendar.validate(doFix=doFix)

        # Detect invalid occurrences and fix by adding RDATEs for them
        if validateRecurrences:
            rfixed, runfixed = self.validRecurrenceIDs(doFix=doFix)
            fixed.extend(rfixed)
            unfixed.extend(runfixed)

        if unfixed:
            log.debug("Calendar data had unfixable problems:\n  {d}", d="\n  ".join(unfixed))
            if doRaise:
                raise InvalidICalendarDataError("Calendar data had unfixable problems:\n  {0}".format("\n  ".join(unfixed),))
        if fixed:
            log.debug("Calendar data had fixable problems:\n  {d}", d="\n  ".join(fixed))

        return fixed, unfixed

    def validRecurrenceIDs(self, doFix=True):

        fixed = []
        unfixed = []

        # Detect invalid occurrences and fix by adding RDATEs for them
        master = self.masterComponent()
        if master is not None:
            # Get the set of all recurrence IDs
            all_rids = set(self.getComponentInstances())
            if None in all_rids:
                all_rids.remove(None)

            # If the master has no recurrence properties treat any other components as invalid
            if master.isRecurring():

                # Remove all EXDATEs with a matching RECURRENCE-ID. Do this before we start
                # processing of valid instances just in case the matching R-ID is also not valid and
                # thus will need RDATE added.
                exdates = {}
                for property in list(master.properties("EXDATE")):
                    for exdate in property.value():
                        exdates[exdate.getValue()] = property
                for rid in all_rids:
                    if rid in exdates:
                        if doFix:
                            property = exdates[rid]
                            for value in property.value():
                                if value.getValue() == rid:
                                    property.value().remove(value)
                                    break
                            master.removeProperty(property)
                            if len(property.value()) > 0:
                                master.addProperty(property)
                            del exdates[rid]
                            fixed.append("Removed EXDATE for valid override: {0}".format(rid,))
                        else:
                            unfixed.append("EXDATE for valid override: {0}".format(rid,))

                # Get the set of all valid recurrence IDs
                valid_rids = self.validInstances(all_rids, ignoreInvalidInstances=True)

                # Get the set of all RDATEs and add those to the valid set
                rdates = []
                for property in master.properties("RDATE"):
                    rdates.extend([_rdate.getValue() for _rdate in property.value()])
                valid_rids.update(set(rdates))

                # Remove EXDATEs predating master
                dtstart = master.propertyValue("DTSTART")
                if dtstart is not None:
                    for property in list(master.properties("EXDATE")):
                        newValues = []
                        changed = False
                        for exdate in property.value():
                            exdateValue = exdate.getValue()
                            if exdateValue < dtstart:
                                if doFix:
                                    fixed.append("Removed earlier EXDATE: {0}".format(exdateValue,))
                                else:
                                    unfixed.append("EXDATE earlier than master: {0}".format(exdateValue,))
                                changed = True
                            else:
                                newValues.append(exdateValue)

                        if changed and doFix:
                            # Remove the property...
                            master.removeProperty(property)
                            if newValues:
                                # ...and add it back only if it still has values
                                property.setValue(newValues)
                                master.addProperty(property)

            else:
                valid_rids = set()

            # Determine the invalid recurrence IDs by set subtraction
            invalid_rids = all_rids - valid_rids

            # Add RDATEs for the invalid ones, or remove any EXDATE.
            for invalid_rid in invalid_rids:
                brokenComponent = self.overriddenComponent(invalid_rid)
                brokenRID = brokenComponent.propertyValue("RECURRENCE-ID")
                if doFix:
                    master.addProperty(Property("RDATE", [brokenRID, ]))
                    fixed.append(
                        "Added RDATE for invalid occurrence: {0}".format(
                            brokenRID,
                        )
                    )
                else:
                    unfixed.append("Invalid occurrence: {0}".format(brokenRID,))

        return fixed, unfixed

    def validCalendarForCalDAV(self, methodAllowed):
        """
        @param methodAllowed: True if METHOD property is allowed, False otherwise.
        @raise InvalidICalendarDataError: if the given calendar component is not valid for
            use as a X{CalDAV} resource.
        """

        # Disallowed in CalDAV-Access-08, section 4.1
        if not methodAllowed and self.hasProperty("METHOD"):
            msg = "METHOD property is not allowed in CalDAV iCalendar data"
            log.debug(msg)
            raise InvalidICalendarDataError(msg)

        #
        # Must not contain more than one type of iCalendar component, except for
        # the required timezone components, and component UIDs must match
        #
        ctype = None
        component_id = None
        component_rids = set()
        timezone_refs = set()
        timezones = set()
        got_master = False
        # got_override     = False
        # master_recurring = False

        for subcomponent in self.subcomponents():
            if subcomponent.name() == "VTIMEZONE":
                timezones.add(subcomponent.propertyValue("TZID"))
            elif subcomponent.ignored():
                continue
            else:
                if ctype is None:
                    ctype = subcomponent.name()
                else:
                    if ctype != subcomponent.name():
                        msg = "Calendar resources may not contain more than one type of calendar component ({0} and {1} found)".format(
                            ctype, subcomponent.name()
                        )
                        log.debug(msg)
                        raise InvalidICalendarDataError(msg)

                if ctype not in allowedComponents:
                    msg = "Component type: {0} not allowed".format(ctype,)
                    log.debug(msg)
                    raise InvalidICalendarDataError(msg)

                uid = subcomponent.propertyValue("UID")
                if uid is None:
                    msg = "All components must have UIDs"
                    log.debug(msg)
                    raise InvalidICalendarDataError(msg)
                rid = subcomponent.getRecurrenceIDUTC()

                # Verify that UIDs are the same
                if component_id is None:
                    component_id = uid
                elif component_id != uid:
                    msg = "Calendar resources may not contain components with different UIDs ({0} and {1} found)".format(
                        component_id, subcomponent.propertyValue("UID")
                    )
                    log.debug(msg)
                    raise InvalidICalendarDataError(msg)

                # Verify that there is only one master component
                if rid is None:
                    if got_master:
                        msg = "Calendar resources may not contain components with the same UIDs and no Recurrence-IDs ({0} and {1} found)".format(
                            component_id, subcomponent.propertyValue("UID")
                        )
                        log.debug(msg)
                        raise InvalidICalendarDataError(msg)
                    else:
                        got_master = True
                        # master_recurring = subcomponent.hasProperty("RRULE") or subcomponent.hasProperty("RDATE")
                else:
                    pass  # got_override = True

                # Check that if an override is present then the master is recurring
                # Leopard iCal sometimes does this for overridden instances that an Attendee receives and
                # it creates a "fake" (invalid) master. We are going to skip this test here. Instead implicit
                # scheduling will verify the validity of the components and raise if they don't make sense.
                # If no scheduling is happening then we allow this - that may cause other clients to choke.
                # If it does we will have to reinstate this check but only after we have checked for implicit.
# UNCOMMENT OUT master_recurring AND got_override ASSIGNMENTS ABOVE
#                if got_override and got_master and not master_recurring:
#                    msg = "Calendar resources must have a recurring master component if there is an overridden one ({uid})".format(uid=subcomponent.propertyValue("UID"),)
#                    log.debug(msg)
#                    raise InvalidICalendarDataError(msg)

                # Check for duplicate RECURRENCE-IDs
                if rid in component_rids:
                    msg = "Calendar resources may not contain components with the same Recurrence-IDs ({0})".format(rid,)
                    log.debug(msg)
                    raise InvalidICalendarDataError(msg)
                else:
                    component_rids.add(rid)

                timezone_refs.update(subcomponent.timezoneIDs())

        #
        # Make sure required timezone components are present
        #
        if not config.EnableTimezonesByReference:
            for timezone_ref in timezone_refs:
                if timezone_ref not in timezones:
                    msg = "Timezone ID {0} is referenced but not defined: {1}".format(timezone_ref, self,)
                    log.debug(msg)
                    raise InvalidICalendarDataError(msg)

        #
        # FIXME:
        #   This test is not part of the spec; it appears to be legal (but
        #   goofy?) to have extra timezone components.
        #
        for timezone in timezones:
            if timezone not in timezone_refs:
                log.debug(
                    "Timezone {tz} is not referenced by any non-timezone component", tz=timezone
                )

        # TZIDs without a VTIMEZONE must be available in the server's TZ database
        missing_timezones = timezone_refs - timezones
        for tzid in missing_timezones:
            # Will raise TimezoneException if tzid not present in server's database
            hasTZ(tzid)

        # Control character check - only HTAB, CR, LF allowed for characters in the range 0x00-0x1F
        s = str(self)
        if len(s.translate(None, "\x00\x01\x02\x03\x04\x05\x06\x07\x08\x0B\x0C\x0E\x0F\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1A\x1B\x1C\x1D\x1E\x1F")) != len(s):
            raise InvalidICalendarDataError("iCalendar contains illegal control character")

    def validOrganizerForScheduling(self, doFix=True):
        """
        Check that the ORGANIZER property is valid for scheduling
        """

        organizers = self.getOrganizersByInstance()
        foundOrganizer = None
        foundRid = None
        missingRids = set()
        for organizer, rid in organizers:
            if organizer:
                if foundOrganizer:
                    if organizer != foundOrganizer:
                        # We have different ORGANIZERs in the same iCalendar object - this is an error
                        msg = "Only one ORGANIZER is allowed in an iCalendar object:\n{0}".format(self,)
                        log.debug(msg)
                        raise InvalidICalendarDataError(msg)
                else:
                    foundOrganizer = organizer
                    foundRid = rid
            else:
                missingRids.add(rid)

        # If there are some components without an ORGANIZER we will fix the data
        if foundOrganizer and missingRids:
            if doFix:
                log.debug("Fixing missing ORGANIZER properties")
                organizerProperty = self.overriddenComponent(foundRid).getProperty("ORGANIZER")
                for rid in missingRids:
                    self.overriddenComponent(rid).addProperty(organizerProperty)
            else:
                raise InvalidICalendarDataError("iCalendar missing ORGANIZER properties in some instances")

        return foundOrganizer

    def gettimezone(self):
        """
        Get the Timezone for a Timezone component.

        @return: L{Timezone} if this is a VTIMEZONE, otherwise None.
        """
        if self.name() == "VTIMEZONE":
            return Timezone(tzid=self._pycalendar.getID())
        elif self.name() == "VCALENDAR":
            for component in self.subcomponents():
                if component.name() == "VTIMEZONE":
                    return component.gettimezone()
            else:
                return None
        else:
            return None

    # #
    # iTIP stuff
    # #

    def isValidMethod(self):
        """
        Verify that this calendar component has a valid iTIP METHOD property.

        @return: True if valid, False if not
        """

        try:
            method = self.propertyValue("METHOD")
            if method not in ("PUBLISH", "REQUEST", "REPLY", "ADD", "CANCEL", "REFRESH", "COUNTER", "DECLINECOUNTER"):
                return False
        except InvalidICalendarDataError:
            return False

        return True

    def isValidITIP(self):
        """
        Verify that this calendar component is valid according to iTIP.

        @return: True if valid, False if not
        """

        try:
            method = self.propertyValue("METHOD")
            if method not in ("PUBLISH", "REQUEST", "REPLY", "ADD", "CANCEL", "REFRESH", "COUNTER", "DECLINECOUNTER"):
                return False

            # First make sure components are all of the same time (excluding VTIMEZONE)
            self.validCalendarForCalDAV(methodAllowed=True)

            # Next we could check the iTIP status for each type of method/component pair, however
            # we can also leave that up to the server except for the REQUEST/VFREEBUSY case which
            # the server will handle itself.

            if (method == "REQUEST") and (self.mainType() == "VFREEBUSY"):
                # TODO: verify REQUEST/VFREEBUSY as being OK

                # Only one VFREEBUSY (actually multiple X-'s are allowed but we will reject)
                if len([c for c in self.subcomponents()]) != 1:
                    return False

        except InvalidICalendarDataError:
            return False

        return True

    def getOrganizer(self):
        """
        Get the organizer value. Works on either a VCALENDAR or on a component.

        @return: the string value of the Organizer property, or None
        """

        # Extract appropriate sub-component if this is a VCALENDAR
        if self.name() == "VCALENDAR":
            for component in self.subcomponents(ignore=True):
                return component.getOrganizer()
        else:
            try:
                # Find the primary subcomponent
                return self.propertyValue("ORGANIZER")
            except InvalidICalendarDataError:
                pass

        return None

    def getOrganizersByInstance(self):
        """
        Get the organizer value for each instance.

        @return: a list of tuples of (organizer value, recurrence-id)
        """

        # Extract appropriate sub-component if this is a VCALENDAR
        if self.name() == "VCALENDAR":
            result = ()
            for component in self.subcomponents(ignore=True):
                result += component.getOrganizersByInstance()
            return result
        else:
            try:
                # Should be just one ORGANIZER
                org = self.propertyValue("ORGANIZER")
                rid = self.getRecurrenceIDUTC()
                return ((org, rid),)
            except InvalidICalendarDataError:
                pass

        return ()

    def getOrganizerProperties(self):
        """
        Get the organizer value. Works on either a VCALENDAR or on a component.

        @return: the string value of the Organizer property, or None
        """

        # Extract appropriate sub-component if this is a VCALENDAR
        if self.name() == "VCALENDAR":
            return [component.getOrganizerProperty() for component in self.subcomponents(ignore=True)]
        else:
            try:
                return self.getProperty("ORGANIZER")
            except InvalidICalendarDataError:
                pass

        return None

    def getOrganizerProperty(self):
        """
        Get the organizer value. Works on either a VCALENDAR or on a component.

        @return: the string value of the Organizer property, or None
        """

        # Extract appropriate sub-component if this is a VCALENDAR
        if self.name() == "VCALENDAR":
            for component in self.subcomponents(ignore=True):
                return component.getOrganizerProperty()
        else:
            try:
                # Find the primary subcomponent
                return self.getProperty("ORGANIZER")
            except InvalidICalendarDataError:
                pass

        return None

    def getOrganizerScheduleAgent(self):

        is_server = False
        organizerProp = self.getOrganizerProperty()
        if organizerProp is not None:
            if organizerProp.hasParameter("SCHEDULE-AGENT"):
                if organizerProp.parameterValue("SCHEDULE-AGENT") == "SERVER":
                    is_server = True
            else:
                is_server = True

        return is_server

    def cleanOrganizerScheduleAgent(self):
        """
        Remove components whose ORGANIZER property does not have
        SCHEDULE-AGENT=SERVER.
        """

        changed = False
        for component in tuple(self.subcomponents(ignore=True)):
            organizerProp = component.getOrganizerProperty()
            if organizerProp is not None:
                if organizerProp.parameterValue("SCHEDULE-AGENT", "SERVER") != "SERVER":
                    self.removeComponent(component)
                    changed = True

        return changed

    def recipientPropertyName(self):
        return "VOTER" if self.name() in ("VPOLL", "VVOTER",) else "ATTENDEE"

    def getRecipientProperties(self):
        """
        Get the attendee properties. Works on either a VCALENDAR or on a component.

        @return: a C{list} of the the Attendee properties
        """

        # Extract appropriate sub-component if this is a VCALENDAR
        if self.name() == "VCALENDAR":
            for component in self.subcomponents(ignore=True):
                return component.getRecipientProperties()
        else:
            # Find the property values
            if self.name() == "VPOLL":
                results = []
                for c in self.subcomponents():
                    if c.name() == "VVOTER":
                        results.extend(c.properties(self.recipientPropertyName()))
                return results
            else:
                return list(self.properties(self.recipientPropertyName()))

        return None

    def getAttendees(self):
        """
        Get the attendee value. Works on either a VCALENDAR or on a component.

        @return: a C{list} of the string values of the Attendee property, or None
        """

        return [p.value() for p in self.getRecipientProperties()]

    def getAttendeesByInstance(self, makeUnique=False, onlyScheduleAgentServer=False):
        """
        Get the attendee values for each instance. Optionally remove duplicates.

        @param makeUnique: if C{True} remove duplicate ATTENDEEs in each component
        @type makeUnique: C{bool}
        @param onlyScheduleAgentServer: if C{True} only return ATETNDEEs with SCHEDULE-AGENT=SERVER set
        @type onlyScheduleAgentServer: C{bool}
        @return: a list of tuples of (organizer value, recurrence-id)
        """

        # Extract appropriate sub-component if this is a VCALENDAR
        if self.name() == "VCALENDAR":
            result = ()
            for component in self.subcomponents(ignore=True):
                result += component.getAttendeesByInstance(makeUnique, onlyScheduleAgentServer)
            return result
        else:
            result = ()
            attendees = set()
            rid = self.getRecurrenceIDUTC()
            for attendee in tuple(self.getRecipientProperties()):

                if onlyScheduleAgentServer:
                    if attendee.hasParameter("SCHEDULE-AGENT"):
                        if attendee.parameterValue("SCHEDULE-AGENT") != "SERVER":
                            continue

                cuaddr = attendee.value()
                if makeUnique and cuaddr in attendees:
                    self.removeProperty(attendee)
                else:
                    result += ((cuaddr, rid),)
                    attendees.add(cuaddr)
            return result

    def getVoterProperty(self, match):
        """
        Get the voters matching a value.

        @param match: a C{list} of calendar user address strings to try and match.
        @return: the matching Voter property, or None
        """

        # Need to normalize http/https cu addresses
        test = set()
        for item in match:
            test.add(normalizeCUAddr(item))

        # Find the primary subcomponent
        for voter in self.properties("VOTER"):
            if normalizeCUAddr(voter.value()) in test:
                return voter

        return None

    def getAttendeeProperty(self, match):
        """
        Get the attendees matching a value. Works on either a VCALENDAR or on a component.

        @param match: a C{list} of calendar user address strings to try and match.
        @return: the matching Attendee property, or None
        """

        # Need to normalize http/https cu addresses
        test = set()
        for item in match:
            test.add(normalizeCUAddr(item))

        # Extract appropriate sub-component if this is a VCALENDAR
        if self.name() == "VCALENDAR":
            for component in self.subcomponents(ignore=True):
                attendee = component.getAttendeeProperty(match)
                if attendee is not None:
                    return attendee
        else:
            # Find the primary subcomponent
            for attendee in self.getRecipientProperties():
                if normalizeCUAddr(attendee.value()) in test:
                    return attendee

        return None

    def getAttendeeProperties(self, match):
        """
        Get all the attendees matching a value in each component. Works on a VCALENDAR component only.

        @param match: a C{list} of calendar user address strings to try and match.
        @return: the string value of the Organizer property, or None
        """

        assert self.name() == "VCALENDAR", "Not a calendar: {0!r}".format(self,)

        # Extract appropriate sub-component if this is a VCALENDAR
        results = []
        for component in self.subcomponents(ignore=True):
            attendee = component.getAttendeeProperty(match)
            if attendee:
                results.append(attendee)

        return results

    def getAllAttendeeProperties(self):
        """
        Yield all attendees as Property objects.  Works on either a VCALENDAR or
        on a component.
        @return: a generator yielding Property objects
        """

        # Extract appropriate sub-component if this is a VCALENDAR
        if self.name() == "VCALENDAR":
            for component in self.subcomponents(ignore=True):
                for attendee in component.getAllAttendeeProperties():
                    yield attendee
        else:
            # Find the primary subcomponent
            for attendee in self.getRecipientProperties():
                yield attendee

    def getAllUniqueAttendees(self, onlyScheduleAgentServer=True):
        attendeesByInstance = self.getAttendeesByInstance(True, onlyScheduleAgentServer=onlyScheduleAgentServer)
        attendees = set()
        for attendee, _ignore in attendeesByInstance:
            attendees.add(attendee)
        return attendees

    def getMaskUID(self):
        """
        Get the X-CALENDARSEREVR-MASK-UID value. Works on either a VCALENDAR or on a component.

        @return: the string value of the X-CALENDARSEREVR-MASK-UID property, or None
        """

        # Extract appropriate sub-component if this is a VCALENDAR
        if self.name() == "VCALENDAR":
            for component in self.subcomponents(ignore=True):
                return component.getMaskUID()
        else:
            try:
                # Find the primary subcomponent
                return self.propertyValue("X-CALENDARSERVER-MASK-UID")
            except InvalidICalendarDataError:
                pass

        return None

    def getExtendedFreeBusy(self):
        """
        Get the X-CALENDARSERVER-EXTENDED-FREEBUSY value. Works on either a VCALENDAR or on a component.

        @return: the string value of the X-CALENDARSERVER-EXTENDED-FREEBUSY property, or None
        """

        # Extract appropriate sub-component if this is a VCALENDAR
        if self.name() == "VCALENDAR":
            for component in self.subcomponents(ignore=True):
                return component.getExtendedFreeBusy()
        else:
            try:
                # Find the primary subcomponent
                return self.propertyValue("X-CALENDARSERVER-EXTENDED-FREEBUSY")
            except InvalidICalendarDataError:
                pass

        return None

    def setParameterToValueForPropertyWithValue(self, paramname, paramvalue, propname, propvalue):
        """
        Add or change the parameter to the specified value on the property having the specified value.
        If C{paramvalue} is L{None} remove the parameter.

        @param paramname: the parameter name
        @type paramname: C{str}
        @param paramvalue: the parameter value to set
        @type paramvalue: C{str}
        @param propname: the property name
        @type propname: C{str}
        @param propvalue: the property value to test
        @type propvalue: C{str} or C{None}
        """

        self.setParametersForPropertyWithValue(
            {paramname: paramvalue},
            propname,
            propvalue,
        )

    def setParametersForPropertyWithValue(self, params, propname, propvalue):
        """
        Add, change or remove a set of parameters to the specified value on the property
        having the specified value. Parameters are specified in a name:value L{dict}. If
        the value is L{None} the parameter will be removed.

        @param params: the parameter name/value pairs to set/remove
        @type params: C{dict}
        @param propname: the property name
        @type propname: C{str}
        @param propvalue: the property value to test
        @type propvalue: C{str} or C{None}
        """

        for component in self.subcomponents(ignore=True):
            for property in component.properties(propname):
                if propvalue is None or property.value() == propvalue:
                    for paramname, paramvalue in params.items():
                        if paramvalue is not None:
                            property.setParameter(paramname, paramvalue)
                        else:
                            property.removeParameter(paramname)

    def hasPropertyInAnyComponent(self, properties):
        """
        Test for the existence of one or more properties in any component.

        @param properties: property name(s) to test for
        @type properties: C{list}, C{tuple} or C{str}
        """

        if isinstance(properties, str):
            properties = (properties,)

        for property in properties:
            if self.hasProperty(property):
                return True

        for component in self.subcomponents():
            if component.hasPropertyInAnyComponent(properties):
                return True

        return False

    def getFirstPropertyInAnyComponent(self, properties):
        """
        Get the first of any set of properties in any component.

        @param properties: property name(s) to test for
        @type properties: C{list}, C{tuple} or C{str}
        """

        if isinstance(properties, str):
            properties = (properties,)

        for property in properties:
            props = tuple(self.properties(property))
            if props:
                return props[0]

        for component in self.subcomponents():
            prop = component.getFirstPropertyInAnyComponent(properties)
            if prop:
                return prop

        return None

    def getAllPropertiesInAnyComponent(self, properties, depth=2):
        """
        Get the all of any set of properties in any component down to a
        specified depth.

        @param properties: property name(s) to test for
        @type properties: C{list}, C{tuple} or C{str}
        @param depth: how deep to go in looking at sub-components:
            0: do not go into sub-components, 1: go into one level of sub-components,
            2: two levels (which is effectively all the levels supported in iCalendar)
        @type depth: int
        """

        results = []

        if isinstance(properties, str):
            properties = (properties,)

        for property in properties:
            props = tuple(self.properties(property))
            if props:
                results.extend(props)

        if depth > 0:
            for component in self.subcomponents():
                results.extend(component.getAllPropertiesInAnyComponent(properties, depth - 1))

        return results

    def hasPropertyValueInAllComponents(self, property):
        """
        Test for the existence of a property with a specific value in any sub-component.

        @param property: property to test for
        @type property: L{Property}
        """

        for component in self.subcomponents(ignore=True):
            found = component.getProperty(property.name())
            if not found or found.value() != property.value():
                return False

        return True

    def addPropertyToAllComponents(self, property):
        """
        Add a property to all top-level components except VTIMEZONE.

        @param property: the property to add
        @type property: L{Property}
        """

        for component in self.subcomponents(ignore=True):
            component.addProperty(property)

    def replacePropertyInAllComponents(self, property):
        """
        Replace a property in all components.
        @param property: the L{Property} to replace in this component.
        """

        for component in self.subcomponents(ignore=True):
            component.replaceProperty(property)

    def hasPropertyWithParameterMatch(self, propname, param_name, param_value, param_value_is_default=False):
        """
        See if property whose name, and parameter name, value match in any components.

        @param property: the L{Property} to replace in this component.
        @param param_name: the C{str} of parameter name to match.
        @param param_value: the C{str} of parameter value to match, if C{None} then just match on the
            presence of the parameter name.
        @param param_value_is_default: C{bool} to indicate whether absence of the named parameter
            also implies a match

        @return: C{True} if matching property found, C{False} if not
        @rtype: C{bool}
        """

        if self.name() == "VCALENDAR":
            for component in self.subcomponents(ignore=True):
                if component.hasPropertyWithParameterMatch(propname, param_name, param_value, param_value_is_default):
                    return True
        else:
            for oldprop in tuple(self.properties(propname)):
                pvalue = oldprop.parameterValue(param_name)
                if pvalue is None and param_value_is_default or pvalue == param_value or param_value is None:
                    return True

        return False

    def replaceAllPropertiesWithParameterMatch(self, property, param_name, param_value, param_value_is_default=False):
        """
        Replace a property whose name, and parameter name, value match in all components.

        @param property: the L{Property} to replace in this component.
        @param param_name: the C{str} of parameter name to match.
        @param param_value: the C{str} of parameter value to match.
        @param param_value_is_default: C{bool} to indicate whether absence of the named parameter
            also implies a match
        """

        if self.name() == "VCALENDAR":
            for component in self.subcomponents(ignore=True):
                component.replaceAllPropertiesWithParameterMatch(property, param_name, param_value, param_value_is_default)
        else:
            for oldprop in tuple(self.properties(property.name())):
                pvalue = oldprop.parameterValue(param_name)
                if pvalue is None and param_value_is_default or pvalue == param_value:
                    self.removeProperty(oldprop)
                    self.addProperty(property)

    def removeAllPropertiesWithParameterMatch(self, propname, param_name, param_value, param_value_is_default=False):
        """
        Remove all properties whose name, and parameter name, value match in all components.
        """

        if self.name() == "VCALENDAR":
            for component in self.subcomponents(ignore=True):
                component.removeAllPropertiesWithParameterMatch(propname, param_name, param_value, param_value_is_default)
        else:
            for oldprop in tuple(self.properties(propname)):
                pvalue = oldprop.parameterValue(param_name)
                if pvalue is None and param_value_is_default or pvalue == param_value:
                    self.removeProperty(oldprop)

    def transferProperties(self, from_calendar, properties):
        """
        Transfer specified properties from old calendar into all components
        of this calendar, synthesizing any for new overridden instances.

        @param from_calendar: the old calendar to copy from
        @type from_calendar: L{Component}
        @param properties: the property names to copy over
        @type properties: C{tuple} or C{list}
        """

        assert from_calendar.name() == "VCALENDAR", "Not a calendar: {0!r}".format(self,)

        if self.name() == "VCALENDAR":
            for component in self.subcomponents(ignore=True):
                component.transferProperties(from_calendar, properties)
        else:
            # Is there a matching component
            rid = self.getRecurrenceIDUTC()
            matched = from_calendar.overriddenComponent(rid)

            # If no match found, we are processing a new overridden instance so copy from the original master
            if not matched:
                matched = from_calendar.masterComponent()

            if matched:
                for propname in properties:
                    for prop in matched.properties(propname):
                        self.addProperty(prop)

    def attendeesView(self, attendees, onlyScheduleAgentServer=False):
        """
        Filter out any components that all attendees are not present in. Use EXDATEs
        on the master to account for changes.
        """

        assert self.name() == "VCALENDAR", "Not a calendar: {0!r}".format(self,)

        # Modify any components that reference the attendee, make note of the ones that don't
        remove_components = []
        master_component = None
        removed_master = False
        for component in self.subcomponents(ignore=True):
            found_all_attendees = True
            for attendee in attendees:
                foundAttendee = component.getAttendeeProperty((attendee,))
                if foundAttendee is None:
                    found_all_attendees = False
                    break
                if onlyScheduleAgentServer:
                    if foundAttendee.hasParameter("SCHEDULE-AGENT"):
                        if foundAttendee.parameterValue("SCHEDULE-AGENT") != "SERVER":
                            found_all_attendees = False
                            break
            if not found_all_attendees:
                remove_components.append(component)
            if component.getRecurrenceIDUTC() is None:
                master_component = component
                if not found_all_attendees:
                    removed_master = True

        # Now remove the unwanted components - but we may need to EXDATE the master
        exdates = []
        for component in remove_components:
            rid = component.getRecurrenceIDUTC()
            if rid is not None:
                exdates.append(rid)
            self.removeComponent(component)

        if not removed_master and master_component is not None:
            for exdate in exdates:
                master_component.addProperty(Property("EXDATE", [exdate, ]))

    def voterComponentForVoter(self, voter):
        """
        Find the VVOTER subcomponent with a VOTER property matching the specified attendee (voter).

        @param voter: the calendar user address of the attendee (voter) to match
        @type voter: L{str}
        """
        for voterComponent in tuple(self.subcomponents(ignore=True)):
            if voterComponent.name() == "VVOTER" and voterComponent.getVoterProperty((voter,)) is not None:
                return voterComponent
        else:
            return None

    def voteMap(self):
        """
        Get a dict mapping each VOTE component POLL-ITEM-ID to the VOTE component.
        """
        results = {}
        for component in self.subcomponents():
            if component.name() == "VOTE":
                poll_id = component.propertyValue("POLL-ITEM-ID")
                if poll_id is not None:
                    results[poll_id] = component
        return results

    def filterComponents(self, rids):

        # If master is in rids do nothing
        if not rids or None in rids:
            return True

        assert self.name() == "VCALENDAR", "Not a calendar: {0!r}".format(self,)

        # Remove components not in the list
        components = tuple(self.subcomponents(ignore=True))
        remaining = len(components)
        for component in components:
            rid = component.getRecurrenceIDUTC()
            if rid not in rids:
                self.removeComponent(component)
                remaining -= 1

        return remaining != 0

    def removeAllButOneAttendee(self, attendee):
        """
        Remove all ATTENDEE properties except for the one specified.
        """

        assert self.name() == "VCALENDAR", "Not a calendar: {0!r}".format(self,)

        for component in self.subcomponents(ignore=True):
            if component.name() == "VPOLL":
                for vvoter in tuple(self.subcomponents()):
                    if vvoter.name() == "VVOTER":
                        if vvoter.propertyValue(component.recipientPropertyName()).lower() != attendee.lower():
                            component.removeComponent(vvoter)
            else:
                for p in tuple(component.properties(component.recipientPropertyName())):
                    if p.value().lower() != attendee.lower():
                        component.removeProperty(p)

    def removeAllButTheseAttendees(self, attendees):
        """
        Remove all ATTENDEE properties except for the ones specified.
        """

        assert self.name() == "VCALENDAR", "Not a calendar: {0!r}".format(self,)

        attendees = set([attendee.lower() for attendee in attendees])

        for component in self.subcomponents(ignore=True):
            if component.name() == "VPOLL":
                for vvoter in tuple(self.subcomponents()):
                    if vvoter.name() == "VVOTER":
                        if vvoter.propertyValue(component.recipientPropertyName()).lower() not in attendees:
                            component.removeComponent(vvoter)
            else:
                for p in tuple(component.properties(component.recipientPropertyName())):
                    if p.value().lower() not in attendees:
                        component.removeProperty(p)

    def hasAlarm(self):
        """
        Test whether the component has a VALARM as an immediate sub-component.
        """
        assert self.name().upper() in ("VEVENT", "VTODO",), "Not a VEVENT or VTODO: {0!r}".format(self,)

        for component in self.subcomponents():
            if component.name().upper() == "VALARM":
                return True
        return False

    def addAlarms(self, alarm, ignoreActionNone=True):
        """
        Add an alarm to any VEVENT or VTODO subcomponents that do not already have any.

        @param alarm: the text for a VALARM component
        @type alarm: C{str}

        @param ignoreActionNone: whether or not to skip ACTION:NONE alarms
        @type ignoreActionNone: C{bool}

        @return: indicate whether a change was made
        @rtype: C{bool}
        """

        # Create a fake component for the alarm text
        caldata = """BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
BEGIN:VEVENT
UID:bogus
DTSTART:20110427T000000Z
DURATION:PT1H
DTSTAMP:20110427T000000Z
SUMMARY:bogus
{0}END:VEVENT
END:VCALENDAR
""".replace("\n", "\r\n").format(alarm,)

        try:
            calendar = Component.fromString(caldata)
            if calendar is None:
                return False
        except ValueError:
            return False

        try:
            valarm = tuple(tuple(calendar.subcomponents())[0].subcomponents())[0]
        except IndexError:
            return False

        # Need to add property to indicate this was added by the server
        valarm.addProperty(Property("X-APPLE-DEFAULT-ALARM", "TRUE"))

        # ACTION:NONE not added
        changed = False
        action = valarm.propertyValue("ACTION")
        if not ignoreActionNone or action and action.upper() != "NONE":
            for component in self.subcomponents():
                if component.name().upper() not in ("VEVENT", "VTODO",):
                    continue
                if component.hasAlarm():
                    continue
                component.addComponent(valarm.duplicate())
                changed = True

        return changed

    def removeAlarms(self):
        """
        Remove all Alarms components
        """

        if self.name() == "VCALENDAR":
            for component in self.subcomponents(ignore=True):
                component.removeAlarms()
        else:
            for component in tuple(self.subcomponents()):
                if component.name() == "VALARM":
                    self.removeComponent(component)

    def hasDuplicateAlarms(self, doFix=False):
        """
        Test and optionally remove alarms that have the same ACTION and TRIGGER values in the same component.
        """
        changed = False
        if self.name() in ("VCALENDAR", PERUSER_COMPONENT,):
            for component in self.subcomponents():
                if component.name() in ("VTIMEZONE",):
                    continue
                changed = component.hasDuplicateAlarms(doFix) or changed
        else:
            action_trigger = set()
            for component in tuple(self.subcomponents()):
                if component.name() == "VALARM":
                    item = (component.propertyValue("ACTION"), component.propertyValue("TRIGGER"),)
                    if item in action_trigger:
                        if doFix:
                            self.removeComponent(component)
                        changed = True
                    else:
                        action_trigger.add(item)
        return changed

    def filterProperties(self, remove=None, keep=None, do_subcomponents=True):
        """
        Remove all properties that do not match the provided set.
        """

        if do_subcomponents:
            for component in self.subcomponents():
                component.filterProperties(remove, keep, do_subcomponents=False)
        else:
            if self.ignored():
                return

            for p in tuple(self.properties()):
                if (keep and p.name() not in keep) or (remove and p.name() in remove):
                    self.removeProperty(p)

    def removeXComponents(self, keep_components=()):
        """
        Remove all X- components except the specified ones
        """

        for component in tuple(self.subcomponents()):
            if component.name().startswith("X-") and component.name() not in keep_components:
                self.removeComponent(component)

    def removeXProperties(self, keep_properties=(), keep_parameters={}, do_subcomponents=True):
        """
        Remove all X- properties except the specified ones
        """

        if do_subcomponents and self.name() == "VCALENDAR":
            for component in self.subcomponents():
                component.removeXProperties(keep_properties, keep_parameters, do_subcomponents=False)
        else:
            if self.ignored():
                return
            for p in tuple(self.properties()):
                xpname = p.name().startswith("X-")
                if xpname and p.name() not in keep_properties:
                    self.removeProperty(p)
                elif not xpname:
                    preserve = keep_parameters.get(p.name(), set())
                    preserve.update(keep_parameters.get("", set()))
                    for paramname in p.parameterNames():
                        if paramname.startswith("X-") and paramname not in preserve:
                            p.removeParameter(paramname)

    def removePropertyParameters(self, property, params):
        """
        Remove all specified property parameters
        """

        if self.name() == "VCALENDAR":
            for component in self.subcomponents(ignore=True):
                component.removePropertyParameters(property, params)
        else:
            props = self.properties(property)
            for prop in props:
                for param in params:
                    prop.removeParameter(param)
            if self.name() == "VPOLL":
                for component in self.subcomponents(ignore=True):
                    component.removePropertyParameters(property, params)

    def removePropertyParametersByValue(self, property, paramvalues):
        """
        Remove all specified property parameters
        """

        if self.name() == "VCALENDAR":
            for component in self.subcomponents(ignore=True):
                component.removePropertyParametersByValue(property, paramvalues)
        else:
            props = self.properties(property)
            for prop in props:
                for param, value in paramvalues:
                    prop.removeParameterValue(param, value)
            if self.name() == "VPOLL":
                for component in self.subcomponents(ignore=True):
                    component.removePropertyParametersByValue(property, paramvalues)

    def getITIPInfo(self):
        """
        Get property value details needed to synchronize iTIP components.

        @return: C{tuple} of (uid, seq, dtstamp, r-id) some of which may be C{None} if property does not exist
        """
        try:
            # Extract items from component
            uid = self.propertyValue("UID")
            seq = self.propertyValue("SEQUENCE", 0)
            dtstamp = self.propertyValue("DTSTAMP")
            rid = self.propertyValue("RECURRENCE-ID")

        except ValueError:
            return (None, None, None, None)

        return (uid, seq, dtstamp, rid)

    @staticmethod
    def compareComponentsForITIP(component1, component2, use_dtstamp=True):
        """
        Compare synchronization information for two components to see if they match according to iTIP.

        @param component1: first component to check.
        @type component1: L{Component}
        @param component2: second component to check.
        @type component2: L{Component}
        @param use_dtstamp: whether DTSTAMP is used in addition to SEQUENCE.
        @type component2: C{bool}

        @return: 0, 1, -1 as per compareSyncInfo.
        """
        info1 = (None,) + Component.getITIPInfo(component1)
        info2 = (None,) + Component.getITIPInfo(component2)
        return Component.compareITIPInfo(info1, info2, use_dtstamp)

    @staticmethod
    def compareITIPInfo(info1, info2, use_dtstamp=True):
        """
        Compare two synchronization information records.

        @param info1: a C{tuple} as returned by L{getSyncInfo}.
        @param info2: a C{tuple} as returned by L{getSyncInfo}.
        @return: 1 if info1 > info2, 0 if info1 == info2, -1 if info1 < info2
        """

        _ignore_name1, uid1, seq1, dtstamp1, _ignore_rid1 = info1
        _ignore_name2, uid2, seq2, dtstamp2, _ignore_rid2 = info2

        # UIDs MUST match
        assert uid1 == uid2

        # Look for sequence
        if seq1 > seq2:
            return 1
        if seq1 < seq2:
            return -1

        # Look for DTSTAMP
        if use_dtstamp:
            if (dtstamp1 is not None) and (dtstamp2 is not None):
                if dtstamp1 > dtstamp2:
                    return 1
                if dtstamp1 < dtstamp2:
                    return -1
            elif (dtstamp1 is not None) and (dtstamp2 is None):
                return 1
            elif (dtstamp1 is None) and (dtstamp2 is not None):
                return -1

        return 0

    def needsiTIPSequenceChange(self, oldcalendar):
        """
        Compare this calendar with the old one and indicate whether the current one has SEQUENCE
        that is always greater than the old.
        """

        for component in self.subcomponents(ignore=True):
            oldcomponent = oldcalendar.overriddenComponent(component.getRecurrenceIDUTC())
            if oldcomponent is None:
                oldcomponent = oldcalendar.masterComponent()
                if oldcomponent is None:
                    continue
            newseq = component.propertyValue("SEQUENCE", 0)
            oldseq = oldcomponent.propertyValue("SEQUENCE", 0)
            if newseq <= oldseq:
                return True

        return False

    def bumpiTIPInfo(self, oldcalendar=None, doSequence=False):
        """
        Change DTSTAMP and optionally SEQUENCE on all components.
        """

        if doSequence:

            def maxSequence(calendar):
                seqs = calendar.getAllPropertiesInAnyComponent("SEQUENCE", depth=1)
                return max(seqs, key=lambda x: x.value()).value() if seqs else 0

            # Determine value to bump to from old calendar (if exists) or self
            newseq = maxSequence(oldcalendar if oldcalendar is not None else self) + 1

            # Bump all components
            self.replacePropertyInAllComponents(Property("SEQUENCE", newseq))

        self.replacePropertyInAllComponents(Property("DTSTAMP", DateTime.getNowUTC()))

    def sequenceInSync(self, oldcalendar):
        """
        Make sure SEQUENCE does not decrease in any components.
        """

        def maxSequence(calendar):
            seqs = calendar.getAllPropertiesInAnyComponent("SEQUENCE", depth=1)
            return max(seqs, key=lambda x: x.value()).value() if seqs else 0

        def minSequence(calendar):
            seqs = calendar.getAllPropertiesInAnyComponent("SEQUENCE", depth=1)
            return min(seqs, key=lambda x: x.value()).value() if seqs else 0

        # Determine value to bump to from old calendar (if exists) or self
        oldseq = maxSequence(oldcalendar)
        currentseq = minSequence(self)

        # Sync all components
        if oldseq and currentseq < oldseq:
            self.replacePropertyInAllComponents(Property("SEQUENCE", oldseq))

    def normalizeAll(self):

        # Normalize all properties
        for prop in tuple(self.properties()):
            result = normalizeProps.get(prop.name())
            if result:
                default_value, default_params = result
            else:
                # Assume default VALUE is TEXT
                default_value = None
                default_params = {"VALUE": "TEXT"}

            # Remove any default parameters
            for name in prop.parameterNames():
                value = prop.parameterValue(name)
                if value == default_params.get(name):
                    prop.removeParameter(name)

            # If there are no parameters, remove the property if it has the default value
            if len(prop.parameterNames()) == 0:
                if default_value is not None and prop.value() == default_value:
                    self.removeProperty(prop)
                    continue

            # Otherwise look for value normalization
            normalize_function = normalizePropsValue.get(prop.name())
            if normalize_function:
                prop.setValue(normalize_function(prop.value()))

        # Do datetime/rrule normalization
        self.normalizeDateTimes()

        # Do to all sub-components too
        for component in self.subcomponents():
            component.normalizeAll()

    def normalizeDateTimes(self):
        """
        Normalize various datetime properties into UTC and handle DTEND/DURATION variants in such
        a way that we can compare objects with slight differences.

        Also normalize the RRULE value parts.

        Strictly speaking we should not need to do this as clients should not be messing with
        these properties - i.e. they should round trip them. Unfortunately some do...
        """

        # TODO: what about VJOURNAL and VTODO?
        if self.name() == "VEVENT":

            # Basic time properties
            dtstart = self.getProperty("DTSTART")
            dtend = self.getProperty("DTEND")
            duration = self.getProperty("DURATION")

            timeRange = Period(
                start=dtstart.value(),
                end=dtend.value() if dtend is not None else None,
                duration=duration.value() if duration is not None else None,
            )

            # Have to fake the TZID value here when we convert date-times to UTC
            # as we need to know what the original one was
            if dtstart.hasParameter("TZID"):
                dtstart.setParameter("_TZID", dtstart.parameterValue("TZID"))
                dtstart.removeParameter("TZID")
            dtstart.value().adjustToUTC()
            if dtend is not None:
                if dtend.hasParameter("TZID"):
                    dtend.setParameter("_TZID", dtend.parameterValue("TZID"))
                    dtend.removeParameter("TZID")
                dtend.value().adjustToUTC()
            elif duration is not None:
                self.removeProperty(duration)
                self.addProperty(Property("DTEND", timeRange.getEnd().duplicateAsUTC()))

            rdates = self.properties("RDATE")
            for rdate in rdates:
                if rdate.hasParameter("TZID"):
                    rdate.setParameter("_TZID", rdate.parameterValue("TZID"))
                    rdate.removeParameter("TZID")
                for value in rdate.value():
                    value.getValue().adjustToUTC()

            exdates = self.properties("EXDATE")
            for exdate in exdates:
                if exdate.hasParameter("TZID"):
                    exdate.setParameter("_TZID", exdate.parameterValue("TZID"))
                    exdate.removeParameter("TZID")
                for value in exdate.value():
                    value.getValue().adjustToUTC()

            rid = self.getProperty("RECURRENCE-ID")
            if rid is not None:
                rid.removeParameter("TZID")
                rid.setValue(rid.value().duplicateAsUTC())

            # Recurrence rules - we need to normalize the order of the value parts
#            for rrule in self._pycalendar.getRecurrenceSet().getRules():
#                indexedTokens = {}
#                indexedTokens.update([valuePart.split("=") for valuePart in rrule.value().split(";")])
#                sortedValue = ";".join(["%s=%s" % (key, value,) for key, value in sorted(indexedTokens.iteritems(), key=lambda x:x[0])])
#                rrule.setValue(sortedValue)

    def normalizePropertyValueLists(self, propname):
        """
        Convert properties that have a list of values into single properties, to make it easier
        to do comparisons between two ical objects.
        """

        if self.name() == "VCALENDAR":
            for component in self.subcomponents(ignore=True):
                component.normalizePropertyValueLists(propname)
        else:
            for prop in tuple(self.properties(propname)):
                if type(prop.value()) is list and len(prop.value()) > 1:
                    self.removeProperty(prop)
                    for value in prop.value():
                        self.addProperty(Property(propname, [value.getValue(), ]))

    def normalizeAttachments(self):
        """
        Remove any ATTACH properties that relate to a dropbox.
        """

        if self.name() == "VCALENDAR":
            for component in self.subcomponents(ignore=True):
                component.normalizeAttachments()
        else:
            dropboxPrefix = self.propertyValue("X-APPLE-DROPBOX")
            if dropboxPrefix is None:
                return
            for attachment in tuple(self.properties("ATTACH")):
                valueType = attachment.parameterValue("VALUE")
                if valueType in (None, "URI"):
                    dataValue = attachment.value()
                    if dataValue.find(dropboxPrefix) != -1:
                        self.removeProperty(attachment)

    @inlineCallbacks
    def normalizeCalendarUserAddresses(
        self, lookupFunction, recordFunction, toCanonical=True, toURN_UUID=False,
    ):
        """
        Do the ORGANIZER/ATTENDEE property normalization.

        @param lookupFunction: function returning full name, guid, CUAs for a given CUA
        @type lookupFunction: L{Function}

        @param recordFunction: function taking a CUA and returning a record
        @type recordFunction: L{Function}

        @param toCanonical: whether to convert to the canonical CUA form (True)
            or to the mailto: form (False)
        @type toCanonical: L{bool}

        @param toURN_UUID: whether to convert to urn:x-uid to urn:uuid: for
            compatibility with older servers
        @type toURN_UUID: L{bool}
        """

        # Keep a cache of records because events with lots of recurrence overrides can contain
        # the same attendee cu-address multiple times
        cache = {}

        for component in self.subcomponents(ignore=True):
            for prop in itertools.chain(
                component.properties("ORGANIZER"),
                component.properties("ATTENDEE"),
                component.properties("VOTER")
            ):

                # Check that we can lookup this calendar user address - if not
                # we cannot do anything with it
                cuaddr = normalizeCUAddr(prop.value())
                if cuaddr not in cache:
                    result = yield lookupFunction(cuaddr, recordFunction, config)
                    cache[cuaddr] = result

                name, uid, cutype, cuaddrs = cache[cuaddr]
                if uid is None:
                    continue

                # Get any EMAIL parameter
                oldemail = prop.parameterValue("EMAIL")
                if oldemail:
                    oldemail = "mailto:{0}".format(oldemail,)

                    if config.Scheduling.Options.FakeResourceLocationEmail:
                        if oldemail.endswith("@do_not_reply"):
                            oldemail = None

                # Get any CN parameter
                oldCN = prop.parameterValue("CN")

                if toCanonical:
                    # Always re-write value to urn:x-uid
                    prop.setValue("urn:x-uid:{uid}".format(uid=uid))

                # Look for urn:x-uid: -> urn:uuid: conversion
                elif toURN_UUID and cuaddr.startswith("urn:x-uid:"):
                    prop.setValue(cuaddr.replace("urn:x-uid:", "urn:uuid:"))

                # If it is already a non-x-uid address leave it be
                elif (cuaddr.startswith("urn:x-uid:") or cuaddr.startswith("urn:uuid:")):

                    if oldemail:
                        # Use the EMAIL parameter if it exists
                        newaddr = oldemail
                    else:
                        # Pick the first mailto,
                        # or failing that the first path one,
                        # or failing that the first http one,
                        # or failing that the first one
                        first_mailto = None
                        first_path = None
                        first_http = None
                        first = None
                        for addr in cuaddrs:
                            if addr.startswith("mailto:"):
                                first_mailto = addr
                                break
                            elif addr.startswith("/"):
                                if not first_path:
                                    first_path = addr
                            elif addr.startswith("http:"):
                                if not first_http:
                                    first_http = addr
                            elif not first:
                                first = addr

                        if first_mailto:
                            newaddr = first_mailto
                        elif first_path:
                            newaddr = first_path
                        elif first_http:
                            newaddr = first_http
                        elif first:
                            newaddr = first
                        else:
                            newaddr = None

                    # Make the change
                    if newaddr:
                        prop.setValue(newaddr)

                # Re-write the CN parameter
                if name:
                    if name != oldCN:
                        prop.setParameter("CN", name)
                else:
                    prop.removeParameter("CN")

                # Re-write the EMAIL if its value no longer matches
                if oldemail and oldemail not in cuaddrs or oldemail is None and toCanonical:
                    if cuaddr.startswith("mailto:") and cuaddr in cuaddrs:
                        email = cuaddr[7:]
                    else:
                        for addr in cuaddrs:
                            if addr.startswith("mailto:"):
                                email = addr[7:]
                                break
                        else:
                            email = None

                    if config.Scheduling.Options.FakeResourceLocationEmail:
                        if email and email.endswith("@do_not_reply"):
                            email = ""

                    if email:
                        prop.setParameter("EMAIL", email)
                    else:
                        prop.removeParameter("EMAIL")

                if cutype != prop.parameterValue("CUTYPE"):
                    if cutype and cutype != "INDIVIDUAL":
                        # For groups we need to change the CUTYPE exposed to clients when we have server-managed
                        # group attendee expansion because some clients seem to spontaneous remove CUTYPE=GROUP
                        # for no obvious reason when the event is changed. We can't have that happen for the
                        # server-managed groups so using a different CUTYPE seems to work around that.
                        if config.GroupAttendees.Enabled and cutype == "GROUP":
                            cutype = "X-SERVER-GROUP"
                        prop.setParameter("CUTYPE", cutype)
                    else:
                        prop.removeParameter("CUTYPE")

            # For VPOLL also do immediate children
            if component.name() == "VPOLL":
                yield component.normalizeCalendarUserAddresses(lookupFunction, recordFunction, toCanonical)

    def _reconcileGroupAttendee(self, groupCUA, memberAttendeeProps):
        """
        Make sure there are attendee properties for every member of the group, and no
        other attendee properties marked as a member of the group. Note that attendee
        properties already present with a MEMBER parameter are not given a MEMBER
        parameter if they are in the group. This ensures that manually added attendees
        are not automatically removed when they dissappear from a group.

        @param groupCUA: calendar user address of the group
        @type groupCUA: L{str}
        @param memberAttendeeProps: list of member properties
        @type memberAttendeeProps: L{tuple}
        """

        changed = False
        for component in self.subcomponents(ignore=True):
            oldAttendeeProps = tuple(component.properties("ATTENDEE"))
            oldAttendeeCUAs = set([attendeeProp.value() for attendeeProp in oldAttendeeProps])

            # add new member attendees
            memberCUAs = set()
            for newAttendeeProp in memberAttendeeProps:
                memberCUA = newAttendeeProp.value()
                if memberCUA not in oldAttendeeCUAs:
                    log.debug("Group reconciliation: Adding {m} ({g})", m=memberCUA, g=groupCUA)
                    component.addProperty(newAttendeeProp)
                    changed = True
                memberCUAs.add(memberCUA)

            # remove attendee or update MEMBER attribute for non-primary attendees in this group,
            for attendeeProp in oldAttendeeProps:
                if attendeeProp.hasParameter("MEMBER"):
                    memberValues = attendeeProp.parameterValues("MEMBER")
                    if groupCUA in tuple(memberValues):
                        if attendeeProp.value() not in memberCUAs:
                            memberValues.remove(groupCUA)
                            if len(memberValues) == 0:
                                component.removeProperty(attendeeProp)
                                log.debug("Group reconciliation: removing {a} ({g})", a=attendeeProp.value(), g=groupCUA)
                            changed = True
                    else:
                        if attendeeProp.value() in memberCUAs:
                            memberValues.append(groupCUA)
                            changed = True

        return changed

    def reconcileGroupAttendees(self, groupCUAToAttendeeMemberPropMap):
        """
        Reconcile the attendee properties in this L{Component}.

        @param groupCUAToAttendeeMemberPropMap: map of group to potential attendees
        @type groupCUAToAttendeeMemberPropMap: L{dict}
        """

        # Reconcile the member ship list of each group attendee, keeping track of which
        # groups are actually used
        changed = False
        allMemberCUAs = set()
        nonemptyGroupCUAs = set()
        for groupCUA, memberAttendeeProps in groupCUAToAttendeeMemberPropMap.iteritems():
            changed |= self._reconcileGroupAttendee(groupCUA, memberAttendeeProps)
            allMemberCUAs |= set([memberAttendeeProp.value() for memberAttendeeProp in memberAttendeeProps])
            if memberAttendeeProps:
                nonemptyGroupCUAs.add(groupCUA)

        # Remove attendee properties that have a MEMBER value that contains only groups no longer
        # used in this component
        for component in self.subcomponents(ignore=True):
            for attendeeProp in tuple(component.properties("ATTENDEE")):
                if attendeeProp.hasParameter("MEMBER"):
                    attendeeCUA = attendeeProp.value()
                    if attendeeCUA in allMemberCUAs:
                        # remove orphan member values
                        memberValues = attendeeProp.parameterValues("MEMBER")
                        for orphanGroupCUA in set(memberValues) - nonemptyGroupCUAs:
                            memberValues.remove(orphanGroupCUA)
                            if len(memberValues) == 0:
                                component.removeProperty(attendeeProp)
                            changed = True
                    else:
                        # remove orphaned member property
                        component.removeProperty(attendeeProp)
                        changed = True

        return changed

    def adjustedTransp(self):
        """
        Determine the TRANSP value for this component. Note that for all-day VEVENTs
        we are going to treat the default as TRANSPARENT and not OPAQUE
        """
        transp = self.propertyValue("TRANSP")
        if transp is None and self.name() == "VEVENT" and self.propertyValue("DTSTART").isDateOnly():
            return "TRANSPARENT"
        else:
            return "OPAQUE" if transp is None else transp

    def allPerUserUIDs(self):

        results = set()
        for component in self.subcomponents():
            if component.name() == PERUSER_COMPONENT:
                results.add(component.propertyValue(PERUSER_UID))
        return results

    def perUserData(self, rid):

        # We will create a cache of all user/rid/transparency/adjusted_start/adjusted_end values as we will likely
        # be calling this a lot
        if not hasattr(self, "_perUserData"):
            self._perUserData = {}

            # Do per-user data
            for component in self.subcomponents():
                if component.name() == PERUSER_COMPONENT:
                    uid = component.propertyValue(PERUSER_UID)
                    for subcomponent in component.subcomponents():
                        if subcomponent.name() == PERINSTANCE_COMPONENT:
                            instancerid = subcomponent.propertyValue("RECURRENCE-ID")
                            transp = subcomponent.propertyValue("TRANSP") == "TRANSPARENT"
                            adjusted_start = subcomponent.propertyValue("X-APPLE-TRAVEL-DURATION")
                            adjusted_end = subcomponent.propertyValue("X-APPLE-TRAVEL-RETURN-DURATION")
                            self._perUserData.setdefault(uid, {})[instancerid] = (transp, adjusted_start, adjusted_end,)
                elif not component.ignored():
                    instancerid = component.propertyValue("RECURRENCE-ID")
                    transp = component.propertyValue("TRANSP") == "TRANSPARENT"
                    self._perUserData.setdefault("", {})[instancerid] = (transp, None, None,)

        # Now lookup in cache
        results = []
        for uid, cachedRids in sorted(self._perUserData.items(), key=lambda x: x[0]):
            lookupRid = rid
            if lookupRid not in cachedRids:
                lookupRid = None
            if lookupRid in cachedRids:
                results.append((uid, cachedRids[lookupRid],))
            else:
                results.append((uid, (False, None, None,)))

        return tuple(results)

    def hasInstancesAfter(self, limit):
        """
        Determine whether an event exists completely prior to a given moment.

        @param limit: the moment to compare against.
        @type limit: L{DateTime}

        @return: a C{bool}, True if the event has any instances occurring after
        limit, False otherwise.
        """
        instanceList = self.expandTimeRanges(limit)

        if instanceList.limit is not None:
            # There are instances after the limit
            return True

        # All instances begin prior to limit, but now check their end times to
        # see if they extend beyond limit
        for instance in instanceList.instances.itervalues():
            if instance.end > limit:
                return True

        # Exists completely prior to limit
        return False

    def hasDuplicatePrivateComments(self, doFix=False):
        """
        Test and optionally remove "X-CALENDARSERVER-ATTENDEE-COMMENT" properties that have the same
        "X-CALENDARSERVER-ATTENDEE-REF" parameter values in the same component.

        @return: C{True} if there are duplicates that were not fixed.
        """
        if self.name() == "VCALENDAR":
            for component in self.subcomponents():
                if component.name() in ("VTIMEZONE",):
                    continue
                if component.hasDuplicatePrivateComments(doFix):
                    return True
        else:
            attendee_refs = set()
            for prop in tuple(self.properties(ATTENDEE_COMMENT)):
                ref = prop.parameterValue(ATTENDEE_COMMENT_REF)
                if ref in attendee_refs:
                    if doFix:
                        self.removeProperty(prop)
                    else:
                        return True
                attendee_refs.add(ref)
        return False

    def repairMissingDatestampsFromComments(self):
        """
        Some clients are leaving out the datestamp from comments; this method
        adds this parameter where it's missing (using a value of now in UTC)
        """
        if self.name() == "VCALENDAR":
            for component in self.subcomponents():
                if component.name() in ("VTIMEZONE",):
                    continue
                component.repairMissingDatestampsFromComments()

        else:
            for prop in tuple(self.properties(ATTENDEE_COMMENT)):
                if not prop.hasParameter(DTSTAMP_PARAM):
                    prop.setParameter(DTSTAMP_PARAM, DateTime.getNowUTC().getText())

    def maxAttachmentsPerInstance(self):
        if self.name() == "VCALENDAR":
            count = 0
            for component in self.subcomponents():
                if component.name() in ("VTIMEZONE",):
                    continue
                count = max(count, component.maxAttachmentsPerInstance())
            return count
        else:
            return len(tuple(self.properties("ATTACH")))

# #
# Timezones
# #


def tzexpand(tzdata, start, end):
    """
    Expand a timezone to get onset/utc-offset observance tuples within the specified
    time range.

    @param tzdata: the iCalendar data containing a VTIMEZONE.
    @type tzdata: C{str}
    @param start: date for the start of the expansion.
    @type start: C{date}
    @param end: date for the end of the expansion.
    @type end: C{date}

    @return: a C{list} of tuples of (C{datetime}, C{str})
    """

    icalobj = Component.fromString(tzdata)
    tzcomp = None
    for comp in icalobj.subcomponents():
        if comp.name() == "VTIMEZONE":
            tzcomp = comp
            break
    else:
        raise InvalidICalendarDataError("No VTIMEZONE component in {0}".format(tzdata,))

    tzexpanded = tzcomp._pycalendar.expandAll(start, end)

    results = []

    # Always need to ensure the start appears in the result
    start.setDateOnly(False)
    if tzexpanded:
        if start != tzexpanded[0][0]:
            results.append((str(start), UTCOffsetValue(tzexpanded[0][2]).getText(),))
    else:
        results.append((str(start), UTCOffsetValue(tzcomp._pycalendar.getTimezoneOffsetSeconds(start)).getText(),))
    for tzstart, _ignore_utctzstart, _ignore_tzoffsetfrom, tzoffsetto in tzexpanded:
        results.append((
            tzstart.getText(),
            UTCOffsetValue(tzoffsetto).getText(),
        ))

    return results


def tzexpandlocal(tzdata, start, end, utc_onset=False):
    """
    Expand a timezone to get onset(local)/utc-offset-from/utc-offset-to/name observance tuples within the specified
    time range.

    @param tzdata: the iCalendar data containing a VTIMEZONE.
    @type tzdata: L{Calendar}
    @param start: date for the start of the expansion.
    @type start: C{date}
    @param end: date for the end of the expansion.
    @type end: C{date}
    @param utc_onset: whether or not onset values are in UTC.
    @type utc_onset: C{bool}

    @return: a C{list} of tuples
    """

    icalobj = Component(None, pycalendar=tzdata)
    tzcomp = None
    for comp in icalobj.subcomponents():
        if comp.name() == "VTIMEZONE":
            tzcomp = comp
            break
    else:
        raise InvalidICalendarDataError("No VTIMEZONE component in {0}".format(tzdata,))

    tzexpanded = tzcomp._pycalendar.expandAll(start, end, with_name=True)

    results = []

    # Always need to ensure the start appears in the result
    start.setDateOnly(False)
    if tzexpanded:
        if start != tzexpanded[0][0]:
            results.append((
                start,
                tzexpanded[0][2],
                tzexpanded[0][2],
                tzexpanded[0][4],
            ))
    else:
        results.append((
            start,
            tzcomp._pycalendar.getTimezoneOffsetSeconds(start),
            tzcomp._pycalendar.getTimezoneOffsetSeconds(start),
            tzcomp._pycalendar.getTimezoneDescriptor(start),
        ))
    for tzstart, utctzstart, tzoffsetfrom, tzoffsetto, name in tzexpanded:
        results.append((
            utctzstart if utc_onset else tzstart,
            tzoffsetfrom,
            tzoffsetto,
            name,
        ))

    return results


# #
# Utilities
# #

@inlineCallbacks
def normalizeCUAddress(cuaddr, lookupFunction, recordFunction, toCanonical=True, toURN_UUID=False):
    # Check that we can lookup this calendar user address - if not
    # we cannot do anything with it
    _ignore_name, uid, _ignore_cuType, cuaddrs = (yield lookupFunction(normalizeCUAddr(cuaddr), recordFunction, config))

    if toCanonical:
        # Always re-write value to urn:x-uid
        if uid:
            returnValue("urn:x-uid:{0}".format(uid,))

    # Look for urn:x-uid: -> urn:uuid: conversion
    elif toURN_UUID and cuaddr.startswith("urn:x-uid:"):
        returnValue(cuaddr.replace("urn:x-uid:", "urn:uuid:"))

    # If it is already a non-x-uid address leave it be
    elif (cuaddr.startswith("urn:x-uid:") or cuaddr.startswith("urn:uuid:")):

        # Pick the first mailto,
        # or failing that the first path one,
        # or failing that the first http one,
        # or failing that the first one
        first_mailto = None
        first_path = None
        first_http = None
        first = None
        for addr in cuaddrs:
            if addr.startswith("mailto:"):
                first_mailto = addr
                break
            elif addr.startswith("/"):
                if not first_path:
                    first_path = addr
            elif addr.startswith("http:"):
                if not first_http:
                    first_http = addr
            elif not first:
                first = addr

        if first_mailto:
            newaddr = first_mailto
        elif first_path:
            newaddr = first_path
        elif first_http:
            newaddr = first_http
        elif first:
            newaddr = first
        else:
            newaddr = None

        # Make the change
        if newaddr:
            returnValue(newaddr)

    returnValue(cuaddr)


#
# This function is from "Python Cookbook, 2d Ed., by Alex Martelli, Anna
# Martelli Ravenscroft, and David Ascher (O'Reilly Media, 2005) 0-596-00797-3."
#
def merge(*iterables):
    """
    Merge sorted iterables into one sorted iterable.
    @param iterables: arguments are iterables which yield items in sorted order.
    @return: an iterable of all items generated by every iterable in
    C{iterables} in sorted order.
    """
    heap = []
    for iterable in iterables:
        iterator = iter(iterable)
        for value in iterator:
            heap.append((value, iterator))
            break
    heapq.heapify(heap)
    while heap:
        value, iterator = heap[0]
        yield value
        for value in iterator:
            heapq.heapreplace(heap, (value, iterator))
            break
        else:
            heapq.heappop(heap)


def normalize_iCalStr(icalstr, filter_params=("ATTENDEE;X-CALENDARSERVER-DTSTAMP", "ATTENDEE;X-CALENDARSERVER-RESET-PARTSTAT",)):
    """
    Normalize a string representation of ical data for easy test comparison.
    """

    icalstr = str(icalstr).replace("\r\n ", "")
    icalstr = icalstr.replace("\n ", "")
    lines = [line for line in icalstr.splitlines() if not line.startswith("DTSTAMP")]
    for param in filter_params:
        propname, paramname = param.split(";")
        for ctr, line in enumerate(lines[:]):
            if line.startswith(propname + ";"):
                pos = line.find(";{}=".format(paramname))
                if pos != -1:
                    next_segment = line[pos + len(paramname) + 2:]
                    end_pos = next_segment.find(";")
                    if end_pos == -1:
                        end_pos = next_segment.find(":")
                    lines[ctr] = line[:pos] + line[pos + len(paramname) + 2 + end_pos:]
    icalstr = "\r\n".join(lines)
    return icalstr + "\r\n"


def diff_iCalStrs(icalstr1, icalstr2):

    icalstr1 = normalize_iCalStr(icalstr1).splitlines()
    icalstr2 = normalize_iCalStr(icalstr2).splitlines()
    return "\n".join(unified_diff(icalstr1, icalstr2))
