/*
 * #%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.discussion;

import java.io.Serializable;
import java.util.ArrayList;
import java.util.Date;
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.CannedQuerySortDetails;
import org.alfresco.query.EmptyCannedQueryResults;
import org.alfresco.query.EmptyPagingResults;
import org.alfresco.query.PagingRequest;
import org.alfresco.query.PagingResults;
import org.alfresco.repo.content.MimetypeMap;
import org.alfresco.repo.discussion.cannedqueries.GetDiscussionTopcisWithPostsCannedQuery;
import org.alfresco.repo.discussion.cannedqueries.GetDiscussionTopcisWithPostsCannedQueryFactory;
import org.alfresco.repo.discussion.cannedqueries.NodeWithChildrenEntity;
import org.alfresco.repo.domain.node.NodeDAO;
import org.alfresco.repo.node.getchildren.GetChildrenAuditableCannedQuery;
import org.alfresco.repo.node.getchildren.GetChildrenAuditableCannedQueryFactory;
import org.alfresco.repo.node.getchildren.GetChildrenWithTargetAssocsAuditableCannedQuery;
import org.alfresco.repo.node.getchildren.GetChildrenWithTargetAssocsAuditableCannedQueryFactory;
import org.alfresco.repo.query.NodeBackedEntity;
import org.alfresco.repo.query.NodeWithTargetsEntity;
import org.alfresco.repo.query.NodeWithTargetsEntity.TargetAndTypeId;
import org.alfresco.repo.security.authentication.AuthenticationUtil;
import org.alfresco.repo.site.SiteServiceImpl;
import org.alfresco.repo.tenant.TenantService;
import org.alfresco.service.cmr.discussion.DiscussionService;
import org.alfresco.service.cmr.discussion.PostInfo;
import org.alfresco.service.cmr.discussion.PostWithReplies;
import org.alfresco.service.cmr.discussion.TopicInfo;
import org.alfresco.service.cmr.repository.ChildAssociationRef;
import org.alfresco.service.cmr.repository.ContentReader;
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.search.LimitBy;
import org.alfresco.service.cmr.search.ResultSet;
import org.alfresco.service.cmr.search.ResultSetRow;
import org.alfresco.service.cmr.search.SearchParameters;
import org.alfresco.service.cmr.search.SearchService;
import org.alfresco.service.cmr.site.SiteService;
import org.alfresco.service.cmr.tagging.TaggingService;
import org.alfresco.service.namespace.NamespaceService;
import org.alfresco.service.namespace.QName;
import org.alfresco.service.transaction.TransactionService;
import org.alfresco.util.ISO9075;
import org.alfresco.util.Pair;
import org.alfresco.util.registry.NamedObjectRegistry;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

/**
 * @author Nick Burch (based on existing webscript controllers in the REST API)
 * @since 4.0
 */
public class DiscussionServiceImpl implements DiscussionService
{
    public static final String DISCUSSION_COMPONENT = "discussions";
   
    protected static final String CANNED_QUERY_GET_CHILDREN = "discussionGetChildrenCannedQueryFactory";
    protected static final String CANNED_QUERY_GET_CHILDREN_TARGETS = "discussionGetChildrenWithTargetAssocsAuditableCannedQueryFactory";
    protected static final String CANNED_QUERY_GET_TOPICS_WITH_POSTS = "discussionGetDiscussionTopcisWithPostsCannedQueryFactory";
    protected static final int MAX_REPLIES_FETCH_SIZE = 1000; 
    
    /**
     * The logger
     */
    private static Log logger = LogFactory.getLog(DiscussionServiceImpl.class);
    
    private NodeDAO nodeDAO;
    private NodeService nodeService;
    private SiteService siteService;
    private SearchService searchService;
    private ContentService contentService;
    private TaggingService taggingService;
    private NamespaceService namespaceService;
    private TransactionService transactionService;
    private NamedObjectRegistry<CannedQueryFactory<? extends Object>> cannedQueryRegistry;
    private TenantService tenantService;
    
    public void setNodeDAO(NodeDAO nodeDAO)
    {
        this.nodeDAO = nodeDAO;
    }
    
    public void setNodeService(NodeService nodeService)
    {
        this.nodeService = nodeService;
    }
    
    public void setSiteService(SiteService siteService)
    {
        this.siteService = siteService;
    }
    
    public void setSearchService(SearchService searchService)
    {
        this.searchService = searchService;
    }
    
    public void setContentService(ContentService contentService)
    {
        this.contentService = contentService;
    }
    
    public void setTaggingService(TaggingService taggingService)
    {
        this.taggingService = taggingService;
    }
    
