/*
 * JBoss, a division of Red Hat
 * Copyright 2011, Red Hat Middleware, LLC, and individual
 * contributors as indicated by the @authors tag. See the
 * copyright.txt in the distribution for a full listing of
 * individual contributors.
 *
 * 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.gatein.integration.wsrp.structure;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;

import org.exoplatform.portal.config.DataStorage;
import org.exoplatform.portal.mop.Described;
import org.exoplatform.portal.mop.EventType;
import org.exoplatform.portal.mop.page.PageKey;
import org.exoplatform.portal.pom.spi.wsrp.WSRP;
import org.exoplatform.services.listener.Event;
import org.exoplatform.services.listener.Listener;
import org.gatein.common.util.ParameterValidation;
import org.gatein.mop.api.content.Customization;
import org.gatein.mop.api.workspace.ObjectType;
import org.gatein.mop.api.workspace.Page;
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.pc.api.PortletContext;
import org.gatein.pc.api.PortletStateType;
import org.gatein.pc.api.StatefulPortletContext;
import org.gatein.wsrp.api.context.ConsumerStructureProvider;

/**
 * @author <a href="mailto:chris.laprun@jboss.com">Chris Laprun</a>
 * @version $Revision$
 */
public class MOPConsumerStructureProvider extends Listener implements ConsumerStructureProvider {
    private final PortalStructureAccess structureAccess;
    private Map<String, PageInfo> pageInfos;
    private boolean pagesHaveBeenInitialized;

    public MOPConsumerStructureProvider(PortalStructureAccess structureAccess) {
        ParameterValidation.throwIllegalArgExceptionIfNull(structureAccess, "PortalStructureAccess");

        this.structureAccess = structureAccess;
        pageInfos = new HashMap<String, PageInfo>();
    }

    public List<String> getPageIdentifiers() {
        if (!pagesHaveBeenInitialized) {
            // initialize page information
            Collection<Page> pages = structureAccess.getPages();
            for (Page page : pages) {
                addPage(page);
            }

            pagesHaveBeenInitialized = true;
        }

        LinkedList<String> identifiers = new LinkedList<String>(pageInfos.keySet());
        Collections.sort(identifiers);
        return identifiers;
    }

    private void addPage(Page page) {
        PageInfo pageInfo = new PageInfo(page);
        pageInfos.put(pageInfo.getId(), pageInfo);
        UIContainer container = page.getRootComponent();
        processContainer(container, pageInfo);

        Collection<Page> children = page.getChildren();
        if (ParameterValidation.existsAndIsNotEmpty(children)) {
            for (Page child : children) {
                addPage(child);
            }
        }
    }

    public List<String> getWindowIdentifiersFor(String pageId) {
        PageInfo pageInfo = pageInfos.get(pageId);
        if (pageInfo == null) {
            throw new IllegalArgumentException("Page '" + pageId + "' does not exist.");
        }

        return pageInfo.getChildrenWindows();
    }

    private void processContainer(UIContainer container, PageInfo pageInfo) {
        if (container != null) {
            List<UIComponent> components = container.getComponents();
            for (UIComponent component : components) {
                ObjectType<? extends UIComponent> type = component.getObjectType();
                if (ObjectType.WINDOW.equals(type)) {
                    Described described = component.adapt(Described.class);
                    String name = described.getName();
                    if (name == null) {
                        name = described.getDescription();
                    }

                    pageInfo.addWindow(name, component.getObjectId());
                } else if (ObjectType.CONTAINER.equals(type)) {
                    processContainer((UIContainer) component, pageInfo);
                } else {
                    // ignore
                }
            }
        }
    }

    public void assignPortletToWindow(PortletContext portletContext, String windowId, String pageId,
            String exportedPortletHandle) {
        PageInfo pageInfo = pageInfos.get(pageId);
        String uuid = pageInfo.getWindowUUID(windowId);
        ParameterValidation.throwIllegalArgExceptionIfNull(uuid, "UUID for " + windowId);

        // get the window
        UIWindow window = structureAccess.getWindowFrom(uuid);

        // construct the new customization state
        WSRP wsrp = new WSRP();
        String portletId = portletContext.getId();
        wsrp.setPortletId(portletId);
        if (portletContext instanceof StatefulPortletContext) {
            StatefulPortletContext context = (StatefulPortletContext) portletContext;
            if (PortletStateType.OPAQUE.equals(context.getType())) {
                wsrp.setState((byte[]) context.getState());
            } else {
                throw new IllegalArgumentException("Don't know how to deal with state: " + context.getState());
            }
        }

        // destroy existing customization as otherwise re-customizing will fail
        Customization<?> customization = window.getCustomization();
        customization.destroy();

        // and re-customize
        window.customize(WSRP.CONTENT_TYPE, portletId, wsrp);

        // Change the window's name so that it's less confusing to users
        Described described = window.adapt(Described.class);
        String newName = exportedPortletHandle + " (remote)";
        described.setName(newName); // should be the same as ApplicationRegistryService.REMOTE_DISPLAY_NAME_SUFFIX

        // update window mappings
        pageInfo.updateWindowName(windowId, newName);

        structureAccess.saveChangesTo(window);
    }

