/*
 * 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.watch.impl;

import java.util.ArrayList;
import java.util.List;

import javax.jcr.Node;
import javax.jcr.NodeIterator;
import javax.jcr.Session;
import javax.jcr.Value;
import javax.jcr.nodetype.NodeType;
import javax.jcr.observation.Event;
import javax.jcr.observation.EventListener;
import javax.jcr.observation.ObservationManager;
import javax.jcr.query.Query;
import javax.jcr.query.QueryManager;
import javax.jcr.query.QueryResult;

import org.exoplatform.container.xml.InitParams;
import org.exoplatform.services.cms.templates.TemplateService;
import org.exoplatform.services.cms.watch.WatchDocumentService;
import org.exoplatform.services.jcr.RepositoryService;
import org.exoplatform.services.jcr.config.RepositoryEntry;
import org.exoplatform.services.jcr.core.ManageableRepository;
import org.exoplatform.services.log.ExoLogger;
import org.exoplatform.services.log.Log;
import org.picocontainer.Startable;


/**
 * Created by eXo Platform
 * Author : Pham Xuan Hoa
 *          hoapham@exoplatform.com
 * Nov 30, 2006  
 */
public class WatchDocumentServiceImpl implements WatchDocumentService, Startable {      

  final public static String EXO_WATCHABLE_MIXIN = "exo:watchable".intern() ;  
  final public static String EMAIL_WATCHERS_PROP = "exo:emailWatcher".intern() ;
  final public static String RSS_WATCHERS_PROP = "exo:rssWatcher".intern() ;
  final private String initParamName = "messageConfig".intern();
  final private static String WATCHABLE_MIXIN_QUERY = "//element(*,exo:watchable)" ;

  private RepositoryService repoService_ ;
  private MessageConfig messageConfig_ ;
  private TemplateService templateService_ ;
  private static final Log LOG  = ExoLogger.getLogger(WatchDocumentServiceImpl.class);

  /**
   * Constructor Method 
   * @param params
   * @param repoService
   * @param templateService
   */
  public WatchDocumentServiceImpl(InitParams params, 
      RepositoryService repoService, TemplateService templateService) {        
    repoService_ = repoService ;
    templateService_ = templateService ;
    messageConfig_ = 
      (MessageConfig)params.getObjectParam(initParamName).getObject() ;    
  }

  /**
   * {@inheritDoc}
   */
  public int getNotificationType(Node documentNode, String userName) throws Exception {
    NodeType[] mixinTypes = documentNode.getMixinNodeTypes() ;
    NodeType watchableMixin = null ;
    if(mixinTypes.length>0) {
      for(NodeType nodeType: mixinTypes) {
        if(nodeType.getName().equalsIgnoreCase(EXO_WATCHABLE_MIXIN)) {
          watchableMixin = nodeType ;
          break ;
        }
      }      
    }
    if(watchableMixin == null)  return -1 ;
    boolean notifyByEmail = checkNotifyTypeOfWatcher(documentNode,userName,EMAIL_WATCHERS_PROP) ;
    boolean notifyByRss = checkNotifyTypeOfWatcher(documentNode,userName,RSS_WATCHERS_PROP) ;
    if( notifyByEmail && notifyByRss) return FULL_NOTIFICATION ;
    if(notifyByEmail) return NOTIFICATION_BY_EMAIL ;
    if(notifyByRss) return NOTIFICATION_BY_RSS ;
    return -1 ;
  }

  /**
   * {@inheritDoc}
   */
  public void watchDocument(Node documentNode, String userName, int notifyType) throws Exception {
    Session session = documentNode.getSession() ;
    Value newWatcher = session.getValueFactory().createValue(userName) ;
    if(!documentNode.isNodeType(EXO_WATCHABLE_MIXIN)) {
      documentNode.addMixin(EXO_WATCHABLE_MIXIN) ;
      if(notifyType == NOTIFICATION_BY_EMAIL) {
        documentNode.setProperty(EMAIL_WATCHERS_PROP,new Value[] {newWatcher}) ;
        documentNode.save() ;
        session.save() ;                
        EmailNotifyListener listener = new EmailNotifyListener(documentNode) ;                
        observeNode(documentNode,listener) ;        
      }        
      session.save() ;
    } else {      
      List<Value>  watcherList = new ArrayList<Value>() ;
      if(notifyType == NOTIFICATION_BY_EMAIL) {
        if(documentNode.hasProperty(EMAIL_WATCHERS_PROP)) {
          for(Value watcher : documentNode.getProperty(EMAIL_WATCHERS_PROP).getValues()) {
            watcherList.add(watcher) ;
          }
          watcherList.add(newWatcher) ;
        }
        
        documentNode.setProperty(EMAIL_WATCHERS_PROP,watcherList.toArray(new Value[watcherList.size()])) ;
        documentNode.save() ;
      }
      session.save() ;
      session.logout() ;
    }
  }

  /**
   * {@inheritDoc}
   */
  public void unwatchDocument(Node documentNode, String userName, int notificationType) throws Exception {
    if(!documentNode.isNodeType(EXO_WATCHABLE_MIXIN)) return  ;
    Session session = documentNode.getSession() ;
    if(notificationType == NOTIFICATION_BY_EMAIL) {
      Value[] watchers = documentNode.getProperty(EMAIL_WATCHERS_PROP).getValues() ;
      List<Value> watcherList = new ArrayList<Value>() ;
      for(Value watcher: watchers) {
        if(!watcher.getString().equals(userName)) {
          watcherList.add(watcher) ;
        }
      }
      documentNode.setProperty(EMAIL_WATCHERS_PROP,watcherList.toArray(new Value[watcherList.size()])) ;
    }
    documentNode.save() ;  
    session.save() ;
    session.logout() ;
  }  
  
