/*
 * Copyright (C) 2003-2011 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.documents.impl;

import org.exoplatform.commons.utils.CommonsUtils;
import org.exoplatform.container.xml.PortalContainerInfo;
import org.exoplatform.portal.config.UserPortalConfig;
import org.exoplatform.portal.config.UserPortalConfigService;
import org.exoplatform.portal.mop.SiteKey;
import org.exoplatform.portal.mop.user.UserNavigation;
import org.exoplatform.portal.mop.user.UserPortalContext;
import org.exoplatform.services.cms.BasePath;
import org.exoplatform.services.cms.documents.DocumentService;
import org.exoplatform.services.cms.documents.model.Document;
import org.exoplatform.services.cms.drives.DriveData;
import org.exoplatform.services.cms.drives.ManageDriveService;
import org.exoplatform.services.cms.drives.impl.ManageDriveServiceImpl;
import org.exoplatform.services.cms.impl.Utils;
import org.exoplatform.services.cms.link.LinkManager;
import org.exoplatform.services.jcr.RepositoryService;
import org.exoplatform.services.jcr.core.ManageableRepository;
import org.exoplatform.services.jcr.ext.app.SessionProviderService;
import org.exoplatform.services.jcr.ext.common.SessionProvider;
import org.exoplatform.services.jcr.ext.hierarchy.NodeHierarchyCreator;
import org.exoplatform.services.jcr.impl.core.NodeImpl;
import org.exoplatform.services.log.ExoLogger;
import org.exoplatform.services.log.Log;
import org.exoplatform.services.security.ConversationState;
import org.exoplatform.services.wcm.core.NodetypeConstant;
import org.exoplatform.services.wcm.utils.WCMCoreUtils;
import org.gatein.api.Portal;
import org.gatein.api.navigation.Navigation;
import org.gatein.api.navigation.Nodes;
import org.gatein.api.site.SiteId;

import javax.jcr.Node;
import javax.jcr.NodeIterator;
import javax.jcr.RepositoryException;
import javax.jcr.Session;
import java.net.URLEncoder;
import java.util.*;

/**
 * Created by The eXo Platform SAS Author : eXoPlatform exo@exoplatform.com Mar
 * 22, 2011
 */
public class DocumentServiceImpl implements DocumentService {

  public static final String MIX_REFERENCEABLE = "mix:referenceable";
  public static final String EXO_LAST_MODIFIER_PROP = "exo:lastModifier";
  public static final String EXO_DATE_CREATED_PROP = "exo:dateCreated";
  public static final String JCR_LAST_MODIFIED_PROP = "jcr:lastModified";
  public static final String JCR_CONTENT = "jcr:content";
  public static final String EXO_OWNER_PROP = "exo:owner";
  public static final String EXO_TITLE_PROP = "exo:title";
  public static final String CURRENT_STATE_PROP = "publication:currentState";
  public static final String DOCUMENTS_APP_NAVIGATION_NODE_NAME = "documents";
  public static final String DOCUMENT_NOT_FOUND = "?path=doc-not-found";
  private static final String DOCUMENTS_NODE = "Documents";
  private static final String SHARED_NODE = "Shared";
  private static final Log LOG                 = ExoLogger.getLogger(DocumentServiceImpl.class);
  private ManageDriveService manageDriveService;
  private Portal portal;
  private SessionProviderService sessionProviderService;
  private RepositoryService repoService;
  private NodeHierarchyCreator nodeHierarchyCreator;
  private LinkManager linkManager;

  public DocumentServiceImpl(ManageDriveService manageDriveService, Portal portal, SessionProviderService sessionProviderService, RepositoryService repoService, NodeHierarchyCreator nodeHierarchyCreator, LinkManager linkManager) {
    this.manageDriveService = manageDriveService;
    this.sessionProviderService = sessionProviderService;
    this.repoService = repoService;
    this.nodeHierarchyCreator = nodeHierarchyCreator;
    this.portal = portal;
    this.linkManager = linkManager;
  }

