// 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.tapestry5.corelib.components;

import org.apache.tapestry5.*;
import org.apache.tapestry5.annotations.*;
import org.apache.tapestry5.beaneditor.BeanModel;
import org.apache.tapestry5.beaneditor.PropertyModel;
import org.apache.tapestry5.corelib.data.GridPagerPosition;
import org.apache.tapestry5.grid.*;
import org.apache.tapestry5.internal.TapestryInternalUtils;
import org.apache.tapestry5.internal.beaneditor.BeanModelUtils;
import org.apache.tapestry5.internal.bindings.AbstractBinding;
import org.apache.tapestry5.ioc.annotations.Inject;
import org.apache.tapestry5.ioc.internal.util.InternalUtils;
import org.apache.tapestry5.services.BeanModelSource;
import org.apache.tapestry5.services.ComponentDefaultProvider;
import org.apache.tapestry5.services.ComponentEventResultProcessor;
import org.apache.tapestry5.services.FormSupport;
import org.apache.tapestry5.services.javascript.JavaScriptSupport;

import java.io.IOException;
import java.util.Collections;
import java.util.List;

/**
 * A grid presents tabular data. It is a composite component, created in terms of several sub-components. The
 * sub-components are statically wired to the Grid, as it provides access to the data and other models that they need.
 * <p/>
 * A Grid may operate inside a {@link org.apache.tapestry5.corelib.components.Form}. By overriding the cell renderers of
 * properties, the default output-only behavior can be changed to produce a complex form with individual control for
 * editing properties of each row. There is a big caveat here: if the order of rows provided by
 * the {@link org.apache.tapestry5.grid.GridDataSource} changes between render and form submission, then there's the
 * possibility that data will be applied to the wrong server-side objects.
 * <p/>
 * For this reason, when using Grid and Form together, you should generally
 * provide the Grid with a {@link org.apache.tapestry5.ValueEncoder} (via the
 * encoder parameter), or use an entity type for the "row" parameter for which
 * Tapestry can provide a ValueEncoder automatically. This will allow Tapestry
 * to use a unique ID for each row that doesn't change when rows are reordered.
 *
 * @tapestrydoc
 * @see org.apache.tapestry5.beaneditor.BeanModel
 * @see org.apache.tapestry5.services.BeanModelSource
 * @see org.apache.tapestry5.grid.GridDataSource
 * @see BeanEditForm
 * @see BeanDisplay
 * @see Loop
 */
@SupportsInformalParameters
public class Grid implements GridModel, ClientElement
{
    /**
     * The source of data for the Grid to display. This will usually be a List or array but can also be an explicit
     * {@link GridDataSource}. For Lists and object arrays, a GridDataSource is created automatically as a wrapper
     * around the underlying List.
     */
    @Parameter(required = true, autoconnect = true)
    private GridDataSource source;

    /**
     * A wrapper around the provided GridDataSource that caches access to the availableRows property. This is the source
     * provided to sub-components.
     */
    private GridDataSource cachingSource;

    /**
     * The number of rows of data displayed on each page. If there are more rows than will fit, the Grid will divide up
     * the rows into "pages" and (normally) provide a pager to allow the user to navigate within the overall result
     * set.
     */
    @Parameter(BindingConstants.SYMBOL + ":" + ComponentParameterConstants.GRID_ROWS_PER_PAGE)
    private int rowsPerPage;

    /**
     * Defines where the pager (used to navigate within the "pages" of results) should be displayed: "top", "bottom",
     * "both" or "none".
     */
    @Parameter(value = BindingConstants.SYMBOL + ":" + ComponentParameterConstants.GRID_PAGER_POSITION,
            defaultPrefix = BindingConstants.LITERAL)
    private GridPagerPosition pagerPosition;

    /**
     * Used to store the current object being rendered (for the current row). This is used when parameter blocks are
     * provided to override the default cell renderer for a particular column ... the components within the block can
     * use the property bound to the row parameter to know what they should render.
     */
    @Parameter(principal = true)
    private Object row;

