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

import org.exoplatform.commons.utils.ISO8601;
import org.exoplatform.container.xml.InitParams;
import org.exoplatform.container.xml.PortalContainerInfo;
import org.exoplatform.services.cache.CacheService;
import org.exoplatform.services.cache.ExoCache;
import org.exoplatform.services.cms.BasePath;
import org.exoplatform.services.cms.impl.DMSConfiguration;
import org.exoplatform.services.cms.impl.DMSRepositoryConfiguration;
import org.exoplatform.services.cms.queries.QueryService;
import org.exoplatform.services.jcr.RepositoryService;
import org.exoplatform.services.jcr.access.PermissionType;
import org.exoplatform.services.jcr.core.ExtendedNode;
import org.exoplatform.services.jcr.core.ManageableRepository;
import org.exoplatform.services.jcr.ext.common.SessionProvider;
import org.exoplatform.services.jcr.ext.hierarchy.NodeHierarchyCreator;
import org.exoplatform.services.log.ExoLogger;
import org.exoplatform.services.log.Log;
import org.exoplatform.services.organization.MembershipHandler;
import org.exoplatform.services.organization.OrganizationService;
import org.exoplatform.services.security.ConversationState;
import org.exoplatform.services.security.IdentityConstants;
import org.exoplatform.services.security.Identity;
import org.exoplatform.services.wcm.utils.WCMCoreUtils;
import org.exoplatform.web.application.RequestContext;
import org.picocontainer.Startable;

import javax.jcr.*;
import javax.jcr.query.Query;
import javax.jcr.query.QueryManager;
import javax.jcr.query.QueryResult;

import java.util.*;

public class QueryServiceImpl implements QueryService, Startable{
  private static final String[] perms = {PermissionType.READ, PermissionType.ADD_NODE,
    PermissionType.SET_PROPERTY, PermissionType.REMOVE };
  private String relativePath_;
  private List<QueryPlugin> queryPlugins_ = new ArrayList<QueryPlugin> ();
  private RepositoryService repositoryService_;
  private ExoCache<String, QueryResult> cache_;
  private PortalContainerInfo containerInfo_;
  private OrganizationService organizationService_;
  private String baseUserPath_;
  private String baseQueriesPath_;
  private String group_;
  private DMSConfiguration dmsConfiguration_;
  private Set<String> configuredQueries_;
  
  private NodeHierarchyCreator nodeHierarchyCreator_;

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

  /**
   * Constructor method
   * @param repositoryService
   * @param nodeHierarchyCreator
   * @param params
   * @param containerInfo
   * @param cacheService
   * @param organizationService
   * @param dmsConfiguration
   * @throws Exception
   */
  public QueryServiceImpl(RepositoryService repositoryService, NodeHierarchyCreator nodeHierarchyCreator,
      InitParams params, PortalContainerInfo containerInfo, CacheService cacheService,
      OrganizationService organizationService, DMSConfiguration dmsConfiguration) throws Exception {
    relativePath_ = params.getValueParam("relativePath").getValue();
    group_ = params.getValueParam("group").getValue();
    repositoryService_ = repositoryService;
    containerInfo_ = containerInfo;
    cache_ = cacheService.getCacheInstance(CACHE_NAME);
    organizationService_ = organizationService;
    nodeHierarchyCreator_ = nodeHierarchyCreator;
    baseUserPath_ = nodeHierarchyCreator.getJcrPath(BasePath.CMS_USERS_PATH);
    baseQueriesPath_ = nodeHierarchyCreator.getJcrPath(BasePath.QUERIES_PATH);
    dmsConfiguration_ = dmsConfiguration;
  }

  /**
   * Implemented method from Startable class
   * init all ManageDrivePlugin
   * @see QueryPlugin
   */
  public void start() {
    configuredQueries_ = new HashSet<String>();
    for(QueryPlugin queryPlugin : queryPlugins_){
      try{
        queryPlugin.init(baseQueriesPath_);
        configuredQueries_.addAll(queryPlugin.getAllConfiguredQueries());
      }catch (Exception e) {
        if (LOG.isErrorEnabled()) {
          LOG.error("Can not start query plugin '" + queryPlugin.getName() + "'", e);
        }
      }
    }
  }

  /**
   * Implemented method from Startable class
   */
  public void stop() {
  }
  
