/*
 * #%L
 * Alfresco Repository
 * %%
 * Copyright (C) 2005 - 2016 Alfresco Software Limited
 * %%
 * This file is part of the Alfresco software. 
 * If the software was purchased under a paid Alfresco license, the terms of 
 * the paid license agreement will prevail.  Otherwise, the software is 
 * provided under the following open source license terms:
 * 
 * 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 <http://www.gnu.org/licenses/>.
 * #L%
 */
package org.alfresco.repo.forum;

import java.io.Serializable;
import java.text.SimpleDateFormat;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;

import org.springframework.extensions.surf.util.I18NUtil;
import org.alfresco.model.ApplicationModel;
import org.alfresco.model.ContentModel;
import org.alfresco.model.ForumModel;
import org.alfresco.repo.copy.CopyBehaviourCallback;
import org.alfresco.repo.copy.CopyDetails;
import org.alfresco.repo.copy.CopyServicePolicies;
import org.alfresco.repo.copy.DoNothingCopyBehaviourCallback;
import org.alfresco.repo.node.NodeServicePolicies;
import org.alfresco.repo.policy.JavaBehaviour;
import org.alfresco.repo.policy.PolicyComponent;
import org.alfresco.repo.transaction.TransactionalResourceHelper;
import org.alfresco.repo.version.VersionServicePolicies;
import org.alfresco.repo.version.VersionServicePolicies.AfterVersionRevertPolicy;
import org.alfresco.repo.version.common.VersionUtil;
import org.alfresco.service.cmr.model.FileExistsException;
import org.alfresco.service.cmr.model.FileFolderService;
import org.alfresco.service.cmr.model.FileNotFoundException;
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.StoreRef;
import org.alfresco.service.cmr.version.Version;
import org.alfresco.service.namespace.NamespaceService;
import org.alfresco.service.namespace.QName;
import org.alfresco.service.namespace.RegexQNamePattern;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.dao.ConcurrencyFailureException;

/**
 * Discussion-specific behaviours.
 * 
 * @author Derek Hulley
 * @since 3.2
 */