    /**
     * Optional output parameter used to identify the index of the column being rendered.
     */
    @Parameter
    private int columnIndex;

    /**
     * The model used to identify the properties to be presented and the order of presentation. The model may be
     * omitted, in which case a default model is generated from the first object in the data source (this implies that
     * the objects provided by the source are uniform). The model may be explicitly specified to override the default
     * behavior, say to reorder or rename columns or add additional columns. The add, include,
     * exclude and reorder
     * parameters are <em>only</em> applied to a default model, not an explicitly provided one.
     */
    @Parameter
    private BeanModel model;

    /**
     * The model parameter after modification due to the add, include, exclude and reorder parameters.
     */
    private BeanModel dataModel;

    /**
     * The model used to handle sorting of the Grid. This is generally not specified, and the built-in model supports
     * only single column sorting. The sort constraints (the column that is sorted, and ascending vs. descending) is
     * stored as persistent fields of the Grid component.
     */
    @Parameter
    private GridSortModel sortModel;

    /**
     * A comma-separated list of property names to be added to the {@link org.apache.tapestry5.beaneditor.BeanModel}.
     * Cells for added columns will be blank unless a cell override is provided. This parameter is only used
     * when a default model is created automatically.
     */
    @Parameter(defaultPrefix = BindingConstants.LITERAL)
    private String add;

    /**
     * A comma-separated list of property names to be retained from the
     * {@link org.apache.tapestry5.beaneditor.BeanModel}.
     * Only these properties will be retained, and the properties will also be reordered. The names are
     * case-insensitive. This parameter is only used
     * when a default model is created automatically.
     */
    @SuppressWarnings("unused")
    @Parameter(defaultPrefix = BindingConstants.LITERAL)
    private String include;

    /**
     * A comma-separated list of property names to be removed from the {@link org.apache.tapestry5.beaneditor.BeanModel}
     * .
     * The names are case-insensitive. This parameter is only used
     * when a default model is created automatically.
     */
    @Parameter(defaultPrefix = BindingConstants.LITERAL)
    private String exclude;

    /**
     * A comma-separated list of property names indicating the order in which the properties should be presented. The
     * names are case insensitive. Any properties not indicated in the list will be appended to the end of the display
     * order. This parameter is only used
     * when a default model is created automatically.
     */
    @Parameter(defaultPrefix = BindingConstants.LITERAL)
    private String reorder;

    /**
     * A Block to render instead of the table (and pager, etc.) when the source is empty. The default is simply the text
     * "There is no data to display". This parameter is used to customize that message, possibly including components to
     * allow the user to create new objects.
     */
    //@Parameter(value = BindingConstants.SYMBOL + ":" + ComponentParameterConstants.GRID_EMPTY_BLOCK,
    @Parameter(value = "block:empty",
            defaultPrefix = BindingConstants.LITERAL)
    private Block empty;

    /**
     * CSS class for the &lt;table&gt; element. In addition, informal parameters to the Grid are rendered in the table
     * element.
     */
    @Parameter(name = "class", defaultPrefix = BindingConstants.LITERAL,
            value = BindingConstants.SYMBOL + ":" + ComponentParameterConstants.GRID_TABLE_CSS_CLASS)
    @Property(write = false)
    private String tableClass;

    /**
     * If true, then the Grid will be wrapped in an element that acts like a
     * {@link org.apache.tapestry5.corelib.components.Zone}; all the paging and sorting links will refresh the zone,
     * repainting the entire grid within it, but leaving the rest of the page (outside the zone) unchanged.
     */
    @Parameter
    private boolean inPlace;

    /**
     * If true, then the Grid will also render a table element complete with headers if the data source is empty.
     * If set to true, a model parameter will have to be specified. A default model for a specific class can be
     * created using {@link BeanModelSource#createDisplayModel(Class, org.apache.tapestry5.ioc.Messages)}.
     */
    @Parameter
    private boolean renderTableIfEmpty = false;