  /**
   * Init query node with current repository
   */
  public void init() throws Exception {
    configuredQueries_ = new HashSet<String>();
    for(QueryPlugin queryPlugin : queryPlugins_){
      try{
        queryPlugin.init(baseQueriesPath_);
        configuredQueries_.addAll(queryPlugin.getAllConfiguredQueries());
      }catch (Exception e) {
        if (LOG.isErrorEnabled()) {
          LOG.error("Can not init query plugin '" + queryPlugin.getName() + "'", e);
        }
      }
    }
  }  

  /**
   * Add new QueryPlugin to queryPlugins_
   * @see                   QueryPlugin
   * @param queryPlugin     QueryPlugin
   */
  public void setQueryPlugin(QueryPlugin queryPlugin) {
    queryPlugins_.add(queryPlugin);
  }

  /**
   * {@inheritDoc}
   */
  public String getRelativePath() { return relativePath_; }
  
  /**
   * {@inheritDoc}
   */
  public List<Query> getQueries(String userName, SessionProvider provider) throws Exception {
    List<Query> queries = new ArrayList<Query>();
    if (userName == null) return queries;
    Session session = getSession(provider, true);
    QueryManager manager = session.getWorkspace().getQueryManager();
    Node usersHome;
    try {
      usersHome = (Node)session.getItem(baseUserPath_);
    } catch (PathNotFoundException e) {
      usersHome = (Node)getSession(provider, false).getItem(baseUserPath_);
    }
    Node userHome = null;
    try {
      userHome = nodeHierarchyCreator_.getUserNode(provider, userName);
    } catch(Exception e) {
      if (LOG.isWarnEnabled()) {
        LOG.warn(e.getMessage());
      }
    }
    if (userHome == null) {
      if(usersHome.hasNode(userName)) {
        userHome = usersHome.getNode(userName);
      } else{
        userHome = usersHome.addNode(userName);
        if(userHome.canAddMixin("exo:privilegeable")){
          userHome.addMixin("exo:privilegeable");
          
        }
        ((ExtendedNode)userHome).setPermissions(getPermissions(userName));
        Node query = null;
        if(userHome.hasNode(relativePath_)) {
          query = userHome.getNode(relativePath_);
        } else {
          query = getNodeByRelativePath(userHome, relativePath_);
        }
        if (query.canAddMixin("exo:privilegeable")){
          query.addMixin("exo:privilegeable");
        }
        ((ExtendedNode)query).setPermissions(getPermissions(userName));
        usersHome.save();
      }
    }
    Node queriesHome = null;
    if(userHome.hasNode(relativePath_)) {
      queriesHome = userHome.getNode(relativePath_);
    } else {
      queriesHome = getNodeByRelativePath(userHome, relativePath_);
    }
    NodeIterator iter = queriesHome.getNodes();
    while (iter.hasNext()) {
      Node node = iter.nextNode();
      if(node.isNodeType("nt:query")) queries.add(manager.getQuery(node));
    }
    return queries;
  }  

  /**
   * Get node by giving the node user and the relative path to its
   * @param userHome      Node user
   * @param relativePath  The relative path to its
   * @return
   * @throws Exception
   */
  private Node getNodeByRelativePath(Node userHome, String relativePath) throws Exception {
    String[] paths = relativePath.split("/");
    StringBuffer relPath = null;
    Node queriesHome = null;
    for (String path : paths) {
      if (relPath == null)
        relPath = new StringBuffer(path);
      else
        relPath.append("/").append(path);
      if (!userHome.hasNode(relPath.toString()))
        queriesHome = userHome.addNode(relPath.toString());
    }
    return queriesHome;
  }

  /**
   * Get all permission of the giving owner
   * @param owner
   * @return
   */
  private Map<String,String[]> getPermissions(String owner) {
    Map<String, String[]> permissions = new HashMap<String, String[]>();
    permissions.put(owner, perms);
    permissions.put(group_, perms);
    return permissions;
  }
  