public class DiscussableAspect implements
            NodeServicePolicies.OnAddAspectPolicy,
            CopyServicePolicies.OnCopyNodePolicy,
            CopyServicePolicies.OnCopyCompletePolicy,
            VersionServicePolicies.AfterVersionRevertPolicy
{
    private static final String KEY_WORKING_COPIES = DiscussableAspect.class.getName() + ".WorkingCopies";
    
    private static final Log logger = LogFactory.getLog(DiscussableAspect.class);
    
    private PolicyComponent policyComponent;
    private NodeService nodeService;
    private NodeService dbNodeService;
    private FileFolderService fileFolderService;
    
    public void setPolicyComponent(PolicyComponent policyComponent) 
    {
        this.policyComponent = policyComponent;
    }
    
    public void setNodeService(NodeService nodeService)
    {
        this.nodeService = nodeService;
    }
    
    public void setDbNodeService(NodeService dbNodeService)
    {
        this.dbNodeService = dbNodeService;
    }

    public final void setFileFolderService(FileFolderService fileFolderService)
    {
        this.fileFolderService = fileFolderService;
    }

    /**
     * Initialise method
     */
    public void init()
    {
        // All forum-related copy behaviour uses the same copy callback
        this.policyComponent.bindClassBehaviour(
        QName.createQName(NamespaceService.ALFRESCO_URI, "onAddAspect"),
                ForumModel.ASPECT_DISCUSSABLE,
                new JavaBehaviour(this, "onAddAspect"));
        this.policyComponent.bindClassBehaviour(
                QName.createQName(NamespaceService.ALFRESCO_URI, "getCopyCallback"),
                ForumModel.ASPECT_DISCUSSABLE,
                new JavaBehaviour(this, "getCopyCallback"));
        this.policyComponent.bindClassBehaviour(
                QName.createQName(NamespaceService.ALFRESCO_URI, "onCopyComplete"),
                ForumModel.ASPECT_DISCUSSABLE,
                new JavaBehaviour(this, "onCopyComplete"));
        this.policyComponent.bindClassBehaviour(
                AfterVersionRevertPolicy.QNAME,
                ContentModel.ASPECT_VERSIONABLE,
                new JavaBehaviour(this, "afterVersionRevert"));           
    }
    
    /**
     * @return              Returns CopyBehaviourCallback
     */
    public CopyBehaviourCallback getCopyCallback(QName classRef, CopyDetails copyDetails)
    {
        return DiscussableAspectCopyBehaviourCallback.INSTANCE;
    }
    
    /**
     * Copy behaviour for the <b>fm:discussable</b> aspect.
     * <p>
     * Only the aspect is copied (to get the default behaviour).  All topics are copied later.
     * 
     * @author Derek Hulley
     * @since 3.2
     */
    private static class DiscussableAspectCopyBehaviourCallback extends DoNothingCopyBehaviourCallback
    {
        private static final CopyBehaviourCallback INSTANCE = new DiscussableAspectCopyBehaviourCallback();
        
        /**
         * Copy the aspect over only if the source document has also been checked out
         */
        @Override
        public boolean getMustCopy(QName classQName, CopyDetails copyDetails)
        {
            if (copyDetails.getSourceNodeAspectQNames().contains(ContentModel.ASPECT_WORKING_COPY) &&
                    !copyDetails.isTargetNodeIsNew())
            {
                // We are copying back from a working copy to the original node (probably)
                // We need to do a full merge of the discussions.  Keep track of the nodes
                // that need this behaviour and complete the copy after the copy completes.
                Set<NodeRef> nodeRefs = TransactionalResourceHelper.getSet(KEY_WORKING_COPIES);
                nodeRefs.add(copyDetails.getSourceNodeRef());
            }
            return false;
        }
    }
    
    /**
     * Ensure that the node has a <b>fm:forum</b> child node otherwise create one
     */
    public void onAddAspect(NodeRef discussableNodeRef, QName aspectTypeQName)
    {
        String name = (String)this.nodeService.getProperty(
                discussableNodeRef, 
                ContentModel.PROP_NAME);
        String forumName = I18NUtil.getMessage("discussion.discussion_for", new Object[] {name});
        
        NodeRef forumNodeRef = getForum(discussableNodeRef);
        
        if (forumNodeRef == null)
        {
            Map<QName, Serializable> forumProps = new HashMap<QName, Serializable>(1);
            forumProps.put(ContentModel.PROP_NAME, forumName);
            
            ChildAssociationRef childRef = nodeService.createNode(
                    discussableNodeRef, 
                ForumModel.ASSOC_DISCUSSION,
                QName.createQName(NamespaceService.FORUMS_MODEL_1_0_URI, "discussion"), 
                ForumModel.TYPE_FORUM, forumProps);
          
            forumNodeRef = childRef.getChildRef();
        }
        else
        {
            // Just adjust the name
            nodeService.setProperty(forumNodeRef, ContentModel.PROP_NAME, forumName);
        }
      
        // apply the uifacets aspect
        Map<QName, Serializable> uiFacetsProps = new HashMap<QName, Serializable>(5);
        uiFacetsProps.put(ApplicationModel.PROP_ICON, "forum");
        this.nodeService.addAspect(forumNodeRef, ApplicationModel.ASPECT_UIFACETS, uiFacetsProps);
        
        // Done
        if (logger.isDebugEnabled())
        {
            logger.debug(
                    "Created forum node for discussion: \n" +
                    "   Discussable Node: " + discussableNodeRef + "\n" +
                    "   Forum Node:       " + forumNodeRef);
        }
    }

    /**
     * Retrieves the forum node the the given discussable
     * 
     * @return          Returns the <b>fm:forum</b> node or <tt>null</tt>
     */
    private NodeRef getForum(NodeRef discussableNodeRef)
    {
        List<ChildAssociationRef> destChildren = nodeService.getChildAssocs(
                discussableNodeRef,
                ForumModel.ASSOC_DISCUSSION,
                RegexQNamePattern.MATCH_ALL);
        // Take the first one
        if (destChildren.size() == 0)
        {
            return null;
        }
        else
        {
            // We just take the first one
            ChildAssociationRef discussionAssoc = destChildren.get(0);
            return discussionAssoc.getChildRef();
        }
    }
//
//    /**
//     * Copies the discussions to the new node, whilst being sensitive to any existing discussions.
//     */
//    public void onCopyComplete(
//            QName classQName,
//            NodeRef sourceNodeRef,
//            NodeRef targetNodeRef,
//            boolean copyToNewNode,
//            Map<NodeRef,NodeRef> copyMap)
//    {
//        // if the copy is not a new node it is a checkin, we therefore
//        // need to copy any discussions from the working copy document
//        // to the document being checked in
//        if (copyToNewNode)
//        {
//            // We don't care about new copies, just copies to existing nodes
//            return;
//        }
//        List<ChildAssociationRef> sourceChildren = nodeService.getChildAssocs(sourceNodeRef, 
//                ForumModel.ASSOC_DISCUSSION, RegexQNamePattern.MATCH_ALL);
//      
//        if (sourceChildren.size() != 1)
//        {
//           throw new CheckOutCheckInServiceException(
//                 "The source node has the discussable aspect but does not have 1 child, it has " + 
//                 sourceChildren.size() + " children!");
//        }
//        
//        NodeRef sourceForum = sourceChildren.get(0).getChildRef();
//      
//        // get the forum for the destination node, it's created if necessary
//        NodeRef destinationForum = getDestinationForum(targetNodeRef);
//          
//        // copy any topics from the source forum to the destination forum
//        int copied = 0;
//        List<ChildAssociationRef> sourceForums = nodeService.getChildAssocs(sourceForum);
//        for (ChildAssociationRef childRef : sourceForums)
//        {
//            String topicName = null;
//            NodeRef childNode = childRef.getChildRef();
//            if (nodeService.getType(childNode).equals(ForumModel.TYPE_TOPIC))
//            {
//                try
//                {
//                    // work out the name for the copied topic
//                    String childName = nodeService.getProperty(childNode, 
//                          ContentModel.PROP_NAME).toString();
//                    Serializable labelProp = nodeService.getProperty(targetNodeRef, 
//                          ContentModel.PROP_VERSION_LABEL);
//                    if (labelProp == null)
//                    {
//                        SimpleDateFormat dateFormat = new SimpleDateFormat("dd-MM-yyyy-HH-mm-ss");
//                        topicName = childName + " - " + dateFormat.format(new Date());
//                    }
//                    else
//                    {
//                        topicName = childName + " (" + labelProp.toString() + ")";
//                    }
//
//                    fileFolderService.copy(childNode, destinationForum, topicName);
//                    copied++;
//                }
//                catch (FileNotFoundException fnfe)
//                {
//                    throw new CheckOutCheckInServiceException(
//                          "Failed to copy topic from working copy to checked out content", fnfe);
//                }
//                catch (FileExistsException fee)
//                {
//                    throw new CheckOutCheckInServiceException("Failed to checkin content as a topic called " + 
//                          topicName + " already exists on the checked out content", fee);
//                }
//            }
//        }
//          
//        if (logger.isDebugEnabled())
//            logger.debug("Copied " + copied + " topics from the working copy to the checked out content");
//    }
//

    public void onCopyComplete(
            QName classRef,
            NodeRef sourceNodeRef,
            NodeRef targetNodeRef,
            boolean copyToNewNode,
            Map<NodeRef, NodeRef> copyMap)
    {
        Set<NodeRef> workingCopyNodeRefs = TransactionalResourceHelper.getSet(KEY_WORKING_COPIES);
        if (!workingCopyNodeRefs.contains(sourceNodeRef))
        {
            // This is not one of the nodes that needs to have discussions copied over
            return;
        }
        
        // First check that the source node has forums
        NodeRef sourceForumNodeRef = getForum(sourceNodeRef);
        if (sourceForumNodeRef == null)
        {
            // Missing!  Clean the source node up!
            nodeService.removeAspect(sourceNodeRef, ForumModel.ASPECT_DISCUSSABLE);
            return;
        }
        
        // The aspect may or may not exist on the target node
        if (!nodeService.hasAspect(targetNodeRef, ForumModel.ASPECT_DISCUSSABLE))
        {
            // Add the aspect
            nodeService.addAspect(targetNodeRef, ForumModel.ASPECT_DISCUSSABLE, null);
        }
        // Get the forum node
        NodeRef targetForumNodeRef = getForum(targetNodeRef);
        // Merge the forum topics
        List<ChildAssociationRef> topicAssocRefs = nodeService.getChildAssocs(
                sourceForumNodeRef,
                Collections.singleton(ForumModel.TYPE_TOPIC));
        int copied = 0;
        for (ChildAssociationRef topicAssocRef : topicAssocRefs)
        {
            NodeRef topicNodeRef = topicAssocRef.getChildRef();
            try
            {
                // work out the name for the copied topic
                String topicName;
                String topicNodeName = nodeService.getProperty(topicNodeRef, ContentModel.PROP_NAME).toString();
                Serializable labelProp = nodeService.getProperty(targetNodeRef, ContentModel.PROP_VERSION_LABEL);
                if (labelProp == null)
                {
                    SimpleDateFormat dateFormat = new SimpleDateFormat("dd-MM-yyyy-HH-mm-ss");
                    topicName = topicNodeName + " - " + dateFormat.format(new Date());
                }
                else
                {
                    topicName = topicNodeName + " (" + labelProp.toString() + ")";
                }

                if (fileFolderService.searchSimple(targetForumNodeRef, topicName) != null)
                {
                    // A topic with that name already exists
                    continue;
                }
                fileFolderService.copy(topicNodeRef, targetForumNodeRef, topicName);
                copied++;
            }
            catch (FileExistsException e)
            {
                // We checked for this, so this is a concurrency condition
                throw new ConcurrencyFailureException("Target topic exists: " + e.getMessage(), e);
            }
            catch (FileNotFoundException e)
            {
                // The node was there, but now it's gone
                throw new ConcurrencyFailureException("Forum was deleted: " + e.getMessage(), e);
            }
        }
    }
    
    @Override
    public void afterVersionRevert(NodeRef nodeRef, Version version)
    {
        NodeRef versionNodeRef = version.getFrozenStateNodeRef();
        if (!this.nodeService.hasAspect(versionNodeRef, ForumModel.ASPECT_DISCUSSABLE))
        {
            return;
        }
        
        // Get the discussion assoc references from the version store
        List<ChildAssociationRef> childAssocRefs = this.nodeService.getChildAssocs(VersionUtil.convertNodeRef(versionNodeRef), ForumModel.ASSOC_DISCUSSION,
                RegexQNamePattern.MATCH_ALL);
        for (ChildAssociationRef childAssocRef : childAssocRefs)
        {
            // Get the child reference
            NodeRef childRef = childAssocRef.getChildRef();
            NodeRef referencedNode = (NodeRef) this.dbNodeService.getProperty(childRef, ContentModel.PROP_REFERENCE);

            if (referencedNode != null && this.nodeService.exists(referencedNode) == false)
            {
                StoreRef orginalStoreRef = referencedNode.getStoreRef();
                NodeRef archiveRootNodeRef = this.nodeService.getStoreArchiveNode(orginalStoreRef);
                if (archiveRootNodeRef == null)
                {
                    // Store doesn't support archiving
                    continue;
                }
                NodeRef archivedNodeRef = new NodeRef(archiveRootNodeRef.getStoreRef(), referencedNode.getId());

                if (!this.nodeService.exists(archivedNodeRef) || !nodeService.hasAspect(archivedNodeRef, ContentModel.ASPECT_ARCHIVED))
                {
                    // Node doesn't support archiving or it was deleted within parent node.
                    continue;
                }

                NodeRef existingChild = this.nodeService.getChildByName(nodeRef, childAssocRef.getTypeQName(), this.nodeService
                        .getProperty(archivedNodeRef, ContentModel.PROP_NAME).toString());
                if (existingChild != null)
                {
                    this.nodeService.deleteNode(existingChild);
                }

                this.nodeService.restoreNode(archivedNodeRef, null, null, null);
            }
        }
    }
    
}