    /**
     * The name of the pseudo-zone that encloses the Grid. Starting in 5.4, this is always either
     * null or "^" and is not really used the way it was in 5.3; instead it triggers the addition
     * of a {@code data-inplace-grid-links} attribute in a div surrounding any links related to
     * sorting or pagination. The rest is sorted out on the client. See module {@code t5/core/zone}.
     */
    @Property(write = false)
    private String zone;

    private boolean didRenderZoneDiv;


    /**
     * The pagination model for the Grid, which encapsulates current page, sort column id,
     * and sort ascending/descending. If not bound, a persistent property of the Grid is used.
     * When rendering the Grid in a loop, this should be bound in some way to keep successive instances
     * of the Grid configured individually.
     *
     * @since 5.4
     */
    @Parameter(value = "defaultPaginationModel")
    private GridPaginationModel paginationModel;

    @Property
    @Persist
    private GridPaginationModel defaultPaginationModel;

    @Inject
    private ComponentResources resources;

    @Inject
    private BeanModelSource modelSource;

    @Environmental
    private JavaScriptSupport javaScriptSupport;

    @Component(parameters =
            {"index=inherit:columnIndex", "lean=inherit:lean", "overrides=overrides", "zone=zone"})
    private GridColumns columns;

    @Component(parameters =
            {"columnIndex=inherit:columnIndex", "rowsPerPage=rowsPerPage", "currentPage=currentPage", "row=row",
                    "overrides=overrides"}, publishParameters = "rowIndex,rowClass,volatile,encoder,lean")
    private GridRows rows;

    @Component(parameters =
            {"source=dataSource", "rowsPerPage=rowsPerPage", "currentPage=currentPage", "zone=zone"})
    private GridPager pager;

    @Component(parameters = "to=pagerTop")
    private Delegate pagerTop;

    @Component(parameters = "to=pagerBottom")
    private Delegate pagerBottom;

    @Component(parameters = "class=tableClass", inheritInformalParameters = true)
    private Any table;

    @Environmental(false)
    private FormSupport formSupport;

    /**
     * Defines where block and label overrides are obtained from. By default, the Grid component provides block
     * overrides (from its block parameters).
     */
    @Parameter(value = "this", allowNull = false)
    @Property(write = false)
    private PropertyOverrides overrides;

    /**
     * Set up via the traditional or Ajax component event request handler
     */
    @Environmental
    private ComponentEventResultProcessor componentEventResultProcessor;

    @Inject
    private ComponentDefaultProvider defaultsProvider;

    ValueEncoder defaultEncoder()
    {
        return defaultsProvider.defaultValueEncoder("row", resources);
    }

    /**
     * A version of GridDataSource that caches the availableRows property. This addresses TAPESTRY-2245.
     */
    static class CachingDataSource implements GridDataSource
    {
        private final GridDataSource delegate;

        private boolean availableRowsCached;

        private int availableRows;

        CachingDataSource(GridDataSource delegate)
        {
            this.delegate = delegate;
        }

        public int getAvailableRows()
        {
            if (!availableRowsCached)
            {
                availableRows = delegate.getAvailableRows();
                availableRowsCached = true;
            }

            return availableRows;
        }

        public void prepare(int startIndex, int endIndex, List<SortConstraint> sortConstraints)
        {
            delegate.prepare(startIndex, endIndex, sortConstraints);
        }

        public Object getRowValue(int index)
        {
            return delegate.getRowValue(index);
        }

        public Class getRowType()
        {
            return delegate.getRowType();
        }
    }

    /**
     * Default implementation that only allows a single column to be the sort column, and stores the sort information as
     * persistent fields of the Grid component.
     */
    class DefaultGridSortModel implements GridSortModel
    {
        public ColumnSort getColumnSort(String columnId)
        {
            if (paginationModel == null || !TapestryInternalUtils.isEqual(columnId, paginationModel.getSortColumnId()))
            {
                return ColumnSort.UNSORTED;
            }

            return getColumnSort();
        }

