// 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.hibernate.web;

import org.apache.tapestry5.grid.GridDataSource;
import org.apache.tapestry5.grid.SortConstraint;
import org.hibernate.Criteria;
import org.hibernate.Session;
import org.hibernate.criterion.Order;
import org.hibernate.criterion.Projections;

import java.util.List;

/**
 * A simple implementation of {@link org.apache.tapestry5.grid.GridDataSource} based on a Hibernate Session and a known
 * entity class.  This implementation does support multiple {@link org.apache.tapestry5.grid.SortConstraint sort
 * constraints}; however it assumes a direct mapping from sort constraint property to Hibernate property.
 *
 * This class is <em>not</em> thread-safe; it maintains internal state.
 *
 * Typically, an instance of this object is created fresh as needed (that is, it is not stored between requests).
 */
public class HibernateGridDataSource implements GridDataSource
{
    private final Session session;

    private final Class entityType;

    private int startIndex;

    private List preparedResults;

    public HibernateGridDataSource(Session session, Class entityType)
    {
        assert session != null;
        assert entityType != null;
        this.session = session;
        this.entityType = entityType;
    }

    /**
     * Returns the total number of rows for the configured entity type.
     */
    @Override
    public int getAvailableRows()
    {
        Criteria criteria = session.createCriteria(entityType);

        applyAdditionalConstraints(criteria);

        criteria.setProjection(Projections.rowCount());

        Number result = (Number) criteria.uniqueResult();

        return result.intValue();
    }

    /**
     * Prepares the results, performing a query (applying the sort results, and the provided start and end index). The
     * results can later be obtained from {@link #getRowValue(int)} }.
     *
     * @param startIndex      index, from zero, of the first item to be retrieved
     * @param endIndex        index, from zero, of the last item to be retrieved
     * @param sortConstraints zero or more constraints used to set the order of the returned values
     */
    @Override
    public void prepare(int startIndex, int endIndex, List<SortConstraint> sortConstraints)
    {
        assert sortConstraints != null;
        Criteria crit = session.createCriteria(entityType);

        crit.setFirstResult(startIndex).setMaxResults(endIndex - startIndex + 1);

        for (SortConstraint constraint : sortConstraints)
        {

            String propertyName = constraint.getPropertyModel().getPropertyName();

            switch (constraint.getColumnSort())
            {

                case ASCENDING:

                    crit.addOrder(Order.asc(propertyName));
                    break;

                case DESCENDING:
                    crit.addOrder(Order.desc(propertyName));
                    break;

                default:
            }
        }

        applyAdditionalConstraints(crit);

        this.startIndex = startIndex;

        preparedResults = crit.list();
    }

    /**
     * Invoked after the main criteria has been set up (firstResult, maxResults and any sort contraints). This gives
     * subclasses a chance to apply additional constraints before the list of results is obtained from the criteria.
     * This implementation does nothing and may be overridden.
     */
    protected void applyAdditionalConstraints(Criteria crit)
    {
    }

    /**
     * Returns a row value at the given index (which must be within the range defined by the call to {@link
     * #prepare(int, int, java.util.List)} ).
     *
     * @param index of object
     * @return object at that index
     */
    @Override
    public Object getRowValue(int index)
    {
        return preparedResults.get(index - startIndex);
    }

    /**
     * Returns the entity type, as provided via the constructor.
     */
    @Override
    public Class getRowType()
    {
        return entityType;
    }
}
