//  Copyright 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.tapestry.contrib.table.components;

import java.io.Serializable;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;
import java.util.List;

import org.apache.commons.collections.CollectionUtils;
import org.apache.tapestry.ApplicationRuntimeException;
import org.apache.tapestry.BaseComponent;
import org.apache.tapestry.IBinding;
import org.apache.tapestry.IComponent;
import org.apache.tapestry.IMarkupWriter;
import org.apache.tapestry.IRequestCycle;
import org.apache.tapestry.contrib.table.model.IBasicTableModel;
import org.apache.tapestry.contrib.table.model.ITableColumn;
import org.apache.tapestry.contrib.table.model.ITableColumnModel;
import org.apache.tapestry.contrib.table.model.ITableDataModel;
import org.apache.tapestry.contrib.table.model.ITableModel;
import org.apache.tapestry.contrib.table.model.ITableModelSource;
import org.apache.tapestry.contrib.table.model.ITablePagingState;
import org.apache.tapestry.contrib.table.model.ITableSessionStateManager;
import org.apache.tapestry.contrib.table.model.ITableSessionStoreManager;
import org.apache.tapestry.contrib.table.model.common.BasicTableModelWrap;
import org.apache.tapestry.contrib.table.model.simple.SimpleListTableDataModel;
import org.apache.tapestry.contrib.table.model.simple.SimpleTableColumnModel;
import org.apache.tapestry.contrib.table.model.simple.SimpleTableModel;
import org.apache.tapestry.contrib.table.model.simple.SimpleTableState;
import org.apache.tapestry.event.PageDetachListener;
import org.apache.tapestry.event.PageEvent;
import org.apache.tapestry.event.PageRenderListener;

/**
 * A low level Table component that wraps all other low level Table components.
 * This component carries the {@link org.apache.tapestry.contrib.table.model.ITableModel}
 * that is used by the other Table components. Please see the documentation of
 * {@link org.apache.tapestry.contrib.table.model.ITableModel} if you need to know more
 * about how a table is represented.
 * <p>
 * This component also handles the saving of the state of the model using an 
 * {@link org.apache.tapestry.contrib.table.model.ITableSessionStateManager}
 * to determine what part of the model is to be saved and an 
 * {@link  org.apache.tapestry.contrib.table.model.ITableSessionStoreManager}
 * to determine how to save it.
 * <p>
 * Upon the beginning of a new request cycle when the table model is first needed,
 * the model is obtained using the following process:
 * <ul>
 * <li>The persistent state of the table is loaded.
 * If the tableSessionStoreManager binding has not been bound, the state is loaded 
 * from a persistent property within the component (it is null at the beginning). 
 * Otherwise the supplied
 * {@link  org.apache.tapestry.contrib.table.model.ITableSessionStoreManager} is used
 * to load the persistent state.
 * <li>The table model is recreated using the 
 * {@link org.apache.tapestry.contrib.table.model.ITableSessionStateManager} that
 * could be supplied using the tableSessionStateManager binding 
 * (but has a default value and is therefore not required).
 * <li>If the {@link org.apache.tapestry.contrib.table.model.ITableSessionStateManager}
 * returns null, then a table model is taken from the tableModel binding. Thus, if
 * the {@link org.apache.tapestry.contrib.table.model.common.NullTableSessionStateManager}
 * is used, the table model would be taken from the tableModel binding every time.
 * </ul>
 * Just before the rendering phase the persistent state of the model is saved in
 * the session. This process occurs in reverse:
 * <ul>
 * <li>The persistent state of the model is taken via the 
 * {@link org.apache.tapestry.contrib.table.model.ITableSessionStateManager}.
 * <li>If the tableSessionStoreManager binding has not been bound, the persistent
 * state is saved as a persistent page property. Otherwise the supplied
 * {@link  org.apache.tapestry.contrib.table.model.ITableSessionStoreManager} is used
 * to save the persistent state. Use of the 
 * {@link  org.apache.tapestry.contrib.table.model.ITableSessionStoreManager} 
 * is usually necessary when tables with the same model have to be used across 
 * multiple pages, and hence the state has to be saved in the Visit, rather than
 * in a persistent component property.
 * </ul>
 * <p>
 * 
 * <p> 
 * Please see the Component Reference for details on how to use this component. 
 * 
 *  [<a href="../../../../../../../ComponentReference/contrib.TableView.html">Component Reference</a>]
 * 
 * @author mindbridge
 * @version $Id: TableView.java 243864 2004-03-31 16:32:43Z tsvetelin $
 */
