/*
 * Copyright (C) 2005-2013 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.forum;
import java.io.Serializable;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.alfresco.model.ContentModel;
import org.alfresco.model.ForumModel;
import org.alfresco.query.CannedQueryFactory;
import org.alfresco.query.CannedQueryResults;
import org.alfresco.query.EmptyPagingResults;
import org.alfresco.query.PagingRequest;
import org.alfresco.query.PagingResults;
import org.alfresco.repo.activities.ActivityType;
import org.alfresco.repo.content.MimetypeMap;
import org.alfresco.repo.node.getchildren.GetChildrenCannedQuery;
import org.alfresco.repo.node.getchildren.GetChildrenCannedQueryFactory;
import org.alfresco.repo.security.authentication.AuthenticationUtil;
import org.alfresco.repo.security.authentication.AuthenticationUtil.RunAsWork;
import org.alfresco.service.cmr.activities.ActivityService;
import org.alfresco.service.cmr.repository.ChildAssociationRef;
import org.alfresco.service.cmr.repository.ContentData;
import org.alfresco.service.cmr.repository.ContentService;
import org.alfresco.service.cmr.repository.ContentWriter;
import org.alfresco.service.cmr.repository.NodeRef;
import org.alfresco.service.cmr.repository.NodeService;
import org.alfresco.service.cmr.site.SiteService;
import org.alfresco.service.namespace.NamespaceService;
import org.alfresco.service.namespace.QName;
import org.alfresco.util.Pair;
import org.alfresco.util.registry.NamedObjectRegistry;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.json.simple.JSONObject;
/**
 * @author Neil Mc Erlean
 * @since 4.0
 */
// TODO consolidate this and ScriptCommentService and the implementations of comments.* web scripts.
public class CommentServiceImpl implements CommentService
{
    private static Log logger = LogFactory.getLog(CommentServiceImpl.class);  
    /**
     * Naming convention for Share comment model. fm:forum contains fm:topic
     */
    private static final QName FORUM_TO_TOPIC_ASSOC_QNAME = QName.createQName(NamespaceService.CONTENT_MODEL_1_0_URI, "Comments");
    private static final String COMMENTS_TOPIC_NAME = "Comments";
    private static final String CANNED_QUERY_GET_CHILDREN = "commentsGetChildrenCannedQueryFactory";
    
    // Injected services
    private NodeService nodeService;
    private ContentService contentService;
    private ActivityService activityService;
    private SiteService siteService;
    
    private NamedObjectRegistry> cannedQueryRegistry;
	public void setSiteService(SiteService siteService)
    {
		this.siteService = siteService;
	}
	public void setActivityService(ActivityService activityService)
    {
		this.activityService = activityService;
	}
    
	public void setNodeService(NodeService nodeService)
    {
        this.nodeService = nodeService;
    }
	public void setContentService(ContentService contentService)
    {
        this.contentService = contentService;
    }
    