  /**
   * {@inheritDoc}
   */
  public void addQuery(String queryName, String statement, String language, String userName) throws Exception {
    if (userName == null)
      return;
    Session session = getSession();
    QueryManager manager = session.getWorkspace().getQueryManager();
    Query query = manager.createQuery(statement, language);
    SessionProvider sessionProvider = WCMCoreUtils.getSystemSessionProvider();
    Node userNode = nodeHierarchyCreator_.getUserNode(sessionProvider, userName);
    if (!userNode.hasNode(getRelativePath())) {
      getNodeByRelativePath(userNode, relativePath_);
      session.save();
    }
    String absPath = userNode.getPath() + "/" + relativePath_ + "/" + queryName;
    query.storeAsNode(absPath);
    session.save();  
  }
  
  /**
   * {@inheritDoc}
   */
  public void removeQuery(String queryPath, String userName) throws Exception {
    if (userName == null)
      return;
    Session session = getSession();
    
    Node queryNode = null;
    try {
      queryNode = (Node) session.getItem(queryPath);
    } catch (PathNotFoundException pe) {
      queryNode = (Node) getSession(WCMCoreUtils.getSystemSessionProvider(), true).getItem(queryPath);
    }
    Node queriesHome = queryNode.getParent();
    queryNode.remove();
    queriesHome.save();
    removeFromCache(queryPath);
  }  

  /**
   * {@inheritDoc}
   */
  public void addSharedQuery(String queryName,
                             String statement,
                             String language,
                             String[] permissions,
                             boolean cachedResult) throws Exception {
    addSharedQuery(queryName,
                   statement,
                   language,
                   permissions,
                   cachedResult,
                   WCMCoreUtils.getUserSessionProvider());
  }
  
  public void addSharedQuery(String queryName,
                             String statement,
                             String language,
                             String[] permissions,
                             boolean cachedResult,
                             SessionProvider provider) throws Exception {
    Session session = getSession(provider, true);
    ValueFactory vt = session.getValueFactory();
    String queryPath;
    List<Value> perm = new ArrayList<Value>();
    for (String permission : permissions) {
      Value vl = vt.createValue(permission);
      perm.add(vl);
    }
    Value[] vls = perm.toArray(new Value[] {});

    String queriesPath = baseQueriesPath_;
    Node queryHome = (Node)session.getItem(baseQueriesPath_);
    QueryManager queryManager = session.getWorkspace().getQueryManager();
    queryManager.createQuery(statement, language);
    if (queryHome.hasNode(queryName)) {
      Node query = queryHome.getNode(queryName);
      query.setProperty("jcr:language", language);
      query.setProperty("jcr:statement", statement);
      query.setProperty("exo:accessPermissions", vls);
      query.setProperty("exo:cachedResult", cachedResult);
      query.save();
      session.save();
      queryPath = query.getPath();
    } else {
      QueryManager manager = session.getWorkspace().getQueryManager();
      Query query = manager.createQuery(statement, language);
      Node newQuery = query.storeAsNode(baseQueriesPath_ + "/" + queryName);
      newQuery.addMixin("mix:sharedQuery");
      newQuery.setProperty("exo:accessPermissions", vls);
      newQuery.setProperty("exo:cachedResult", cachedResult);
      session.getItem(queriesPath).save();
      queryPath = queriesPath;
    }
    removeFromCache(queryPath);
  }  
  
  /**
   * {@inheritDoc}
   */
  public Node getSharedQuery(String queryName, SessionProvider provider) throws Exception {
    Session session = getSession(provider, true);
    try {
      Node sharedQueryNode = (Node) session.getItem(baseQueriesPath_ + "/" + queryName);
      return sharedQueryNode;
    } catch (PathNotFoundException e) {
      return null;
    }
  }  

  /**
   * {@inheritDoc}
   */
  public List<Node> getSharedQueries(SessionProvider provider) throws Exception {
    Session session = getSession(provider, true);
    List<Node> queries = new ArrayList<Node>();
    Node sharedQueryHome = (Node) session.getItem(baseQueriesPath_);
    NodeIterator iter = sharedQueryHome.getNodes();
    while (iter.hasNext()) {
      Node node = iter.nextNode();
      if(node.isNodeType("nt:query")) {
        queries.add(node);
      }
    }
    return queries;
  }
  
  /**
   * {@inheritDoc}
   */
  public List<Node> getSharedQueries(String userId, SessionProvider provider) throws Exception {
    List<Node> sharedQueries = new ArrayList<Node>();
    for(Node query : getSharedQueries(provider)) {
      if (canUseQuery(userId, query)) {
        sharedQueries.add(query);
      }
    }
    return sharedQueries;
  }  
  
