/**
 * 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.portal.config;

import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import java.util.ResourceBundle;
import java.util.Set;

import org.exoplatform.commons.utils.LazyPageList;
import org.exoplatform.container.component.ComponentPlugin;
import org.exoplatform.container.xml.InitParams;
import org.exoplatform.container.xml.ValueParam;
import org.exoplatform.portal.config.model.Application;
import org.exoplatform.portal.config.model.Container;
import org.exoplatform.portal.config.model.ModelObject;
import org.exoplatform.portal.config.model.Page;
import org.exoplatform.portal.config.model.PortalConfig;
import org.exoplatform.portal.config.model.TransientApplicationState;
import org.exoplatform.portal.mop.SiteKey;
import org.exoplatform.portal.mop.SiteType;
import org.exoplatform.portal.mop.description.DescriptionService;
import org.exoplatform.portal.mop.importer.ImportMode;
import org.exoplatform.portal.mop.navigation.NavigationContext;
import org.exoplatform.portal.mop.navigation.NavigationService;
import org.exoplatform.portal.mop.navigation.NavigationState;
import org.exoplatform.portal.mop.page.PageContext;
import org.exoplatform.portal.mop.page.PageKey;
import org.exoplatform.portal.mop.page.PageService;
import org.exoplatform.portal.mop.user.UserNavigation;
import org.exoplatform.portal.mop.user.UserPortalContext;
import org.exoplatform.services.log.ExoLogger;
import org.exoplatform.services.log.Log;
import org.exoplatform.services.organization.Group;
import org.exoplatform.services.organization.OrganizationService;
import org.picocontainer.Startable;

/**
 * Created by The eXo Platform SAS Apr 19, 2007 This service is used to load the PortalConfig, Page config and Navigation config
 * for a given user.
 */
public class UserPortalConfigService implements Startable {
    DataStorage storage_;

    UserACL userACL_;

    OrganizationService orgService_;

    private NewPortalConfigListener newPortalConfigListener_;

    /** . */
    final NavigationService navService;

    /** . */
    final DescriptionService descriptionService;

    /** . */
    final PageService pageService;

    /** . */
    boolean createUserPortal;

    /** . */
    boolean destroyUserPortal;

    /** . */
    private final ImportMode defaultImportMode;

    private Log log = ExoLogger.getLogger("Portal:UserPortalConfigService");

    public UserPortalConfigService(UserACL userACL, DataStorage storage, OrganizationService orgService,
            NavigationService navService, DescriptionService descriptionService, PageService pageService, InitParams params)
            throws Exception {

        //
        ValueParam createUserPortalParam = params == null ? null : params.getValueParam("create.user.portal");
        boolean createUserPortal = createUserPortalParam == null
                || createUserPortalParam.getValue().toLowerCase().trim().equals("true");

        //
        ValueParam destroyUserPortalParam = params == null ? null : params.getValueParam("destroy.user.portal");
        boolean destroyUserPortal = destroyUserPortalParam == null
                || destroyUserPortalParam.getValue().toLowerCase().trim().equals("true");

        //
        ValueParam defaultImportModeParam = params == null ? null : params.getValueParam("default.import.mode");
        ImportMode defaultImportMode = defaultImportModeParam == null ? ImportMode.CONSERVE : ImportMode
                .valueOf(defaultImportModeParam.getValue().toUpperCase().trim());

        //
        this.storage_ = storage;
        this.orgService_ = orgService;
        this.userACL_ = userACL;
        this.navService = navService;
        this.pageService = pageService;
        this.descriptionService = descriptionService;
        this.createUserPortal = createUserPortal;
        this.destroyUserPortal = destroyUserPortal;
        this.defaultImportMode = defaultImportMode;
    }

    public PageService getPageService() {
        return pageService;
    }

    public DataStorage getDataStorage() {
        return storage_;
    }

    public ImportMode getDefaultImportMode() {
        return defaultImportMode;
    }

    public boolean getCreateUserPortal() {
        return createUserPortal;
    }

    public void setCreateUserPortal(boolean createUserPortal) {
        this.createUserPortal = createUserPortal;
    }

    public boolean getDestroyUserPortal() {
        return destroyUserPortal;
    }