    public void setCannedQueryRegistry(NamedObjectRegistry> cannedQueryRegistry)
    {
		this.cannedQueryRegistry = cannedQueryRegistry;
	}
	@Override
    public NodeRef getDiscussableAncestor(NodeRef descendantNodeRef)
    {
        // For "Share comments" i.e. fm:post nodes created via the Share commenting UI, the containment structure is as follows:
        // fm:discussable
        //    - fm:forum
        //        - fm:topic
        //            - fm:post
        // For other fm:post nodes the ancestor structure may be slightly different. (cf. Share discussions, which don't have 'forum')
        //
        // In order to ensure that we only return the discussable ancestor relevant to Share comments, we'll climb the
        // containment tree in a controlled manner.
        // We could navigate up looking for the first fm:discussable ancestor, but that might not find the correct node - it could,
        // for example, find a parent folder which was discussable.
        
        NodeRef result = null;
        if (nodeService.getType(descendantNodeRef).equals(ForumModel.TYPE_POST))
        {
            NodeRef topicNode = nodeService.getPrimaryParent(descendantNodeRef).getParentRef();
            
            if (nodeService.getType(topicNode).equals(ForumModel.TYPE_TOPIC))
            {
                NodeRef forumNode = nodeService.getPrimaryParent(topicNode).getParentRef();
                
                if (nodeService.getType(forumNode).equals(ForumModel.TYPE_FORUM))
                {
                    NodeRef discussableNode = nodeService.getPrimaryParent(forumNode).getParentRef();
                    
                    if (nodeService.hasAspect(discussableNode, ForumModel.ASPECT_DISCUSSABLE))
                    {
                        result = discussableNode;
                    }
                }
            }
        }
        
        return result;
    }
    @Override
    public PagingResults listComments(NodeRef discussableNode, PagingRequest paging)
    {
    	NodeRef commentsFolder = getShareCommentsTopic(discussableNode);
    	if(commentsFolder != null)
    	{
    		List> sort = new ArrayList>();
    		sort.add(new Pair(ContentModel.PROP_CREATED, false));
	        // Run the canned query
	        GetChildrenCannedQueryFactory getChildrenCannedQueryFactory = (GetChildrenCannedQueryFactory)cannedQueryRegistry.getNamedObject(CANNED_QUERY_GET_CHILDREN);
	        GetChildrenCannedQuery cq = (GetChildrenCannedQuery)getChildrenCannedQueryFactory.getCannedQuery(commentsFolder, null, null, null, null, sort, paging);
	
	        // Execute the canned query
	        CannedQueryResults results = cq.execute();
	        return results;
    	}
    	else
    	{
    		return new EmptyPagingResults();
    	}
    }
    
    @Override
    public NodeRef getShareCommentsTopic(NodeRef discussableNode)
    {
        NodeRef result = null;
        
        if (nodeService.hasAspect(discussableNode, ForumModel.ASPECT_DISCUSSABLE))
        {
            // We navigate down the "Share comments" containment model, which is based on the more general forum model,
            // but with certain naming conventions.
            List fora = nodeService.getChildAssocs(discussableNode, ForumModel.ASSOC_DISCUSSION, ForumModel.ASSOC_DISCUSSION, true);
            
            // There should only be one such assoc.
            if ( !fora.isEmpty())
            {
                final NodeRef firstForumNode = fora.get(0).getChildRef();
                List topics = nodeService.getChildAssocs(firstForumNode, ContentModel.ASSOC_CONTAINS, FORUM_TO_TOPIC_ASSOC_QNAME, true);
                
                // Likewise, only one.
                if ( !topics.isEmpty())
                {
                    final NodeRef firstTopicNode = topics.get(0).getChildRef();
                    result = firstTopicNode;
                }
            }
        }
        
        return result;
    }
//    private ScriptNode createCommentsFolder(ScriptNode node)
//    {
//        final NodeRef nodeRef = node.getNodeRef();
//        
//        NodeRef commentsFolder = AuthenticationUtil.runAs(new AuthenticationUtil.RunAsWork()
//        {
//            public NodeRef doWork() throws Exception
//            {
//                NodeRef commentsFolder = null;
//                
//                // ALF-5240: turn off auditing round the discussion node creation to prevent
//                // the source document from being modified by the first user leaving a comment
//                behaviourFilter.disableBehaviour(nodeRef, ContentModel.ASPECT_AUDITABLE);
//                
//                try
//                {
//                    nodeService.addAspect(nodeRef, QName.createQName(NamespaceService.FORUMS_MODEL_1_0_URI, "discussable"), null);
//                    List assocs = nodeService.getChildAssocs(nodeRef, QName.createQName(NamespaceService.FORUMS_MODEL_1_0_URI, "discussion"), RegexQNamePattern.MATCH_ALL);
//                    if (assocs.size() != 0)
//                    {
//                        NodeRef forumFolder = assocs.get(0).getChildRef();
//                        
//                        Map props = new HashMap(1, 1.0f);
//                        props.put(ContentModel.PROP_NAME, COMMENTS_TOPIC_NAME);
//                        commentsFolder = nodeService.createNode(
//                                forumFolder,
//                                ContentModel.ASSOC_CONTAINS, 
//                                QName.createQName(NamespaceService.CONTENT_MODEL_1_0_URI, COMMENTS_TOPIC_NAME), 
//                                QName.createQName(NamespaceService.FORUMS_MODEL_1_0_URI, "topic"),
//                                props).getChildRef();
//                    }
//                }
//                finally
//                {
//                    behaviourFilter.enableBehaviour(nodeRef, ContentModel.ASPECT_AUDITABLE);
//                }
//                
//                return commentsFolder;
//            }
//    
//        }, AuthenticationUtil.getSystemUserName()); 
//        
//        return new ScriptNode(commentsFolder, serviceRegistry, getScope());
//    }
    
