/*
 * Copyright 2000-2004 The Apache Software Foundation.
 * 
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * 
 *      http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.apache.jetspeed.modules.actions.controllers;

// Jetspeed stuff
import org.apache.jetspeed.om.registry.PortletEntry;
import org.apache.jetspeed.om.profile.IdentityElement;
import org.apache.jetspeed.om.profile.Entry;
import org.apache.jetspeed.om.profile.psml.PsmlControl;
import org.apache.jetspeed.om.profile.MetaInfo;
import org.apache.jetspeed.om.profile.Layout;
import org.apache.jetspeed.om.profile.Parameter;
import org.apache.jetspeed.om.profile.Portlets;
import org.apache.jetspeed.om.profile.Reference;
import org.apache.jetspeed.om.profile.psml.PsmlParameter;
import org.apache.jetspeed.om.profile.psml.PsmlLayout;
import org.apache.jetspeed.portal.Portlet;
import org.apache.jetspeed.portal.PortletSet;
import org.apache.jetspeed.portal.PortletController;
import org.apache.jetspeed.services.logging.JetspeedLogFactoryService;
import org.apache.jetspeed.services.logging.JetspeedLogger;
import org.apache.jetspeed.services.rundata.JetspeedRunData;
import org.apache.jetspeed.services.Registry;
import org.apache.jetspeed.services.statemanager.SessionState;
import org.apache.jetspeed.services.PsmlManager;

// Turbine stuff
import org.apache.turbine.modules.ActionLoader;
import org.apache.turbine.services.localization.Localization;
import org.apache.turbine.util.RunData;

// Velocity Stuff
import org.apache.velocity.context.Context;

// Java stuff
import java.util.ArrayList;
import java.util.Collections;
import java.util.Vector;
import java.util.List;
import java.util.Enumeration;
import java.util.Iterator;
import java.util.Map;
import java.util.HashMap;
import java.util.StringTokenizer;

/**
 * This action builds a context suitable for controllers handling
 * grid positioned layout using PortletSet.Constraints
 *
 * @author <a href="mailto:raphael@apache.org">Raphaël Luta</a>
 * @author <a href="mailto:paulsp@apache.org">Paul Spencer</a>
 * @version $Id: MultiColumnControllerAction.java,v 1.30 2004/02/23 02:49:58 jford Exp $
 */
public class MultiColumnControllerAction extends VelocityControllerAction
{

    private static final String REFERENCES_REMOVED = "references-removed";

    /**
     * Static initialization of the logger for this class
     */    
    private static final JetspeedLogger logger = JetspeedLogFactoryService.getLogger(MultiColumnControllerAction.class.getName());    
    