    @Override
    public void onEvent(Event event) throws Exception {
        String eventName = event.getEventName();

        // get the MOP page from the event data
        final Object eventData = event.getData();
        final Page page;
        PageKey pageKey = null;
        if (eventData instanceof PageKey) {
            pageKey = (PageKey) eventData;
            page = structureAccess.getPageFrom(pageKey);
        } else {
            org.exoplatform.portal.config.model.Page portalPage = (org.exoplatform.portal.config.model.Page) eventData;
            page = structureAccess.getPageFrom(portalPage);
        }

        if (pageKey != null && page == null && EventType.PAGE_DESTROYED.equals(eventName)) {
            // if we try to remove a page, when we get this event, the page has already been removed from JCR
            // so we need to work around that fact by retrieving the corresponding PageInfo from the portal page title
            // which should match the Described name and check that it matches the internal name before removing it
            removePageByInternalName(pageKey.getName());
            return;
        }

        if (page != null) {
            if (EventType.PAGE_CREATED.equals(eventName)) {
                // add information for new page
                addPage(page);
            } else if (EventType.PAGE_UPDATED.equals(eventName) || DataStorage.PAGE_UPDATED.equals(eventName)) {
                removePage(page);
                addPage(page);
            }
        }
    }

    private void removePage(Page page) {
        final String id = PageInfo.getPageIdFor(page);

        PageInfo pageInfo = pageInfos.get(id);
        if (pageInfo != null && page.getName().equals(pageInfo.getInternalName())) {
            // remove page info
            pageInfos.remove(id);
        }
    }

    private void removePageByInternalName(String internalName) {
        Iterator<PageInfo> itr = pageInfos.values().iterator();
        while (itr.hasNext()) {
            if (itr.next().getInternalName().equals(internalName)) {
                itr.remove();
                return;
            }
        }
    }

    static class PageInfo {
        private final String uuid;
        private final Map<String, String> childrenWindows = new HashMap<String, String>();

        /** Name as provided by Described */
        private final String name;

        /** Human readable identifier across sites as pages with the same name exist in different sites */
        private final String id;

        /** Name as automatically generated */
        private final String internalName;

        private PageInfo(Page page) {
            this.uuid = page.getObjectId();
            this.internalName = page.getName();

            Described described = page.adapt(Described.class);
            this.name = described.getName();

            this.id = getPageIdFor(page);
        }

        static String getPageIdFor(Page page) {
            Described described = page.adapt(Described.class);
            return getPageIdFor(page.getSite().getName(), described.getName());
        }

        static String getPageIdFor(String siteName, String pageName) {
            return siteName + " :: " + pageName;
        }

        public String getUUID() {
            return uuid;
        }

        public String getInternalName() {
            return internalName;
        }

        public List<String> getChildrenWindows() {
            return new ArrayList<String>(childrenWindows.keySet());
        }

        public void addWindow(String windowName, String uuid) {
            // if we don't have a window name, use the UUID
            if (ParameterValidation.isNullOrEmpty(windowName)) {
                windowName = uuid;
            }

            // add suffix in case we have several windows with the same name in the page
            if (childrenWindows.containsKey(windowName)) {
                if (windowName.endsWith("|")) {
                    windowName += "|";
                } else {
                    windowName += windowName + " |";
                }
            }

            childrenWindows.put(windowName, uuid);
        }

        public void updateWindowName(String oldWindowName, String newWindowName) {
            String windowUUID = getWindowUUID(oldWindowName);
            childrenWindows.remove(oldWindowName);
            childrenWindows.put(newWindowName, windowUUID);
        }

        public String getName() {
            return name;
        }

        public String getWindowUUID(String windowId) {
            return childrenWindows.get(windowId);
        }

        public String getId() {
            return id;
        }
    }
}