    public void setDestroyUserPortal(boolean destroyUserPortal) {
        this.destroyUserPortal = destroyUserPortal;
    }

    /**
     * Returns the navigation service associated with this service.
     *
     * @return the navigation service;
     */
    public NavigationService getNavigationService() {
        return navService;
    }

    public DescriptionService getDescriptionService() {
        return descriptionService;
    }

    public UserACL getUserACL() {
        return userACL_;
    }

    public OrganizationService getOrganizationService() {
        return orgService_;
    }

    /** Temporary until the {@link #getUserPortalConfig(String, String)} is removed. */
    private static final UserPortalContext NULL_CONTEXT = new UserPortalContext() {
        public ResourceBundle getBundle(UserNavigation navigation) {
            return null;
        }

        public Locale getUserLocale() {
            return Locale.ENGLISH;
        }
    };

    /**
     * <p>
     * Build and returns an instance of <tt>UserPortalConfig</tt>.
     * </p>
     * <p/>
     * <p>
     * To return a valid config, the current thread must be associated with an identity that will grant him access to the portal
     * as returned by the {@link UserACL#hasPermission(org.exoplatform.portal.config.model.PortalConfig)} method.
     * </p>
     * <p/>
     * <p>
     * The navigation loaded on the <tt>UserPortalConfig<tt> object are obtained according to the specified user
     * argument. The portal navigation is always loaded. If the specified user is null then the navigation of the guest
     * group as configured by {@link org.exoplatform.portal.config.UserACL#getGuestsGroup()} is also loaded, otherwise
     * the navigations are loaded according to the following rules:
     * <p/>
     * <ul> <li>The navigation corresponding to the user is loaded.</li> <li>When the user is root according to the value
     * returned by {@link org.exoplatform.portal.config.UserACL#getSuperUser()} then the navigation of all groups are
     * loaded.</li> <li>When the user is not root, then all its groups are added except the guest group as configued per
     * {@link org.exoplatform.portal.config.UserACL#getGuestsGroup()}.</li> </ul>
     * <p/>
     * All the navigations are sorted using the value returned by {@link org.exoplatform.portal.config.model.PageNavigation#getPriority()}.
     * </p>
     *
     * @param portalName the portal name
     * @param accessUser the user name
     * @return the config
     * @throws Exception any exception
     * @deprecated the method {@link #getUserPortalConfig(String, String, org.exoplatform.portal.mop.user.UserPortalContext)}
     *             should be used instead
     */
    @Deprecated
    public UserPortalConfig getUserPortalConfig(String portalName, String accessUser) throws Exception {
        return getUserPortalConfig(portalName, accessUser, NULL_CONTEXT);
    }

    public UserPortalConfig getUserPortalConfig(String portalName, String accessUser, UserPortalContext userPortalContext)
            throws Exception {
        PortalConfig portal = storage_.getPortalConfig(portalName);
        if (portal == null || !userACL_.hasPermission(portal)) {
            return null;
        }

        return new UserPortalConfig(portal, this, portalName, accessUser, userPortalContext);
    }

    /**
     * Compute and returns the list that the specified user can manage. If the user is root then all existing groups are
     * returned otherwise the list is computed from the groups in which the user has a configured membership. The membership is
     * configured from the value returned by {@link org.exoplatform.portal.config.UserACL#getMakableMT()}
     *
     * @param remoteUser the user to get the makable navigations
     * @param withSite true if a site must exist
     * @return the list of groups
     * @throws Exception any exception
     */
    public List<String> getMakableNavigations(String remoteUser, boolean withSite) throws Exception {
        Collection<Group> groups;
        if (remoteUser.equals(userACL_.getSuperUser())) {
            groups = orgService_.getGroupHandler().getAllGroups();
        } else {
            groups = orgService_.getGroupHandler().findGroupByMembership(remoteUser, userACL_.getMakableMT());
        }

        //
        List<String> list = new ArrayList<String>();
        if (groups != null) {
            Set<String> existingNames = null;
            if (withSite) {
                existingNames = new HashSet<String>();
                Query<PortalConfig> q = new Query<PortalConfig>("group", null, PortalConfig.class);
                LazyPageList<PortalConfig> lpl = storage_.find(q);
                for (PortalConfig groupSite : lpl.getAll()) {
                    existingNames.add(groupSite.getName());
                }
            }

            //
            for (Group group : groups) {
                String groupId = group.getId().trim();
                if (existingNames == null || existingNames.contains(groupId)) {
                    list.add(groupId);
                }
            }
        }

        //
        return list;
    }

