/*
 * Copyright (C) 2009-2015 Alfresco Software Limited.
 *
 * This file is part of Alfresco
 *
 * Alfresco is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * Alfresco 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 Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with Alfresco. If not, see .
 */
package org.alfresco.repo.transfer;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import org.alfresco.repo.transfer.CorrespondingNodeResolver.ResolvedParentChildPair;
import org.alfresco.repo.transfer.manifest.TransferManifestDeletedNode;
import org.alfresco.repo.transfer.manifest.TransferManifestHeader;
import org.alfresco.repo.transfer.manifest.TransferManifestNormalNode;
import org.alfresco.service.cmr.repository.ChildAssociationRef;
import org.alfresco.service.cmr.repository.NodeRef;
import org.alfresco.service.cmr.repository.NodeService;
import org.alfresco.service.cmr.repository.Path;
import org.alfresco.service.cmr.transfer.TransferReceiver;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
/**
 * @author mrogers
 * 
 * The tertiary manifest processor performs a third parse of the snapshot file.
 * 
 * For a complete transfer it is responsible for deleting any replicated nodes 
 * which exist in the target repository that do not exist in the source repository.
 * 
 * If the transfer is not "sync" then this processor does nothing.
 */
public class RepoTertiaryManifestProcessorImpl extends AbstractManifestProcessorBase
{
    private NodeService nodeService;
    private AlienProcessor alienProcessor;
    CorrespondingNodeResolver nodeResolver;
    
    private static final Log log = LogFactory.getLog(RepoTertiaryManifestProcessorImpl.class);
    /**
     *  Is this a "sync" transfer.  If not then does nothing.
     */
    boolean isSync = false;
    String manifestRepositoryId;
    /**
     * @param receiver TransferReceiver
     * @param transferId String
     */
    public RepoTertiaryManifestProcessorImpl(TransferReceiver receiver, String transferId)
    {
        super(receiver, transferId);
    }
    protected void endManifest()
    {
        //NOOP
    }
    
