/**
 * 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.pom.data;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import java.util.regex.Pattern;

import org.exoplatform.portal.config.NoSuchDataException;
import org.exoplatform.portal.config.StaleModelException;
import org.exoplatform.portal.config.UserACL;
import org.exoplatform.portal.config.model.ApplicationState;
import org.exoplatform.portal.config.model.ApplicationType;
import org.exoplatform.portal.config.model.CloneApplicationState;
import org.exoplatform.portal.config.model.Container;
import org.exoplatform.portal.config.model.PersistentApplicationState;
import org.exoplatform.portal.config.model.PortalConfig;
import org.exoplatform.portal.config.model.TransientApplicationState;
import org.exoplatform.portal.mop.Described;
import org.exoplatform.portal.mop.ProtectedContainer;
import org.exoplatform.portal.mop.ProtectedResource;
import org.exoplatform.portal.mop.redirects.Condition;
import org.exoplatform.portal.mop.redirects.DeviceProperty;
import org.exoplatform.portal.mop.redirects.Mappings;
import org.exoplatform.portal.mop.redirects.NodeMap;
import org.exoplatform.portal.mop.redirects.Redirect;
import org.exoplatform.portal.mop.redirects.Redirectable;
import org.exoplatform.portal.pom.config.POMSession;
import org.exoplatform.portal.pom.config.Utils;
import org.exoplatform.portal.pom.spi.portlet.Portlet;
import org.exoplatform.services.jcr.util.Text;
import org.gatein.common.logging.Logger;
import org.gatein.common.logging.LoggerFactory;
import org.gatein.mop.api.Attributes;
import org.gatein.mop.api.content.ContentType;
import org.gatein.mop.api.content.Customization;
import org.gatein.mop.api.workspace.ObjectType;
import org.gatein.mop.api.workspace.Site;
import org.gatein.mop.api.workspace.Templatized;
import org.gatein.mop.api.workspace.WorkspaceObject;
import org.gatein.mop.api.workspace.ui.UIBody;
import org.gatein.mop.api.workspace.ui.UIComponent;
import org.gatein.mop.api.workspace.ui.UIContainer;
import org.gatein.mop.api.workspace.ui.UIWindow;
import org.gatein.mop.core.util.Tools;

/**
 * @author <a href="mailto:julien.viet@exoplatform.com">Julien Viet</a>
 * @version $Revision$
 */
public class Mapper {

    /** . */
    private static final Set<String> propertiesBlackList = Tools.set("jcr:uuid", "jcr:primaryType");

    /** . */
    private static final Set<String> portalPropertiesBlackList = Tools.set(MappedAttributes.LOCALE.getName(),
            MappedAttributes.SKIN.getName());

    /** . */
    private static final Set<String> windowPropertiesBlackList = Tools.set(MappedAttributes.THEME.getName(),
            MappedAttributes.TYPE.getName(), MappedAttributes.ICON.getName(), MappedAttributes.WIDTH.getName(),
            MappedAttributes.HEIGHT.getName());

    /** . */
    private final POMSession session;

    /** . */
    private final Logger log = LoggerFactory.getLogger(Mapper.class);

    public Mapper(POMSession session) {
        this.session = session;
    }

    public PortalData load(Site src) {
        String type = Mapper.getOwnerType(src.getObjectType());
        Attributes attrs = src.getAttributes();

        //
        Templatized templarized = src.getRootNavigation().getTemplatized();
        org.gatein.mop.api.workspace.Page template = templarized.getTemplate();
        UIContainer srcLayout = template.getRootComponent();

        //
        Map<String, String> properties = new HashMap<String, String>();
        load(attrs, properties, portalPropertiesBlackList);

        //
        List<ComponentData> layoutChildren = loadChildren(srcLayout);
        ContainerData layout = load(srcLayout, layoutChildren);

        //
        List<String> accessPermissions = Collections.emptyList();
        String editPermission = null;
        if (src.isAdapted(ProtectedResource.class)) {
            ProtectedResource pr = src.adapt(ProtectedResource.class);
            accessPermissions = pr.getAccessPermissions();
            editPermission = pr.getEditPermission();
        }

        //
        List<RedirectData> redirects = null;
        if (src.isAdapted(Redirectable.class)) {
            Redirectable redirectAble = src.adapt(Redirectable.class);
            redirects = loadRedirects(src, redirectAble.getRedirects());
        }

        Described described = src.adapt(Described.class);

        //
        return new PortalData(src.getObjectId(), src.getName(), type, attrs.getValue(MappedAttributes.LOCALE),
                described.getName(), described.getDescription(), accessPermissions, editPermission,
                Collections.unmodifiableMap(properties), attrs.getValue(MappedAttributes.SKIN), layout, redirects);
    }

    private List<RedirectData> loadRedirects(Site src, Map<String, Redirect> redirects) {
        if (redirects == null || redirects.isEmpty()) {
            return null;
        } else {
            List<RedirectData> redirectsData = new ArrayList<RedirectData>();
            for (Redirect redirect : redirects.values()) {
                RedirectData redirectDate = new RedirectData(src.getObjectId(), redirect.getSite(), redirect.getName(),
                        redirect.getEnabled(), buildConditionData(src, redirect.getConditions()), buildMappingsData(
                                src.getObjectId(), redirect.getMapping()));
                redirectsData.add(redirectDate);
            }
            return redirectsData;
        }
    }