  @Override
  public Document findDocById(String documentId) throws RepositoryException {
    RepositoryService repositoryService = WCMCoreUtils.getService(RepositoryService.class);
    ManageableRepository manageRepo = repositoryService.getCurrentRepository();
    SessionProvider sessionProvider = WCMCoreUtils.getUserSessionProvider();

    String ws = documentId.split(":/")[0];
    String uuid = documentId.split(":/")[1];

    Node node = sessionProvider.getSession(ws, manageRepo).getNodeByUUID(uuid);
    // Create Document
    String title = node.hasProperty(EXO_TITLE_PROP) ? node.getProperty(EXO_TITLE_PROP).getString() : "";
    String id = node.isNodeType(MIX_REFERENCEABLE) ? node.getUUID() : "";
    String state = node.hasProperty(CURRENT_STATE_PROP) ? node.getProperty(CURRENT_STATE_PROP).getValue().getString() : "";
    String author = node.hasProperty(EXO_OWNER_PROP) ? node.getProperty(EXO_OWNER_PROP).getString() : "";
    Calendar lastModified = (node.hasNode(JCR_CONTENT) ? node.getNode(JCR_CONTENT)
                                                             .getProperty(JCR_LAST_MODIFIED_PROP)
                                                             .getValue()
                                                             .getDate() : null);
    Calendar dateCreated = (node.hasProperty(EXO_DATE_CREATED_PROP) ? node.getProperty(EXO_DATE_CREATED_PROP)
                                                                          .getValue()
                                                                          .getDate()
                                                                   : null);
    String lastEditor = (node.hasProperty(EXO_LAST_MODIFIER_PROP) ? node.getProperty(EXO_LAST_MODIFIER_PROP)
                                                                        .getValue()
                                                                        .getString()
                                                                 : "");
    Document doc = new Document(id, node.getName(), title, node.getPath(), 
                                ws, state, author, lastEditor, lastModified, dateCreated);
    return doc;
  }

  /**
   *
   * {@inheritDoc}
   */
  public String getDocumentUrlInPersonalDocuments(Node currentNode, String username) throws Exception {
    Node rootNode = null;
    try {
      SessionProvider sessionProvider = sessionProviderService.getSystemSessionProvider(null);
      ManageableRepository repository = repoService.getCurrentRepository();
      Session session = sessionProvider.getSession(repository.getConfiguration().getDefaultWorkspaceName(), repository);
      //add symlink to user folder destination
      nodeHierarchyCreator.getJcrPath(BasePath.CMS_USERS_PATH);
      rootNode = (Node) session.getItem(nodeHierarchyCreator.getJcrPath(BasePath.CMS_USERS_PATH) + getPrivatePath(username));
      String sharedLink = getSharedLink(currentNode, rootNode);
      return sharedLink;
    } catch (Exception e) {
      LOG.error(e.getMessage(), e);
      return "";
    }
  }

  /**
   *
   * {@inheritDoc}
   */
  public String getDocumentUrlInSpaceDocuments(Node currentNode, String spaceId) throws Exception {
    Node rootNode = null;
    try {
      SessionProvider sessionProvider = sessionProviderService.getSystemSessionProvider(null);
      ManageableRepository repository = repoService.getCurrentRepository();
      Session session = sessionProvider.getSession(repository.getConfiguration().getDefaultWorkspaceName(), repository);
      //add symlink to space destination
      nodeHierarchyCreator.getJcrPath(BasePath.CMS_GROUPS_PATH);
      rootNode = (Node) session.getItem(nodeHierarchyCreator.getJcrPath(BasePath.CMS_GROUPS_PATH) + spaceId);
      String sharedLink = getSharedLink(currentNode, rootNode);
      return sharedLink;
    } catch (Exception e) {
      LOG.error(e.getMessage(), e);
      return "";
    }
  }

  /**
   * Get the short link to display a document in the Documents app by its id.
   * @param workspaceName The workspace of the node
   * @param nodeId The id of the node
   * @return The link to open the document
   * @throws Exception
   */
  @Override
  public String getShortLinkInDocumentsApp(String workspaceName, String nodeId) throws Exception {
    StringBuilder url = new StringBuilder();
    url.append("/")
            .append(CommonsUtils.getRestContextName())
            .append("/documents/view/")
            .append(workspaceName)
            .append("/")
            .append(nodeId);
    return url.toString();
  }

  /**
   * Get link to open a document in the Documents application.
   * This method will try to guess what is the best drive to use based on the node path.
   * @param nodePath path of the nt:file node to open
   * @return Link to open the document
   * @throws Exception
   */
  @Override
  public String getLinkInDocumentsApp(String nodePath) throws Exception {
    if(nodePath == null) {
      return null;
    }

    // find the best matching drive to display the document
    DriveData drive = this.getDriveOfNode(nodePath);

    return getLinkInDocumentsApp(nodePath, drive);
  }

