##################################################################
#
# (C) Copyright 2006-2007 ObjectRealms, LLC
# All Rights Reserved
#
# This file is part of iterate.
#
# iterate is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# iterate is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with CMFDeployment; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
##################################################################

"""
$Id: copier.py 1824 2007-02-08 17:59:41Z hazmat $
"""

from zope import interface, component
from zope.annotation.interfaces import IAnnotations
from zope.event import notify
from zope.lifecycleevent import ObjectMovedEvent

from Acquisition import aq_base, aq_parent, aq_inner
from ZODB.PersistentMapping import PersistentMapping

from Products.Archetypes.Referenceable import Referenceable
from Products.CMFCore.utils import getToolByName
from Products.CMFCore import interfaces as cmf_ifaces
from Products.DCWorkflow.DCWorkflow import DCWorkflowDefinition

import interfaces
from relation import WorkingCopyRelation
from interfaces import CheckinException


class ContentCopier(object):

    interface.implements(interfaces.IObjectCopier)
    component.adapts(interfaces.IIterateAware)

    def __init__(self, context):
        self.context = context

    def copyTo(self, container):
        wc = self._copyBaseline(container)
        wc_ref = wc.addReference(
            self.context,
            relationship=WorkingCopyRelation.relationship,
            referenceClass=WorkingCopyRelation)
        self._handleReferences(self.context, wc, "checkout", wc_ref)
        return wc, wc_ref

    def merge(self):
        baseline = self._getBaseline()

        # delete the working copy reference to the baseline
        wc_ref = self._deleteWorkingCopyRelation()

        # reassemble references on the new baseline
        self._handleReferences(baseline, self.context, "checkin", wc_ref)

        # move the working copy to the baseline container, deleting
        # the baseline
        new_baseline = self._replaceBaseline(baseline)

        # patch the working copy with baseline info not preserved
        # during checkout
        self._reassembleWorkingCopy(new_baseline, baseline)

        return new_baseline

    def _getBaseline(self):
        # follow the working copy's reference back to the baseline
        refs = self.context.getRefs(WorkingCopyRelation.relationship)

        if not len(refs) == 1:
            raise CheckinException("Baseline count mismatch")

        if not refs or refs[0] is None:
            raise CheckinException("Baseline has disappeared")

        baseline = refs[0]
        return baseline

    def _replaceBaseline(self, baseline):
        # move the working copy object to the baseline, returns the
        # new baseline
        baseline_id = baseline.getId()

        # delete the baseline from the folder to make room for the
        # committed working copy
        baseline_container = aq_parent(aq_inner(baseline))
        # Check if we are a default_page, because this property of the
        # container might get lost.
        is_default_page = (
            baseline_container.getProperty('default_page', '') == baseline_id)
        baseline_pos = baseline_container.getObjectPosition(baseline_id)
        baseline_container._delOb(baseline_id)

        # uninedxing the deleted baseline object from portal_catalog
        portal_catalog = getToolByName(self.context, 'portal_catalog')
        portal_catalog.unindexObject(baseline)

        # delete the working copy from the its container
        wc_container = aq_parent(aq_inner(self.context))

        # trick out the at machinery to not delete references
        self.context._v_cp_refs = 1
        self.context._v_is_cp = 0

        wc_id = self.context.getId()
        wc_container.manage_delObjects([wc_id])

        # move the working copy back to the baseline container
        working_copy = aq_base(self.context)
        working_copy.setId(baseline_id)
        baseline_container._setOb(baseline_id, working_copy)
        baseline_container.moveObjectToPosition(baseline_id, baseline_pos)

        new_baseline = baseline_container._getOb(baseline_id)
        if is_default_page:
            # Restore default_page to container.  Note that the property might
            # have been removed by an event handler in the mean time.
            if baseline_container.hasProperty('default_page'):
                baseline_container._updateProperty('default_page', baseline_id)
            else:
                baseline_container._setProperty('default_page', baseline_id)

        # reregister our references with the reference machinery after moving
        Referenceable.manage_afterAdd(new_baseline, new_baseline,
                                      baseline_container)

        notify(ObjectMovedEvent(new_baseline, wc_container,
                                wc_id, baseline_container, baseline_id))

        return new_baseline

    def _reassembleWorkingCopy(self, new_baseline, baseline):
        # reattach the source's workflow history, try avoid a dangling ref
        try:
            new_baseline.workflow_history = PersistentMapping(
                baseline.workflow_history.items())
        except AttributeError:
            # No workflow apparently.  Oh well.
            pass

        # reset wf state security directly
        workflow_tool = getToolByName(self.context, 'portal_workflow')
        wfs = workflow_tool.getWorkflowsFor(self.context)
        for wf in wfs:
            if not isinstance(wf, DCWorkflowDefinition):
                continue
            wf.updateRoleMappingsFor(new_baseline)

        # Reattach the source's uid, this will update wc refs to point
        # back to the new baseline.  This may introduce duplicate
        # references, so we check that and fix them.
        self._recursivelyReattachUIDs(baseline, new_baseline)

        # reattach the source's history id, to get the previous
        # version ancestry
        histid_handler = getToolByName(self.context, 'portal_historyidhandler')
        huid = histid_handler.getUid(baseline)
        histid_handler.setUid(new_baseline, huid, check_uniqueness=False)

        return new_baseline

    def _recursivelyReattachUIDs(self, baseline, new_baseline):
        original_refs = len(new_baseline.getRefs())
        original_back_refs = len(new_baseline.getBRefs())
        new_baseline._setUID(baseline.UID())
        new_refs = len(new_baseline.getRefs())
        new_back_refs = len(new_baseline.getBRefs())
        if original_refs != new_refs:
            self._removeDuplicateReferences(new_baseline, backrefs=False)
        if original_back_refs != new_back_refs:
            self._removeDuplicateReferences(new_baseline, backrefs=True)

        if cmf_ifaces.IFolderish.providedBy(baseline):
            new_ids = new_baseline.contentIds()
            for child in baseline.contentValues():
                if child.getId() in new_ids:
                    self._recursivelyReattachUIDs(
                        child, new_baseline[child.getId()])

    def _removeDuplicateReferences(self, item, backrefs=False):
        # Remove duplicate (back) references from this item.
        reference_tool = getToolByName(self.context, 'reference_catalog')
        if backrefs:
            ref_func = reference_tool.getBackReferences
        else:
            ref_func = reference_tool.getReferences
        try:
            # Plone 4.1 or later
            brains = ref_func(item, objects=False)
        except TypeError:
            # Plone 4.0 or earlier.  Nothing to fix here
            return
        for brain in brains:
            if brain.getObject() is None:
                reference_tool.uncatalog_object(brain.getPath())

    def _deleteWorkingCopyRelation(self):
        # delete the wc reference keeping a reference to it for its annotations
        refs = self.context.getReferenceImpl(WorkingCopyRelation.relationship)
        wc_ref = refs[0]
        self.context.deleteReferences(WorkingCopyRelation.relationship)
        return wc_ref

    #################################
    # Checkout Support Methods

    def _copyBaseline(self, container):
        # copy the context from source to the target container
        source_container = aq_parent(aq_inner(self.context))
        clipboard = source_container.manage_copyObjects([self.context.getId()])
        result = container.manage_pasteObjects(clipboard)

        # get a reference to the working copy
        target_id = result[0]['new_id']
        target = container._getOb(target_id)
        return target

    def _handleReferences(self, baseline, wc, mode, wc_ref):

        annotations = IAnnotations(wc_ref)

        baseline_adapter = interfaces.ICheckinCheckoutReference(baseline)

        # handle forward references
        for relationship in baseline.getRelationships():
            # look for a named relation adapter first
            adapter = component.queryAdapter(
                baseline, interfaces.ICheckinCheckoutReference, relationship)

            if adapter is None:  # default
                adapter = baseline_adapter

            references = baseline.getReferenceImpl(relationship)

            mode_method = getattr(adapter, mode)
            mode_method(baseline, wc, references, annotations)

        mode = mode + "BackReferences"

        # handle backward references
        for relationship in baseline.getBRelationships():
            adapter = component.queryAdapter(
                baseline, interfaces.ICheckinCheckoutReference, relationship)
            if adapter is None:
                adapter = baseline_adapter

            references = baseline.getBackReferenceImpl(relationship)

            mode_method = getattr(adapter, mode)
            mode_method(baseline, wc, references, annotations)