    private List<RedirectConditionData> buildConditionData(Site src, Map<String, Condition> conditions) {
        List<RedirectConditionData> conditionDatas = new ArrayList<RedirectConditionData>();

        for (Condition condition : conditions.values()) {
            RedirectConditionData conditionData = new RedirectConditionData(src.getObjectId(), null, condition.getName());

            RedirectUserAgentConditionData userAgentConditionData = new RedirectUserAgentConditionData(src.getObjectId(), null);
            userAgentConditionData.getUserAgentContains().addAll(condition.getUserAgentContains());
            userAgentConditionData.getUserAgentDoesNotContain().addAll(condition.getUserAgentDoesNotContain());

            conditionData.setUserAgentConditionData(userAgentConditionData);

            for (DeviceProperty deviceProperty : condition.getDeviceProperties().values()) {
                RedirectDevicePropertyConditionData propertyConditionData = new RedirectDevicePropertyConditionData(
                        src.getObjectId(), null, deviceProperty.getName());
                if (deviceProperty.getEquals() != null) {
                    propertyConditionData.setEquals(deviceProperty.getEquals());
                }

                if (deviceProperty.getGreaterThan() != null) {
                    propertyConditionData.setGreaterThan(deviceProperty.getGreaterThan());
                }

                if (deviceProperty.getLessThan() != null) {
                    propertyConditionData.setLessThan(deviceProperty.getLessThan());
                }

                if (deviceProperty.getPattern() != null) {
                    propertyConditionData.setMatches(Pattern.compile(deviceProperty.getPattern()));
                }

                conditionData.getDevicePropertyConditionData().add(propertyConditionData);
            }

            conditionDatas.add(conditionData);
        }

        return conditionDatas;
    }

    private RedirectMappingsData buildMappingsData(String storageId, Mappings mappings) {
        if (mappings != null) {
            RedirectMappingsData redirectMappingsData = new RedirectMappingsData(storageId);

            if (mappings.getUnresolvedNodeMatching() != null) {
                redirectMappingsData.setUnresolvedNode(mappings.getUnresolvedNodeMatching());
            }

            if (mappings.getNodeNameMatching() != null) {
                redirectMappingsData.setUseNodeNameMatching(mappings.getNodeNameMatching());
            }

            if (mappings.getNodeMap() != null) {
                HashMap<String, String> map = new HashMap<String, String>();
                for (NodeMap nodeMap : mappings.getNodeMap().values()) {
                    map.put(nodeMap.getOriginNode(), nodeMap.getRedirectNode());
                }
                redirectMappingsData.getMappings().putAll(map);
            }

            return redirectMappingsData;
        } else {
            return null;
        }
    }

    public void save(PortalData src, Site dst) {
        try {
            if (src.getStorageId() != null && !src.getStorageId().equals(dst.getObjectId())) {
                String msg = "Attempt to save a site " + src.getType() + "/" + src.getName() + " on the wrong target site "
                        + dst.getObjectType() + "/" + dst.getName();
                throw new IllegalArgumentException(msg);
            }

            //
            Attributes attrs = dst.getAttributes();
            attrs.setValue(MappedAttributes.LOCALE, src.getLocale());
            attrs.setValue(MappedAttributes.SKIN, src.getSkin());
            if (src.getProperties() != null) {
                save(src.getProperties(), attrs, portalPropertiesBlackList);
            }

            ProtectedResource pr = dst.adapt(ProtectedResource.class);
            pr.setAccessPermissions(src.getAccessPermissions());
            pr.setEditPermission(src.getEditPermission());

            Described described = dst.adapt(Described.class);
            described.setName(src.getLabel());
            described.setDescription(src.getDescription());

            Redirectable redirectable = dst.adapt(Redirectable.class);
            Map<String, Redirect> redirects = redirectable.getRedirects();

            List<RedirectData> redirectsData = src.getRedirects();

            redirects.clear(); // clear the redirects map since we need to rebuild it based on the new redirects
            if (src.getRedirects() != null) {
                for (RedirectData redirectData : redirectsData) {
                    Redirect redirect = redirectable.createRedirect();
                    redirects.put(redirectData.getRedirectName(), redirect);

                    redirect.setName(redirectData.getRedirectName());
                    redirect.setSite(redirectData.getRedirectSiteName());
                    redirect.setEnabled(redirectData.isEnabled());

                    if (redirectData.getConditions() != null) {
                        redirect.getConditions().clear(); // clear the map so that we can rebuild it
                        for (RedirectConditionData conditionData : redirectData.getConditions()) {
                            Condition condition = redirect.createCondition();
                            redirect.getConditions().put(conditionData.getRedirectName(), condition);
                            buildCondition(conditionData, condition);
                        }
                    }

                    if (redirectData.getMappings() != null) {
                        Mappings mappings = redirect.getMapping();
                        if (redirect.getMapping() == null) {
                            mappings = redirect.createMapping();
                            redirect.setMapping(mappings);
                        }
                        buildMappings(redirectData.getMappings(), mappings);
                    }
                }
            }

            //
            org.gatein.mop.api.workspace.Page templates = dst.getRootPage().getChild("templates");
            org.gatein.mop.api.workspace.Page template = templates.getChild("default");
            if (template == null) {
                template = templates.addChild("default");
            }

            //
            ContainerData srcContainer = src.getPortalLayout();
            UIContainer dstContainer = template.getRootComponent();

            // Workaround to have the real source container used as the model / UI layer lose this
            // ID which lead to bugs
            ContainerData realSrcContainer = new ContainerData(dstContainer.getObjectId(), srcContainer.getId(),
                    srcContainer.getName(), srcContainer.getIcon(), srcContainer.getTemplate(), srcContainer.getFactoryId(),
                    srcContainer.getTitle(), srcContainer.getDescription(), srcContainer.getWidth(), srcContainer.getHeight(),
                    srcContainer.getAccessPermissions(), srcContainer.getMoveAppsPermissions(),
                    srcContainer.getMoveContainersPermissions(), srcContainer.getChildren());

            //
            save(realSrcContainer, dstContainer);
            saveChildren(realSrcContainer, dstContainer);

            //
            Templatized templatized = dst.getRootNavigation().getTemplatized();
            if (templatized != null) {
                templatized.setTemplate(template);
            } else {
                template.templatize(dst.getRootNavigation());
            }
        } catch (Exception e) {
            log.error(e.getMessage(), e);
        }
    }