    /**
     * Create a user site for the specified user. It will perform the following:
     * <ul>
     * <li>create the user site by calling {@link #createUserPortalConfig(String, String, String)} which may create a site or
     * not according to the default configuration</li>
     * <li>if not site exists then it creates a site then it creates an empty site</li>
     * <li>if not navigation exists for the user site then it creates an empty navigation</li>
     * </ul>
     *
     * @param userName the user name
     * @throws Exception a nasty exception
     */
    public void createUserSite(String userName) throws Exception {
        // Create the portal from the template
        createUserPortalConfig(PortalConfig.USER_TYPE, userName, "user");

        // Need to insert the corresponding user site if needed
        PortalConfig cfg = storage_.getPortalConfig(PortalConfig.USER_TYPE, userName);
        if (cfg == null) {
            cfg = new PortalConfig(PortalConfig.USER_TYPE);
            cfg.setPortalLayout(new Container());
            cfg.setName(userName);
            storage_.create(cfg);
        }

        // Create a blank navigation if needed
        SiteKey key = SiteKey.user(userName);
        NavigationContext nav = navService.loadNavigation(key);
        if (nav == null) {
            nav = new NavigationContext(key, new NavigationState(5));
            navService.saveNavigation(nav);
        }
    }

    /**
     * Create a group site for the specified group. It will perform the following:
     * <ul>
     * <li>create the group site by calling {@link #createUserPortalConfig(String, String, String)} which may create a site or
     * not according to the default configuration</li>
     * <li>if not site exists then it creates a site then it creates an empty site</li>
     * </ul>
     *
     * @param groupId the group id
     * @throws Exception a nasty exception
     */
    public void createGroupSite(String groupId) throws Exception {
        // Create the portal from the template
        createUserPortalConfig(PortalConfig.GROUP_TYPE, groupId, "group");

        // Need to insert the corresponding group site
        PortalConfig cfg = storage_.getPortalConfig(PortalConfig.GROUP_TYPE, groupId);
        if (cfg == null) {
            cfg = new PortalConfig(PortalConfig.GROUP_TYPE);
            cfg.setPortalLayout(new Container());
            cfg.setName(groupId);
            storage_.create(cfg);
        }
    }

    /**
     * This method should create a the portal config, pages and navigation according to the template name.
     *
     * @param siteType the site type
     * @param siteName the Site name
     * @param template the template to use
     * @throws Exception any exception
     */
    public void createUserPortalConfig(String siteType, String siteName, String template) throws Exception {
        String templatePath = newPortalConfigListener_.getTemplateConfig(siteType, template);

        NewPortalConfig portalConfig = new NewPortalConfig(templatePath);
        portalConfig.setTemplateName(template);
        portalConfig.setOwnerType(siteType);
        newPortalConfigListener_.createPortalConfig(portalConfig, siteName);
        newPortalConfigListener_.createPage(portalConfig, siteName);
        newPortalConfigListener_.createPageNavigation(portalConfig, siteName);
    }

    /**
     * This method removes the PortalConfig, Page and PageNavigation that belong to the portal in the database.
     *
     * @param portalName the portal name
     * @throws Exception any exception
     */
    public void removeUserPortalConfig(String portalName) throws Exception {
        removeUserPortalConfig(PortalConfig.PORTAL_TYPE, portalName);
    }

    /**
     * This method removes the PortalConfig, Page and PageNavigation that belong to the portal in the database.
     *
     * @param ownerType the owner type
     * @param ownerId the portal name
     * @throws Exception any exception
     */
    public void removeUserPortalConfig(String ownerType, String ownerId) throws Exception {
        PortalConfig config = storage_.getPortalConfig(ownerType, ownerId);
        if (config != null) {
            storage_.remove(config);
        }
    }

