/*
 * Copyright (C) 2003-2009 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.poll.service.impl;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Date;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

import javax.jcr.Node;
import javax.jcr.NodeIterator;
import javax.jcr.PathNotFoundException;
import javax.jcr.RepositoryException;
import javax.jcr.Session;
import javax.jcr.query.Query;
import javax.jcr.query.QueryManager;
import javax.jcr.query.QueryResult;

import org.exoplatform.commons.utils.ActivityTypeUtils;
import org.exoplatform.container.component.ComponentPlugin;
import org.exoplatform.forum.common.CommonUtils;
import org.exoplatform.forum.common.jcr.KSDataLocation;
import org.exoplatform.forum.common.jcr.PropertyReader;
import org.exoplatform.forum.common.jcr.SessionManager;
import org.exoplatform.poll.service.DataStorage;
import org.exoplatform.poll.service.InitialDefaultDataPlugin;
import org.exoplatform.poll.service.Poll;
import org.exoplatform.poll.service.PollData;
import org.exoplatform.poll.service.PollInitialData;
import org.exoplatform.poll.service.PollNodeTypes;
import org.exoplatform.poll.service.PollSummary;
import org.exoplatform.poll.service.Utils;
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;

public class JCRDataStorage implements DataStorage, PollNodeTypes {
  private static final Log               log                = ExoLogger.getLogger(JCRDataStorage.class);

  private List<InitialDefaultDataPlugin> defaultDataPlugins = new ArrayList<InitialDefaultDataPlugin>();

  private NodeHierarchyCreator           nodeHierarchyCreator_;

  private SessionManager                 sessionManager;

  KSDataLocation                         dataLocator;

  public JCRDataStorage(NodeHierarchyCreator nodeHierarchyCreator, KSDataLocation dataLocator) {
    nodeHierarchyCreator_ = nodeHierarchyCreator;
    this.dataLocator = dataLocator;
    this.sessionManager = dataLocator.getSessionManager();
  }

  void addInitialDefaultDataPlugin(ComponentPlugin plugin) throws Exception {
    if (plugin instanceof InitialDefaultDataPlugin) {
      defaultDataPlugins.add((InitialDefaultDataPlugin) plugin);
    }
  }

  public Node getNodeByPath(String nodePath, SessionProvider sessionProvider) throws Exception {
    return (Node) getSession(sessionProvider).getItem(nodePath);
  }

  @SuppressWarnings("deprecation")
  public Session getSession(SessionProvider sprovider) throws Exception {
    return sessionManager.getSession(sprovider);
  }

  // Path: /exo:applications/eXoPolls/ using for: $PORTAL/Polls
  private Node getPublicPollHomeNode(SessionProvider sProvider) throws Exception {
    Node publicApp = getNodeByPath(nodeHierarchyCreator_.getJcrPath("eXoApplications"), sProvider);
    try {
      return publicApp.getNode(EXO_POLLS);
    } catch (Exception e) {
      Node pollApp = publicApp.addNode(EXO_POLLS, NT_UNSTRUCTURED);
      publicApp.getSession().save();
      return pollApp;
    }
  }

  // Path: /Groups/ using for: $GROUP/ApplicationData/eXoPolls
  private Node getGroupPollHomeNode(SessionProvider sProvider) throws Exception {
    Node privateApp = getNodeByPath(nodeHierarchyCreator_.getJcrPath("groupsPath"), sProvider);
    return privateApp;
  }

  private Node getParentNode(SessionProvider sProvider, String parentId) throws Exception {
    Node appNode = null;
    try { // id = /exo:applications/../../${forumId}/${topicId}/
      appNode = getNodeByPath(parentId, sProvider);
    } catch (Exception e) {
      if (e instanceof PathNotFoundException || e instanceof RepositoryException) {
        if (parentId.indexOf(APPLICATION_DATA) > 0) {// id = /Groups/$GROUP/ApplicationData/eXoPolls
          return getNode(getGroupPollHomeNode(sProvider), parentId);
        } else if (parentId.indexOf(POLLS) > 0) {// id = $PORTAL/Polls
          return getNode(getPublicPollHomeNode(sProvider), parentId);
        }
      } else
        log.error("Failed to get parent node of poll: " + parentId, e);
    }
    return appNode;
  }

  public void initDefaultData() throws Exception {
    SessionProvider sProvider = SessionProvider.createSystemProvider();
    List<Poll> polls = new ArrayList<Poll>();
    try {
      for (InitialDefaultDataPlugin pln : defaultDataPlugins) {
        PollInitialData initialData = pln.getPollInitialData();
        List<PollData> datas = initialData.getPollDatas();
        if (datas.size() > 0) {
          String parentPath = datas.get(0).getParentPath();
          Node parentNode = getParentNode(sProvider, parentPath);
          if (parentNode.hasNode(PollData.DEFAULT_ID))
            return;
          else {
            boolean first = true;
            int size, j;
            for (PollData pollData : datas) {
              Poll poll = new Poll();
              if (first)
                poll.setId(PollData.DEFAULT_ID);
              first = false;
              size = pollData.getOptions().size();
              String[] vote = new String[size];
              for (j = 0; j < size; j++) {
                vote[j] = "0.0";
              }
              poll.setParentPath(pollData.getParentPath());
              poll.setQuestion(pollData.getQuestion());
              poll.setOption(pollData.getOptions().toArray(new String[size]));
              poll.setVote(vote);
              poll.setOwner(pollData.getOwner());
              poll.setModifiedBy(pollData.getOwner());
              poll.setCreatedDate(new Date());
              poll.setModifiedDate(new Date());
              poll.setUserVote(new String[] {});
              poll.setTimeOut(Long.parseLong(pollData.getTimeOut()));
              poll.setIsAgainVote(Boolean.parseBoolean(pollData.getIsAgainVote()));
              poll.setIsMultiCheck(Boolean.parseBoolean(pollData.getIsMultiCheck()));
              poll.setIsClosed(Boolean.parseBoolean(pollData.getIsClosed()));
              polls.add(poll);
            }
          }
        }
      }
    } finally {
      sProvider.close();
    }
    for (Poll poll : polls) {
      savePoll(poll, true, false);
    }
  }

  public Poll getPoll(String pollId) throws Exception {
    SessionProvider sProvider = SessionProvider.createSystemProvider();
    try {
      String parentId = "";
      if (pollId.lastIndexOf("/") > 0) {
        parentId = pollId.substring(0, pollId.lastIndexOf("/") + 1);
        pollId = pollId.substring(pollId.lastIndexOf("/") + 1);
      }
      Node appNode = getParentNode(sProvider, parentId);
      Node pollNode = appNode.getNode(pollId);
      return getPollNode(pollNode);
    } catch (Exception e) {
      return getPollNode(getNodeById(sProvider, pollId));
    } finally {
      sProvider.close();
    }
  }

  private Node getNodeById(SessionProvider sProvider, String pollId) throws Exception {
    QueryManager qm = getSession(sProvider).getWorkspace().getQueryManager();
    StringBuffer queryString = new StringBuffer(JCR_ROOT);
    queryString.append("//element(*,").append(EXO_POLL).append(")").append("[fn:name() = '").append(pollId).append("']");
    Query query = qm.createQuery(queryString.toString(), Query.XPATH);
    QueryResult result = query.execute();
    NodeIterator iter = result.getNodes();
    if (iter.getSize() > 0)
      return iter.nextNode();
    return null;
  }

  private Poll getPollNode(Node pollNode) throws Exception {
    if (pollNode == null)
      return null;
    Poll pollNew = new Poll();
    pollNew.setId(pollNode.getName());
    pollNew.setParentPath(pollNode.getParent().getPath());
    PropertyReader reader = new PropertyReader(pollNode);
    pollNew.setOwner(reader.string(EXO_OWNER));
    pollNew.setModifiedBy(reader.string(EXO_MODIFIED_BY));
    pollNew.setCreatedDate(reader.date(EXO_CREATED_DATE));
    pollNew.setModifiedDate(reader.date(EXO_MODIFIED_DATE));
    pollNew.setLastVote(reader.date(EXO_LASTVOTE));
    pollNew.setTimeOut(reader.l(EXO_TIME_OUT, 0));
    pollNew.setQuestion(reader.string(EXO_QUESTION));
    pollNew.setOption(reader.strings(EXO_OPTION, new String[] {}));
    pollNew.setVote(reader.strings(EXO_VOTE, new String[] {}));
    pollNew.setUserVote(reader.strings(EXO_USER_VOTE, new String[] {}));
    pollNew.setIsMultiCheck(reader.bool(EXO_IS_MULTI_CHECK));
    pollNew.setIsAgainVote(reader.bool(EXO_IS_AGAIN_VOTE, false));
    pollNew.setIsClosed(reader.bool(EXO_IS_CLOSED, false));
    pollNew.setInTopic(pollNode.getParent().isNodeType(EXO_TOPIC));
    setPollLink(pollNode, pollNew);
    return pollNew;
  }
  
  private void setPollLink(Node pollNode, Poll poll) throws Exception {
    Node parent = pollNode.getParent();
    PropertyReader reader = new PropertyReader(parent);
    if(parent.isNodeType("exo:topic")) {
      poll.setLink (reader.string(EXO_LINK));
    }
  }

  public List<Poll> getPagePoll() throws Exception {
    SessionProvider sProvider = SessionProvider.createSystemProvider();
    List<Poll> listPoll = new ArrayList<Poll>();
    try {
      NodeIterator iter = getIterNodePoll(sProvider);
      while (iter.hasNext()) {
        Node node = iter.nextNode();
        listPoll.add(getPollNode(node));
      }
    } catch (Exception e) {
      log.error("Failed to get page poll", e);
    } finally {
      sProvider.close();
    }
    return listPoll;
  }

  private NodeIterator getIterNodePoll(SessionProvider sProvider) throws Exception {
    QueryManager qm = getSession(sProvider).getWorkspace().getQueryManager();
    StringBuffer queryString = new StringBuffer(JCR_ROOT);
    queryString.append("//element(*,").append(EXO_POLL).append(")").append(" order by @").append(EXO_CREATED_DATE).append(" descending");
    Query query = qm.createQuery(queryString.toString(), Query.XPATH);
    QueryResult result = query.execute();
    return result.getNodes();
  }
  
  private long getUserRoleOfForum(SessionProvider sProvider, String userName) {
    if(Utils.isEmpty(userName)) return 3;
    try {
      String userPatch = "/"+dataLocator.getUserProfilesLocation() + "/" + userName;
      Node userNode = getNodeByPath(userPatch, sProvider);
      return new PropertyReader(userNode).l("exo:userRole", 3);
    } catch (Exception e) {
      log.debug("Failed to get user role of forum.", e);
    }
    return 3;
  }
  
  public boolean hasPermissionInForum(String pollPath, List<String> allInfoOfUser) throws Exception {
    SessionProvider sProvider = SessionProvider.createSystemProvider();
    try {
      Node pollNode = getNodeByPath(pollPath, sProvider);
      return hasPermissionInForum(sProvider, pollNode, allInfoOfUser);
    } catch (Exception e) {
      log.error("Failed to checking has permission in poll from forum", e);
    } finally {
      sProvider.close();
    }
   return false;
  }
  
  private boolean hasPermissionInForum(SessionProvider sProvider, Node pollNode, List<String> allInfoOfUser) throws Exception {
    try {
      long userRole = getUserRoleOfForum(sProvider, (allInfoOfUser.size() > 0) ? allInfoOfUser.get(0) : "");
      // check for administrators. If is admin --> return true;
      if(userRole == 0) return true;
      Node topicNode = pollNode.getParent();
      Node forumNode = topicNode.getParent();
      Node categoryNode = forumNode.getParent();
      PropertyReader reader = new PropertyReader(topicNode);
      List<String> privates = new ArrayList<String>(reader.set("exo:userPrivate", new HashSet<String>()));
      // check user private.
      if (!Utils.isListEmpty(privates)) {
        if (collectionsDisjoint(allInfoOfUser, privates) || Utils.isListEmpty(allInfoOfUser)) {
          return false;
        }
      }
      // permission in topic
      Set<String> viewers = reader.set("exo:canView", new HashSet<String>());
      // user's permission of the topic content this poll.
      boolean hasNotPremissionByTopic = (reader.bool("exo:isClosed") || !reader.bool("exo:isApproved") ||
                                         reader.bool("exo:isWaiting") || !reader.bool("exo:isActive"));
      // permission in forum
      reader = new PropertyReader(forumNode);
      viewers.addAll(reader.set("exo:viewer", new HashSet<String>()));
      // forum is closed --> return false;
      if(reader.bool("exo:isClosed")) return false;
      // check for moderators
      if(userRole == 1) {
        List<String> moderators = reader.list("exo:moderators", new ArrayList<String>());
        if(!Utils.isListEmpty(allInfoOfUser)) {
          for (String string : moderators) {
            // user's moderator of the forum content the poll.
            if(allInfoOfUser.contains(string)) return true;
          }
        }
      }
      //checking when user has not moderator of the forum content the poll.
      if(hasNotPremissionByTopic) return false;
      // permission in category
      reader = new PropertyReader(categoryNode);
      // check viewer
      viewers.addAll(reader.set("exo:viewer", new HashSet<String>()));
      // if viewer is empty then poll public.
      if (Utils.isListEmpty(new ArrayList<String>(viewers))) {
        return true;
      }
      if (!Utils.isListEmpty(allInfoOfUser)) {
        // if user login and viewer list not empty.
        if (!collectionsDisjoint(viewers, allInfoOfUser)) {
           return true;
        }
      }
    } catch (Exception e) {
      log.debug("Failed to checking has premission viewing poll add in forum.");
    }
    return false;
  }
  
  private boolean collectionsDisjoint(Collection<String> c1, Collection<String> c2) {
    for (String e : c1) {
      if (c2.contains(e.trim())) {
        return false;
      }
    }
    return true;
  }

  public PollSummary getPollSummary(List<String> allInfoOfUser) throws Exception {
    SessionProvider sProvider = SessionProvider.createSystemProvider();
    PollSummary pollSmr = new PollSummary();
    try {
      NodeIterator iter = getIterNodePoll(sProvider);
      List<String> pollIds = new ArrayList<String>();
      List<String> pollNames = new ArrayList<String>();
      List<String> groupPrivates = new ArrayList<String>();
      String path;
      boolean isAdd = false;
      while (iter.hasNext()) {
        Node node = iter.nextNode();
        path = node.getPath();
        isAdd = false;
        // check permission for poll of forum
        if (path.indexOf(dataLocator.getForumCategoriesLocation()) >= 0) {
          isAdd = hasPermissionInForum(sProvider, node, allInfoOfUser);
        } else {
          // check permission for poll private.
          if (path.indexOf(APPLICATION_DATA) > 0 && allInfoOfUser != null) {
            path = path.substring(path.indexOf(GROUPS + "/") + GROUPS.length(), path.indexOf("/" + APPLICATION_DATA));
            for (String group : allInfoOfUser) {
              if (group.indexOf(":") < 0) {
                if (group.indexOf(path) == 0) {
                  isAdd = true;
                  break;
                }
              }
            }
          } else {
            isAdd = true;
          }
        }
        if (isAdd) {
          pollIds.add(node.getName());
          pollNames.add(node.getProperty(EXO_QUESTION).getString());
          groupPrivates.add(path);
        }
      }
      pollSmr.setPollId(pollIds);
      pollSmr.setPollName(pollNames);
      pollSmr.setGroupPrivate(groupPrivates);
    } catch (Exception e) {
      log.error("Failed to get poll summary", e);
    } finally {
      sProvider.close();
    }
    return pollSmr;
  }

  public Poll removePoll(String pollId){
    SessionProvider sProvider = SessionProvider.createSystemProvider();
    Poll poll = null;
    try {
      Node pollNode = null;
      if ((pollId.lastIndexOf("/") > 0)) {
        pollNode = getNodeByPath(pollId, sProvider);
      } else {
        pollNode = getNodeById(sProvider, pollId);
      }
      poll = getPollNode(pollNode);
      Node parentNode = pollNode.getParent();
      pollNode.remove();
      if (parentNode.hasProperty(EXO_IS_POLL)) {
        parentNode.setProperty(EXO_IS_POLL, false);
      }
      if (parentNode.isNew()) {
        parentNode.getSession().save();
      } else {
        parentNode.save();
      }
    } catch (Exception e) {
      log.error("Failed to remove poll: " + pollId, e);
    } finally {
      sProvider.close();
    }
    return poll;
  }

  private Node getNode(Node nodeApp, String ids) throws Exception {
    Node node = null;
    if (ids.indexOf("/") < 0) {
      node = nodeApp.addNode(ids);
    } else {
      String[] ar = ids.split("/");
      for (int i = 0; i < ar.length; i++) {
        try {
          node = nodeApp.getNode(ar[i]);
        } catch (PathNotFoundException e) {
          node = nodeApp.addNode(ar[i], NT_UNSTRUCTURED);
        }
        nodeApp = node;
      }
      if (nodeApp.isNew()) {
        nodeApp.getSession().save();
      } else {
        nodeApp.getParent().save();
      }
    }
    return node;
  }

  public void savePoll(Poll poll, boolean isNew, boolean isVote) throws Exception {
    SessionProvider sProvider = SessionProvider.createSystemProvider();
    try {
      Node pollNode;
      String pollId = poll.getId();
      Node parentNode = getParentNode(sProvider, (poll.getParentPath() != null) ? poll.getParentPath() : pollId);
      if (isVote) {
        pollNode = parentNode.getNode(pollId);
        pollNode.setProperty(EXO_VOTE, poll.getVote());
        pollNode.setProperty(EXO_USER_VOTE, poll.getUserVote());
        pollNode.setProperty(EXO_LASTVOTE, Utils.getGreenwichMeanTime());// new property 2.0 to 2.1
      } else {
        if (isNew) {
          if (parentNode.hasNode(pollId))
            return;
          pollNode = parentNode.addNode(pollId, EXO_POLL);// add node
          pollNode.setProperty(EXO_ID, pollId);
          pollNode.setProperty(EXO_OWNER, poll.getOwner());
          pollNode.setProperty(EXO_USER_VOTE, new String[] {});
          pollNode.setProperty(EXO_CREATED_DATE, Utils.getGreenwichMeanTime());
          pollNode.setProperty(EXO_MODIFIED_DATE, Utils.getGreenwichMeanTime());
          if (parentNode.hasProperty(EXO_IS_POLL)) {
            parentNode.setProperty(EXO_IS_POLL, true);
          }
        } else {
          if (!Utils.isEmpty(poll.getOldParentPath()) && !parentNode.getPath().equals(poll.getOldParentPath())) {
            Session session = getSession(sProvider);
            session.move(poll.getOldParentPath() + "/" + pollId, parentNode.getPath() + "/" + pollId);
            session.save();
            pollNode = parentNode.getNode(pollId);
          }
          pollNode = parentNode.getNode(pollId);
        }
        if (poll.getUserVote() != null) {
          pollNode.setProperty(EXO_USER_VOTE, poll.getUserVote());
        }
        pollNode.setProperty(EXO_VOTE, poll.getVote());
        pollNode.setProperty(EXO_MODIFIED_BY, poll.getModifiedBy());

        if (!isNew) {
          if (pollNode.getProperty(EXO_TIME_OUT).getLong() != poll.getTimeOut())
            pollNode.setProperty(EXO_MODIFIED_DATE, Utils.getGreenwichMeanTime());
        }
        pollNode.setProperty(EXO_TIME_OUT, poll.getTimeOut());
        pollNode.setProperty(EXO_QUESTION, poll.getQuestion());
        pollNode.setProperty(EXO_OPTION, poll.getOption());
        pollNode.setProperty(EXO_IS_MULTI_CHECK, poll.getIsMultiCheck());
        pollNode.setProperty(EXO_IS_CLOSED, poll.getIsClosed());
        pollNode.setProperty(EXO_IS_AGAIN_VOTE, poll.getIsAgainVote());
      }
      setPollLink(pollNode, poll);
      poll.setInTopic(pollNode.getParent().isNodeType(EXO_TOPIC));
      if (parentNode.isNew())
        parentNode.getSession().save();
      else
        parentNode.save();
    } catch (Exception e) {
      log.error("Failed to save poll: " + poll.getId(), e);
    } finally {
      sProvider.close();
    }

  }

  public void setClosedPoll(Poll poll) {
    SessionProvider sProvider = SessionProvider.createSystemProvider();
    try {
      Node appNode = getParentNode(sProvider, poll.getParentPath());
      Node pollNode = appNode.getNode(poll.getId());
      pollNode.setProperty(EXO_IS_CLOSED, poll.getIsClosed());
      if (poll.getTimeOut() == 0) {
        pollNode.setProperty(EXO_MODIFIED_DATE, Utils.getGreenwichMeanTime());
        pollNode.setProperty(EXO_TIME_OUT, 0);
      }
      appNode.save();
    } catch (Exception e) {
      log.error("Failed to close poll: " + poll.getId(), e);
    } finally {
      sProvider.close();
    }
  }
  
  @Override
  public void saveActivityIdForOwner(String ownerId, String activityId) {
    try {
      SessionProvider provider = CommonUtils.createSystemProvider();
      Node ownerNode = getParentNode(provider, ownerId);
      ActivityTypeUtils.attachActivityId(ownerNode, activityId);
      ownerNode.save();
    } catch (Exception e) {
      log.error(String.format("Failed to attach activityId %s for node %s ", activityId, ownerId), e);
    }
  }

  @Override
  public String getActivityIdForOwner(String ownerId) {
    try {
      SessionProvider provider = CommonUtils.createSystemProvider();
      Node ownerNode = getParentNode(provider, ownerId);
      return ActivityTypeUtils.getActivityId(ownerNode);
    } catch (Exception e) {
      log.error(String.format("Failed to get attach activityId for %s ", ownerId), e);
    }
    return null;
  }


}