    private void buildCondition(RedirectConditionData redirectConditionData, Condition condition) {
        condition.setName(redirectConditionData.getRedirectName());

        if (redirectConditionData.getUserAgentConditionData() != null) {
            condition.setUserAgentContains(redirectConditionData.getUserAgentConditionData().getUserAgentContains());
        }
        if (redirectConditionData.getUserAgentConditionData() != null) {
            condition
                    .setUserAgentDoesNotContain(redirectConditionData.getUserAgentConditionData().getUserAgentDoesNotContain());
        }

        // if the device property conditions is empty and there are currently no stored device properties, then skip
        // otherwise we need to either remove or add device properties
        if (redirectConditionData.getDevicePropertyConditionData() != null || !condition.getDeviceProperties().isEmpty()) {
            // A list of stored redirect names which have not been found in the current RedirectConditionData list.
            //
            // This is because during a save we receive a list of RedirectData, and we need to keep track if an existing
            // stored redirect has been deleted or not.
            Set<String> notFoundProperties = new HashSet<String>(condition.getDeviceProperties().keySet());

            for (RedirectDevicePropertyConditionData propertyConditionData : redirectConditionData
                    .getDevicePropertyConditionData()) {

                DeviceProperty deviceProperty = condition.getDeviceProperties().get(propertyConditionData.getPropertyName());
                // if not found, then create it
                if (deviceProperty == null) {
                    deviceProperty = condition.createDeviceProperty();
                    condition.getDeviceProperties().put(propertyConditionData.getPropertyName(), deviceProperty);
                } else {
                    // if found, remove from the nonFound list
                    notFoundProperties.remove(propertyConditionData.getPropertyName());
                }

                deviceProperty.setName(propertyConditionData.getPropertyName());

                if (propertyConditionData.getGreaterThan() != null) {
                    deviceProperty.setGreaterThan(propertyConditionData.getGreaterThan());
                }

                if (propertyConditionData.getLessThan() != null) {
                    deviceProperty.setLessThan(propertyConditionData.getLessThan());
                }

                if (propertyConditionData.getEquals() != null) {
                    deviceProperty.setEquals(propertyConditionData.getEquals());
                }

                if (propertyConditionData.getMatches() != null) {
                    deviceProperty.setPattern(propertyConditionData.getMatches().toString());
                }
            }

            // remove all the device conditions which were not in the RedirectConditionData list
            for (String devicePropertyName : notFoundProperties) {
                condition.getDeviceProperties().remove(devicePropertyName);
            }
        }
    }

    private void buildMappings(RedirectMappingsData mappingsData, Mappings mappings) {
        mappings.setNodeNameMatching(mappingsData.isUseNodeNameMatching());
        mappings.setUnresolvedNodeMatching(mappingsData.getUnresolvedNode());

        // if the mappings conditions is empty and there are currently no stored mappings, then skip
        // otherwise we need to either remove or add mappings
        if (!mappingsData.getMappings().isEmpty() || !mappings.getNodeMap().isEmpty()) {

            // A list of stored node mappings which have not been found in the current RedirectMappingsData list.
            //
            // This is because during a save we receive a list of nodes from RedirectMappingsData, and we need to keep track
            // if an existing stored redirect node has been deleted or not.
            Set<String> notFoundNodeMappings = new HashSet<String>(mappings.getNodeMap().keySet());

            for (String key : mappingsData.getMappings().keySet()) {
                NodeMap nodeMap = mappings.getNodeMap().get(Text.escapeIllegalJcrChars(key));
                if (nodeMap == null) {
                    nodeMap = mappings.createNode();
                    mappings.getNodeMap().put(Text.escapeIllegalJcrChars(key), nodeMap);
                } else {
                    notFoundNodeMappings.remove(Text.escapeIllegalJcrChars(key));
                }
                nodeMap.setOriginNode(key);
                nodeMap.setRedirectNode(mappingsData.getMappings().get(key));
            }

            // remove all the device conditions which were not in the RedirectConditionData list
            for (String nodeName : notFoundNodeMappings) {
                mappings.getNodeMap().remove(nodeName);
            }
        }
    }