    public void setNamespaceService(NamespaceService namespaceService)
    {
        this.namespaceService = namespaceService;
    }
    
    public void setTransactionService(TransactionService transactionService)
    {
        this.transactionService = transactionService;
    }
    
    /**
     * Set the registry of {@link CannedQueryFactory canned queries}
     */
    public void setCannedQueryRegistry(NamedObjectRegistry<CannedQueryFactory<? extends Object>> cannedQueryRegistry)
    {
        this.cannedQueryRegistry = cannedQueryRegistry;
    }
    
    /**
     * Set the {@link TenantService}
     */
    public void setTenantService(TenantService tenantService)
    {
        this.tenantService = tenantService;
    }
    
    /**
     * Fetches the Discussions Container on a site, creating as required if requested.
     */
    protected NodeRef getSiteDiscussionsContainer(final String siteShortName, boolean create)
    {
       return SiteServiceImpl.getSiteContainer(
             siteShortName, DISCUSSION_COMPONENT, create, 
             siteService, transactionService, taggingService);
    }
    
    private String generateName()
    {
       // Generate a unique name
       // (Should be unique, but will retry for a new one if not)
       String name = "post-" +(new Date()).getTime() + "_" + 
                     Math.round(Math.random()*10000);
       return name;
    }
    
    private TopicInfo buildTopic(NodeRef nodeRef, NodeRef container, String name)
    {
       TopicInfoImpl topic = new TopicInfoImpl(nodeRef, container, name);
       
       // Grab all the properties, we need the bulk of them anyway
       Map<QName,Serializable> props = nodeService.getProperties(nodeRef);
       
       // Start with the auditable properties
       topic.setCreator((String)props.get(ContentModel.PROP_CREATOR));
       topic.setModifier((String)props.get(ContentModel.PROP_MODIFIER));
       topic.setCreatedAt((Date)props.get(ContentModel.PROP_CREATED));
       topic.setModifiedAt((Date)props.get(ContentModel.PROP_MODIFIED));
       
       // Now do the discussion ones
       // Title is special - in older cases from share it was set on the
       //  First Post but not on the Topic
       String title = (String)props.get(ContentModel.PROP_TITLE);
       if (title == null)
       {
          // Try the first child
          PostInfo primaryPost = getPrimaryPost(topic);
          if (primaryPost != null)
          {
             title = primaryPost.getTitle();
          }
       }
       topic.setTitle(title);
       
       // Finally tags
       topic.setTags(taggingService.getTags(nodeRef));
       
       // All done
       return topic;
    }
    
    private PostInfo buildPost(NodeRef nodeRef, TopicInfo topic, String name, String preLoadedContents)
    {
       PostInfoImpl post = new PostInfoImpl(nodeRef, name, topic);
       
       // Grab all the properties, we need the bulk of them anyway
       Map<QName,Serializable> props = nodeService.getProperties(nodeRef);
       
       // Start with the auditable properties
       post.setCreator((String)props.get(ContentModel.PROP_CREATOR));
       post.setModifier((String)props.get(ContentModel.PROP_MODIFIER));
       post.setCreatedAt((Date)props.get(ContentModel.PROP_CREATED));
       post.setModifiedAt((Date)props.get(ContentModel.PROP_MODIFIED));
       post.setUpdatedAt((Date)props.get(ContentModel.PROP_UPDATED));
       
       // Now do the discussion ones
       post.setTitle((String)props.get(ContentModel.PROP_TITLE));
       
       // Finally, do the content
       String contents = preLoadedContents;
       if (contents == null)
       {
          ContentReader reader = contentService.getReader(nodeRef, ContentModel.PROP_CONTENT);
          if (reader != null)
          {
             contents = reader.getContentString();
          }
       }
       post.setContents(contents);
       
       // All done
       return post;
    }
    
    
    @Override
    public TopicInfo getTopic(String siteShortName, String topicName) 
    {
       NodeRef container = getSiteDiscussionsContainer(siteShortName, false);
       if (container == null)
       {
          // No discussions
          return null;
       }
       
       // We can now fetch by parent nodeRef
       return getTopic(container, topicName);
    }
    
    @Override
    public TopicInfo getTopic(NodeRef parentNodeRef, String topicName) 
    {
       NodeRef topic = nodeService.getChildByName(parentNodeRef, ContentModel.ASSOC_CONTAINS, topicName);
       if (topic != null)
       {
          return buildTopic(topic, parentNodeRef, topicName);
       }
       return null;
    }