        private ColumnSort getColumnSort()
        {
            return getSortAscending() ? ColumnSort.ASCENDING : ColumnSort.DESCENDING;
        }

        public void updateSort(String columnId)
        {
            assert InternalUtils.isNonBlank(columnId);

            setupPaginationModel();

            if (columnId.equals(paginationModel.getSortColumnId()))
            {
                setSortAscending(!getSortAscending());
                return;
            }

            paginationModel.setSortColumnId(columnId);
            setSortAscending(true);
        }

        public List<SortConstraint> getSortConstraints()
        {
            // In a few limited cases we may not have yet hit the SetupRender phase, and the model may be null.
            if (paginationModel == null || paginationModel.getSortColumnId() == null)
            {
                return Collections.emptyList();
            }

            PropertyModel sortModel = getDataModel().getById(paginationModel.getSortColumnId());

            SortConstraint constraint = new SortConstraint(sortModel, getColumnSort());

            return Collections.singletonList(constraint);
        }

        public void clear()
        {
            setupPaginationModel();
            paginationModel.setSortColumnId(null);
            paginationModel.setSortAscending(null);
        }
    }

    GridSortModel defaultSortModel()
    {
        return new DefaultGridSortModel();
    }

    /**
     * Returns a {@link org.apache.tapestry5.Binding} instance that attempts to identify the model from the source
     * parameter (via {@link org.apache.tapestry5.grid.GridDataSource#getRowType()}. Subclasses may override to provide
     * a different mechanism. The returning binding is variant (not invariant).
     *
     * @see BeanModelSource#createDisplayModel(Class, org.apache.tapestry5.ioc.Messages)
     */
    protected Binding defaultModel()
    {

        return new AbstractBinding()
        {
            public Object get()
            {
                // Get the default row type from the data source

                GridDataSource gridDataSource = source;

                Class rowType = gridDataSource.getRowType();

                if (renderTableIfEmpty || rowType == null)
                    throw new RuntimeException(
                            String.format(
                                    "Unable to determine the bean type for rows from %s. You should bind the model parameter explicitly.",
                                    gridDataSource));

                // Properties do not have to be read/write

                return modelSource.createDisplayModel(rowType, overrides.getOverrideMessages());
            }

            /**
             * Returns false. This may be overkill, but it basically exists because the model is
             * inherently mutable and therefore may contain client-specific state and needs to be
             * discarded at the end of the request. If the model were immutable, then we could leave
             * invariant as true.
             */
            @Override
            public boolean isInvariant()
            {
                return false;
            }
        };
    }

    static final ComponentAction<Grid> SETUP_DATA_SOURCE = new ComponentAction<Grid>()
    {
        private static final long serialVersionUID = 8545187927995722789L;

        public void execute(Grid component)
        {
            component.setupDataSource();
        }

        @Override
        public String toString()
        {
            return "Grid.SetupDataSource";
        }
    };

    Object setupRender()
    {

        setupPaginationModel();

        if (formSupport != null)
        {
            formSupport.store(this, SETUP_DATA_SOURCE);
        }

        setupDataSource();

        // If there's no rows, display the empty block placeholder.

        return !renderTableIfEmpty && cachingSource.getAvailableRows() == 0 ? empty : null;
    }

    void cleanupRender()
    {
        // if an inPlace Grid is rendered inside a Loop, be sure to generate a new wrapper
        // zone for each iteration (TAP5-2256)
        zone = null;
    }

    private void setupPaginationModel()
    {
        if (paginationModel == null)
        {
            paginationModel = new GridPaginationModelImpl();
        }
    }