    public PageData load(org.gatein.mop.api.workspace.Page src) {
        Site site = src.getSite();
        String ownerType = getOwnerType(site.getObjectType());
        String ownerId = site.getName();
        String name = src.getName();
        List<ComponentData> children = loadChildren(src.getRootComponent());

        List<String> moveAppsPermissions = null;
        List<String> moveContainersPermissions = null;
        if (src.isAdapted(ProtectedContainer.class)) {
            ProtectedContainer pc = src.adapt(ProtectedContainer.class);
            moveAppsPermissions = pc.getMoveAppsPermissions();
            moveContainersPermissions = pc.getMoveContainersPermissions();
        } else {
            moveAppsPermissions = Collections.emptyList();
            moveContainersPermissions = Collections.emptyList();
        }
        //
        return new PageData(src.getObjectId(), null, name, null, null, null, null, null, null, null,
                Collections.<String> emptyList(), children, ownerType, ownerId, null, false, moveAppsPermissions,
                moveContainersPermissions);
    }

    private ContainerData load(UIContainer src, List<ComponentData> children) {
        //
        List<String> accessPermissions = Collections.emptyList();
        if (src.isAdapted(ProtectedResource.class)) {
            ProtectedResource pr = src.adapt(ProtectedResource.class);
            accessPermissions = pr.getAccessPermissions();
        }
        List<String> moveAppsPermissions = null;
        List<String> moveContainersPermissions = null;
        if (src.isAdapted(ProtectedContainer.class)) {
            ProtectedContainer pc = src.adapt(ProtectedContainer.class);
            moveAppsPermissions = pc.getMoveAppsPermissions();
            moveContainersPermissions = pc.getMoveContainersPermissions();
        } else {
            /* legacy mode */
            moveAppsPermissions = ProtectedContainer.DEFAULT_MOVE_APPLICATIONS_PERMISSIONS;
            moveContainersPermissions = ProtectedContainer.DEFAULT_MOVE_CONTAINERS_PERMISSIONS;
        }

        //
        Described described = src.adapt(Described.class);

        Attributes attrs = src.getAttributes();
        return new ContainerData(src.getObjectId(), attrs.getValue(MappedAttributes.ID), attrs.getValue(MappedAttributes.NAME),
                attrs.getValue(MappedAttributes.ICON), attrs.getValue(MappedAttributes.TEMPLATE),
                attrs.getValue(MappedAttributes.FACTORY_ID), described.getName(), described.getDescription(),
                attrs.getValue(MappedAttributes.WIDTH), attrs.getValue(MappedAttributes.HEIGHT),
                Utils.safeImmutableList(accessPermissions), Utils.safeImmutableList(moveAppsPermissions),
                Utils.safeImmutableList(moveContainersPermissions), children);
    }

    private List<ComponentData> loadChildren(UIContainer src) {
        if (src == null)
            throw new NoSuchDataException("Can not load children");
        ArrayList<ComponentData> children = new ArrayList<ComponentData>();
        for (UIComponent component : src.getComponents()) {

            // Obtain a model object from the ui component
            ComponentData mo;
            if (component instanceof UIContainer) {
                UIContainer srcContainer = (UIContainer) component;
                Attributes attrs = srcContainer.getAttributes();
                String type = attrs.getValue(MappedAttributes.TYPE);
                if ("dashboard".equals(type)) {
                    Site owner = src.getPage().getSite();
                    TransientApplicationState<Portlet> state = new TransientApplicationState<Portlet>(
                            "dashboard/DashboardPortlet", null, getOwnerType(owner.getObjectType()), owner.getName());

                    //
                    boolean showInfoBar = attrs.getValue(MappedAttributes.SHOW_INFO_BAR, false);
                    boolean showMode = attrs.getValue(MappedAttributes.SHOW_MODE, false);
                    boolean showWindowState = attrs.getValue(MappedAttributes.SHOW_WINDOW_STATE, false);
                    String theme = attrs.getValue(MappedAttributes.THEME, null);

                    Described described = srcContainer.adapt(Described.class);

                    String id = attrs.getValue(MappedAttributes.ID, null);
                    String icon = attrs.getValue(MappedAttributes.ICON, null);
                    String width = attrs.getValue(MappedAttributes.WIDTH, null);
                    String height = attrs.getValue(MappedAttributes.HEIGHT, null);

                    //
                    List<String> a = Collections.singletonList(UserACL.EVERYONE);
                    if (srcContainer.isAdapted(ProtectedResource.class)) {
                        ProtectedResource pr = srcContainer.adapt(ProtectedResource.class);
                        a = pr.getAccessPermissions();
                    }

                    //
                    mo = new ApplicationData<Portlet>(srcContainer.getObjectId(), component.getName(), ApplicationType.PORTLET,
                            state, id, described.getName(), icon, described.getDescription(), showInfoBar, showWindowState,
                            showMode, theme, width, height, Collections.<String, String> emptyMap(), a);
                } else {
                    List<ComponentData> dstChildren = loadChildren(srcContainer);
                    mo = load(srcContainer, dstChildren);
                }
            } else if (component instanceof UIWindow) {
                UIWindow window = (UIWindow) component;
                ApplicationData<?> application = load(window);
                mo = application;
            } else if (component instanceof UIBody) {
                mo = new BodyData(component.getObjectId(), BodyType.PAGE);
            } else {
                throw new AssertionError();
            }

            // Add among children
            children.add(mo);
        }
        return children;
    }