    /**
     * Use {@link PageService} to load metadata of specify page
     *
     * @param pageRef the PageKey
     * @return the PageContext
     */
    public PageContext getPage(PageKey pageRef) {
        if (pageRef == null) {
            return null;
        }

        PageContext page = pageService.loadPage(pageRef);
        if (page == null || !userACL_.hasPermission(page)) {
            return null;
        }
        return page;
    }

    /**
     * Creates a page from an existing template.
     *
     * @param temp the template name
     * @param ownerType the new owner type
     * @param ownerId the new owner id
     * @return the page
     * @throws Exception any exception
     */
    public Page createPageTemplate(String temp, String ownerType, String ownerId) throws Exception {
        Page page = newPortalConfigListener_.createPageFromTemplate(ownerType, ownerId, temp);
        updateOwnership(page, ownerType, ownerId);
        return page;
    }

    /**
     * Returns the list of all portal site names.
     *
     * @return the list of all portal site names
     * @throws Exception any exception
     */
    public List<String> getAllPortalNames() throws Exception {
        return getAllSiteNames(SiteType.PORTAL);
    }

    /**
     * Returns the list of all group site names.
     *
     * @return the list of all group site names
     * @throws Exception any exception
     */
    public List<String> getAllGroupNames() throws Exception {
        return getAllSiteNames(SiteType.GROUP);
    }

    private List<String> getAllSiteNames(SiteType siteType) throws Exception {
        List<String> list;
        switch (siteType) {
            case PORTAL:
                list = storage_.getAllPortalNames();
                break;
            case GROUP:
                list = storage_.getAllGroupNames();
                break;
            default:
                throw new AssertionError();
        }
        for (Iterator<String> i = list.iterator(); i.hasNext();) {
            String name = i.next();
            PortalConfig config = storage_.getPortalConfig(siteType.getName(), name);
            if (config == null || !userACL_.hasPermission(config)) {
                i.remove();
            }
        }
        return list;
    }

    /**
     * Update the ownership recursively on the model graph.
     *
     * @param object the model object graph root
     * @param ownerType the new owner type
     * @param ownerId the new owner id
     */
    private void updateOwnership(ModelObject object, String ownerType, String ownerId) {
        if (object instanceof Container) {
            Container container = (Container) object;
            if (container instanceof Page) {
                Page page = (Page) container;
                page.setOwnerType(ownerType);
                page.setOwnerId(ownerId);
            }
            for (ModelObject child : container.getChildren()) {
                updateOwnership(child, ownerType, ownerId);
            }
        } else if (object instanceof Application) {
            Application application = (Application) object;
            TransientApplicationState applicationState = (TransientApplicationState) application.getState();
            if (applicationState != null && (applicationState.getOwnerType() == null || applicationState.getOwnerId() == null)) {
                applicationState.setOwnerType(ownerType);
                applicationState.setOwnerId(ownerId);
            }
        }
    }

    public void initListener(ComponentPlugin listener) {
        if (listener instanceof NewPortalConfigListener) {
            synchronized (this) {
                if (newPortalConfigListener_ == null) {
                    this.newPortalConfigListener_ = (NewPortalConfigListener) listener;
                } else {
                    newPortalConfigListener_.mergePlugin((NewPortalConfigListener) listener);
                }
            }
        }
    }

    public void deleteListenerElements(ComponentPlugin listener) {
        if (listener instanceof NewPortalConfigListener) {
            synchronized (this) {
                if (newPortalConfigListener_ != null) {
                    newPortalConfigListener_.deleteListenerElements((NewPortalConfigListener) listener);
                }
            }
        }
    }

    public void start() {
        try {
            if (newPortalConfigListener_ == null) {
                return;
            }

            //
            newPortalConfigListener_.run();
        } catch (Exception e) {
            log.error("Could not import initial data", e);
        }
    }

    public void stop() {
    }

    public String getDefaultPortal() {
        return newPortalConfigListener_.getDefaultPortal();
    }

    public Set<String> getPortalTemplates() {
        return newPortalConfigListener_.getTemplateConfigs(PortalConfig.PORTAL_TYPE);
    }

    public PortalConfig getPortalConfigFromTemplate(String templateName) {
        return newPortalConfigListener_.getPortalConfigFromTemplate(PortalConfig.PORTAL_TYPE, templateName);
    }
}