    @Override
    public PostInfo getPost(TopicInfo topic, String postName) 
    {
       // Sanity check what we were given
       if (topic.getNodeRef() == null)
       {
          throw new IllegalArgumentException("Can't get posts for a topic that was never persisted!");
       }
       
       // Fetch
       NodeRef post = nodeService.getChildByName(topic.getNodeRef(), ContentModel.ASSOC_CONTAINS, postName);
       if (post != null)
       {
          return buildPost(post, topic, postName, null);
       }
       return null;
    }

    
    @Override
    public TopicInfo createTopic(final String siteShortName, final String title) 
    {
       // Grab the location to store in
       NodeRef container = getSiteDiscussionsContainer(siteShortName, true);
       
       // Add by Parent NodeRef
       return createTopic(container, title);
    }

    @Override
    public TopicInfo createTopic(final NodeRef parentNodeRef, final String title) 
    {
       // Build the name
       String name = generateName();
       
       // Get the properties for the node
       Map<QName, Serializable> props = new HashMap<QName, Serializable>();
       props.put(ContentModel.PROP_NAME,  name);
       props.put(ContentModel.PROP_TITLE, title);
       
       // Build the node
       NodeRef nodeRef = nodeService.createNode(
             parentNodeRef,
             ContentModel.ASSOC_CONTAINS,
             QName.createQName(name),
             ForumModel.TYPE_TOPIC,
             props
       ).getChildRef();
       
       // We require that the parent container of the topic
       //  is always a tag scope. This should always be the case
       //  for site containers, but perhaps not others
       if (! taggingService.isTagScope(parentNodeRef))
       {
          AuthenticationUtil.runAs(new AuthenticationUtil.RunAsWork<Void>() {
             public Void doWork() throws Exception
             {
                // Add the tag scope aspect
                taggingService.addTagScope(parentNodeRef);
                return null;
             }
          }, AuthenticationUtil.getSystemUserName());
       }
       
       // Generate the wrapping object for it
       // Build it that way, so creator and created date come through
       return buildTopic(nodeRef, parentNodeRef, name);
    }

    @Override
    public PostInfo createPost(TopicInfo topic, String contents) 
    {
       // Sanity check what we were given
       if (topic.getNodeRef() == null)
       {
          throw new IllegalArgumentException("Can't create posts for a topic that was never persisted!");
       }
       
       // Are we going to be the primary post?
       boolean isPrimary = false;
       if (getPrimaryPost(topic) == null)
       {
          isPrimary = true;
       }
       
       // Decide on the name. If this is the first post in a topic,
       //  it should share the topic's name, otherwise needs a new one
       String name = generateName();
       if (isPrimary)
       {
          name = topic.getSystemName();
       }
       
       // Create the properties for the node
       Map<QName, Serializable> props = new HashMap<QName, Serializable>();
       props.put(ContentModel.PROP_NAME,  name);
       
       // TODO Remove this shortly, when the webscripts have been
       //  fixed up to avoid their current broken-ness
       props.put(ContentModel.PROP_PUBLISHED, new Date());
       
       // Do we want a title? By default, primary posts share a title
       //  with the topic, but replies are title-free
       if (isPrimary)
       {
          props.put(ContentModel.PROP_TITLE, topic.getTitle());
       }
       
       // Build the node
       NodeRef nodeRef = nodeService.createNode(
             topic.getNodeRef(),
             ContentModel.ASSOC_CONTAINS,
             QName.createQName(name),
             ForumModel.TYPE_POST,
             props
       ).getChildRef();
       
       // Store the content
       ContentWriter writer = contentService.getWriter(nodeRef, ContentModel.PROP_CONTENT, true);
       writer.setEncoding("UTF-8");
       writer.setMimetype(MimetypeMap.MIMETYPE_HTML);
       writer.putContent(contents);
       
       // Generate the wrapping object for it
       // Build it that way, so creator and created date come through
       return buildPost(nodeRef, topic, name, contents);
    }
    
    @Override
    public PostInfo createReply(PostInfo parentPost, String contents)
    {
       // Sanity check what we were given
       if (parentPost.getNodeRef() == null)
       {
          throw new IllegalArgumentException("Can't reply to a post that was never persisted");
       }
       if (parentPost.getTopic() == null)
       {
          throw new IllegalArgumentException("Can't reply to a post with no attached topic");
       }
       
       // Have the post created
       PostInfo reply = createPost(parentPost.getTopic(), contents);
       
       // Now make it a reply
       nodeService.createAssociation(
             reply.getNodeRef(), parentPost.getNodeRef(), ContentModel.ASSOC_REFERENCES);
       
       // All done
       return reply;
    }

    
    @Override
    public TopicInfo updateTopic(TopicInfo topic) 
    {
       // Sanity check what we were given
       if (topic.getNodeRef() == null)
       {
          throw new IllegalArgumentException("Can't update a topic that was never persisted, call create instead");
       }
       
       // Update the properties
       NodeRef nodeRef = topic.getNodeRef();
       nodeService.setProperty(nodeRef, ContentModel.PROP_TITLE, topic.getTitle());
       
       // Now do the tags
       taggingService.setTags(nodeRef, topic.getTags());
       
       // All done
       return topic;
    }