    public List<ModelChange> save(PageData src, Site site, String name) throws IllegalStateException {
        org.gatein.mop.api.workspace.Page root = site.getRootPage();
        org.gatein.mop.api.workspace.Page pages = root.getChild("pages");
        org.gatein.mop.api.workspace.Page dst = pages.getChild(name);

        //
        LinkedList<ModelChange> changes = new LinkedList<ModelChange>();

        //
        if (dst == null) {
            throw new NoSuchDataException("The page " + name + " not found");
        } else {
            changes.add(new ModelChange.Update(src));
        }

        //
        UIContainer rootContainer = dst.getRootComponent();

        // We are creating a new Page with the root container id as this one is lost
        // in the model / ui layer. Not doing this cause a class cast exception later
        // so it's likely the best fix we can do at the moment
        PageData src2 = new PageData(rootContainer.getObjectId(), src.getId(), src.getName(), src.getIcon(), src.getTemplate(),
                null, null, null, src.getWidth(), src.getHeight(), Collections.<String> emptyList(), src.getChildren(),
                src.getOwnerType(), src.getOwnerId(), null, false, src.getMoveAppsPermissions(),
                src.getMoveContainersPermissions());

        //
        LinkedList<ModelChange> childrenChanges = saveChildren(src2, rootContainer);

        //
        changes.addAll(childrenChanges);

        //
        return changes;
    }

    private void save(ContainerData src, UIContainer dst) {

        ProtectedResource pr = dst.adapt(ProtectedResource.class);
        pr.setAccessPermissions(src.getAccessPermissions());

        ProtectedContainer pc = dst.adapt(ProtectedContainer.class);
        pc.setMoveAppsPermissions(src.getMoveAppsPermissions());
        pc.setMoveContainersPermissions(src.getMoveContainersPermissions());

        Described described = dst.adapt(Described.class);
        described.setName(src.getTitle());
        described.setDescription(src.getDescription());

        Attributes dstAttrs = dst.getAttributes();
        dstAttrs.setValue(MappedAttributes.ID, src.getId());
        dstAttrs.setValue(MappedAttributes.TYPE, src instanceof DashboardData ? "dashboard" : null);
        dstAttrs.setValue(MappedAttributes.ICON, src.getIcon());
        dstAttrs.setValue(MappedAttributes.TEMPLATE, src.getTemplate());
        dstAttrs.setValue(MappedAttributes.FACTORY_ID, src.getFactoryId());
        dstAttrs.setValue(MappedAttributes.WIDTH, src.getWidth());
        dstAttrs.setValue(MappedAttributes.HEIGHT, src.getHeight());
        dstAttrs.setValue(MappedAttributes.NAME, src.getName());
    }

    /*
     * Performs routing of the corresponding save method
     */
    private void save(ModelData src, WorkspaceObject dst, LinkedList<ModelChange> changes,
            Map<String, String> hierarchyRelationships) {
        if (src instanceof ContainerData) {
            save((ContainerData) src, (UIContainer) dst);
            saveChildren((ContainerData) src, (UIContainer) dst, changes, hierarchyRelationships);
        } else if (src instanceof ApplicationData) {
            save((ApplicationData<?>) src, (UIWindow) dst);
        } else if (src instanceof BodyData) {
            // Stateless
        } else {
            throw new AssertionError("Was not expecting child " + src);
        }
    }

    private LinkedList<ModelChange> saveChildren(final ContainerData src, UIContainer dst) {
        LinkedList<ModelChange> changes = new LinkedList<ModelChange>();

        // The relationship in the hierarchy
        // basically it's a map of the relationships between parent/child nodes
        // that is helpful to detect move operations
        // that we make immutable to avoid any bug
        Map<String, String> hierarchyRelationships = new HashMap<String, String>();
        build(src, hierarchyRelationships);
        hierarchyRelationships = Collections.unmodifiableMap(hierarchyRelationships);

        //
        saveChildren(src, dst, changes, hierarchyRelationships);

        //
        return changes;
    }

    private void build(ContainerData parent, Map<String, String> hierarchyRelationships) {
        String parentId = parent.getStorageId();
        for (ModelData child : parent.getChildren()) {
            String childId = child.getStorageId();
            if (childId != null) {
                if (hierarchyRelationships.containsKey(childId)) {
                    throw new AssertionError("The same object is present two times in the object hierarchy");
                }

                // Note that we are aware that parent id may be null
                hierarchyRelationships.put(childId, parentId);
            }
            if (child instanceof ContainerData) {
                build((ContainerData) child, hierarchyRelationships);
            }
        }
    }