  /**
   * {@inheritDoc}
   */
  public List<Node> getSharedQueries(String queryType,
                                     String userId,
                                     SessionProvider provider) throws Exception {
    List<Node> resultList = new ArrayList<Node>();
    String language = null;
    for (Node queryNode: getSharedQueries(provider)) {
      language = queryNode.getProperty("jcr:language").getString();
      if (!queryType.equalsIgnoreCase(language)) continue;
      if (canUseQuery(userId,queryNode)) {
        resultList.add(queryNode);
      }
    }
    return resultList;
  }  
  
  /**
   * {@inheritDoc}
   */
  public Query getQueryByPath(String queryPath, String userName, SessionProvider provider) throws Exception {
    List<Query> queries = getQueries(userName, provider);
    for (Query query : queries) {
      if (query.getStoredQueryPath().equals(queryPath)) return query;
    }
    return null;
  }  
  
  /**
   * {@inheritDoc}
   */
  public void removeSharedQuery(String queryName, SessionProvider provider) throws Exception {
    Session session = getSession(provider, true);
    session.getItem(baseQueriesPath_ + "/" + queryName).remove();
    session.save();
  }  
  
  /**
   * {@inheritDoc}
   */
  public QueryResult execute(String queryPath,
                             String workspace,
                             SessionProvider provider,
                             String userId) throws Exception {
    Session session = getSession(provider, true);
    Session querySession = getSession(workspace, provider);
    Node queryNode = null;
    try {
      queryNode = (Node) session.getItem(queryPath);
    } catch (PathNotFoundException e) {
      if (LOG.isWarnEnabled()) {
        LOG.warn("Can not find node by path " + queryPath + " in dms-system workspace");
      }
      queryNode = (Node) querySession.getItem(queryPath);
    }
    if (queryNode != null && queryNode.hasProperty("exo:cachedResult")
        && queryNode.getProperty("exo:cachedResult").getBoolean()) {
      String portalName = containerInfo_.getContainerName();
      String key = portalName + queryPath;
      QueryResult result = cache_.get(key);
      if (result != null) return result;
      result = execute(querySession, queryNode, userId);
      cache_.put(key, result);
      return result;
    }
    QueryResult queryResult = execute(querySession, queryNode, userId);
    return queryResult;
  }  

  /**
   * Execute the query by giving the session, query node and userid
   * @param session     The Session
   * @param queryNode   The node of query
   * @param userId      The userid
   * @return
   * @throws Exception
   */
  private QueryResult execute(Session session, Node queryNode, String userId) throws Exception {
    return createQuery(session, queryNode, userId).execute();
  }

  /**
   * This method replaces tokens in the statement by their actual values
   * Current supported tokens are :
   * ${UserId}$ corresponds to the current user
   * ${Date}$   corresponds to the current date
   * That way, predefined queries can be equipped with dynamic values. This is
   * useful when querying for documents made by the current user, or documents
   * in publication state.
   *
   * @return the processed String, with replaced tokens
   */
  private String computeStatement(String statement, String userId) {

    // The returned computed statement
    String ret = statement;

    // Replace ${UserId}$
    ret = ret.replace("${UserId}$",userId);

    // Replace ${Date}$
    String currentDate = ISO8601.format(new GregorianCalendar());
    ret = ret.replace("${Date}$",currentDate);

    return ret;
  }

  /**
   * Remove query from cache by giving the query path
   * @param queryPath   The path to query
   * @throws Exception
   */
  private void removeFromCache(String queryPath) throws Exception {
    String portalName = containerInfo_.getContainerName();
    String key = portalName + queryPath;
    QueryResult result = cache_.get(key);
    if (result != null) cache_.remove(key);
  }

  /**
   * Get the session with curent repository
   * @return
   * @throws Exception
   */
  private Session getSession() throws Exception {
    ManageableRepository manageableRepository = repositoryService_.getCurrentRepository();
    SessionProvider sessionProvider = WCMCoreUtils.getSystemSessionProvider();
    return sessionProvider.getSession(manageableRepository.getConfiguration().getDefaultWorkspaceName(), 
        manageableRepository);

  }