    @Override
    public PostInfo updatePost(PostInfo post) 
    {
       // Sanity check what we were given
       if (post.getNodeRef() == null)
       {
          throw new IllegalArgumentException("Can't update a post that was never persisted, call create instead");
       }
       
       // Update the properties
       NodeRef nodeRef = post.getNodeRef();
       nodeService.setProperty(nodeRef, ContentModel.PROP_TITLE, post.getTitle());
       
       // Change the content
       ContentWriter writer = contentService.getWriter(nodeRef, ContentModel.PROP_CONTENT, true);
       writer.setEncoding("UTF-8");
       writer.putContent(post.getContents());
       
       // Mark it as having been updated
       Date updatedAt = new Date();
       nodeService.setProperty(nodeRef, ContentModel.PROP_UPDATED, updatedAt);
       if (post instanceof PostInfoImpl)
       {
          ((PostInfoImpl)post).setUpdatedAt(updatedAt);
          ((PostInfoImpl)post).setModifiedAt(updatedAt);
       }
       else
       {
          // Re-create to get the updated date
          post = buildPost(nodeRef, post.getTopic(), post.getSystemName(), post.getContents()); 
       }
       
       // All done
       return post;
    }

    
    @Override
    public void deleteTopic(TopicInfo topic) 
    {
       if (topic.getNodeRef() == null)
       {
          throw new IllegalArgumentException("Can't delete a topic that was never persisted");
       }

       nodeService.deleteNode(topic.getNodeRef());
    }

    @Override
    public void deletePost(PostInfo post) 
    {
       if (post.getNodeRef() == null)
       {
          throw new IllegalArgumentException("Can't delete a post that was never persisted");
       }

       nodeService.deleteNode(post.getNodeRef());
    }
    
    @Override
    public Pair<TopicInfo,PostInfo> getForNodeRef(NodeRef nodeRef)
    {
       QName type = nodeService.getType(nodeRef);
       TopicInfo topic = null;
       PostInfo post = null;
       
       if (type.equals(ForumModel.TYPE_TOPIC))
       {
          ChildAssociationRef ref = nodeService.getPrimaryParent(nodeRef);
          String topicName = ref.getQName().getLocalName();
          topic = getTopic(ref.getParentRef(), topicName);
       }
       else if (type.equals(ForumModel.TYPE_POST))
       {
          ChildAssociationRef toTopic = nodeService.getPrimaryParent(nodeRef);
          String postName = toTopic.getQName().getLocalName();
          NodeRef topicNodeRef = toTopic.getParentRef();
          
          ChildAssociationRef toParent = nodeService.getPrimaryParent(topicNodeRef);
          String topicName = toParent.getQName().getLocalName();
          topic = getTopic(toParent.getParentRef(), topicName);
          post = getPost(topic, postName);
       }
       else
       {
          logger.debug("Invalid type " + type + " found");
          return null;
       }
       
       // Return what we found
       return new Pair<TopicInfo, PostInfo>(topic, post);
    }

    @Override
    public PostInfo getPrimaryPost(TopicInfo topic) 
    {
       // First up, see if there is a post with the same name as the topic
       // (That's the normal Share case)
       PostInfo post = getPost(topic, topic.getSystemName());
       if (post != null)
       {
          return post;
       }

       // Cater for the explorer case, we want the first child
       List<ChildAssociationRef> children = nodeService.getChildAssocs(topic.getNodeRef());
       if (children.size() == 0)
       {
          // No child posts yet
          return null;
       }

       // We want the first one in the list
       NodeRef postNodeRef = children.get(0).getChildRef();
       String postName = children.get(0).getQName().getLocalName();
       return buildPost(postNodeRef, topic, postName, null);
    }
    
    @Override
    public PostInfo getMostRecentPost(TopicInfo topic)
    {
       // Do a listing at the Node level, ordered by created date
       //  to get the most recent nodes
       PagingRequest paging = new PagingRequest(0, 1);
       CannedQueryResults<NodeBackedEntity> results = 
           listEntries(topic.getNodeRef(), ForumModel.TYPE_POST, null, null, null, false, paging);
          
       // Bail if the topic lacks posts
       if (results.getPage().size() == 0)
       {
          // No posts in the topic
          return null;
       }
       
       // Grab the newest node
       NodeBackedEntity node = results.getPage().get(0);
       
       // Wrap and return
       return buildPost(tenantService.getBaseName(node.getNodeRef()), topic, node.getName(), null);
    }

   
   @Override
   public PagingResults<TopicInfo> listTopics(String siteShortName,
         boolean sortAscending, PagingRequest paging) 
   {
      NodeRef container = getSiteDiscussionsContainer(siteShortName, false);
      if (container == null)
      {
         // No topics
         return new EmptyPagingResults<TopicInfo>();
      }
      
      // We can now fetch by parent nodeRef
      return listTopics(container, sortAscending, paging);
   }

