/**
 * 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.webui.application;

import java.io.Serializable;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Map;

import org.exoplatform.application.gadget.GadgetRegistryService;
import org.exoplatform.container.ExoContainer;
import org.exoplatform.portal.config.DataStorage;
import org.exoplatform.portal.config.model.ApplicationState;
import org.exoplatform.portal.config.model.ApplicationType;
import org.exoplatform.portal.config.model.TransientApplicationState;
import org.exoplatform.portal.pc.ExoPortletState;
import org.exoplatform.portal.pc.ExoPortletStateType;
import org.exoplatform.portal.pom.spi.gadget.Gadget;
import org.exoplatform.portal.pom.spi.portlet.Portlet;
import org.exoplatform.portal.pom.spi.portlet.PortletBuilder;
import org.exoplatform.portal.pom.spi.portlet.Preference;
import org.exoplatform.portal.pom.spi.wsrp.WSRP;
import org.exoplatform.portal.pom.spi.wsrp.WSRPPortletStateType;
import org.gatein.pc.api.PortletContext;
import org.gatein.pc.api.PortletInvoker;
import org.gatein.pc.api.StatefulPortletContext;

/**
 * @author <a href="mailto:julien.viet@exoplatform.com">Julien Viet</a>
 * @version $Revision$
 */
public abstract class ModelAdapter<S, C extends Serializable> {
    private static final String LOCAL_STATE_ID = PortletContext.LOCAL_CONSUMER_CLONE.getId();

    public static <S, C extends Serializable, I> ModelAdapter<S, C> getAdapter(ApplicationType<S> type) {
        if (type == ApplicationType.PORTLET) {
            @SuppressWarnings("unchecked")
            ModelAdapter<S, C> adapter = (ModelAdapter<S, C>) PORTLET;
            return adapter;
        } else if (type == ApplicationType.GADGET) {
            @SuppressWarnings("unchecked")
            ModelAdapter<S, C> adapter = (ModelAdapter<S, C>) GADGET;
            return adapter;
        } else if (type == ApplicationType.WSRP_PORTLET) {
            @SuppressWarnings("unchecked")
            ModelAdapter<S, C> adapter = (ModelAdapter<S, C>) WSRP;
            return adapter;
        } else {
            throw new AssertionError();
        }
    }

    /** . */
    private static final ModelAdapter<Portlet, ExoPortletState> PORTLET = new ModelAdapter<Portlet, ExoPortletState>() {

        @Override
        public StatefulPortletContext<ExoPortletState> getPortletContext(ExoContainer container, String applicationId,
                ApplicationState<Portlet> applicationState) throws Exception {
            DataStorage dataStorage = (DataStorage) container.getComponentInstanceOfType(DataStorage.class);
            Portlet preferences = dataStorage.load(applicationState, ApplicationType.PORTLET);
            PortletContext producerOfferedPortletContext = getProducerOfferedPortletContext(applicationId);
            ExoPortletState map = new ExoPortletState(producerOfferedPortletContext.getId());
            if (preferences != null) {
                for (Preference pref : preferences) {
                    map.getState().put(pref.getName(), pref.getValues());
                }
            }
            return StatefulPortletContext.create(LOCAL_STATE_ID, ExoPortletStateType.getInstance(), map);
        }

        @Override
        public ApplicationState<Portlet> update(ExoContainer container, ExoPortletState updateState,
                ApplicationState<Portlet> applicationState) throws Exception {
            // Compute new preferences
            PortletBuilder builder = new PortletBuilder();
            for (Map.Entry<String, List<String>> entry : updateState.getState().entrySet()) {
                builder.add(entry.getKey(), entry.getValue());
            }

            if (applicationState instanceof TransientApplicationState) {
                TransientApplicationState<Portlet> transientState = (TransientApplicationState<Portlet>) applicationState;
                transientState.setContentState(builder.build());
                return transientState;
            } else {
                DataStorage dataStorage = (DataStorage) container.getComponentInstanceOfType(DataStorage.class);
                return dataStorage.save(applicationState, builder.build());
            }
        }

        @Override
        public PortletContext getProducerOfferedPortletContext(String applicationState) {
            int indexOfSeparator = applicationState.lastIndexOf("/");
            String appName = applicationState.substring(0, indexOfSeparator);
            String portletName = applicationState.substring(indexOfSeparator + 1);
            return PortletContext.reference(PortletInvoker.LOCAL_PORTLET_INVOKER_ID,
                    PortletContext.createPortletContext(appName, portletName));
        }

        @Override
        public Portlet getState(ExoContainer container, ApplicationState<Portlet> applicationState) throws Exception {
            if (applicationState instanceof TransientApplicationState) {
                TransientApplicationState<Portlet> transientState = (TransientApplicationState<Portlet>) applicationState;
                Portlet pref = transientState.getContentState();
                if (pref == null) {
                    pref = new Portlet();
                }
                return pref;
            } else {
                DataStorage dataStorage = (DataStorage) container.getComponentInstanceOfType(DataStorage.class);
                Portlet pref = dataStorage.load(applicationState, ApplicationType.PORTLET);
                if (pref == null) {
                    pref = new Portlet();
                }
                return pref;
            }
        }

        @Override
        public ExoPortletState getStateFromModifiedContext(PortletContext originalPortletContext,
                PortletContext modifiedPortletContext) {
            if (modifiedPortletContext != null && modifiedPortletContext instanceof StatefulPortletContext) {
                StatefulPortletContext statefulContext = (StatefulPortletContext) modifiedPortletContext;
                if (statefulContext.getState() instanceof ExoPortletState) {
                    return (ExoPortletState) statefulContext.getState();
                }
            }
            return null;
        }

        @Override
        public ExoPortletState getstateFromClonedContext(PortletContext originalPortletContext,
                PortletContext clonedPortletContext) {
            if (clonedPortletContext != null && clonedPortletContext instanceof StatefulPortletContext) {
                StatefulPortletContext statefulContext = (StatefulPortletContext) clonedPortletContext;
                if (statefulContext.getState() instanceof ExoPortletState) {
                    return (ExoPortletState) statefulContext.getState();
                }
            }
            return null;
        }
    };