    /**
     * Subclasses must override this method to provide default behavior
     * for the portlet action
     */
    protected void buildNormalContext(PortletController controller,
                                      Context context,
                                      RunData rundata)
    {
        try
        {
        // retrieve the number of columns
        String cols = controller.getConfig().getInitParameter("cols");
        int colNum = 0;
        int rowNum = 0;
        try
        {
            colNum = Integer.parseInt(cols);
        }
        catch (Exception e)
        {
            // not an integer or null, default to standarrd value
            colNum = 3;
        }
        context.put("colNum", String.valueOf(colNum));

        //retrieve the size for each of the columns
        String sizes = controller.getConfig().getInitParameter("sizes");
        context.put("sizes", getCellSizes(sizes));

        //retrieve the class for each of the columns
        String columnClasses = controller.getConfig().getInitParameter("col_classes");
        context.put("col_classes", getCellClasses(columnClasses));

        PortletSet set = controller.getPortlets();
        // normalize the constraints and calculate max num of rows needed
        Enumeration en = set.getPortlets();
        int row = 0;
        int col = 0;
        while (en.hasMoreElements())
        {
            Portlet p = (Portlet) en.nextElement();

            PortletSet.Constraints
                constraints = p.getPortletConfig().getConstraints();

            if ((constraints != null)
                 && (constraints.getColumn() != null)
                 && (constraints.getRow() != null))
            {
                col = constraints.getColumn().intValue();
                if (col > colNum)
                {
                    constraints.setColumn(new Integer(col % colNum));
                }

                row = constraints.getRow().intValue();
                if (row > rowNum)
                {
                    rowNum = row;
                }
            }
        }
        row = (int) Math.ceil(set.size() / colNum);
        if (row > rowNum)
        {
            rowNum = row;
        }

        if ( logger.isDebugEnabled() ) {
            logger.debug("Controller calculated setSize " + set.size() + " row " + row + " colNum: " + colNum +  " rowNum: " + rowNum);
        }
        // initialize the result position table and the work list
        List[] table = new List[colNum];
        List filler = Collections.nCopies(rowNum + 1, null);
        for (int i = 0; i < colNum; i++)
        {
            table[i] = new ArrayList();
            table[i].addAll(filler);
        }

        List work = new ArrayList();

        //position the constrained elements and keep a reference to the
        //others
        for (int i = 0; i < set.size(); i++)
        {
            Portlet p = set.getPortletAt(i);

            PortletSet.Constraints
                constraints = p.getPortletConfig().getConstraints();

            if ((constraints != null)
                 && (constraints.getColumn() != null)
                 && (constraints.getRow() != null)
                 && (constraints.getColumn().intValue() < colNum))
            {
                row = constraints.getRow().intValue();
                col = constraints.getColumn().intValue();
                table[col].set(row, p);
            }
            else
            {
                work.add(p);
            }
        }

        //insert the unconstrained elements in the table
        Iterator i = work.iterator();
        for (row = 0; row < rowNum; row++)
        {
            for (col = 0; i.hasNext() && (col < colNum); col++)
            {
                if (table[col].get(row) == null)
                {
                    table[col].set(row, i.next());
                }
            }
        }

        // now cleanup any remaining null elements
        for (int j = 0; j < table.length; j++)
        {
            i = table[j].iterator();
            while (i.hasNext())
            {
                Object obj = i.next();
                if (obj == null)
                {
                    i.remove();
                }
            }
        }

        if (logger.isDebugEnabled())
        {
            dumpColumns(table);
        }

        context.put("portlets", table);
        }
        catch (Exception e)
        {
            logger.error("Exception", e);
        }
    }