    protected void processNode(TransferManifestDeletedNode node)
    {
        //NOOP
    }
    protected void processNode(TransferManifestNormalNode node)
    {
        
        if (log.isDebugEnabled())
        {
            log.debug("Processing node with incoming noderef of " + node.getNodeRef());
        }
        logComment("Tertiary Processing incoming node: " + node.getNodeRef() + " --  Source path = " + node.getParentPath() + "/" + node.getPrimaryParentAssoc().getQName());
        /**
         * This processor only does processes sync requests.
         */
        if(isSync)
        {  
            ChildAssociationRef primaryParentAssoc = node.getPrimaryParentAssoc();
        
            CorrespondingNodeResolver.ResolvedParentChildPair resolvedNodes = nodeResolver.resolveCorrespondingNode(node
                .getNodeRef(), primaryParentAssoc, node.getParentPath());    
        
            NodeRef nodeRef = resolvedNodes.resolvedChild;
                               
            if(nodeService.exists(nodeRef))
            {
                log.debug("destination node exists - check the children");
                
                //TODO Use more efficient query here.
                List expectedChildren = node.getChildAssocs();
                if (log.isDebugEnabled())
                {
                    log.debug("Checking node in TERTIARY_MANIFEST_PROCESSOR");
                }
                if ((null != resolvedNodes.resolvedParent) && nodeService.exists(resolvedNodes.resolvedParent))
                {
                    if (log.isTraceEnabled())
                    {
                        logInvasionHierarchy(resolvedNodes.resolvedParent, nodeRef, nodeService, log);
                    }
                }
                else
                {
                    List parentAssocs = nodeService.getParentAssocs(nodeRef);
                    if (log.isTraceEnabled())
                    {
                        logInvasionHierarchy(parentAssocs.iterator().next().getParentRef(), nodeRef, nodeService, log);
                    }
                }
                List expectedChildNodeRefs = new ArrayList();
                Set expectedChildNodePaths = new HashSet();
                if (log.isDebugEnabled())
                {
                    log.debug("Expected children:");
                }
                for(ChildAssociationRef ref : expectedChildren)
                {
                    if(log.isDebugEnabled())
                    {
                        log.debug("Expecting child node " + ref);
                    }
                    NodeRef childRef = null;
                    if (nodeService.exists(ref.getChildRef()))
                    {
                        childRef = ref.getChildRef();
                        if (log.isTraceEnabled())
                        {
                            logInvasionHierarchy(nodeRef, ref.getChildRef(), nodeService, log);
                        }
                    }
                    else
                    {
                        Path parentPath = node.getParentPath();
                        parentPath = parentPath.subPath(0, (parentPath.size() - 1));
                        parentPath.append(new Path.ChildAssocElement(ref));
                        ResolvedParentChildPair resolvedChild = nodeResolver.resolveCorrespondingNode(ref.getChildRef(), ref, parentPath);
                        if (null != resolvedChild.resolvedChild)
                        {
                            childRef = resolvedChild.resolvedChild;
                            if (log.isDebugEnabled())
                            {
                                log.debug("The node has been RESOLVED!");
                            }
                            if (log.isTraceEnabled())
                            {
                                logInvasionHierarchy(resolvedChild.resolvedParent, resolvedChild.resolvedChild, nodeService, log);
                            }
                        }
                        else
                        {
                            if (log.isDebugEnabled())
                            {
                                log.debug("The node DOES NOT exist in current repository! Processing will be made by its PATH!");
                            }
                            expectedChildNodePaths.add(parentPath);
                        }
                    }
                    if (null != childRef)
                    {
                        expectedChildNodeRefs.add(childRef);
                    }
                }
                
                List actualChildren = nodeService.getChildAssocs(nodeRef);
                /**
                 * For each actual child association
                 */
                if (log.isDebugEnabled())
                {
                    log.debug("Traversing ACTUAL children:");
                }
                for(ChildAssociationRef child : actualChildren)
                {
                    log.debug("checking child: " + child);
                    if(child.isPrimary())
                    {
                        if (log.isTraceEnabled())
                        {
                            logInvasionHierarchy(child.getParentRef(), child.getChildRef(), nodeService, log);
                        }
                        /**
                         * yes it is a primary assoc
                         * should it be there ?
                         */
                        NodeRef childNodeRef = child.getChildRef();
                        Path actualChildPath = nodeService.getPath(childNodeRef);
                        if(!expectedChildNodeRefs.contains(childNodeRef) && !expectedChildNodePaths.contains(actualChildPath))
                        {
                            if (log.isDebugEnabled())
                            {
                                log.debug("This child IS NOT EXPECTED!");
                            }
                            /**
                             * An unexpected child - if this node has been transferred then
                             * it may need to be deleted.  
                             *  
                             * If from another repository then we have to prune the alien children 
                             * rather than deleting it.
                             */
                            if(nodeService.hasAspect(childNodeRef, TransferModel.ASPECT_TRANSFERRED))
                            {
                                log.debug("an unexpected transferred child node:" + child);
                                logComment("Transfer sync mode - checking unexpected child node:" + child);
                                String fromRepositoryId = (String) nodeService.getProperty(childNodeRef, TransferModel.PROP_FROM_REPOSITORY_ID);
                                
                                // Yes this is a transferred node.  When syncing we only delete nodes that are "from" 
                                // the system that is transferring to this repo.
                                if (log.isDebugEnabled())
                                {
                                    log.debug("'manifestRepositoryId': " + manifestRepositoryId);
                                }
                                if(fromRepositoryId != null &&  manifestRepositoryId != null)
                                {
                                    if(nodeService.hasAspect(childNodeRef, TransferModel.ASPECT_ALIEN))
                                    {
                                         /**
                                         * This node can't be deleted since it contains alien content
                                         * it needs to be "pruned" of the transferring repo's content instead.
                                         */
                                        log.debug("node to be deleted contains alien content so needs to be pruned." + childNodeRef);
                                        logComment("Transfer sync mode - node contains alien content so can't be deleted. " +  childNodeRef);
                                        alienProcessor.pruneNode(childNodeRef, manifestRepositoryId);
                                    }
                                    else
                                    {
                                        // Node
                                        if (log.isDebugEnabled())
                                        {
                                            log.debug("Node not alien. Trying to delete the node...");
                                        }
                                        String initialRepositoryId = (String) nodeService.getProperty(childNodeRef, TransferModel.PROP_FROM_REPOSITORY_ID);
                                        if(manifestRepositoryId.equalsIgnoreCase(initialRepositoryId))
                                        {
                                            if (log.isDebugEnabled())
                                            {
                                                log.debug("Replication is initiated from the same repository from which this node was transferred! Deleting");
                                            }
                                            // Yes the manifest repository Id and the from repository Id match.
                                            // Destination node if from the transferring repo and needs to be deleted. 
                                            logDeleted(node.getNodeRef(), childNodeRef, nodeService.getPath(childNodeRef).toString());
                                            logSummaryDeleted(node.getNodeRef(), childNodeRef, nodeService.getPath(childNodeRef).toString());
                                            nodeService.deleteNode(childNodeRef);
                                        }
                                        else
                                        {
                                            if (log.isDebugEnabled())
                                            {
                                                log
                                                        .debug("It is not an alien, but 'fromRepositoryId' is not equal to the 'manifestRepositoryId'! Cannot delete the foreign node...");
                                            }
                                        }
                                    }
                                }
                                else
                                {
                                    log.debug("node does not have a transferred aspect");
                                }
                            }
                        }
                    }
                }
            }
        
            else
            {
                log.debug("not sync mode - do nothing");
            }
        }
    }
    
    protected void processHeader(TransferManifestHeader header)
    {
        isSync = header.isSync();
        log.debug("isSync :" + isSync);
        
        manifestRepositoryId = header.getRepositoryId();
        log.debug("fromRepositoryId:" +  manifestRepositoryId);
    }
    /*
     * (non-Javadoc)
     * 
     * @see org.alfresco.repo.transfer.manifest.TransferManifestProcessor#startTransferManifest()
     */
    protected void startManifest()
    {
        //NOOP
    }
    /**
     * @param nodeService
     *            the nodeService to set
     */
    public void setNodeService(NodeService nodeService)
    {
        this.nodeService = nodeService;
    }
    
    public void setAlienProcessor(AlienProcessor alienProcessor)
    {
        this.alienProcessor = alienProcessor;
    }
    public AlienProcessor getAlienProcessor()
    {
        return alienProcessor;
    }
    
    /**
     * @param nodeResolver
     *            the nodeResolver to set
     */
    public void setNodeResolver(CorrespondingNodeResolver nodeResolver)
    {
        this.nodeResolver = nodeResolver;
    }
}