    private static final String DASHBOARD = "dashboard";
    private static final String GADGET_PORTLET = "GadgetPortlet";
    private static final PortletContext WRAPPER_CONTEXT = PortletContext.reference(PortletInvoker.LOCAL_PORTLET_INVOKER_ID,
            PortletContext.createPortletContext(DASHBOARD, GADGET_PORTLET));
    private static final String WRAPPER_ID = WRAPPER_CONTEXT.getId();
    private static final ModelAdapter<Gadget, ExoPortletState> GADGET = new ModelAdapter<Gadget, ExoPortletState>() {
        @Override
        public StatefulPortletContext<ExoPortletState> getPortletContext(ExoContainer container, String applicationId,
                ApplicationState<Gadget> applicationState) throws Exception {
            GadgetRegistryService gadgetService = (GadgetRegistryService) container
                    .getComponentInstanceOfType(GadgetRegistryService.class);
            org.exoplatform.application.gadget.Gadget model = gadgetService.getGadget(applicationId);
            String url = GadgetUtil.reproduceUrl(model.getUrl(), model.isLocal());
            ExoPortletState prefs = new ExoPortletState(WRAPPER_ID);
            prefs.getState().put("url", Arrays.asList(url));
            DataStorage dataStorage = (DataStorage) container.getComponentInstanceOfType(DataStorage.class);
            Gadget gadget = dataStorage.load(applicationState, ApplicationType.GADGET);
            if (gadget != null && gadget.getUserPref() != null) {
                prefs.getState().put("userPref", Collections.singletonList(gadget.getUserPref()));
            }
            return StatefulPortletContext.create(LOCAL_STATE_ID, ExoPortletStateType.getInstance(), prefs);
        }

        @Override
        public ApplicationState<Gadget> update(ExoContainer container, ExoPortletState updateState,
                ApplicationState<Gadget> gadgetApplicationState) throws Exception {
            // Compute new preferences
            String userPref = null;
            for (Map.Entry<String, List<String>> entry : updateState.getState().entrySet()) {
                if (entry.getKey().equals("userPref") && entry.getValue().size() > 0) {
                    userPref = entry.getValue().get(0);
                }
            }

            if (gadgetApplicationState instanceof TransientApplicationState<?>) {
                throw new UnsupportedOperationException("todo");
            } else {
                if (userPref != null) {
                    Gadget gadget = new Gadget();
                    gadget.addUserPref(userPref);
                    DataStorage dataStorage = (DataStorage) container.getComponentInstanceOfType(DataStorage.class);
                    dataStorage.save(gadgetApplicationState, gadget);
                }
            }

            //
            return gadgetApplicationState;
        }

        @Override
        public PortletContext getProducerOfferedPortletContext(String applicationState) {
            return WRAPPER_CONTEXT;
        }

        @Override
        public Portlet getState(ExoContainer container, ApplicationState<Gadget> applicationState) throws Exception {
            // For now we return null as it does not make sense to edit the gadget preferences
            return null;
        }

        @Override
        public ExoPortletState getStateFromModifiedContext(PortletContext originalPortletContext,
                PortletContext modifiedPortletContext) {
            if (modifiedPortletContext != null && modifiedPortletContext instanceof StatefulPortletContext) {
                StatefulPortletContext statefulContext = (StatefulPortletContext) modifiedPortletContext;
                if (statefulContext.getState() instanceof ExoPortletState) {
                    return (ExoPortletState) statefulContext.getState();
                }
            }
            return null;
        }

        @Override
        public ExoPortletState getstateFromClonedContext(PortletContext originalPortletContext,
                PortletContext clonedPortletContext) {
            if (clonedPortletContext != null && clonedPortletContext instanceof StatefulPortletContext) {
                StatefulPortletContext statefulContext = (StatefulPortletContext) clonedPortletContext;
                if (statefulContext.getState() instanceof ExoPortletState) {
                    return (ExoPortletState) statefulContext.getState();
                }
            }
            return null;
        }
    };

