/*
 * Copyright (C) 2009 eXo Platform SAS.
 *
 * This is free software; you can redistribute it and/or modify it
 * under the terms of the GNU Lesser General Public License as
 * published by the Free Software Foundation; either version 2.1 of
 * the License, or (at your option) any later version.
 *
 * This software 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
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this software; if not, write to the Free
 * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
 * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
 */
package org.exoplatform.services.jcr.ext.hierarchy.impl;

import org.exoplatform.commons.utils.PropertyManager;
import org.exoplatform.container.component.ComponentPlugin;
import org.exoplatform.container.xml.InitParams;
import org.exoplatform.services.jcr.RepositoryService;
import org.exoplatform.services.jcr.core.ManageableRepository;
import org.exoplatform.services.jcr.ext.common.SessionProvider;
import org.exoplatform.services.jcr.ext.distribution.DataDistributionManager;
import org.exoplatform.services.jcr.ext.distribution.DataDistributionMode;
import org.exoplatform.services.jcr.ext.distribution.DataDistributionType;
import org.exoplatform.services.jcr.ext.hierarchy.NodeHierarchyCreator;
import org.exoplatform.services.jcr.ext.hierarchy.impl.HierarchyConfig.JcrPath;
import org.exoplatform.services.log.ExoLogger;
import org.exoplatform.services.log.Log;
import org.picocontainer.Startable;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

import javax.jcr.Node;
import javax.jcr.PathNotFoundException;
import javax.jcr.RepositoryException;
import javax.jcr.Session;

/**
 * 
 * Created by The eXo Platform SAS Author : Dang Van Minh minh.dang@exoplatform.com Nov 15, 2007
 * 2:21:57 PM
 */
public class NodeHierarchyCreatorImpl implements NodeHierarchyCreator, Startable
{

   private static final Log LOG = ExoLogger.getLogger("exo.jcr.component.ext.NodeHierarchyCreatorImpl");

   private static final String USERS_PATH = "usersPath";

   private static final String USER_APPLICATION = "userApplicationData";

   private static final String PUBLIC_APPLICATION = "eXoApplications";

   private static final String USER_PRIVATE = "userPrivate";

   private static final String USER_PUBLIC = "userPublic";

   private final RepositoryService jcrService_;

   private final DataDistributionManager dataDistributionManager_;

   private final List<AddPathPlugin> pathPlugins_ = new ArrayList<AddPathPlugin>();

   private final Map<String, String> paths_ = new HashMap<String, String>();

   private final boolean oldDistribution;

   private final boolean autoMigrate;

   public NodeHierarchyCreatorImpl(RepositoryService jcrService, InitParams params)
   {
      this(jcrService, null, params);
   }

   public NodeHierarchyCreatorImpl(RepositoryService jcrService, DataDistributionManager dataDistributionManager,
      InitParams params)
   {
      if (dataDistributionManager == null)
      {
         throw new IllegalArgumentException("The DataDistributionManager is now mandatory if you use the "
            + "NodeHierarchyCreator, so please define it in your configuration "
            + "as described in the JCR documentation");
      }
      jcrService_ = jcrService;
      dataDistributionManager_ = dataDistributionManager;
      oldDistribution =
         params != null && params.getValueParam("old-user-distribution") != null
            && Boolean.valueOf(params.getValueParam("old-user-distribution").getValue());
      autoMigrate =
         params != null && params.getValueParam("auto-migrate") != null
            && Boolean.valueOf(params.getValueParam("auto-migrate").getValue());
      
      if (PropertyManager.isDevelopping() && !oldDistribution)
      {
         LOG.info("The NodeHierarchyCreator is configured to use the new distribution mechanism for the"
            + " users directories, if you prefer to use the old mechanism set the value parameter "
            + "'old-user-distribution' to 'true'.");
      }
   }

   /**
    * {@inheritDoc}
    */
   public void start()
   {
      try
      {
         processAddPathPlugin();
      }
      catch (Exception e)
      {
         LOG.error("An error occurs while processing the plugins", e);
      }

      if (isNeededToMigrate())
      {
         try
         {
            migrate();
         }
         catch (RepositoryException e)
         {
            LOG.error("An error occurs while upgrading JCR structure", e);
         }
      }
   }

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