   @Override
   public PagingResults<TopicInfo> listTopics(NodeRef nodeRef,
         boolean sortAscending, PagingRequest paging) 
   {
      // Do the listing, oldest first
      CannedQueryResults<NodeBackedEntity> nodes = 
         listEntries(nodeRef, ForumModel.TYPE_TOPIC, null, null, null, sortAscending, paging);
      
      // Wrap and return
      return wrap(nodes, nodeRef);
   }
   
   @Override
   public PagingResults<TopicInfo> listTopics(String siteShortName,
         String username, boolean sortAscending, PagingRequest paging) 
   {
      NodeRef container = getSiteDiscussionsContainer(siteShortName, false);
      if (container == null)
      {
         // No topics
         return new EmptyPagingResults<TopicInfo>();
      }
      
      // We can now fetch by parent nodeRef
      return listTopics(container, username, sortAscending, paging);
   }

   @Override
   public PagingResults<TopicInfo> listTopics(NodeRef nodeRef,
         String username, boolean sortAscending, PagingRequest paging) 
   {
      // Do the listing, oldest first
      CannedQueryResults<NodeBackedEntity> nodes = 
         listEntries(nodeRef, ForumModel.TYPE_TOPIC, username, null, null, sortAscending, paging);
      
      // Wrap and return
      return wrap(nodes, nodeRef);
   }
   
   @Override
   public PagingResults<TopicInfo> listTopics(String siteShortName,
         Date from, Date to, boolean sortAscending, PagingRequest paging) 
   {
      NodeRef container = getSiteDiscussionsContainer(siteShortName, false);
      if (container == null)
      {
         // No topics
         return new EmptyPagingResults<TopicInfo>();
      }
      
      // We can now fetch by parent nodeRef
      return listTopics(container, from, to, sortAscending, paging);
   }

   @Override
   public PagingResults<TopicInfo> listTopics(NodeRef nodeRef,
         Date from, Date to, boolean sortAscending, PagingRequest paging) 
   {
      // Do the listing, with the sort order as requested
      CannedQueryResults<NodeBackedEntity> nodes = 
         listEntries(nodeRef, ForumModel.TYPE_TOPIC, null, from, to, sortAscending, paging);
      
      // Wrap and return
      return wrap(nodes, nodeRef);
   }

   
   @Override
   public PagingResults<Pair<TopicInfo, Integer>> listHotTopics(
         String siteShortName, Date since, PagingRequest paging) 
   {
      NodeRef container = getSiteDiscussionsContainer(siteShortName, false);
      if (container == null)
      {
         // No topics
         return new EmptyPagingResults<Pair<TopicInfo,Integer>>();
      }
      
      // We can now fetch by parent nodeRef
      return listHotTopics(container, since, paging);
   }
   
   @Override
   public PagingResults<Pair<TopicInfo, Integer>> listHotTopics(
         NodeRef nodeRef, Date since, PagingRequest paging) 
   {
      // Do the query
      GetDiscussionTopcisWithPostsCannedQueryFactory getCQFactory = 
                  (GetDiscussionTopcisWithPostsCannedQueryFactory)cannedQueryRegistry.getNamedObject(CANNED_QUERY_GET_TOPICS_WITH_POSTS);
      GetDiscussionTopcisWithPostsCannedQuery cq = (GetDiscussionTopcisWithPostsCannedQuery)getCQFactory.getCannedQuery(
            nodeRef, null, since, true, null, paging);
      
      // Execute the canned query
      CannedQueryResults<NodeWithChildrenEntity> results = cq.execute();
      
      // Wrap and return
      return wrapWithCount(results, nodeRef);
   }

   @Override
   public PagingResults<TopicInfo> findTopics(String siteShortName,
         String username, String tag, boolean sortAscending, PagingRequest paging) 
   {
      NodeRef container = getSiteDiscussionsContainer(siteShortName, false);
      if (container == null)
      {
         // No topics
         return new EmptyPagingResults<TopicInfo>();
      }
      
      // We can now search by parent nodeRef
      return findTopics(container, username, tag, sortAscending, paging);
   }

