/*
 * Copyright (C) 2003-2007 eXo Platform SAS.
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Affero General Public License
 * as published by the Free Software Foundation; either version 3
 * of the License, or (at your option) any later version.
 *
 * This program 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 this program; if not, see<http://www.gnu.org/licenses/>.
 */
package org.exoplatform.services.cms.comments.impl;

import java.util.ArrayList;
import java.util.Calendar;
import java.util.Collections;
import java.util.Comparator;
import java.util.Date;
import java.util.GregorianCalendar;
import java.util.List;

import javax.jcr.Node;
import javax.jcr.NodeIterator;
import javax.jcr.Session;

import org.exoplatform.commons.utils.ActivityTypeUtils;
import org.exoplatform.services.cache.CacheService;
import org.exoplatform.services.cache.ExoCache;
import org.exoplatform.services.cms.comments.CommentsService;
import org.exoplatform.services.cms.i18n.MultiLanguageService;
import org.exoplatform.services.cms.jcrext.activity.ActivityCommonService;
import org.exoplatform.services.jcr.core.ManageableRepository;
import org.exoplatform.services.listener.ListenerService;
import org.exoplatform.services.log.ExoLogger;
import org.exoplatform.services.log.Log;
import org.exoplatform.services.organization.OrganizationService;
import org.exoplatform.services.organization.User;
import org.exoplatform.services.wcm.core.NodetypeConstant;
import org.exoplatform.services.wcm.utils.WCMCoreUtils;

public class CommentsServiceImpl implements CommentsService {

  private static final Log LOG = ExoLogger.getLogger(CommentsServiceImpl.class.getName());

  private final static String COMMENTS = "comments" ;
  private final static String COMMENTABLE = "mix:commentable" ;
  private final static String EXO_COMMENTS = "exo:comments" ;
  private final static String NT_UNSTRUCTURE = "nt:unstructured" ;
  private final static String MESSAGE = "exo:commentContent" ;
  private final static String COMMENTOR = "exo:commentor" ;
  private final static String COMMENTOR_FULLNAME = "exo:commentorFullName" ;
  private final static String COMMENTOR_EMAIL = "exo:commentorEmail" ;
  private final static String COMMENTOR_SITE = "exo:commentorSite" ;
  private final static String CREATED_DATE = "exo:commentDate" ;
  private static final String LANGUAGES = "languages" ;
  private static final String ANONYMOUS = "anonymous" ;

  private ExoCache<String, List<Node>> commentsCache_ ;
  private MultiLanguageService         multiLangService_ ;
  private ListenerService              listenerService;
  private ActivityCommonService        activityService;
  /**
   * Constructor Method
   * @param cacheService        CacheService Object
   * @param multiLangService    MultiLanguageService Object
   */
  public CommentsServiceImpl(CacheService cacheService,
                             MultiLanguageService multiLangService) throws Exception {
    commentsCache_ = cacheService.getCacheInstance(CommentsService.class.getName()) ;
    multiLangService_ = multiLangService ;
    activityService = WCMCoreUtils.getService(ActivityCommonService.class);
  }

