##
# Copyright (c) 2009-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.
##

from twistedcaldav.datafilters.filter import CalendarFilter
from twistedcaldav.ical import Component, Property, PERUSER_COMPONENT, \
    PERUSER_UID, PERINSTANCE_COMPONENT

__all__ = [
    "PerUserDataFilter",
]

"""
Object model for calendar data is as follows:

VCALENDAR
  VTIMEZONE*
  VEVENT* / VTODO* / VJOURNAL*
  BEGIN:X-CALENDARSERVER-PERUSER*
    X-CALENDARSERVER-PERUSER-UID
    UID
    BEGIN:X-CALENDARSERVER-PERINSTANCE
      RECURRENCE-ID?
      TRANSP?
      VALARM*

So we will store per user data inside the top-level component (alongside VEVENT, VTODO etc). That new component will
contain properties to identify the user and the UID of the VEVENT, VTODO it affects. It will contain sub-components
for each instance overridden by the per-user data. These per-user overridden components may not correspond to an
actual overridden component. In that situation the server has to re-construct the per-user data appropriately:

e.g.,

1. VEVENT contains an overridden instance, but X-CALENDARSERVER-PERUSER does not - server uses the must instance
X-CALENDARSERVER-PERUSER data (if any) for the overridden instance.

2. VEVENT does not contain an overridden instance, but X-CALENDARSERVER-PERUSER does - server synthesizes an
overridden instance to match the X-CALENDARSERVER-PERUSER one.

3. VEVENT contains overridden instance and X-CALENDARSERVER-PERUSER does - server merges X-CALENDARSERVER-PERUSER
data into overridden instance.

"""