   @Override
   public PagingResults<TopicInfo> findTopics(NodeRef nodeRef,
         String username, String tag, boolean sortAscending, PagingRequest paging) 
   {
      // Build the query
      StringBuilder luceneQuery = new StringBuilder();
      luceneQuery.append(" +TYPE:\"" + ForumModel.TYPE_TOPIC + "\"");
      luceneQuery.append(" +PATH:\"" + nodeService.getPath(nodeRef).toPrefixString(namespaceService) + "/*\"");

      if (username != null)
      {
         luceneQuery.append(" +@cm\\:creator:\"" + username + "\"");
      }
      if (tag != null)
      {
         luceneQuery.append(" +PATH:\"/cm:taggable/cm:" + ISO9075.encode(tag) + "/member\"" );
      }
      
      String sortOn = "@{http://www.alfresco.org/model/content/1.0}created";

      // Query
      SearchParameters sp = new SearchParameters();
      sp.addStore(nodeRef.getStoreRef());
      sp.setLanguage(SearchService.LANGUAGE_LUCENE);
      sp.setQuery(luceneQuery.toString());
      sp.addSort(sortOn, sortAscending);
      if (paging.getSkipCount() > 0)
      {
          sp.setSkipCount(paging.getSkipCount());
      }
      
      
      // Build the results
      PagingResults<TopicInfo> pagedResults = new EmptyPagingResults<TopicInfo>();
      ResultSet results = null;
      
      try 
      {
         results = searchService.query(sp);
         pagedResults = wrap(results, nodeRef, paging);
      }
      finally
      {
         if (results != null)
         {
            results.close();
         }
      }
      
      return pagedResults;
   }

   
   @Override
   public PagingResults<PostInfo> listPosts(TopicInfo topic, PagingRequest paging)
   {
      // Do the listing, oldest first
      CannedQueryResults<NodeBackedEntity> nodes = 
         listEntries(topic.getNodeRef(), ForumModel.TYPE_POST, null, null, null, true, paging);
      
      // Wrap and return
      return wrap(nodes, topic);
   }


   @Override
   public PostWithReplies listPostReplies(TopicInfo topic, int levels)
   {
       PostInfo primaryPost = getPrimaryPost(topic);
       if (primaryPost == null)
       {
          return null;
       }
       return listPostReplies(primaryPost, levels);
   }
   
   @Override
   public PostWithReplies listPostReplies(PostInfo primaryPost, int levels)
   {
      // Grab the factory
      GetChildrenWithTargetAssocsAuditableCannedQueryFactory cqFactory = 
                  (GetChildrenWithTargetAssocsAuditableCannedQueryFactory)cannedQueryRegistry.getNamedObject(CANNED_QUERY_GET_CHILDREN_TARGETS);
      
      // Sort by date
      CannedQuerySortDetails sorting = cqFactory.createDateAscendingCQSortDetails();
      
      // Run the canned query
      GetChildrenWithTargetAssocsAuditableCannedQuery cq = (GetChildrenWithTargetAssocsAuditableCannedQuery)cqFactory.getCannedQuery(
            primaryPost.getTopic().getNodeRef(), ForumModel.TYPE_POST,
            ContentModel.ASSOC_REFERENCES, sorting, new PagingRequest(MAX_REPLIES_FETCH_SIZE));
      
      // Execute the canned query
      CannedQueryResults<NodeWithTargetsEntity> results = cq.execute();
      
      // Prepare to invert
      Map<Long,NodeRef> idToNodeRef = new HashMap<Long, NodeRef>();
      for(NodeWithTargetsEntity e : results.getPage())
      {
         idToNodeRef.put(e.getId(), tenantService.getBaseName(e.getNodeRef()));
      }
      
      Map<NodeRef,List<NodeWithTargetsEntity>> idToReplies = new HashMap<NodeRef, List<NodeWithTargetsEntity>>();
      for (NodeWithTargetsEntity e : results.getPage())
      {
         for (TargetAndTypeId idP : e.getTargetIds())
         {
            Long id = idP.getTargetId();
            NodeRef nodeRef = idToNodeRef.get(id);
            if (nodeRef == null)
            {
               // References a node outside of this topic
               continue;
            }
            nodeRef = tenantService.getBaseName(nodeRef);
            if (id.equals(e.getId()))
            {
               // Self reference
               continue;
            }
            if (! idToReplies.containsKey(nodeRef))
            {
               idToReplies.put(nodeRef, new ArrayList<NodeWithTargetsEntity>());
            }
            idToReplies.get(nodeRef).add(e);
         }
      }
      
      // Grab the list of NodeRefs to pre-load, and pre-load them
      List<NodeRef> preLoad = new ArrayList<NodeRef>();
      calculateRepliesPreLoad(primaryPost.getNodeRef(), preLoad, idToReplies, levels);
      nodeDAO.cacheNodes(preLoad);
      
      // Wrap
      return wrap(primaryPost, idToReplies, levels);
   }
   