  /**
   * {@inheritDoc}
   */
  public void addComment(Node node, String commentor,String email, String site, String comment,String language)
      throws Exception {
    if (listenerService==null) {
      listenerService = WCMCoreUtils.getService(ListenerService.class);
    }
    Session session = node.getSession();
    ManageableRepository  repository = (ManageableRepository)session.getRepository();
    //TODO check if really need delegate to system session
    Session systemSession = repository.getSystemSession(session.getWorkspace().getName()) ;
    try {
      Node document = (Node)systemSession.getItem(node.getPath()) ;
      if(!document.isNodeType(COMMENTABLE)) {
        if(document.canAddMixin(COMMENTABLE)) document.addMixin(COMMENTABLE) ;
        else throw new Exception("This node does not support comments.") ;
      }
      Node multiLanguages =null, languageNode= null, commentNode = null ;

      if(!document.hasNode(LANGUAGES) || language.equals(multiLangService_.getDefault(document))) {
        if(document.hasNode(COMMENTS)) commentNode = document.getNode(COMMENTS) ;
        else {
          commentNode = document.addNode(COMMENTS,NT_UNSTRUCTURE) ;          
          commentNode.addMixin("exo:hiddenable");
        }
      } else {
        multiLanguages = document.getNode(LANGUAGES) ;
        if(multiLanguages.hasNode(language)) {
          languageNode = multiLanguages.getNode(language) ;
        } else {
          languageNode = multiLanguages.addNode(language) ;
        }
        if(languageNode.hasNode(COMMENTS)) {
          commentNode = languageNode.getNode(COMMENTS) ;
        } else{
          commentNode = languageNode.addNode(COMMENTS,NT_UNSTRUCTURE) ;
          commentNode.addMixin("exo:hiddenable");
        }
      }

      if(commentor == null || commentor.length() == 0) {
        commentor = ANONYMOUS ;
      }      

      Calendar commentDate = new GregorianCalendar() ;
      String name = Long.toString(commentDate.getTimeInMillis()) ;
      Node newComment = commentNode.addNode(name,EXO_COMMENTS) ;
      newComment.setProperty(COMMENTOR,commentor) ;

      OrganizationService organizationService = WCMCoreUtils.getService(OrganizationService.class);
      User user = organizationService.getUserHandler().findUserByName(commentor);
     
      if(user == null)
        newComment.setProperty(COMMENTOR_FULLNAME,"ANONYMOUS") ;
      else {
        String fullName = user.getDisplayName();
        if(fullName == null) fullName = user.getUserName();
        newComment.setProperty(COMMENTOR_FULLNAME,fullName) ; 
      }
      newComment.setProperty(CREATED_DATE,commentDate) ;
      newComment.setProperty(MESSAGE,comment) ;
      if(email!=null && email.length()>0) {
        newComment.setProperty(COMMENTOR_EMAIL,email) ;
      }
      if(site !=null && site.length()>0) {
        newComment.setProperty(COMMENTOR_SITE,site) ;
      }
      document.save();
      systemSession.save();
      if (listenerService!=null) {
        try {
          if (activityService.isAcceptedNode(document) 
              || (document.getPrimaryNodeType().getName().equals(NodetypeConstant.NT_FILE) 
                  && activityService.isBroadcastNTFileEvents(document))) {
            listenerService.broadcast(ActivityCommonService.COMMENT_ADDED_ACTIVITY, document, newComment);
          }
        } catch (Exception e) {
          if (LOG.isErrorEnabled()) {
            LOG.error("Can not notify CommentAddedActivity because of: " + e.getMessage());
          }
        }
      }
      commentsCache_.remove(commentNode.getPath()) ;
    } catch(Exception e) {
      if (LOG.isErrorEnabled()) {
        LOG.error("Unexpected problem happen when try to add comment", e);
      }
    } finally {
      systemSession.logout();
    }

  }

  /**
   * {@inheritDoc}
   */
  public void updateComment(Node commentNode, String newComment) throws Exception {
    Calendar commentDate = new GregorianCalendar() ;
    commentNode.setProperty(CREATED_DATE, commentDate);
    commentNode.setProperty(MESSAGE, newComment);
    commentNode.save();
    Node documentNode = commentNode.getParent().getParent();
    if (listenerService!=null && activityService!=null) {
      try {
        if (activityService.isAcceptedNode(documentNode) || 
            (documentNode.getPrimaryNodeType().getName().equals(NodetypeConstant.NT_FILE) && 
                activityService.isBroadcastNTFileEvents(documentNode))) {
          listenerService.broadcast(ActivityCommonService.COMMENT_UPDATED_ACTIVITY, documentNode, commentNode);
        }
      } catch (Exception e) {
        if (LOG.isErrorEnabled()) {
          LOG.error("Can not notify CommentModifiedActivity because of: " + e.getMessage());
        }
      }
    }
  }