    private void saveChildren(final ContainerData src, UIContainer dst, LinkedList<ModelChange> changes,
            Map<String, String> hierarchyRelationships) {
        final List<String> orders = new ArrayList<String>();
        final Map<String, ModelData> modelObjectMap = new HashMap<String, ModelData>();

        //
        for (ModelData srcChild : src.getChildren()) {
            String srcChildId = srcChild.getStorageId();
            // Flag variable, become non null if and only if we are saving a transient dashboard
            ApplicationData<?> transientDashboardData = null;

            // Replace dashboard application by container if needed
            // this should be removed once we make the dashboard as first class
            // citizen of the portal
            if (srcChild instanceof ApplicationData) {
                ApplicationData<?> app = (ApplicationData<?>) srcChild;
                // todo julien: shouldn't we be checking for WSRP as well here?
                if (app.getType() == ApplicationType.PORTLET && app.getState() instanceof TransientApplicationState) {
                    TransientApplicationState<?> state = (TransientApplicationState<?>) app.getState();
                    String contentId = state.getContentId();
                    if ("dashboard/DashboardPortlet".equals(contentId)) {
                        DashboardData data;
                        if (app.getStorageId() != null) {
                            UIContainer dstDashboard = session.findObjectById(ObjectType.CONTAINER, app.getStorageId());
                            data = loadDashboard(dstDashboard);

                            // Update those attributes as we have to do it now, they don't exist in a container
                            // but do exist in a dashboard container
                            Attributes attrs = dstDashboard.getAttributes();
                            attrs.setValue(MappedAttributes.SHOW_INFO_BAR, app.isShowInfoBar());
                            attrs.setValue(MappedAttributes.SHOW_MODE, app.isShowApplicationMode());
                            attrs.setValue(MappedAttributes.SHOW_WINDOW_STATE, app.isShowApplicationState());
                            attrs.setValue(MappedAttributes.THEME, app.getTheme());
                        } else {
                            data = DashboardData.INITIAL_DASHBOARD;
                            transientDashboardData = (ApplicationData<?>) srcChild;
                        }

                        //
                        String icon = data.getIcon();
                        if (icon == null)
                            icon = app.getIcon();

                        String title = data.getTitle();
                        if (title == null)
                            title = app.getTitle();

                        String description = data.getDescription();
                        if (description == null)
                            description = app.getDescription();

                        String width = data.getWidth();
                        if (width == null)
                            width = app.getWidth();

                        String height = data.getHeight();
                        if (height == null)
                            height = app.getHeight();

                        data = new DashboardData(data.getStorageId(), data.getId(), data.getName(), icon, data.getTemplate(),
                                data.getFactoryId(), title, description, width, height, app.getAccessPermissions(),
                                data.getMoveAppsPermissions(), data.getMoveContainersPermissions(), data.getChildren());

                        //
                        srcChild = data;
                    }
                }
            }

            //
            UIComponent dstChild;
            if (srcChildId != null) {
                dstChild = session.findObjectById(ObjectType.COMPONENT, srcChildId);
                if (dstChild == null) {
                    throw new StaleModelException("Could not find supposed present child with id " + srcChildId);
                }

                // julien : this can fail due to a bug in chromattic not implementing equals method properly
                // and is replaced with the foreach below
                /*
                 * if (!dst.contains(dstChild)) { throw new IllegalArgumentException("Attempt for updating a ui component " +
                 * session.pathOf(dstChild) + "that is not present in the target ui container " + session.pathOf(dst)); }
                 */
                boolean found = false;
                for (UIComponent child : dst.getComponents()) {
                    if (child.getObjectId().equals(srcChildId)) {
                        found = true;
                        break;
                    }
                }

                //
                if (!found) {
                    if (hierarchyRelationships.containsKey(srcChildId)) {
                        String srcId = hierarchyRelationships.get(srcChildId);

                        // It's a move operation, so we move the node first
                        dst.getComponents().add(dstChild);

                        //
                        changes.add(new ModelChange.Move(srcId, dst.getObjectId(), srcChildId));
                    } else {
                        throw new IllegalArgumentException("Attempt for updating a ui component " + session.pathOf(dstChild)
                                + " that is not present in the target ui container " + session.pathOf(dst));
                    }
                }

                //
                changes.add(new ModelChange.Update(srcChild));
            } else {
                String name = srcChild.getStorageName();
                if (name == null) {
                    // We manufacture one name
                    name = UUID.randomUUID().toString();
                }
                if (srcChild instanceof ContainerData) {
                    dstChild = dst.add(ObjectType.CONTAINER, name);
                } else if (srcChild instanceof ApplicationData) {
                    dstChild = dst.add(ObjectType.WINDOW, name);
                } else if (srcChild instanceof BodyData) {
                    dstChild = dst.add(ObjectType.BODY, name);
                } else {
                    throw new StaleModelException("Was not expecting child " + srcChild);
                }
                changes.add(new ModelChange.Create(dst.getObjectId(), srcChild));
            }

            //
            if (transientDashboardData != null) {
                Attributes attrs = dstChild.getAttributes();
                attrs.setValue(MappedAttributes.SHOW_INFO_BAR, transientDashboardData.isShowInfoBar());
                attrs.setValue(MappedAttributes.SHOW_MODE, transientDashboardData.isShowApplicationMode());
                attrs.setValue(MappedAttributes.SHOW_WINDOW_STATE, transientDashboardData.isShowApplicationState());
                attrs.setValue(MappedAttributes.THEME, transientDashboardData.getTheme());
            }
            save(srcChild, dstChild, changes, hierarchyRelationships);

            //
            String dstChildId = dstChild.getObjectId();
            modelObjectMap.put(dstChildId, srcChild);
            orders.add(dstChildId);
        }

        // Take care of move operation that could be seen as a remove otherwise
        for (UIComponent dstChild : dst.getComponents()) {
            String dstChildId = dstChild.getObjectId();
            if (!modelObjectMap.containsKey(dstChildId)) {
                String parentId = hierarchyRelationships.get(dstChildId);
                if (parentId != null) {
                    // Get the new parent
                    UIContainer parent = session.findObjectById(ObjectType.CONTAINER, parentId);

                    // Perform the move
                    parent.getComponents().add(dstChild);

                    //
                    changes.add(new ModelChange.Move(dst.getObjectId(), parentId, dstChildId));

                    // julien : we do not need to create an update operation
                    // as later the update operation will be created when the
                    // object
                    // will be processed
                } else if (hierarchyRelationships.containsKey(dstChildId)) {
                    // The dstChild is placed under transient Chromattic entity whose storageId == null. However,
                    // the hierachyRelationships contains dstChildId in key set, so we have to mark dstChild as
                    // moved object
                    modelObjectMap.put(dstChildId, null);
                }
            }
        }

        // Delete removed children
        for (Iterator<UIComponent> i = dst.getComponents().iterator(); i.hasNext();) {
            UIComponent dstChild = i.next();
            String dstChildId = dstChild.getObjectId();
            if (!modelObjectMap.containsKey(dstChildId)) {
                i.remove();
                changes.add(new ModelChange.Destroy(dstChildId));
            }
        }

        // Now sort children according to the order provided by the container
        // need to replace that with Collections.sort once the set(int index, E element) is implemented in Chromattic lists
        UIComponent[] a = dst.getComponents().toArray(new UIComponent[dst.getComponents().size()]);
        Arrays.sort(a, new Comparator<UIComponent>() {
            public int compare(UIComponent o1, UIComponent o2) {
                int i1 = orders.indexOf(o1.getObjectId());
                int i2 = orders.indexOf(o2.getObjectId());
                return i1 - i2;
            }
        });
        for (int j = 0; j < a.length; j++) {
            dst.getComponents().add(j, a[j]);
        }
    }