    /**
     * todo: this ModelAdapter is not quite good, what is really needed is a ModelAdapter<WSRP, byte[]> so that the
     * StatefulPortletContext returned by getPortletContext is actually of type PortletStateType.OPAQUE so that it can be
     * properly handled in WSRP... This model needs to be revisited if we want to properly support consumer-side state
     * management. See GTNPORTAL-736.
     */
    private static final ModelAdapter<WSRP, WSRP> WSRP = new ModelAdapter<WSRP, WSRP>() {
        @Override
        public Portlet getState(ExoContainer container, ApplicationState<WSRP> state) throws Exception {
            return null; // return null for now
        }

        @Override
        public PortletContext getProducerOfferedPortletContext(String applicationId) {
            return PortletContext.createPortletContext(applicationId);
        }

        @Override
        public StatefulPortletContext<WSRP> getPortletContext(ExoContainer container, String applicationId,
                ApplicationState<WSRP> state) throws Exception {
            DataStorage dataStorage = (DataStorage) container.getComponentInstanceOfType(DataStorage.class);
            WSRP wsrp = dataStorage.load(state, ApplicationType.WSRP_PORTLET);
            if (wsrp == null) {
                // create
                wsrp = new WSRP();
                wsrp.setPortletId(applicationId);
                if (!(state instanceof TransientApplicationState)) {
                    // only save state if it's not transient
                    dataStorage.save(state, wsrp);
                }
            }
            return StatefulPortletContext.create(wsrp.getPortletId(), WSRPPortletStateType.instance, wsrp);
        }

        @Override
        public ApplicationState<WSRP> update(ExoContainer container, WSRP updateState, ApplicationState<WSRP> state)
                throws Exception {
            if (state instanceof TransientApplicationState) {
                TransientApplicationState<WSRP> transientState = (TransientApplicationState<WSRP>) state;
                transientState.setContentState(updateState);
                return transientState;
            } else {
                // todo: it is possible to get a CloneApplicationState for some reason, need to investigate

                DataStorage dataStorage = (DataStorage) container.getComponentInstanceOfType(DataStorage.class);
                return dataStorage.save(state, updateState);
            }
        }

        @Override
        public WSRP getStateFromModifiedContext(PortletContext originalPortletContext, PortletContext modifiedPortletContext) {
            WSRP wsrp = new WSRP();
            wsrp.setPortletId(modifiedPortletContext.getId());

            // from the originalPortletContext see if we are dealing with a cloned context or not.
            if (originalPortletContext instanceof StatefulPortletContext) {
                Object originalState = ((StatefulPortletContext) originalPortletContext).getState();
                if (originalState instanceof WSRP) {
                    wsrp.setCloned(((WSRP) originalState).isCloned());
                }
            }

            if (modifiedPortletContext instanceof StatefulPortletContext) {
                Object modifiedState = ((StatefulPortletContext) modifiedPortletContext).getState();
                if (modifiedState instanceof byte[]) {
                    wsrp.setState((byte[]) modifiedState);
                }
            }

            return wsrp;
        }

        @Override
        public WSRP getstateFromClonedContext(PortletContext originalPortletContext, PortletContext clonedPortletContext) {
            WSRP wsrp = new WSRP();
            wsrp.setPortletId(clonedPortletContext.getId());
            wsrp.setCloned(true);

            // if we have an associated state, record it as well...
            if (clonedPortletContext instanceof StatefulPortletContext) {
                StatefulPortletContext statefulPortletContext = (StatefulPortletContext) clonedPortletContext;
                wsrp.setState((byte[]) statefulPortletContext.getState());
            }

            return wsrp;
        }
    };

    public abstract PortletContext getProducerOfferedPortletContext(String applicationId);

    public abstract StatefulPortletContext<C> getPortletContext(ExoContainer container, String applicationId,
            ApplicationState<S> applicationState) throws Exception;

    public abstract ApplicationState<S> update(ExoContainer container, C updateState, ApplicationState<S> applicationState)
            throws Exception;

    /**
     * Returns the state of the gadget as preferences or null if the preferences cannot be edited as such.
     *
     * @param container the container
     * @param applicationState the application state
     * @return the preferences
     * @throws Exception any exception
     */
    public abstract Portlet getState(ExoContainer container, ApplicationState<S> applicationState) throws Exception;

    /**
     * Extracts the state based on what the current PortletContext is and the new modified PortletContext.
     *
     * @param originalPortletContext The current PortletContext for the Portlet
     * @param modifiedPortletContext The new modified PortletContext
     * @return
     */
    public abstract C getStateFromModifiedContext(PortletContext originalPortletContext, PortletContext modifiedPortletContext);

    /**
     * Extracts the state based on what the current PortletContext is and the new cloned PortletContext
     *
     * @param originalPortletContext The current PortletContext for the Portlet
     * @param clonedPortletContext The new cloned PortletContext
     * @return
     */
    public abstract C getstateFromClonedContext(PortletContext originalPortletContext, PortletContext clonedPortletContext);

}