  /**
   * {@inheritDoc}
   */
  public void deleteComment(Node commentNode) throws Exception {
    Node document = commentNode.getParent();
    String activityID;
    try {
      activityID = ActivityTypeUtils.getActivityId(commentNode);
    }catch (Exception e) {
      activityID = null;
    }
    commentNode.remove();
    document.save();    
    if (listenerService!=null && activityID !=null && activityService !=null) {
      Node parentNode = document.getParent();
      try {
        if (activityService.isAcceptedNode(parentNode) || 
            (parentNode.getPrimaryNodeType().getName().equals(NodetypeConstant.NT_FILE) && 
                activityService.isBroadcastNTFileEvents(parentNode))) {
          listenerService.broadcast(ActivityCommonService.COMMENT_REMOVED_ACTIVITY, parentNode, activityID);
        }
      } catch (Exception e) {
        if (LOG.isErrorEnabled()) {
          LOG.error("Can not notify CommentRemovedActivity because of: " + e.getMessage());
        }
      }
    }
  }

  /**
   * {@inheritDoc}
   */
  @SuppressWarnings("unchecked")
  public List<Node> getComments(Node document,String language) throws Exception {
    Node commentsNode = null ;
    Node languagesNode = null ;
    Node languageNode = null ;
    if(!isSupportedLocalize(document,language)) {
      if(document.hasProperty("exo:language")) language = document.getProperty("exo:language").getString() ;
    }
    if(document.hasNode(LANGUAGES)) {
      languagesNode = document.getNode(LANGUAGES) ;
      if(languagesNode.hasNode(language)) {
        languageNode = languagesNode.getNode(language) ;
        if(languageNode.hasNode(COMMENTS)) commentsNode = languageNode.getNode(COMMENTS) ;
      } else if(language.equals(multiLangService_.getDefault(document))) {
        languageNode = document ;
      }
    } else {
      languageNode = document ;
    }
    if(!languageNode.hasNode(COMMENTS)) return new ArrayList<Node>() ;
    Session session = document.getSession();
    //TODO check if really need delegate to system session
    Session systemSession = WCMCoreUtils.getSystemSessionProvider().getSession(session.getWorkspace().getName(),
                                                                               WCMCoreUtils.getRepository()) ;
    List<Node> list = new ArrayList<Node>() ;
    try {
      commentsNode = (Node)systemSession.getItem(languageNode.getPath() + "/" + COMMENTS) ;
      String cacheKey = document.getPath().concat(commentsNode.getPath());
      Object comments = commentsCache_.get(cacheKey) ;
      if(comments !=null) return (List<Node>)comments ;
      for(NodeIterator iter = commentsNode.getNodes(); iter.hasNext();) {
        list.add(iter.nextNode()) ;
      }
      Collections.sort(list,new DateComparator()) ;
      commentsCache_.put(commentsNode.getPath(),list) ;
    } catch(Exception e) {
      if (LOG.isErrorEnabled()) {
        LOG.error("Unexpected problem happen when try to get comments", e);
      }
    }
    return list;
  }


  /**
   * This Class implements Comparator<Node> to compare the created date of nodes.
   */
  private class DateComparator implements Comparator<Node> {

    /**
     * Compare the created date of nodes
     * @param node1     node is used to compare
     * @param node2     node is used to compare
     */
    public int compare(Node node1, Node node2) {
      try{
        Date date1 = node1.getProperty(CREATED_DATE).getDate().getTime() ;
        Date date2 = node2.getProperty(CREATED_DATE).getDate().getTime() ;
        return date2.compareTo(date1) ;
      }catch (Exception e) {
        return 0;
      }
    }
  }

  /**
   * Check language of comment is supported in a document
   * @param  document    The document node is commented
   * @param  language    The language of comment node
   * @throws Exception
   */
  private boolean isSupportedLocalize(Node document,String language)throws Exception {
    List<String> locales= multiLangService_.getSupportedLanguages(document) ;
    if(Collections.frequency(locales,language) >0) return true ;
    return false ;
  }

}