  /**
   * Get the session by specify the repository, sessionprovider and flag params
   * @param provider      The SessionProvider
   * @param flag          The boolean to decide which session will be chosen
   * @return
   * @throws Exception
   */
  private Session getSession(SessionProvider provider, boolean flag) throws Exception {
    ManageableRepository manageableRepository = repositoryService_.getCurrentRepository();
    if (!flag) {
      String workspace = manageableRepository.getConfiguration().getDefaultWorkspaceName();
      return provider.getSession(workspace, manageableRepository);
    }
    DMSRepositoryConfiguration dmsRepoConfig = dmsConfiguration_.getConfig();
    return provider.getSession(dmsRepoConfig.getSystemWorkspace(), manageableRepository);
  }

  /**
   * Get the session by specify the repository, workspace and sessionprovider
   * @param workspace     The workspace name
   * @param provider      The SessionProvider
   * @return
   * @throws Exception
   */
  private Session getSession(String workspace, SessionProvider provider) throws Exception {
    ManageableRepository manageableRepository = repositoryService_.getCurrentRepository();
    return provider.getSession(workspace,manageableRepository);
  }

  /**
   * Check the given user can use this query
   * @param userId      The user id
   * @param queryNode   The node of query
   * @return
   * @throws Exception
   */
  private boolean canUseQuery(String userId, Node queryNode) throws Exception{
    Value[] values = queryNode.getProperty("exo:accessPermissions").getValues();
    for(Value value : values) {
      String accessPermission = value.getString();
      if (hasMembership(userId,accessPermission)) {
        return true;
      }
    }
    return false;
  }

  /**
   * Check the user which has a given membership
   * @param userId          The user id
   * @param roleExpression  The expression of membership
   * @return
   */
  private boolean hasMembership(String userId, String roleExpression) {
    if (userId == null || userId.length() == 0) {
      return false;
    }
    if(roleExpression.equals("*") || roleExpression.equals(IdentityConstants.ANY))
      return true;
    ConversationState conversationState = ConversationState.getCurrent();
    Identity identity = conversationState.getIdentity();
    String membershipType = roleExpression.substring(0, roleExpression.indexOf(":"));
    String groupName = roleExpression.substring(roleExpression.indexOf(":") + 1);
    try {
      MembershipHandler membershipHandler = organizationService_.getMembershipHandler();
      if ("*".equals(membershipType)) {
    	// Determine if there exists at least one membership
        if (userId.equals(ConversationState.getCurrent().getIdentity().getUserId())) {
          return identity.isMemberOf(groupName);
        } else {
          return !membershipHandler.findMembershipsByUserAndGroup( userId,groupName).isEmpty();
        }
      }
      if (userId.equals(ConversationState.getCurrent().getIdentity().getUserId())) {
    	  return identity.isMemberOf(groupName, membershipType);
      } else {
        // Determine if there exists the membership of specified type
        return membershipHandler.findMembershipByUserGroupAndType(userId,groupName,membershipType) != null;
      }
    }
    catch(Exception e) {
      return false;
    }
  }

  /**
   * {@inheritDoc}
   */
  public Query getQuery(String queryPath, String workspace, SessionProvider provider, String userId) throws Exception {
    Session session = getSession(provider, true);
    Session querySession = getSession(workspace, provider);
    Node queryNode = null;
    try {
      queryNode = (Node) session.getItem(queryPath);
    } catch (PathNotFoundException e) {
      if (LOG.isWarnEnabled()) {
        LOG.warn("Can not find node by path " + queryPath + " in dms-system workspace");
      }
      queryNode = (Node) querySession.getItem(queryPath);
    }
    return createQuery(querySession, queryNode, userId);
  }
  
  /**
   * Creates the Query object by giving the session, query node and userid
   * @param session     The Session
   * @param queryNode   The node of query
   * @param userId      The userid
   * @return
   * @throws Exception
   */
  private Query createQuery(Session session, Node queryNode, String userId) throws Exception {
    String statement = this.computeStatement(queryNode.getProperty("jcr:statement").getString(), userId);
    String language = queryNode.getProperty("jcr:language").getString();
    Query query = session.getWorkspace().getQueryManager().createQuery(statement,language);
    return query;
  }

  @Override
  public Set<String> getAllConfiguredQueries() {
    return configuredQueries_;
  }
  
}