    void setupDataSource()
    {
        // TAP5-34: We pass the source into the CachingDataSource now; previously
        // we were accessing source directly, but during submit the value wasn't
        // cached, and therefore access was very inefficient, and sorting was
        // very inconsistent during the processing of the form submission.

        cachingSource = new CachingDataSource(source);

        int availableRows = cachingSource.getAvailableRows();

        if (availableRows == 0)
            return;

        int maxPage = ((availableRows - 1) / rowsPerPage) + 1;

        // This captures when the number of rows has decreased, typically due to deletions.

        int effectiveCurrentPage = getCurrentPage();

        if (effectiveCurrentPage > maxPage)
            effectiveCurrentPage = maxPage;

        int startIndex = (effectiveCurrentPage - 1) * rowsPerPage;

        int endIndex = Math.min(startIndex + rowsPerPage - 1, availableRows - 1);

        dataModel = null;

        cachingSource.prepare(startIndex, endIndex, sortModel.getSortConstraints());
    }

    Object beginRender(MarkupWriter writer)
    {
        // Skip rendering of component (template, body, etc.) when there's nothing to display.
        // The empty placeholder will already have rendered.

        if (cachingSource.getAvailableRows() == 0)
            return !renderTableIfEmpty ? false : null;

        if (inPlace && zone == null)
        {
            javaScriptSupport.require("t5/core/zone");

            writer.element("div", "data-container-type", "zone");

            didRenderZoneDiv = true;

            // Through Tapestry 5.3, we had a specific id for the zone that had to be passed down to the
            // GridPager and etc.  That's no longer necessary, so zone will always be null or "^".  We don't
            // even need any special ids to be allocated!
            zone = "^";
        }

        return null;
    }

    void afterRender(MarkupWriter writer)
    {
        if (didRenderZoneDiv)
        {
            writer.end(); // div
            didRenderZoneDiv = false;
        }
    }

    public BeanModel getDataModel()
    {
        if (dataModel == null)
        {
            dataModel = model;

            BeanModelUtils.modify(dataModel, add, include, exclude, reorder);
        }

        return dataModel;
    }

    public int getNumberOfProperties()
    {
        return getDataModel().getPropertyNames().size();
    }

    public GridDataSource getDataSource()
    {
        return cachingSource;
    }

    public GridSortModel getSortModel()
    {
        return sortModel;
    }

    public Object getPagerTop()
    {
        return pagerPosition.isMatchTop() ? pager : null;
    }

    public Object getPagerBottom()
    {
        return pagerPosition.isMatchBottom() ? pager : null;
    }

    public int getCurrentPage()
    {
        Integer currentPage = paginationModel.getCurrentPage();

        return currentPage == null ? 1 : currentPage;
    }

    public void setCurrentPage(int currentPage)
    {
        paginationModel.setCurrentPage(currentPage);
    }

    private boolean getSortAscending()
    {
        Boolean sortAscending = paginationModel.getSortAscending();

        return sortAscending != null && sortAscending.booleanValue();
    }

    private void setSortAscending(boolean sortAscending)
    {
        paginationModel.setSortAscending(sortAscending);
    }

    public int getRowsPerPage()
    {
        return rowsPerPage;
    }

    public Object getRow()
    {
        return row;
    }

    public void setRow(Object row)
    {
        this.row = row;
    }

    /**
     * Resets the Grid to inital settings; this sets the current page to one, and
     * {@linkplain org.apache.tapestry5.grid.GridSortModel#clear() clears the sort model}.
     */
    public void reset()
    {
        setCurrentPage(1);
        sortModel.clear();
    }

    /**
     * Event handler for inplaceupdate event triggered from nested components when an Ajax update occurs. The event
     * context will carry the zone, which is recorded here, to allow the Grid and its sub-components to properly
     * re-render themselves. Invokes
     * {@link org.apache.tapestry5.services.ComponentEventResultProcessor#processResultValue(Object)} passing this (the
     * Grid component) as the content provider for the update.
     */
    void onInPlaceUpdate() throws IOException
    {
        this.zone = "^";

        componentEventResultProcessor.processResultValue(this);
    }

    public String getClientId()
    {
        return table.getClientId();
    }
}