    public <S> ApplicationData<S> load(UIWindow src) {
        Attributes attrs = src.getAttributes();

        //
        Customization<?> customization = src.getCustomization();

        //
        ContentType<?> contentType = customization.getType();

        //
        String customizationid = customization.getId();

        // julien: should type check that
        ApplicationType<S> type = (ApplicationType<S>) ApplicationType.getType(contentType);

        //
        PersistentApplicationState<S> instanceState = new PersistentApplicationState<S>(customizationid);

        //
        HashMap<String, String> properties = new HashMap<String, String>();
        load(attrs, properties, windowPropertiesBlackList);

        //
        List<String> accessPermissions = Collections.emptyList();
        if (src.isAdapted(ProtectedResource.class)) {
            ProtectedResource pr = src.adapt(ProtectedResource.class);
            accessPermissions = pr.getAccessPermissions();
        }

        //
        Described described = src.adapt(Described.class);

        //
        boolean showInfoBar = attrs.getValue(MappedAttributes.SHOW_INFO_BAR, false);
        boolean showWindowState = attrs.getValue(MappedAttributes.SHOW_WINDOW_STATE, false);
        boolean showMode = attrs.getValue(MappedAttributes.SHOW_MODE, false);
        String theme = attrs.getValue(MappedAttributes.THEME, null);

        //
        return new ApplicationData<S>(src.getObjectId(), src.getName(), type, instanceState, null, described.getName(),
                attrs.getValue(MappedAttributes.ICON), described.getDescription(), showInfoBar, showWindowState, showMode,
                theme, attrs.getValue(MappedAttributes.WIDTH), attrs.getValue(MappedAttributes.HEIGHT),
                Utils.safeImmutableMap(properties), Utils.safeImmutableList(accessPermissions));
    }