class PerUserDataFilter(CalendarFilter):
    """
    Filter per-user data
    """

    # Regular properties that need to be treated as per-user
    PERUSER_PROPERTIES = ("TRANSP",)

    # Regular components that need to be treated as per-user
    PERUSER_SUBCOMPONENTS = ("VALARM",)

    # X- properties that are ignored - by default all X- properties are treated as per-user except for the
    # ones listed here
    IGNORE_X_PROPERTIES = [Component.HIDDEN_INSTANCE_PROPERTY]

    def __init__(self, uid):
        """

        @param uid: unique identifier of the user for whom the data is being filtered
        @type uid: C{str}
        """

        self.uid = uid

    def filter(self, ical):
        """
        Filter the supplied iCalendar object using the request information.
        Assume that the object is a CalDAV calendar resource.

        @param ical: iCalendar object - this will be modified and returned
        @type ical: L{Component} or C{str}

        @return: L{Component} for the filtered calendar data
        """

        # Make sure input is valid
        ical = self.validCalendar(ical)

        # Look for matching per-user sub-component, removing all the others
        peruser_component = None
        for component in tuple(ical.subcomponents()):
            if component.name() == PERUSER_COMPONENT:

                # Check user id - remove if not matches
                if component.propertyValue(PERUSER_UID) != self.uid:
                    ical.removeComponent(component)
                elif peruser_component is None:
                    peruser_component = component
                    ical.removeComponent(component)
                else:
                    raise AssertionError("Can't have two X-CALENDARSERVER-PERUSER components for the same user")

        # Now transfer any components over
        if peruser_component:
            self._filterBack(ical, peruser_component)
        else:
            self._defaultFilter(ical)

        return ical

    def _filterBack(self, ical, peruser):
        """
        Merge the per-user data back into the main calendar data.

        @param ical: main calendar data to merge into
        @type ical: L{Component}
        @param peruser: the per-user data to merge in
        @type peruser: L{Component}
        """

        # Iterate over each instance in the per-user data and build mapping
        peruser_recurrence_map = {}
        for subcomponent in peruser.subcomponents():
            if subcomponent.name() != PERINSTANCE_COMPONENT:
                raise AssertionError("Wrong sub-component '%s' in a X-CALENDARSERVER-PERUSER component" % (subcomponent.name(),))
            peruser_recurrence_map[subcomponent.getRecurrenceIDUTC()] = subcomponent

        ical_recurrence_set = set(ical.getComponentInstances())
        peruser_recurrence_set = set(peruser_recurrence_map.keys())

        # Set operations to find union and differences
        union_set = ical_recurrence_set.intersection(peruser_recurrence_set)
        ical_only_set = ical_recurrence_set.difference(peruser_recurrence_set)
        peruser_only_set = peruser_recurrence_set.difference(ical_recurrence_set)

        # For ones in per-user data but no main data, we synthesize an instance and copy over per-user data
        # NB We have to do this before we do any merge that may change the master
        if ical.masterComponent() is not None:
            for rid in peruser_only_set:
                ical_component = ical.deriveInstance(rid)
                if ical_component is None:
                    continue
                peruser_component = peruser_recurrence_map[rid]
                self._filterBackComponent(ical_component, peruser_component)
                ical.addComponent(ical_component)
        elif peruser_only_set:
            # We used to error out here, but instead we should silently ignore this error and keep going
            pass

        # Process the unions by merging in per-user data
        for rid in union_set:
            ical_component = ical.overriddenComponent(rid)
            peruser_component = peruser_recurrence_map[rid]
            self._filterBackComponent(ical_component, peruser_component)

        # For ones in main data but no per-user data, we try and copy over the master per-user data
        if ical_only_set:
            peruser_master = peruser_recurrence_map.get(None)
            if peruser_master:
                for rid in ical_only_set:
                    ical_component = ical.overriddenComponent(rid)
                    self._filterBackComponent(ical_component, peruser_master)

    def _filterBackComponent(self, ical, peruser):
        """
        Copy all properties and sub-components from per-user data into the main component

        @param ical: main calendar component data to merge into
        @type ical: L{Component}
        @param peruser: the per-user data to merge in
        @type peruser: L{Component}
        """

        # Each sub-component
        for subcomponent in peruser.subcomponents():
            ical.addComponent(subcomponent)

        # Each property except RECURRENCE-ID
        for property in peruser.properties():
            if property.name() == "RECURRENCE-ID":
                continue
            ical.addProperty(property)

    def _defaultFilter(self, ical):
        """
        There is no per-user component. Instead apply default properties to the data for this user.

        @param ical: the iCalendar object to process
        @type ical: L{Component}
        """

        # TRANSP property behavior: if the event is all-day add TRANSP:TRANSPARENT, otherwise do nothing
        comp = ical.mainComponent()
        if comp.name() == "VEVENT" and comp.propertyValue("DTSTART").isDateOnly() and not comp.hasProperty("TRANSP"):
            ical.addPropertyToAllComponents(Property("TRANSP", "TRANSPARENT"))

    def merge(self, icalnew, icalold):
        """
        Merge the new data with the old taking per-user information into account.

        @param icalnew: new calendar data
        @type icalnew: L{Component} or C{str}
        @param icalold: existing calendar data
        @type icalold: L{Component} or C{str}

        @return: L{Component} for the merged calendar data
        """

        # Make sure input is valid
        icalnew = self.validCalendar(icalnew)

        # There cannot be any X-CALENDARSERVER-PERUSER components in the new data
        for component in tuple(icalnew.subcomponents()):
            if component.name() == PERUSER_COMPONENT:
                raise ValueError("Cannot merge calendar data with X-CALENDARSERVER-PERUSER components in it")

        # First split the new data into common and per-user pieces
        self._splitPerUserData(icalnew)
        if icalold is None:
            return icalnew

        # Make sure input is valid
        icalold = self.validCalendar(icalold)

        self._mergeRepresentations(icalnew, icalold)
        return icalnew

    def _splitPerUserData(self, ical):
        """
        Split the per-user data out of the "normal" iCalendar components into separate per-user
        components. Along the way keep the iCalendar representation in a "minimal" state by eliminating
        any components that are the same as the master derived component.

        @param ical: calendar data to process
        @type ical: L{Component}
        """

        def init_peruser_component():
            peruser = Component(PERUSER_COMPONENT)
            peruser.addProperty(Property("UID", ical.resourceUID()))
            peruser.addProperty(Property(PERUSER_UID, self.uid))
            return peruser

        components = tuple(ical.subcomponents())
        peruser_component = init_peruser_component() if self.uid else None
        perinstance_components = {}

        for component in components:
            if component.name() == "VTIMEZONE":
                continue
            rid = component.propertyValue("RECURRENCE-ID")
            rid = rid.duplicate() if rid is not None else None

            perinstance_component = Component(PERINSTANCE_COMPONENT) if self.uid else None
            perinstance_id_different = False

            # Special case certain default property values
            self._defaultMerge(component)

            # Transfer per-user properties from main component to per-instance component
            for property in tuple(component.properties()):
                if property.name() in PerUserDataFilter.PERUSER_PROPERTIES or property.name().startswith("X-") and property.name() not in PerUserDataFilter.IGNORE_X_PROPERTIES:
                    if self.uid:
                        perinstance_component.addProperty(property)
                    component.removeProperty(property)
                    perinstance_id_different = True

            # Transfer per-user components from main component to per-instance component
            for subcomponent in tuple(component.subcomponents()):
                if subcomponent.name() in PerUserDataFilter.PERUSER_SUBCOMPONENTS or subcomponent.name().startswith("X-"):
                    if self.uid:
                        perinstance_component.addComponent(subcomponent)
                    component.removeComponent(subcomponent)
                    perinstance_id_different = True

            if perinstance_id_different and perinstance_component:
                perinstance_components[rid] = perinstance_component

        if self.uid:
            # Add unique per-instance components into the per-user component
            peruser_component_different = False
            master_perinstance = perinstance_components.get(None)
            if master_perinstance:
                peruser_component.addComponent(master_perinstance)
                peruser_component_different = True
            for rid, perinstance in sorted(perinstance_components.iteritems(), key=lambda x: x[0]):
                if rid is None:
                    continue
                if master_perinstance is None or perinstance != master_perinstance:
                    perinstance.addProperty(Property("RECURRENCE-ID", rid))
                    peruser_component.addComponent(perinstance)
                    peruser_component_different = True

            if peruser_component_different:
                ical.addComponent(peruser_component)

            self._compactInstances(ical)

    def _compactInstances(self, ical):
        """
        Remove recurrences instances that are the same as their master-derived counterparts. This gives the most
        compact representation of the calendar data.

        @param ical: calendar data to process
        @type ical: L{Component}
        """

        # Must have a master component in order to do this
        master = ical.masterComponent()
        if master is None:
            return

        masterDerived = ical.masterDerived()

        for subcomponent in tuple(ical.subcomponents()):
            if subcomponent.name() == "VTIMEZONE" or subcomponent.name().startswith("X-"):
                continue
            rid = subcomponent.getRecurrenceIDUTC()
            if rid is None:
                continue
            derived = ical.deriveInstance(rid, newcomp=masterDerived)
            if derived is not None and derived == subcomponent:
                ical.removeComponent(subcomponent)

    def _defaultMerge(self, component):
        """
        Handle default property values during a merge.

        @param component: the iCalendar component to process
        @type component: L{Component}
        """

        # Special handling for TRANSP: if it is not present on an all-day event,
        # add TRANSP:OPAQUE so that the default is explicitly included, since otherwise
        # L_defaultFilter) will add TRANSP:TRANSPARENT
        if component.name() == "VEVENT" and component.propertyValue("DTSTART").isDateOnly() and not component.hasProperty("TRANSP"):
            component.addProperty(Property("TRANSP", "OPAQUE"))

    def _mergeRepresentations(self, icalnew, icalold):

        # Test for simple case first
        if icalnew.isRecurring() and icalold.isRecurring():
            # Test each instance from old data to see whether it is still valid in the new one
            self._complexMerge(icalnew, icalold)
        else:
            self._simpleMerge(icalnew, icalold)

    def _simpleMerge(self, icalnew, icalold):

        # Take all per-user components from old and add to new, except for our user
        new_recur = icalnew.isRecurring()
        old_recur = icalold.isRecurring()
        new_recur_has_no_master = new_recur and (icalnew.masterComponent() is None)
        for component in icalold.subcomponents():
            if component.name() == PERUSER_COMPONENT:
                if component.propertyValue(PERUSER_UID) != self.uid and not new_recur_has_no_master:
                    newcomponent = component.duplicate()

                    # Only transfer the master components from the old data to the new when the old
                    # was recurring and the new is not recurring
                    if not new_recur and old_recur:
                        for subcomponent in tuple(newcomponent.subcomponents()):
                            if subcomponent.getRecurrenceIDUTC() is not None:
                                newcomponent.removeComponent(subcomponent)

                    if len(tuple(newcomponent.subcomponents())):
                        icalnew.addComponent(newcomponent)

    def _complexMerge(self, icalnew, icalold):

        # Take all per-user components from old and add to new, except for our user
        for component in icalold.subcomponents():
            if component.name() == PERUSER_COMPONENT:
                if component.propertyValue(PERUSER_UID) != self.uid:
                    newcomponent = component.duplicate()

                    # See which of the instances are still valid
                    old_rids = dict([(subcomponent.getRecurrenceIDUTC(), subcomponent,) for subcomponent in newcomponent.subcomponents()])
                    valid_rids = icalnew.validInstances(old_rids.keys())
                    for old_rid, subcomponent in old_rids.iteritems():
                        if old_rid not in valid_rids:
                            newcomponent.removeComponent(subcomponent)

                    if len(tuple(newcomponent.subcomponents())):
                        icalnew.addComponent(newcomponent)