  /**
   * Get link to open a document in the Documents application with the given drive
   * @param nodePath path of the nt:file node to open
   * @param drive drive to use to open the nt:file node
   * @return Link to open the document
   * @throws Exception
   */
  @Override
  public String getLinkInDocumentsApp(String nodePath, DriveData drive) throws Exception {
    if(nodePath == null) {
      return null;
    }

    String containerName = WCMCoreUtils.getService(PortalContainerInfo.class).getContainerName();
    StringBuffer url = new StringBuffer();
    url.append("/").append(containerName);
    if (drive == null) {
      SiteKey siteKey = getDefaultSiteKey();
      url.append("/").append(siteKey.getName()).append("/").append(DOCUMENTS_APP_NAVIGATION_NODE_NAME)
          .append(DOCUMENT_NOT_FOUND);
      return url.toString();
    }

    String encodedDriveName = URLEncoder.encode(drive.getName(), "UTF-8");
    String encodedNodePath = URLEncoder.encode(nodePath, "UTF-8");
    if(drive.getName().equals(ManageDriveServiceImpl.GROUPS_DRIVE_NAME)) {
      // handle group drive case
      String groupId = drive.getParameters().get(ManageDriveServiceImpl.DRIVE_PARAMATER_GROUP_ID);
      if(groupId != null) {
        String groupPageName;
        String[] splitedGroupId = groupId.split("/");
        if (splitedGroupId != null && splitedGroupId.length == 3 && splitedGroupId[1].equals("spaces")) {
          // the doc is in a space -> we use the documents application of the space

          // we need to retrieve the root navigation URI of the space since it can differ from
          // the group id if the space has been renamed
          String rootNavigation = getSpaceRootNavigationNodeURI(groupId);

          groupPageName = rootNavigation + "/" + DOCUMENTS_APP_NAVIGATION_NODE_NAME;
        } else {
          // otherwise we use the portal documents application
          groupPageName = DOCUMENTS_APP_NAVIGATION_NODE_NAME;
        }
        url.append("/g/").append(groupId.replaceAll("/", ":")).append("/").append(groupPageName)
                .append("?path=").append(encodedDriveName).append(encodedNodePath)
                .append("&").append(ManageDriveServiceImpl.DRIVE_PARAMATER_GROUP_ID).append("=").append(groupId);
      } else {
        throw new Exception("Cannot get group id from node path " + nodePath);
      }
    } else if(drive.getName().equals(ManageDriveServiceImpl.USER_DRIVE_NAME)
            || drive.getName().equals(ManageDriveServiceImpl.PERSONAL_DRIVE_NAME)) {
      // handle personal drive case
      SiteKey siteKey = getDefaultSiteKey();
      url.append("/").append(siteKey.getName()).append("/").append(DOCUMENTS_APP_NAVIGATION_NODE_NAME)
              .append("?path=" + encodedDriveName + encodedNodePath);
      String[] splitedNodePath = nodePath.split("/");
      if(splitedNodePath != null && splitedNodePath.length >= 6) {
        String userId = splitedNodePath[5];
        url.append("&").append(ManageDriveServiceImpl.DRIVE_PARAMATER_USER_ID).append("=").append(userId);
      }
    } else {
      // default case
      SiteKey siteKey = getDefaultSiteKey();
      url.append("/").append(siteKey.getName()).append("/").append(DOCUMENTS_APP_NAVIGATION_NODE_NAME)
              .append("?path=" + encodedDriveName + encodedNodePath);
    }

    return url.toString();
  }

  /**
   * Retrieve the root navigation node URi of a space
   * This method uses the Portal Navigation API and not the Space API to avoid making ECMS depends on Social
   * @param spaceGroupId The groupId of the space
   * @return The URI of the root navigation node of the space
   */
  protected String getSpaceRootNavigationNodeURI(String spaceGroupId) throws Exception {
    String navigationName = null;

    Navigation spaceNavigation = portal.getNavigation(new SiteId(org.gatein.api.site.SiteType.SPACE, spaceGroupId));
    if(spaceNavigation != null) {
      org.gatein.api.navigation.Node navigationRootNode = spaceNavigation.getRootNode(Nodes.visitChildren());
      if(navigationRootNode != null && navigationRootNode.iterator().hasNext()) {
        // we assume there is only one root navigation node, that's how spaces work
        org.gatein.api.navigation.Node node = navigationRootNode.iterator().next();
        navigationName = node.getName();
      }
    }

    return navigationName;
  }

  @Override
  public DriveData getDriveOfNode(String nodePath) throws Exception {
    return getDriveOfNode(nodePath, ConversationState.getCurrent().getIdentity().getUserId(), Utils.getMemberships());
  }