    public <S> void save(ApplicationData<S> src, UIWindow dst) {

        ProtectedResource pr = dst.adapt(ProtectedResource.class);
        pr.setAccessPermissions(src.getAccessPermissions());

        Described described = dst.adapt(Described.class);
        described.setName(src.getTitle());
        described.setDescription(src.getDescription());

        //
        Attributes attrs = dst.getAttributes();
        attrs.setValue(MappedAttributes.SHOW_INFO_BAR, src.isShowInfoBar());
        attrs.setValue(MappedAttributes.SHOW_WINDOW_STATE, src.isShowApplicationState());
        attrs.setValue(MappedAttributes.SHOW_MODE, src.isShowApplicationMode());
        attrs.setValue(MappedAttributes.THEME, src.getTheme());
        attrs.setValue(MappedAttributes.ICON, src.getIcon());
        attrs.setValue(MappedAttributes.WIDTH, src.getWidth());
        attrs.setValue(MappedAttributes.HEIGHT, src.getHeight());
        save(src.getProperties(), attrs, windowPropertiesBlackList);

        //
        ApplicationState<S> instanceState = src.getState();

        // We modify only transient portlet state
        // and we ignore any persistent portlet state
        if (instanceState instanceof TransientApplicationState) {

            //
            TransientApplicationState<S> transientState = (TransientApplicationState<S>) instanceState;

            // Attempt to get a site from the instance state
            Site site = null;
            if (transientState.getOwnerType() != null && transientState.getOwnerId() != null) {
                ObjectType<Site> siteType = parseSiteType(transientState.getOwnerType());
                site = session.getWorkspace().getSite(siteType, transientState.getOwnerId());
            }

            // The current site
            Site currentSite = dst.getPage().getSite();

            // If it is the same site than the current page
            // set null
            if (site == dst.getPage().getSite()) {
                site = null;
            }

            // The content id
            String contentId = transientState.getContentId();
            ContentType<S> contentType = src.getType().getContentType();

            // The customization that we will inherit from if not null
            Customization<?> customization = null;

            // Destroy existing window previous customization
            if (dst.getCustomization() != null) {
                dst.getCustomization().destroy();
            }

            // If the existing customization is not null and matches the content id
            Customization<S> dstCustomization;
            if (customization != null && customization.getType().equals(contentType)
                    && customization.getContentId().equals(contentId)) {

                // Cast is ok as content type matches
                @SuppressWarnings("unchecked")
                Customization<S> bilto = (Customization<S>) customization;

                // If it's a customization of the current site we extend it
                if (bilto.getContext() == currentSite) {
                    dstCustomization = dst.customize(bilto);
                } else {
                    // Otherwise we clone it propertly
                    S state = bilto.getVirtualState();
                    dstCustomization = dst.customize(contentType, contentId, state);
                }
            } else {
                // Otherwise we create an empty customization
                dstCustomization = dst.customize(contentType, contentId, null);
            }

            // At this point we have customized the window
            // now if we have any additional state payload we must merge it
            // with the current state
            S state = ((TransientApplicationState<S>) instanceState).getContentState();
            if (state != null) {
                dstCustomization.setState(state);
            }
        } else if (instanceState instanceof CloneApplicationState) {
            CloneApplicationState cloneState = (CloneApplicationState) instanceState;

            //
            Customization<?> customization = session.findCustomizationById(cloneState.getStorageId());

            //
            dst.customize(customization);
        } else if (instanceState instanceof PersistentApplicationState) {
            // Do nothing
        } else {
            throw new IllegalArgumentException("Cannot save application with state " + instanceState);
        }
    }

    public DashboardData loadDashboard(UIContainer container) {

        List<String> accessPermissions = Collections.emptyList();
        if (container.isAdapted(ProtectedResource.class)) {
            ProtectedResource pr = container.adapt(ProtectedResource.class);
            accessPermissions = pr.getAccessPermissions();
        }

        List<String> moveAppsPermissions = null;
        List<String> moveContainersPermissions = null;
        if (container.isAdapted(ProtectedContainer.class)) {
            ProtectedContainer pc = container.adapt(ProtectedContainer.class);
            moveAppsPermissions = pc.getMoveAppsPermissions();
            moveContainersPermissions = pc.getMoveContainersPermissions();
        } else {
            moveAppsPermissions = Collections.emptyList();
            moveContainersPermissions = Collections.emptyList();
        }

        //
        Described described = container.adapt(Described.class);

        //
        Attributes attrs = container.getAttributes();
        List<ComponentData> children = loadChildren(container);
        return new DashboardData(container.getObjectId(), attrs.getValue(MappedAttributes.ID),
                attrs.getValue(MappedAttributes.NAME), attrs.getValue(MappedAttributes.ICON),
                attrs.getValue(MappedAttributes.TEMPLATE), attrs.getValue(MappedAttributes.FACTORY_ID), described.getName(),
                described.getDescription(), attrs.getValue(MappedAttributes.WIDTH), attrs.getValue(MappedAttributes.HEIGHT),
                Utils.safeImmutableList(accessPermissions), moveAppsPermissions, moveContainersPermissions, children);
    }

    public void saveDashboard(DashboardData dashboard, UIContainer dst) {
        save(dashboard, dst);
        saveChildren(dashboard, dst);
    }

    private static void load(Attributes src, Map<String, String> dst, Set<String> blackList) {
        for (String name : src.getKeys()) {
            if (!blackList.contains(name) && !propertiesBlackList.contains(name)) {
                Object value = src.getObject(name);
                if (value instanceof String) {
                    dst.put(name, (String) value);
                }
            }
        }
    }

    public static void save(Map<String, String> src, Attributes dst, Set<String> blackList) {
        for (Map.Entry<String, String> property : src.entrySet()) {
            String name = property.getKey();
            if (!blackList.contains(name) && !propertiesBlackList.contains(name)) {
                dst.setString(name, property.getValue());
            }
        }
    }

    public static String getOwnerType(ObjectType<? extends Site> siteType) {
        if (siteType == ObjectType.PORTAL_SITE) {
            return PortalConfig.PORTAL_TYPE;
        } else if (siteType == ObjectType.GROUP_SITE) {
            return PortalConfig.GROUP_TYPE;
        } else if (siteType == ObjectType.USER_SITE) {
            return PortalConfig.USER_TYPE;
        } else {
            throw new IllegalArgumentException("Invalid site type " + siteType);
        }
    }

    public static ObjectType<Site> parseSiteType(String ownerType) {
        if (ownerType.equals(PortalConfig.PORTAL_TYPE)) {
            return ObjectType.PORTAL_SITE;
        } else if (ownerType.equals(PortalConfig.GROUP_TYPE)) {
            return ObjectType.GROUP_SITE;
        } else if (ownerType.equals(PortalConfig.USER_TYPE)) {
            return ObjectType.USER_SITE;
        } else {
            throw new IllegalArgumentException("Invalid owner type " + ownerType);
        }
    }
}