    /**
     * Subclasses must override this method to provide default behavior
     * for the portlet action
     */
    protected void buildCustomizeContext(PortletController controller,
                                         Context context,
                                         RunData rundata)
    {
        JetspeedRunData jdata = (JetspeedRunData) rundata;

        // get the customization state for this page
        SessionState customizationState = jdata.getPageSessionState();

        super.buildCustomizeContext(controller, context, rundata);

        List[] columns = null;

        // retrieve the number of columns
        String cols = controller.getConfig().getInitParameter("cols");
        int colNum = 0;
        try
        {
            colNum = Integer.parseInt(cols);
        }
        catch (Exception e)
        {
            // not an integer or null, default to standarrd value
            colNum = 3;
        }
        context.put("colNum", String.valueOf(colNum));

        //retrieve the size for each of the columns
        String sizes = controller.getConfig().getInitParameter("sizes");
        context.put("sizes", getCellSizes(sizes));

        //retrieve the class for each of the columns
        String columnClasses = controller.getConfig().getInitParameter("col_classes");
        context.put("col_classes", getCellClasses(columnClasses));

        columns = (List[]) customizationState.getAttribute("customize-columns");
        PortletSet customizedSet = (PortletSet) jdata.getCustomized();
        Portlets set = jdata.getCustomizedProfile()
                            .getDocument()
                            .getPortletsById(customizedSet.getID());

        if ( logger.isDebugEnabled() ) {
            logger.debug("MultiCol: columns " + columns + " set " + set);
        }

        if ((columns != null) && (columns.length == colNum))
        {
            int eCount = 0;
            for (int i = 0; i < columns.length; i++)
            {
                eCount += columns[i].size();
            }

            if ( logger.isDebugEnabled() ) {
                logger.debug("MultiCol: eCount " + eCount + " setCount" + set.getEntryCount() + set.getPortletsCount());
            }
            if (eCount != set.getEntryCount() + set.getPortletsCount())
            {
                if ( logger.isDebugEnabled() ) {
                    logger.debug("MultiCol: rebuilding columns ");
                }
                columns = buildColumns(set, colNum);
            }

        }
        else
        {
            if ( logger.isDebugEnabled() ) {
                logger.debug("MultiCol: rebuilding columns ");
            }
            columns = buildColumns(set, colNum);
        }

        customizationState.setAttribute("customize-columns", columns);
        context.put("portlets", columns);

        Map titles = new HashMap();
        for (int col = 0; col < columns.length; col++)
        {
            for (int row = 0; row < columns[col].size(); row++)
            {
                IdentityElement identityElement = (IdentityElement) columns[col].get(row);
                MetaInfo metaInfo = identityElement.getMetaInfo();
                if ((metaInfo != null) && (metaInfo.getTitle() != null))
                {
                    titles.put(identityElement.getId(), metaInfo.getTitle());
                    continue;
                }

                if (identityElement instanceof Entry)
                {
                    Entry entry = (Entry) identityElement;
                    PortletEntry pentry = (PortletEntry) Registry.getEntry(Registry.PORTLET, entry.getParent());
                    if ((pentry != null) && (pentry.getTitle() != null))
                    {
                        titles.put(entry.getId(), pentry.getTitle());
                        continue;
                    }

                    titles.put(entry.getId(), entry.getParent());
                    continue;
                }

                if (identityElement instanceof Reference)
                {
                    titles.put(identityElement.getId(), Localization.getString(rundata, "CUSTOMIZER_REF_DEFAULTTITLE"));
                    continue;
                }

                // Let's make sure their is a title
                titles.put(identityElement.getId(), Localization.getString(rundata, "CUSTOMIZER_NOTITLESET"));
            }
        }

        context.put("titles", titles);
        context.put("action", "controllers.MultiColumnControllerAction");
    }

    /**
     * Cancel the current customizations.  If this was the last customization
     * on the stack, then return the user to the home page.
     */
    public void doCancel(RunData data, Context context)
    {
        // move one level back in customization
        ((JetspeedRunData) data).setCustomized(null);

        // if we are all done customization
        if (((JetspeedRunData) data).getCustomized() == null)
        {
            try
            {
                ActionLoader.getInstance().exec(data, "controls.EndCustomize");
            }
            catch (Exception e)
            {
                logger.error("Unable to load action controls.EndCustomize ", e);
            }
        }
    }

    public void doSave(RunData data, Context context)
    {
        // get the customization state for this page
        SessionState customizationState = ((JetspeedRunData) data).getPageSessionState();

        // update the changes made here to the profile being edited
        List[] columns = (List[]) customizationState.getAttribute("customize-columns");
        for (int col = 0; col < columns.length; col++)
        {
            for (int row = 0; row < columns[col].size(); row++)
            {
                setPosition((IdentityElement) columns[col].get(row), col, row);
            }
        }

        // move one level back in customization
        ((JetspeedRunData) data).setCustomized(null);

        // if we are all done customization
        if (((JetspeedRunData) data).getCustomized() == null)
        {
            // save the edit profile and make it current
            try
            {
                ((JetspeedRunData) data).getCustomizedProfile().store();

                // Because of the way references are stored in memory, we have to completely refresh
                // the profile after a references is removed (otherwise it will continue being displayed)
                String referencesRemoved = (String) customizationState.getAttribute(REFERENCES_REMOVED);
                if (referencesRemoved != null && referencesRemoved.equals("true"))
                {
                    PsmlManager.refresh(((JetspeedRunData) data).getCustomizedProfile());
                }                
            }
            catch (Exception e)
            {
                logger.error("Unable to save profile ", e);
            }

            try
            {
                ActionLoader.getInstance().exec(data, "controls.EndCustomize");
            }
            catch (Exception e)
            {
                logger.error("Unable to load action controls.EndCustomize ", e);
            }
        }
    }