   /**
    * {@inheritDoc}
    */
   public void init(String repository) throws Exception
   {
      initBasePath(repository);
   }

   /**
    * {@inheritDoc}
    */
   public void init() throws Exception
   {
      initBasePath(null);
   }

   private void createNode(Node rootNode, String path, String nodeType, List<String> mixinTypes,
      Map<String, String[]> permissions) throws Exception
   {
      dataDistributionManager_.getDataDistributionType(DataDistributionMode.NONE).getOrCreateDataNode(rootNode, path,
         nodeType, mixinTypes, permissions);
   }

   private void processAddPathPlugin() throws Exception
   {
      Session session = null;
      for (AddPathPlugin pathPlugin : pathPlugins_)
      {
         HierarchyConfig hierarchyConfig = pathPlugin.getPaths();
         if (hierarchyConfig == null)
         {
            continue;
         }         
         List<JcrPath> jcrPaths = hierarchyConfig.getJcrPaths();
         if (jcrPaths == null)
         {
            continue;
         }
         Set<String> workspaceNames = new LinkedHashSet<String>();
         if (hierarchyConfig.getWorkspaces() != null)
         {
            workspaceNames.addAll(hierarchyConfig.getWorkspaces());
         }
         String repositoryName = hierarchyConfig.getRepository();
         ManageableRepository repository = repositoryName == null || repositoryName.isEmpty() ? 
               jcrService_.getCurrentRepository() : 
               jcrService_.getRepository(repositoryName);
         
         for (String workspaceName : workspaceNames)
         {
            JcrPath currentjcrPath = null;
            try
            {
               session = repository.getSystemSession(workspaceName);
               Node rootNode = session.getRootNode();
               for (JcrPath jcrPath : jcrPaths)
               {
                  currentjcrPath = jcrPath;
                  if (!jcrPath.getAlias().equals(USER_APPLICATION) && !jcrPath.getAlias().startsWith(USER_PRIVATE)
                     && !jcrPath.getAlias().startsWith(USER_PUBLIC))
                  {
                     createNode(rootNode, jcrPath.getPath(), jcrPath.getNodeType(), jcrPath.getMixinTypes(),
                        jcrPath.getPermissions(null));
                  }
               }
            }
            catch (RepositoryException e)
            {
               LOG.error("An error occurs while processing the JCR path which alias is "
                  + (currentjcrPath == null ? null : currentjcrPath.getAlias()) + " with the workspace "
                  + workspaceName, e);
            }
            finally
            {
               if (session != null)
               {
                  session.logout();
                  session = null;
               }
            }
         }
      }
   }

   private void initBasePath(String repositoryName) throws Exception
   {
      Session session = null;
      ManageableRepository manageableRepository =
         repositoryName == null || repositoryName.isEmpty() ? jcrService_.getCurrentRepository() : jcrService_
            .getRepository(repositoryName);
      String defaultWorkspace = manageableRepository.getConfiguration().getDefaultWorkspaceName();
      String systemWorkspace = manageableRepository.getConfiguration().getSystemWorkspaceName();
      boolean isSameWorkspace = defaultWorkspace.equalsIgnoreCase(systemWorkspace);
      String[] workspaceNames = manageableRepository.getWorkspaceNames();
      for (AddPathPlugin pathPlugin : pathPlugins_)
      {
         HierarchyConfig hierarchyConfig = pathPlugin.getPaths();
         if (hierarchyConfig == null)
         {
            continue;
         }
         List<JcrPath> jcrPaths = hierarchyConfig.getJcrPaths();
         if (jcrPaths == null)
         {
            continue;
         }
         for (String workspaceName : workspaceNames)
         {
            if (!isSameWorkspace && workspaceName.equalsIgnoreCase(systemWorkspace))
               continue;
            JcrPath currentjcrPath = null;
            try
            {
               session = manageableRepository.getSystemSession(workspaceName);
               Node rootNode = session.getRootNode();
               for (JcrPath jcrPath : jcrPaths)
               {
                  currentjcrPath = jcrPath;
                  if (!jcrPath.getAlias().equals(USER_APPLICATION) && !jcrPath.getAlias().startsWith(USER_PRIVATE)
                     && !jcrPath.getAlias().startsWith(USER_PUBLIC))
                  {
                     createNode(rootNode, jcrPath.getPath(), jcrPath.getNodeType(), jcrPath.getMixinTypes(),
                        jcrPath.getPermissions(null));
                  }
               }
            }
            catch (Exception e)
            {
               LOG.error("An error occurs while processing the JCR path which alias is "
                  + (currentjcrPath == null ? null : currentjcrPath.getAlias()) + " with the workspace "
                  + workspaceName, e);
            }
            finally
            {
               if (session != null)
               {
                  session.logout();
                  session = null;
               }
            }
         }
      }
   }