   private void calculateRepliesPreLoad(NodeRef nodeRef, List<NodeRef> preLoad, 
         Map<NodeRef,List<NodeWithTargetsEntity>> idToReplies, int levels)
   {
      preLoad.add(nodeRef);
      if (levels > 0)
      {
         List<NodeWithTargetsEntity> replies = idToReplies.get(nodeRef);
         if (replies != null && replies.size() > 0)
         {
            for (NodeWithTargetsEntity entity : replies)
            {
               calculateRepliesPreLoad(tenantService.getBaseName(entity.getNodeRef()), preLoad, idToReplies, levels-1);
            }
         }
      }
   }
   
   private PostWithReplies wrap(PostInfo post, Map<NodeRef,List<NodeWithTargetsEntity>> idToReplies, int levels)
   {
      List<PostWithReplies> replies = new ArrayList<PostWithReplies>();
      if (levels > 0)
      {
         List<NodeWithTargetsEntity> replyEntities = idToReplies.get(post.getNodeRef());
         if (replyEntities != null && replyEntities.size() > 0)
         {
            for (NodeWithTargetsEntity entity : replyEntities)
            {
               PostInfo replyPost = buildPost(tenantService.getBaseName(entity.getNodeRef()), post.getTopic(), entity.getName(), null);
               replies.add(wrap(replyPost, idToReplies, levels-1));
            }
         }
      }
      return new PostWithReplies(post, replies);
   }


   /**
    * Finds nodes in the specified parent container, with the given
    *  type, optionally filtered by creator
    */
   private CannedQueryResults<NodeBackedEntity> listEntries(NodeRef parent, 
         QName nodeType, String creatorUsername, Date from, Date to, 
         boolean oldestFirst, PagingRequest paging) 
   {
      // The Canned Query system doesn't allow for zero sized pages
      // If they asked for that (bits of share sometimes do), bail out now
      if (paging != null && paging.getMaxItems() == 0)
      {
         return new EmptyCannedQueryResults<NodeBackedEntity>(null);
      }
      
      // Grab the factory
      GetChildrenAuditableCannedQueryFactory getChildrenCannedQueryFactory = (GetChildrenAuditableCannedQueryFactory)cannedQueryRegistry.getNamedObject(CANNED_QUERY_GET_CHILDREN);
      
      // Do the sorting, newest first or last by created date (as requested)
      CannedQuerySortDetails sorting = null;
      if (oldestFirst)
      {
         sorting = getChildrenCannedQueryFactory.createDateAscendingCQSortDetails();
      }
      else
      {
         sorting = getChildrenCannedQueryFactory.createDateDescendingCQSortDetails();
      }
      
      // Run the canned query
      GetChildrenAuditableCannedQuery cq = (GetChildrenAuditableCannedQuery)getChildrenCannedQueryFactory.getCannedQuery(
            parent, nodeType, creatorUsername, from, to, null,
            null, null, sorting, paging);
      
      // Execute the canned query
      CannedQueryResults<NodeBackedEntity> results = cq.execute();
      
      // Return for wrapping
      return results;
   }
   
   /**
    * Our class to wrap up search results as {@link TopicInfo} instances
    */
   private PagingResults<TopicInfo> wrap(final ResultSet finalLuceneResults, final NodeRef container, final PagingRequest paging)
   {
      final List<TopicInfo> topics = new ArrayList<TopicInfo>();
      int cnt = 1;
      for (ResultSetRow row : finalLuceneResults)
      {
         TopicInfo topic = buildTopic(
               row.getNodeRef(), container, row.getQName().getLocalName());
         topics.add(topic);
         cnt++;
         if (paging.getMaxItems()>0 && cnt>paging.getMaxItems())
         {
             break;
         }
      }
      
      // Wrap
      return new PagingResults<TopicInfo>() 
      {
         @Override
         public boolean hasMoreItems() 
         {
            try
            {
               return finalLuceneResults.hasMore();
            }
            catch(UnsupportedOperationException e)
            {
               // Not all lucene results support paging
               return false;
            }
         }

         @Override
         public Pair<Integer, Integer> getTotalResultCount() 
         {
            int skipCount = 0;
            int itemsRemainingAfterThisPage = 0;
            try
            {
               skipCount = finalLuceneResults.getStart();
            }
            catch(UnsupportedOperationException e) {
               skipCount = paging.getSkipCount();
            }
            try
            {
               itemsRemainingAfterThisPage = finalLuceneResults.length();
            }
            catch(UnsupportedOperationException e) {}
            
            final int totalItemsInUnpagedResultSet = skipCount + itemsRemainingAfterThisPage;
            return new Pair<Integer, Integer>(totalItemsInUnpagedResultSet, totalItemsInUnpagedResultSet);
         }

         @Override
         public List<TopicInfo> getPage() 
         {
            return topics;
         }

         @Override
         public String getQueryExecutionId() 
         {
            return null;
         }
     };
   }
   