    public void doDelete(RunData data, Context context)
    {
        JetspeedRunData jdata = (JetspeedRunData) data;

        // get the customization state for this page
        SessionState customizationState = jdata.getPageSessionState();

        PortletSet customizedSet = (PortletSet) jdata.getCustomized();

        customizationState.setAttribute(REFERENCES_REMOVED, "false");

        int col = data.getParameters().getInt("col", -1);
        int row = data.getParameters().getInt("row", -1);
        List[] columns = (List[]) customizationState.getAttribute("customize-columns");
        if (columns == null)
        {
            return;
        }

        if ((col > -1) && (row > -1))
        {
            try
            {
                IdentityElement identityElement = (IdentityElement) columns[col].get(row);
                columns[col].remove(row);

                Portlets portlets = jdata.getCustomizedProfile()
                                          .getDocument()
                                          .getPortletsById(customizedSet.getID());

                if (portlets != null)
                {
                    if (identityElement instanceof Entry)
                    {
                        for (int i = 0; i < portlets.getEntryCount(); i++)
                        {
                            if (portlets.getEntry(i) == identityElement)
                            {
                                portlets.removeEntry(i);
                            }
                        }
                    }
                    else if (identityElement instanceof Reference)
                    {
                        for (int i = 0; i < portlets.getReferenceCount(); i++)
                        {
                            if (portlets.getReference(i) == identityElement)
                            {
                                customizationState.setAttribute(REFERENCES_REMOVED, "true");
                                portlets.removeReference(i);
                            }
                        }
                    }
                }
            }
            catch (Exception e)
            {
                // probably got wrong coordinates
                logger.error("MultiColumnControllerAction: Probably got wrong coordinates", e);
            }
        }
    }

    public void doLeft(RunData data, Context context)
    {
        // get the customization state for this page
        SessionState customizationState = ((JetspeedRunData) data).getPageSessionState();

        List[] columns = (List[]) customizationState.getAttribute("customize-columns");
        int col = data.getParameters().getInt("col", -1);
        int row = data.getParameters().getInt("row", -1);
        if (columns == null)
        {
            return;
        }

        if ((col > 0) && (row > -1))
        {
            move(columns, col, row, col - 1, row);
        }
    }

    public void doRight(RunData data, Context context)
    {
        // get the customization state for this page
        SessionState customizationState = ((JetspeedRunData) data).getPageSessionState();

        List[] columns = (List[]) customizationState.getAttribute("customize-columns");
        int col = data.getParameters().getInt("col", -1);
        int row = data.getParameters().getInt("row", -1);
        if (columns == null)
        {
            return;
        }

        if ((col > -1) && (row > -1) && (col < columns.length - 1))
        {
            move(columns, col, row, col + 1, row);
        }
    }

    public void doUp(RunData data, Context context)
    {
        // get the customization state for this page
        SessionState customizationState = ((JetspeedRunData) data).getPageSessionState();

        List[] columns = (List[]) customizationState.getAttribute("customize-columns");
        int col = data.getParameters().getInt("col", -1);
        int row = data.getParameters().getInt("row", -1);
        if (columns == null)
        {
            return;
        }

        if ((col > -1) && (row > 0))
        {
            move(columns, col, row, col, row - 1);
        }
    }

    public void doDown(RunData data, Context context)
    {
        // get the customization state for this page
        SessionState customizationState = ((JetspeedRunData) data).getPageSessionState();

        List[] columns = (List[]) customizationState.getAttribute("customize-columns");
        int col = data.getParameters().getInt("col", -1);
        int row = data.getParameters().getInt("row", -1);
        if (columns == null)
        {
            return;
        }

        if ((col > -1) && (row > -1) && (row < columns[col].size() - 1))
        {
            move(columns, col, row, col, row + 1);
        }
    }