  /**
   * This method will observes the specification node by giving the following param : listener, node
   * Its add an event listener to this node to observes anything that changes to this
   * @param node              Specify the node to observe
   * @param listener          The object of EventListener
   * @see                     EventListener
   * @see                     Node
   * @throws Exception
   */
  private void observeNode(Node node, EventListener listener) throws Exception {
    String workspace = node.getSession().getWorkspace().getName() ;
    ManageableRepository manageRepo = (ManageableRepository)node.getSession().getRepository() ;
    String repository = manageRepo.getConfiguration().getName() ;
    Session systemSession = repoService_.getRepository(repository).getSystemSession(workspace) ;
    List<String> list = getDocumentNodeTypes(node) ;          
    String[] observedNodeTypeNames = list.toArray(new String[list.size()]) ;
    ObservationManager observationManager = systemSession.getWorkspace().getObservationManager() ;
    observationManager.addEventListener(listener,Event.PROPERTY_CHANGED,
        node.getPath(),true,null,observedNodeTypeNames,false) ;
    systemSession.logout();
  }

  /**
   * This method will check notify type of watcher, userName is equal value of property with notification type 
   * @param documentNode    specify a node to watch
   * @param userName        userName to watch a document
   * @param notification    Notification Type
   * @return boolean
   * @throws Exception
   */
  private boolean checkNotifyTypeOfWatcher(Node documentNode, String userName,String notificationType) throws Exception {
    if(documentNode.hasProperty(notificationType)) {
      Value [] watchers = documentNode.getProperty(notificationType).getValues() ;
      for(Value value: watchers) {
        if(userName.equalsIgnoreCase(value.getString())) return true ;
      }
    }
    return false ;
  }  

  /**
   * This method will get all node types of node.
   * @param node
   * @return
   * @throws Exception
   */
  private List<String> getDocumentNodeTypes(Node node) throws Exception {
    List<String> nodeTypeNameList = new ArrayList<String>() ;
    NodeType  primaryType = node.getPrimaryNodeType() ;
    ManageableRepository manaRepository = (ManageableRepository)node.getSession().getRepository() ;
    String repository = manaRepository.getConfiguration().getName() ;
    if(templateService_.isManagedNodeType(primaryType.getName(), repository)) {
      nodeTypeNameList.add(primaryType.getName()) ;
    }    
    for(NodeType nodeType: node.getMixinNodeTypes()) {
      if(templateService_.isManagedNodeType(nodeType.getName(), repository)) {
        nodeTypeNameList.add(nodeType.getName()) ;
      }
    }
    return nodeTypeNameList ;
  }
  
  /**
   * This method will re-observer all nodes that have been ever observed with all repositories.
   * @throws Exception
   */
  private void reInitObserver() throws Exception {
    List<RepositoryEntry> repositories = repoService_.getConfig().getRepositoryConfigurations() ;
    for(RepositoryEntry repo : repositories) {
      ManageableRepository repository = repoService_.getRepository(repo.getName()) ;
      String[] workspaceNames = repository.getWorkspaceNames() ;
      for(String workspace: workspaceNames) {
        Session session = repository.getSystemSession(workspace) ;
        QueryManager queryManager = null ;
        try{
          queryManager = session.getWorkspace().getQueryManager() ;
        } catch (Exception e) { 
          LOG.error("Unexpected error", e);
        }
        if(queryManager == null) { 
          session.logout(); 
          continue ;
        }
        try {        
          Query query = queryManager.createQuery(WATCHABLE_MIXIN_QUERY,Query.XPATH) ;
          QueryResult queryResult = query.execute() ;
          for(NodeIterator iter = queryResult.getNodes(); iter.hasNext(); ) {          
            Node observedNode = iter.nextNode() ;
            EmailNotifyListener emailNotifyListener = new EmailNotifyListener(observedNode) ;
            ObservationManager manager = session.getWorkspace().getObservationManager() ;
            List<String> list = getDocumentNodeTypes(observedNode) ;          
            String[] observedNodeTypeNames = list.toArray(new String[list.size()]) ;          
            manager.addEventListener(emailNotifyListener,Event.PROPERTY_CHANGED,
                observedNode.getPath(),true,null,observedNodeTypeNames,false) ;          
          }
          session.logout();
        } catch (Exception e) {
          System.out.println("==>>> Cannot init observer for node: " 
              +e.getLocalizedMessage() + " in '"+repo.getName()+"' repository");
          LOG.error("Unexpected error", e);
        }
      }
    }
  }    

  /**
   * This method will get message configuration when a node is observing and there is some changes 
   * with it's properties.
   * @return MessageCongig
   */
  protected MessageConfig getMessageConfig() { return messageConfig_ ; }

  /**
   * using for re-observer
   */
  public void start() {
    try {
      reInitObserver() ;
    }catch (Exception e) {
      System.out.println("==>>> Exeption when startd WatchDocumentSerice!!!!");
    }
  }

  /**
   * {@inheritDoc}
   */
  public void stop() { }
}