   /**
    * Our class to wrap up paged results of NodeBackedEntities as
    *  {@link TopicInfo} instances
    */
   private PagingResults<TopicInfo> wrap(final PagingResults<NodeBackedEntity> results, final NodeRef container)
   {
      // Pre-load the nodes before we create them
      List<Long> ids = new ArrayList<Long>();
      for (NodeBackedEntity node : results.getPage())
      {
         ids.add(node.getId());
      }
      nodeDAO.cacheNodesById(ids);
      
      // Wrap
      return new PagingResults<TopicInfo>()
      {
          @Override
          public String getQueryExecutionId()
          {
              return results.getQueryExecutionId();
          }
          
          @Override
          public List<TopicInfo> getPage()
          {
              List<TopicInfo> topics = new ArrayList<TopicInfo>();
              for (NodeBackedEntity node : results.getPage())
              {
                 NodeRef nodeRef = node.getNodeRef();
                 String name = node.getName();
                 topics.add(buildTopic(nodeRef, container, name));
              }
              return topics;
          }
          
          @Override
          public boolean hasMoreItems()
          {
              return results.hasMoreItems();
          }
          
          @Override
          public Pair<Integer, Integer> getTotalResultCount()
          {
              return results.getTotalResultCount();
          }
      };
   }
   
   /**
    * Our class to wrap up paged results of NodeBackedEntities as
    *  {@link PostInfo} instances
    */
   private PagingResults<PostInfo> wrap(final PagingResults<NodeBackedEntity> results, final TopicInfo topic)
   {
      // Pre-load the nodes before we create them
      List<Long> ids = new ArrayList<Long>();
      for (NodeBackedEntity node : results.getPage())
      {
         ids.add(node.getId());
      }
      nodeDAO.cacheNodesById(ids);
      
      // Wrap
      return new PagingResults<PostInfo>()
      {
          @Override
          public String getQueryExecutionId()
          {
              return results.getQueryExecutionId();
          }
          
          @Override
          public List<PostInfo> getPage()
          {
              List<PostInfo> posts = new ArrayList<PostInfo>();
              for (NodeBackedEntity node : results.getPage())
              {
                 NodeRef nodeRef = node.getNodeRef();
                 String name = node.getName();
                 posts.add(buildPost(nodeRef, topic, name, null));
              }
              return posts;
          }
          
          @Override
          public boolean hasMoreItems()
          {
              return results.hasMoreItems();
          }
          
          @Override
          public Pair<Integer, Integer> getTotalResultCount()
          {
              return results.getTotalResultCount();
          }
      };
   }
   
   /**
    * Our class to wrap up paged results of NodeWithChildrenEntity as
    *  {@link TopicInfo} instances
    */
   private PagingResults<Pair<TopicInfo,Integer>> wrapWithCount(final PagingResults<NodeWithChildrenEntity> results, final NodeRef container)
   {
      // Pre-load the nodes before we create them
      List<Long> ids = new ArrayList<Long>();
      for (NodeBackedEntity node : results.getPage())
      {
         ids.add(node.getId());
      }
      nodeDAO.cacheNodesById(ids);
      
      // Wrap
      return new PagingResults<Pair<TopicInfo,Integer>>()
      {
          @Override
          public String getQueryExecutionId()
          {
              return results.getQueryExecutionId();
          }
          
          @Override
          public List<Pair<TopicInfo,Integer>> getPage()
          {
              List<Pair<TopicInfo,Integer>> topics = new ArrayList<Pair<TopicInfo,Integer>>();
              for (NodeWithChildrenEntity node : results.getPage())
              {
                 NodeRef nodeRef = node.getNodeRef();
                 String name = node.getName();
                 int count = node.getChildren().size();
                 
                 TopicInfo topic = buildTopic(nodeRef, container, name);
                 topics.add(new Pair<TopicInfo,Integer>(topic, count));
              }
              return topics;
          }
          
          @Override
          public boolean hasMoreItems()
          {
              return results.hasMoreItems();
          }
          
          @Override
          public Pair<Integer, Integer> getTotalResultCount()
          {
              return results.getTotalResultCount();
          }
      };
   }
}