    public void doControl(RunData data, Context context)
    {
        JetspeedRunData jdata = (JetspeedRunData) data;

        String controlName = data.getParameters().getString("control");
        String id = data.getParameters().getString("js_peid");

        try
        {
            Entry entry = jdata.getCustomizedProfile().getDocument().getEntryById(id);

            if (entry != null)
            {
                if (controlName != null && controlName.trim().length() > 0)
                {
                    PsmlControl control = new PsmlControl();
                    control.setName(controlName);
                    if (control != null)
                    {
                        entry.setControl(control);
                    }
                }
                else
                {
                    entry.setControl(null);
                }
            }
        }
        catch (Exception e)
        {
            logger.error("Exception", e);
        }
    }

    protected static void setPosition(IdentityElement identityElement, int col, int row)
    {
        boolean colFound = false;
        boolean rowFound = false;

        if (identityElement != null)
        {
            Layout layout = identityElement.getLayout();

            if (layout == null)
            {
                layout = new PsmlLayout();
                identityElement.setLayout(layout);
            }

            for (int i = 0; i < layout.getParameterCount(); i++)
            {
                Parameter p = layout.getParameter(i);

                if (p.getName().equals("column"))
                {
                    p.setValue(String.valueOf(col));
                    colFound = true;
                }
                else if (p.getName().equals("row"))
                {
                    p.setValue(String.valueOf(row));
                    rowFound = true;
                }
            }

            if (!colFound)
            {
                Parameter p = new PsmlParameter();
                p.setName("column");
                p.setValue(String.valueOf(col));
                layout.addParameter(p);
            }

            if (!rowFound)
            {
                Parameter p = new PsmlParameter();
                p.setName("row");
                p.setValue(String.valueOf(row));
                layout.addParameter(p);
            }
        }
    }

    protected static void move(List[] cols, int oCol, int oRow, int nCol, int nRow)
    {
        Object obj = null;

        if ((oCol < cols.length) && (oRow < cols[oCol].size()))
        {
            obj = cols[oCol].get(oRow);
            if (obj != null)
            {
                cols[oCol].remove(oRow);
            }
        }

        if (obj != null)
        {
            if (nCol < cols.length)
            {
                if (nRow < cols[nCol].size())
                {
                    cols[nCol].add(nRow, obj);
                }
                else
                {
                    cols[nCol].add(obj);
                }
            }
        }
    }

    protected static List[] buildColumns(Portlets set, int colNum)
    {
        // normalize the constraints and calculate max num of rows needed
        Iterator iterator = set.getEntriesIterator();
        int row = 0;
        int col = 0;
        int rowNum = 0;
        while (iterator.hasNext())
        {
            IdentityElement identityElement = (IdentityElement) iterator.next();

            Layout layout = identityElement.getLayout();

            if (layout != null)
            {
                for (int p = 0; p < layout.getParameterCount(); p++)
                {
                    Parameter prop = layout.getParameter(p);

                    try
                    {
                        if (prop.getName().equals("row"))
                        {
                            row = Integer.parseInt(prop.getValue());
                            if (row > rowNum)
                            {
                                rowNum = row;
                            }
                        }
                        else if (prop.getName().equals("column"))
                        {
                            col = Integer.parseInt(prop.getValue());
                            if (col > colNum)
                            {
                                prop.setValue(String.valueOf(col % colNum));
                            }
                        }
                    }
                    catch (Exception e)
                    {
                        //ignore any malformed layout properties
                    }
                }
            }
        }

        int sCount = set.getEntryCount() + set.getPortletsCount();
        row = (sCount / colNum) + 1;
        if (row > rowNum)
        {
            rowNum = row;
        }

        if ( logger.isDebugEnabled() ) {
            logger.debug("Controller customize colNum: " + colNum + " rowNum: " + rowNum);
        }
        // initialize the result position table and the work list
        List[] table = new List[colNum];
        List filler = Collections.nCopies(rowNum + 1, null);
        for (int i = 0; i < colNum; i++)
        {
            table[i] = new ArrayList();
            table[i].addAll(filler);
        }

        List work = new ArrayList();

        //position the constrained elements and keep a reference to the
        //others
        for (int i = 0; i < set.getEntryCount(); i++)
        {
            addElement(set.getEntry(i), table, work, colNum);
        }

        // Add references
        for (int i = 0; i < set.getReferenceCount(); i++)
        {
            addElement(set.getReference(i), table, work, colNum);
        }

        //insert the unconstrained elements in the table
        Iterator i = work.iterator();
        for (row = 0; row < rowNum; row++)
        {
            for (col = 0; i.hasNext() && (col < colNum); col++)
            {
                if (table[col].get(row) == null)
                {
                    if ( logger.isDebugEnabled() ) {
                        logger.debug("Set portlet at col " + col + " row " + row);
                    }
                    table[col].set(row, i.next());
                }
            }
        }

        // now cleanup any remaining null elements
        for (int j = 0; j < table.length; j++)
        {
            if ( logger.isDebugEnabled() ) {
                logger.debug("Column " + j);
            }
            i = table[j].iterator();
            while (i.hasNext())
            {
                Object obj = i.next();
                if ( logger.isDebugEnabled() ) {
                    logger.debug("Element " + obj);
                }
                if (obj == null)
                {
                    i.remove();
                }

            }
        }

        return table;
    }