    private String getSiteId(final NodeRef nodeRef)
    {
        String siteId = AuthenticationUtil.runAsSystem(new RunAsWork()
        {
			@Override
			public String doWork() throws Exception
			{
				return siteService.getSiteShortName(nodeRef);
			}
        });
        return siteId;
    }
    @SuppressWarnings("unchecked")
	private JSONObject getActivityData(String siteId, final NodeRef nodeRef)
    {
        if(siteId != null)
        {
	        // create an activity (with some Share-specific parts)
	        JSONObject json = new JSONObject();
	        json.put("title", nodeService.getProperty(nodeRef, ContentModel.PROP_NAME));
			try
			{
				StringBuilder sb = new StringBuilder("document-details?nodeRef=");
				sb.append(URLEncoder.encode(nodeRef.toString(), "UTF-8"));
				json.put("page", sb.toString());
				// MNT-11667 "createComment" method creates activity for users who are not supposed to see the file
				json.put("nodeRef", nodeRef.toString());
			}
			catch (UnsupportedEncodingException e)
			{
				logger.warn("Unable to urlencode page for create comment activity");
			}
			return json;
        }
        else
        {
        	logger.warn("Unable to determine site in which node " + nodeRef + " resides.");
        	return null;
        }
    }
    
	private void postActivity(String siteId, String activityType, JSONObject activityData)
    {
		if(activityData != null)
		{
			activityService.postActivity(activityType, siteId, "comments", activityData.toString());
		}
    }
	@Override
    public NodeRef createComment(final NodeRef discussableNode, String title, String comment, boolean suppressRollups)
    {
    	if(comment == null)
    	{
    		throw new IllegalArgumentException("Must provide a non-null comment");
    	}
        // There is no CommentService, so we have to create the node structure by hand.
        // This is what happens within e.g. comment.put.json.js when comments are submitted via the REST API.
        if (!nodeService.hasAspect(discussableNode, ForumModel.ASPECT_DISCUSSABLE))
        {
            nodeService.addAspect(discussableNode, ForumModel.ASPECT_DISCUSSABLE, null);
        }
        if (!nodeService.hasAspect(discussableNode, ForumModel.ASPECT_COMMENTS_ROLLUP) && !suppressRollups)
        {
            nodeService.addAspect(discussableNode, ForumModel.ASPECT_COMMENTS_ROLLUP, null);
        }
        // Forum node is created automatically by DiscussableAspect behaviour.
        NodeRef forumNode = nodeService.getChildAssocs(discussableNode, ForumModel.ASSOC_DISCUSSION, QName.createQName(NamespaceService.FORUMS_MODEL_1_0_URI, "discussion")).get(0).getChildRef();
        
        final List existingTopics = nodeService.getChildAssocs(forumNode, ContentModel.ASSOC_CONTAINS, QName.createQName(NamespaceService.CONTENT_MODEL_1_0_URI, "Comments"));
        NodeRef topicNode = null;
        if (existingTopics.isEmpty())
        {
            Map props = new HashMap(1, 1.0f);
            props.put(ContentModel.PROP_NAME, COMMENTS_TOPIC_NAME);
            topicNode = nodeService.createNode(forumNode, ContentModel.ASSOC_CONTAINS, QName.createQName(NamespaceService.CONTENT_MODEL_1_0_URI, "Comments"), ForumModel.TYPE_TOPIC, props).getChildRef();
        }
        else
        {
            topicNode = existingTopics.get(0).getChildRef();
        }
        NodeRef postNode = nodeService.createNode(topicNode, ContentModel.ASSOC_CONTAINS, ContentModel.ASSOC_CONTAINS, ForumModel.TYPE_POST).getChildRef();
        nodeService.setProperty(postNode, ContentModel.PROP_CONTENT, new ContentData(null, MimetypeMap.MIMETYPE_TEXT_PLAIN, 0L, null));
        nodeService.setProperty(postNode, ContentModel.PROP_TITLE, title);
        ContentWriter writer = contentService.getWriter(postNode, ContentModel.PROP_CONTENT, true);
        writer.setMimetype(MimetypeMap.MIMETYPE_HTML);
        writer.setEncoding("UTF-8");
        writer.putContent(comment);
    	// determine the siteId and activity data of the comment NodeRef
        String siteId = getSiteId(discussableNode);
        JSONObject activityData = getActivityData(siteId, discussableNode);
        postActivity(siteId, ActivityType.COMMENT_CREATED, activityData);
        return postNode;
    }
    public void updateComment(NodeRef commentNodeRef, String title, String comment)
    {
    	QName nodeType = nodeService.getType(commentNodeRef);
    	if(!nodeType.equals(ForumModel.TYPE_POST))
    	{
    		throw new IllegalArgumentException("Node to update is not a comment node.");
    	}
    	ContentWriter writer = contentService.getWriter(commentNodeRef, ContentModel.PROP_CONTENT, true);
    	writer.setMimetype(MimetypeMap.MIMETYPE_HTML); // TODO should this be set by the caller?
    	writer.putContent(comment);
    	if(title != null)
    	{
    		nodeService.setProperty(commentNodeRef, ContentModel.PROP_TITLE, title);
    	}
    	// determine the siteId and activity data of the comment NodeRef
        String siteId = getSiteId(commentNodeRef);
        NodeRef discussableNodeRef = getDiscussableAncestor(commentNodeRef);
        if(discussableNodeRef != null)
        {
        	JSONObject activityData = getActivityData(siteId, discussableNodeRef);
        	postActivity(siteId, "org.alfresco.comments.comment-updated", activityData);
        }
        else
        {
        	logger.warn("Unable to determine discussable node for the comment with nodeRef " + commentNodeRef + ", not posting an activity");
        }
    }
    public void deleteComment(NodeRef commentNodeRef)
    {
    	QName nodeType = nodeService.getType(commentNodeRef);
    	if(!nodeType.equals(ForumModel.TYPE_POST))
    	{
    		throw new IllegalArgumentException("Node to delete is not a comment node.");
    	}
    	// determine the siteId and activity data of the comment NodeRef (do this before removing the comment NodeRef)
        String siteId = getSiteId(commentNodeRef);
        NodeRef discussableNodeRef = getDiscussableAncestor(commentNodeRef);
        JSONObject activityData = null;
        if(discussableNodeRef != null)
        {
        	activityData = getActivityData(siteId, discussableNodeRef);
        }
    	nodeService.deleteNode(commentNodeRef);
    	if(activityData != null)
    	{
    		postActivity(siteId, "org.alfresco.comments.comment-deleted", activityData);
    	}
        else
        {
        	logger.warn("Unable to determine discussable node for the comment with nodeRef " + commentNodeRef + ", not posting an activity");
        }
    }
}