public abstract class TableView
    extends BaseComponent
    implements PageDetachListener, PageRenderListener, ITableModelSource
{
    // Component properties
    private ITableSessionStateManager m_objDefaultSessionStateManager = null;
    private ITableColumnModel m_objColumnModel = null;

    // Transient objects
    private ITableModel m_objTableModel;
    private ITableModel m_objCachedTableModelValue;

    // enhanced parameter methods
    public abstract ITableModel getTableModelValue();
    public abstract Object getSource();
    public abstract Object getColumns();
    public abstract IBinding getColumnsBinding();
    public abstract IBinding getPageSizeBinding();
    public abstract String getInitialSortColumn();
    public abstract boolean getInitialSortOrder();
    public abstract ITableSessionStateManager getTableSessionStateManager();
    public abstract ITableSessionStoreManager getTableSessionStoreManager();
    public abstract IComponent getColumnSettingsContainer();

    // enhanced property methods
    public abstract Serializable getSessionState();
    public abstract void setSessionState(Serializable sessionState);

    /**
     *  The component constructor. Invokes the component member initializations. 
     */
    public TableView()
    {
        initialize();
    }

    /**
     *  Invokes the component member initializations.
     *  
     *  @see org.apache.tapestry.event.PageDetachListener#pageDetached(PageEvent)
     */
    public void pageDetached(PageEvent objEvent)
    {
        initialize();
    }

    /**
     *  Initialize the component member variables.
     */
    private void initialize()
    {
        m_objTableModel = null;
        m_objCachedTableModelValue = null;
    }

    /**
     *  Resets the table by removing any stored table state. 
     *  This means that the current column to sort on and the current page will be
     *  forgotten and all data will be reloaded.
     */
    public void reset()
    {
		initialize();
        storeSessionState(null);
    }

    public ITableModel getCachedTableModelValue()
    {
        if (m_objCachedTableModelValue == null)
            m_objCachedTableModelValue = getTableModelValue();
        return m_objCachedTableModelValue;
    }

    /**
     *  Returns the tableModel.
     * 
     *  @return ITableModel the table model used by the table components
     */
    public ITableModel getTableModel()
    {
        // if null, first try to recreate the model from the session state
        if (m_objTableModel == null)
        {
            Serializable objState = loadSessionState();
            m_objTableModel = getTableSessionStateManager().recreateTableModel(objState);
        }

        // if the session state does not help, get the model from the binding
        if (m_objTableModel == null)
            m_objTableModel = getCachedTableModelValue();

        // if the model from the binding is null, build a model from source and columns
        if (m_objTableModel == null)
            m_objTableModel = generateTableModel(null);

        if (m_objTableModel == null)
            throw new ApplicationRuntimeException(
                TableUtils.format("missing-table-model", getExtendedId()));

        return m_objTableModel;
    }

    /**
     *  Generate a table model using the 'source' and 'columns' parameters.
     * 
     *  @return the newly generated table model
     */
    protected ITableModel generateTableModel(SimpleTableState objState)
    {
        // create a new table state if none is passed
        if (objState == null)
        {
            objState = new SimpleTableState();
            objState.getSortingState().setSortColumn(getInitialSortColumn(), getInitialSortOrder());
        }

        // update the page size if set in the parameter
        IBinding objPageSizeBinding = getPageSizeBinding();
        if (objPageSizeBinding != null)
            objState.getPagingState().setPageSize(objPageSizeBinding.getInt());

        // get the column model. if not possible, return null.
        ITableColumnModel objColumnModel = getTableColumnModel();
        if (objColumnModel == null)
            return null;

        Object objSourceValue = getSource();
        if (objSourceValue == null)
            return null;

        // if the source parameter is of type {@link IBasicTableModel}, 
        // create and return an appropriate wrapper
        if (objSourceValue instanceof IBasicTableModel)
            return new BasicTableModelWrap(
                (IBasicTableModel) objSourceValue,
                objColumnModel,
                objState);

        // otherwise, the source parameter must contain the data to be displayed
        ITableDataModel objDataModel = null;
        if (objSourceValue instanceof Object[])
            objDataModel = new SimpleListTableDataModel((Object[]) objSourceValue);
        else if (objSourceValue instanceof List)
            objDataModel = new SimpleListTableDataModel((List) objSourceValue);
        else if (objSourceValue instanceof Collection)
            objDataModel = new SimpleListTableDataModel((Collection) objSourceValue);
        else if (objSourceValue instanceof Iterator)
            objDataModel = new SimpleListTableDataModel((Iterator) objSourceValue);

        if (objDataModel == null)
            throw new ApplicationRuntimeException(
                TableUtils.format(
                    "invalid-table-source",
                    getExtendedId(),
                    objSourceValue.getClass()));

        return new SimpleTableModel(objDataModel, objColumnModel, objState);
    }

    /**
     *  Returns the table column model as specified by the 'columns' binding.
     *  If the value of the 'columns' binding is of a type different than
     *  ITableColumnModel, this method makes the appropriate conversion. 
     * 
     *  @return The table column model as specified by the 'columns' binding
     */
    protected ITableColumnModel getTableColumnModel()
    {
        Object objColumns = getColumns();

        if (objColumns == null)
            return null;

        if (objColumns instanceof ITableColumnModel)
        {
            return (ITableColumnModel) objColumns;
        }

        if (objColumns instanceof Iterator)
        {
            // convert to List
            Iterator objColumnsIterator = (Iterator) objColumns;
            List arrColumnsList = new ArrayList();
            CollectionUtils.addAll(arrColumnsList, objColumnsIterator);
            objColumns = arrColumnsList;
        }

        if (objColumns instanceof List)
        {
            // validate that the list contains only ITableColumn instances
            List arrColumnsList = (List) objColumns;
            int nColumnsNumber = arrColumnsList.size();
            for (int i = 0; i < nColumnsNumber; i++)
            {
                if (!(arrColumnsList.get(i) instanceof ITableColumn))
                    throw new ApplicationRuntimeException(
                        TableUtils.format("columns-only-please", getExtendedId()));
            }
            //objColumns = arrColumnsList.toArray(new ITableColumn[nColumnsNumber]);
            return new SimpleTableColumnModel(arrColumnsList);
        }

        if (objColumns instanceof ITableColumn[])
        {
            return new SimpleTableColumnModel((ITableColumn[]) objColumns);
        }

        if (objColumns instanceof String)
        {
            String strColumns = (String) objColumns;
            if (getColumnsBinding().isInvariant())
            {
                // if the binding is invariant, create the columns only once
                if (m_objColumnModel == null)
                    m_objColumnModel = generateTableColumnModel(strColumns);
                return m_objColumnModel;
            }

            // if the binding is not invariant, create them every time
            return generateTableColumnModel(strColumns);
        }

        throw new ApplicationRuntimeException(
            TableUtils.format("invalid-table-columns", getExtendedId(), objColumns.getClass()));
    }

    /**
     *  Generate a table column model out of the description string provided.
     *  Entries in the description string are separated by commas.
     *  Each column entry is of the format name, name:expression, 
     *  or name:displayName:expression.
     *  An entry prefixed with ! represents a non-sortable column.
     *  If the whole description string is prefixed with *, it represents
     *  columns to be included in a Form. 
     * 
     *  @param strDesc the description of the column model to be generated
     *  @return a table column model based on the provided description
     */
    protected ITableColumnModel generateTableColumnModel(String strDesc)
    {
        IComponent objColumnSettingsContainer = getColumnSettingsContainer();
        return TableUtils.generateTableColumnModel(strDesc, this, objColumnSettingsContainer);
    }

    /**
     *  The default session state manager to be used in case no such manager
     *  is provided by the corresponding parameter.
     * 
     *  @return the default session state manager
     */
    public ITableSessionStateManager getDefaultTableSessionStateManager()
    {
        if (m_objDefaultSessionStateManager == null)
            m_objDefaultSessionStateManager = new TableViewSessionStateManager(this);
        return m_objDefaultSessionStateManager;
    }

    /**
     *  Invoked when there is a modification of the table state and it needs to be saved
     *  
     *  @see org.apache.tapestry.contrib.table.model.ITableModelSource#fireObservedStateChange()
     */
    public void fireObservedStateChange()
    {
        saveSessionState();
    }

    /**
     *  Ensures that the table state is saved before the render phase begins 
     *  in case there are modifications for which {@link #fireObservedStateChange()} 
     *  has not been invoked.
     * 
     * @see org.apache.tapestry.event.PageRenderListener#pageBeginRender(org.apache.tapestry.event.PageEvent)
     */
    public void pageBeginRender(PageEvent event)
    {
        // 'suspenders': save the table model if it has been already loaded.
        // this means that if a change has been made explicitly in a listener, 
        // it will be saved. this is the last place before committing the changes 
        // where a save can occur  
        if (m_objTableModel != null)
            saveSessionState();
    }

    /**
     *  @see org.apache.tapestry.event.PageRenderListener#pageEndRender(PageEvent)
     */
    public void pageEndRender(PageEvent objEvent)
    {
    }

    /**
     *  Saves the table state using the SessionStateManager to determine 
     *  what to save and the SessionStoreManager to determine where to save it.  
     *
     */
    protected void saveSessionState()
    {
        ITableModel objModel = getTableModel();
        Serializable objState = getTableSessionStateManager().getSessionState(objModel);
        storeSessionState(objState);
    }

    /**
     *  Loads the table state using the SessionStoreManager.
     * 
     *  @return the stored table state
     */
    protected Serializable loadSessionState()
    {
        ITableSessionStoreManager objManager = getTableSessionStoreManager();
        if (objManager != null)
            return objManager.loadState(getPage().getRequestCycle());
        return getSessionState();
    }

    /**
     *  Stores the table state using the SessionStoreManager.
     * 
     *  @param objState the table state to store
     */
    protected void storeSessionState(Serializable objState)
    {
        ITableSessionStoreManager objManager = getTableSessionStoreManager();
        if (objManager != null)
            objManager.saveState(getPage().getRequestCycle(), objState);
        else
            setSessionState(objState);
    }

    /**
     *  Make sure that the values stored in the model are useable and correct.
     *  The changes made here are not saved.  
     */
    protected void validateValues()
    {
        ITableModel objModel = getTableModel();

        // make sure current page is within the allowed range
        ITablePagingState objPagingState = objModel.getPagingState();
        int nCurrentPage = objPagingState.getCurrentPage();
        int nPageCount = objModel.getPageCount();
        if (nCurrentPage >= nPageCount)
        {
            // the current page is greater than the page count. adjust.
            nCurrentPage = nPageCount - 1;
            objPagingState.setCurrentPage(nCurrentPage);
        }
        if (nCurrentPage < 0)
        {
            // the current page is before the first page. adjust.
            nCurrentPage = 0;
            objPagingState.setCurrentPage(nCurrentPage);
        }
    }

    /**
     *  Stores a pointer to this component in the Request Cycle while rendering
     *  so that wrapped components have access to it.
     * 
     *  @see org.apache.tapestry.BaseComponent#renderComponent(IMarkupWriter, IRequestCycle)
     */
    protected void renderComponent(IMarkupWriter writer, IRequestCycle cycle)
    {
        Object objOldValue = cycle.getAttribute(ITableModelSource.TABLE_MODEL_SOURCE_ATTRIBUTE);
        cycle.setAttribute(ITableModelSource.TABLE_MODEL_SOURCE_ATTRIBUTE, this);

        initialize();
        validateValues();
        super.renderComponent(writer, cycle);

        cycle.setAttribute(ITableModelSource.TABLE_MODEL_SOURCE_ATTRIBUTE, objOldValue);
    }

}