    /** Parses the size config info and returns a list of
     *  size values for the current set
     *
     *  @param sizeList java.lang.String a comma separated string a values
     *  @return a List of values
     */
    protected static List getCellSizes(String sizeList)
    {
        List list = new Vector();

        if (sizeList != null)
        {
            StringTokenizer st = new StringTokenizer(sizeList, ",");
            while (st.hasMoreTokens())
            {
                list.add(st.nextToken());
            }
        }

        return list;
    }

    protected static List getCellClasses(String classlist)
    {
        List list = new Vector();

        if (classlist != null)
        {
            StringTokenizer st = new StringTokenizer(classlist, ",");
            while (st.hasMoreTokens())
            {
                list.add(st.nextToken());
            }
        }

        return list;
    }

    /**
     * Add an element to the "table" or "work" objects.  If the element is
     * unconstrained, and the position is within the number of columns, then
     * the element is added to "table".  Othewise the element is added to "work"
     *
     * @param element to add
     * @param table of positioned elements
     * @param work list of un-positioned elements
     * @param columnCount Number of colum
     */
    protected static void addElement(IdentityElement element, List[] table, List work, int columnCount)
    {
            Layout layout = element.getLayout();
            int row = -1;
            int col = -1;

            if (layout != null)
            {
                try
                {
                    for (int p = 0; p < layout.getParameterCount(); p++)
                    {
                        Parameter prop = layout.getParameter(p);

                        if (prop.getName().equals("row"))
                        {
                            row = Integer.parseInt(prop.getValue());
                        }
                        else if (prop.getName().equals("column"))
                        {
                            col = Integer.parseInt(prop.getValue());
                        }
                    }
                }
                catch (Exception e)
                {
                    //ignore any malformed layout properties
                }
            }

            if ( logger.isDebugEnabled() ) {
                logger.debug("Constraints col " + col + " row " + row);
            }
            if ((row >= 0) && (col >= 0) && (col < columnCount))
            {
                table[col].set(row, element);
            }
            else
            {
                if (layout != null)
                {
                    // We got here because the column, as defined in the layout,
                    // is greater then the numner of columns. This usually
                    // happens when the number of column has been decreased.
                    // Delete the offending layout.  It may be recreated with
                    // the correct values.
                    element.setLayout(null);
                    layout = null;
                }
                work.add(element);
            }

    }

    protected void dumpColumns(List[] cols)
    {
        for (int i = 0; i < cols.length; i++)
        {
            logger.debug("Column " + i);
            for (int j = 0; j < cols[i].size(); j++)
            {
                logger.debug("Row " + j + " object: " + cols[i].get(j));
            }
        }
    }
}