  @Override
  public DriveData getDriveOfNode(String nodePath, String userId, List<String> memberships) throws Exception {
    DriveData nodeDrive = null;
    List<DriveData> drives = manageDriveService.getDriveByUserRoles(userId, memberships);

    // Manage special cases
    String[] splitedPath = nodePath.split("/");
    if (splitedPath != null && splitedPath.length >= 2 && splitedPath.length >= 6
        && splitedPath[1].equals(ManageDriveServiceImpl.PERSONAL_DRIVE_ROOT_NODE)) {
      if (splitedPath[5].equals(userId)) {
        nodeDrive = manageDriveService.getDriveByName(ManageDriveServiceImpl.PERSONAL_DRIVE_NAME);
      } else {
        nodeDrive = manageDriveService.getDriveByName(ManageDriveServiceImpl.USER_DRIVE_NAME);
      }
      if (nodeDrive != null) {
        nodeDrive = nodeDrive.clone();
        nodeDrive.getParameters().put(ManageDriveServiceImpl.DRIVE_PARAMATER_USER_ID,
                                      splitedPath[2] + "/" + splitedPath[3] + "/" + splitedPath[4] + "/" + splitedPath[5]);
      }
    }
    if (splitedPath != null && splitedPath.length >= 2 && splitedPath[1].equals(ManageDriveServiceImpl.GROUPS_DRIVE_ROOT_NODE)) {
      int groupDocumentsRootNodeName = nodePath.indexOf("/Documents");
      if(groupDocumentsRootNodeName >= 0) {
        // extract group id for doc path
        String groupId = nodePath.substring(ManageDriveServiceImpl.GROUPS_DRIVE_ROOT_NODE.length() + 1, groupDocumentsRootNodeName);
        nodeDrive = manageDriveService.getDriveByName(groupId.replaceAll("/", "."));
      }
    }
    if (nodeDrive == null) {
      for (DriveData drive : drives) {
        if (nodePath.startsWith(drive.getResolvedHomePath())) {
          if (nodeDrive == null || nodeDrive.getResolvedHomePath().length() < drive.getResolvedHomePath().length()) {
            nodeDrive = drive;
          }
        }
      }
    }
    return nodeDrive;
  }


  protected UserPortalConfig getDefaultUserPortalConfig() throws Exception {
    UserPortalConfigService userPortalConfigSer = WCMCoreUtils.getService(UserPortalConfigService.class);
    UserPortalContext NULL_CONTEXT = new UserPortalContext() {
      public ResourceBundle getBundle(UserNavigation navigation) {
        return null;
      }
      public Locale getUserLocale() {
        return Locale.ENGLISH;
      }
    };
    String remoteId = ConversationState.getCurrent().getIdentity().getUserId() ;
    UserPortalConfig userPortalCfg = userPortalConfigSer.
            getUserPortalConfig(userPortalConfigSer.getDefaultPortal(), remoteId, NULL_CONTEXT);
    return userPortalCfg;
  }

  protected SiteKey getDefaultSiteKey() throws Exception {
    UserPortalConfig prc = getDefaultUserPortalConfig();
    if (prc == null) {
      return null;
    }
    SiteKey siteKey = SiteKey.portal(prc.getPortalConfig().getName());
    return siteKey;
  }

  private String getPrivatePath(String user) {
    return "/" + user.substring(0, 1) + "___/" + user.substring(0, 2) + "___/" + user.substring(0, 3) + "___/" + user + "/Private";
  }

  private String getSharedLink(Node currentNode, Node rootNode) {
    Node shared = null;
    Node link = null;
    try {
      rootNode = rootNode.getNode(DOCUMENTS_NODE);
      if (!rootNode.hasNode(SHARED_NODE)) {
        shared = rootNode.addNode(SHARED_NODE);
      } else {
        shared = rootNode.getNode(SHARED_NODE);
      }
      if (currentNode.isNodeType(NodetypeConstant.EXO_SYMLINK)) {
        currentNode = linkManager.getTarget(currentNode);
      }
      List<String> path = new ArrayList<>();
      Node targetNode = null;
      boolean existingSymlink = false;
      for (NodeIterator it = shared.getNodes(); it.hasNext(); ) {
        Node node = it.nextNode();
        path.add(((NodeImpl) node).getInternalPath().getAsString());
        if (path.contains(((NodeImpl) shared).getInternalPath().getAsString() + "[]" + currentNode.getName() + ":1")) {
          existingSymlink = true;
          targetNode = node;
          break;
        }
      }
      if (existingSymlink) {
        link = targetNode;
      } else {
        link = linkManager.createLink(shared, currentNode);
      }
      return CommonsUtils.getCurrentDomain() + getShortLinkInDocumentsApp(link.getSession().getWorkspace().getName(), ((NodeImpl) link).getInternalIdentifier());
    } catch (Exception e) {
      LOG.error(e.getMessage(), e);
      return "";
    }
  }

}