   /**
    * {@inheritDoc}
    */
   public Node getUserApplicationNode(SessionProvider sessionProvider, String userName) throws Exception
   {
      Node userNode = getUserNode(sessionProvider, userName);
      return dataDistributionManager_.getDataDistributionType(DataDistributionMode.NONE).getOrCreateDataNode(userNode,
         getJcrPath(USER_APPLICATION));
   }

   /**
    * {@inheritDoc}
    */
   public Node getPublicApplicationNode(SessionProvider sessionProvider) throws Exception
   {
      Session session = getSession(sessionProvider);
      Node rootNode = session.getRootNode();
      return dataDistributionManager_.getDataDistributionType(DataDistributionMode.NONE).getDataNode(rootNode,
         getJcrPath(PUBLIC_APPLICATION));
   }

   /**
    * {@inheritDoc}
    */
   public Node getUserNode(SessionProvider sessionProvider, String userName) throws Exception
   {
      Session session = getSession(sessionProvider);

      Node usersNode = getRootOfUsersNodes(session);

      DataDistributionType type =
         dataDistributionManager_.getDataDistributionType(oldDistribution ? DataDistributionMode.NONE
            : DataDistributionMode.READABLE);
      return type.getOrCreateDataNode(usersNode, userName);
   }

   /**
    * {@inheritDoc}
    */
   public void removeUserNode(SessionProvider sessionProvider, String userName) throws Exception
   {
      Session session = getSession(sessionProvider);
      try
      {
         Node usersNode = getRootOfUsersNodes(session);

         DataDistributionType type =
            dataDistributionManager_.getDataDistributionType(oldDistribution ? DataDistributionMode.NONE
               : DataDistributionMode.READABLE);
         type.removeDataNode(usersNode, userName);
      }
      catch (PathNotFoundException e)
      {
         if (LOG.isTraceEnabled())
         {
            LOG.trace("An exception occurred: " + e.getMessage());
         }
      }
   }

   private Session getSession(SessionProvider sessionProvider) throws RepositoryException
   {
      ManageableRepository repo = jcrService_.getCurrentRepository();
      return sessionProvider.getSession(repo.getConfiguration().getDefaultWorkspaceName(), repo);
   }

   /**
    * {@inheritDoc}
    */
   public String getJcrPath(String alias)
   {
      return paths_.get(alias);
   }

   /**
    * {@inheritDoc}
    */
   public void addPlugin(ComponentPlugin plugin)
   {
      if (plugin instanceof AddPathPlugin)
      {
         AddPathPlugin app = (AddPathPlugin)plugin;
         pathPlugins_.add(app);
         if (app.getPaths() != null && app.getPaths().getJcrPaths() != null)
         {
            for (JcrPath jcrPath : app.getPaths().getJcrPaths())
            {
               if (jcrPath.getAlias() != null && jcrPath.getPath() != null)
               {
                  paths_.put(jcrPath.getAlias(), jcrPath.getPath());
               }
            }
         }
      }
   }

   private boolean isNeededToMigrate()
   {
      return !oldDistribution && autoMigrate;
   }

   private void migrate() throws RepositoryException
   {
      Session session = getSession(SessionProvider.createSystemProvider());

      try
      {
         Node rootNode = getRootOfUsersNodes(session);
         dataDistributionManager_.getDataDistributionType(DataDistributionMode.READABLE).migrate(rootNode);
      }
      finally
      {
         session.logout();
      }
   }

   private Node getRootOfUsersNodes(Session session) throws PathNotFoundException, RepositoryException
   {
      String usersPath = getJcrPath(USERS_PATH);
      return (Node)session.getItem(usersPath);
   }
}
