package org.jboss.seam.framework;

import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import javax.faces.model.DataModel;

import org.jboss.seam.annotations.Create;
import org.jboss.seam.annotations.Transactional;
import org.jboss.seam.core.Expressions.ValueExpression;
import org.jboss.seam.faces.DataModels;
import org.jboss.seam.persistence.QueryParser;

/**
 * Base class for components which manage a query
 * result set. This class may be reused by either
 * configuration or extension, and may be bound
 * directly to a view, or accessed by some
 * intermediate Seam component.
 * 
 * @author Gavin King
 *
 */
public abstract class Query<T, E> 
      extends PersistenceController<T> //TODO: extend MutableController!
{
   private static final Pattern FROM_PATTERN = Pattern.compile("(^|\\s)(from)\\s", Pattern.CASE_INSENSITIVE);
   private static final Pattern ORDER_PATTERN = Pattern.compile("\\s(order)(\\s)+by\\s", Pattern.CASE_INSENSITIVE);
   private static final Pattern WHERE_PATTERN = Pattern.compile("\\s(where)\\s", Pattern.CASE_INSENSITIVE);

   private static final Pattern ORDER_CLAUSE_PATTERN = Pattern.compile("^[\\w\\.,\\s]*$");

   
   private String ejbql;
   private Integer firstResult;
   private Integer maxResults;
   private List<String> restrictions = new ArrayList<String>(0);
   private String order;
   
   private DataModel dataModel;
   
   private String parsedEjbql;
   private List<ValueExpression> queryParameters;
   private List<String> parsedRestrictions;
   private List<ValueExpression> restrictionParameters;
   
   private List<Object> queryParameterValues;
   private List<Object> restrictionParameterValues;
   
   public abstract List<E> getResultList();
   public abstract E getSingleResult();
   public abstract Long getResultCount();

   @Create
   public void validate()
   {
      if ( getEjbql()==null )
      {
         throw new IllegalStateException("ejbql is null");
      }
   }
   
   /**
    * Wrap the result set in a JSF {@link DataModel}
    * 
    * Delegates to {@link DataModels#getDataModel(Query)}
    * 
    */
   @Transactional
   public DataModel getDataModel()
   {
      if (dataModel==null)
      {
         dataModel = DataModels.instance().getDataModel(this);
      }
      return dataModel;
   }
   
   /**
    * Get the selected row of the JSF {@link DataModel}
    * 
    */
   public E getDataModelSelection()
   {
      return (E) getDataModel().getRowData();
   }
   
   /**
    * Get the index of the selected row of the JSF {@link DataModel}
    * 
    */
   public int getDataModelSelectionIndex()
   {
      return getDataModel().getRowIndex();
   }
   
   
   public void refresh()
   {
      clearDataModel();
   }
   
   /**
    * Move the result set cursor to the beginning of the last page
    * 
    */
   @Transactional
   public void last()
   {
      setFirstResult( getLastFirstResult().intValue() );
   }
   
   /**
    * Move the result set cursor to the beginning of the next page
    * 
    */
   public void next()
   {
      setFirstResult( getNextFirstResult() );
   }

   /**
    * Move the result set cursor to the beginning of the previous page
    * 
    */
   public void previous()
   {
      setFirstResult( getPreviousFirstResult() );
   }
   
   /**
    * Move the result set cursor to the beginning of the first page
    * 
    */
   public void first()
   {
      setFirstResult(0);
   }
   
   protected void clearDataModel()
   {
      dataModel = null;
   }

   /**
    * Get the index of the first result of the last page
    * 
    */
   @Transactional
   public Long getLastFirstResult()
   {
      Integer pc = getPageCount();
      return pc==null ? null : ( pc.longValue()-1 ) * getMaxResults();
   }
   
   /**
    * Get the index of the first result of the next page
    * 
    */
   public int getNextFirstResult()
   {
      Integer fr = getFirstResult();
      return ( fr==null ? 0 : fr ) + getMaxResults();
   }

   /**
    * Get the index of the first result of the previous page
    * 
    */
   public int getPreviousFirstResult()
   {
      Integer fr = getFirstResult();
      Integer mr = getMaxResults();
      return mr >= ( fr==null ? 0 : fr ) ? 
               0 : fr - mr;
   }
   
   /**
    * Get the total number of pages
    * 
    */
   @Transactional
   public Integer getPageCount()
   {
      if ( getMaxResults()==null )
      {
         return null;
      }
      else
      {
         int rc = getResultCount().intValue();
         int mr = getMaxResults().intValue();
         int pages = rc / mr;
         return rc % mr == 0 ? pages : pages+1;
      }
   }
          
   
   protected void parseEjbql()
   {
      if (parsedEjbql==null || parsedRestrictions==null)
      {
         QueryParser qp = new QueryParser( getEjbql() );
         queryParameters = qp.getParameterValueBindings();
         parsedEjbql = qp.getEjbql();
         
         List<String> restrictionFragments = getRestrictions();
         parsedRestrictions = new ArrayList<String>( restrictionFragments.size() );
         restrictionParameters = new ArrayList<ValueExpression>( restrictionFragments.size() );         
         for ( String restriction: restrictionFragments )
         {
            QueryParser rqp = new QueryParser( restriction, queryParameters.size() + restrictionParameters.size() );            
            if ( rqp.getParameterValueBindings().size()!=1 ) 
            {
               throw new IllegalArgumentException("there should be exactly one value binding in a restriction: " + restriction);
            }            
            parsedRestrictions.add( rqp.getEjbql() );
            restrictionParameters.addAll( rqp.getParameterValueBindings() );
         }
         
      }
   }
   
   protected String getRenderedEjbql()
   {
      StringBuilder builder = new StringBuilder().append(parsedEjbql);
      
      for (int i=0; i<getRestrictions().size(); i++)
      {
         Object parameterValue = restrictionParameters.get(i).getValue();
         if ( isRestrictionParameterSet(parameterValue) )
         {
            if ( WHERE_PATTERN.matcher(builder).find() )
            {
               builder.append(" and ");
            }
            else
            {
               builder.append(" where ");
            }
            builder.append( parsedRestrictions.get(i) );
         }
      }
         
      if ( getOrder()!=null ) builder.append(" order by ").append( getOrder() );
      
      return builder.toString();
   }
   
   protected boolean isRestrictionParameterSet(Object parameterValue)
   {
      return parameterValue != null && !"".equals(parameterValue) && ( parameterValue instanceof Collection ? !((Collection) parameterValue).isEmpty() : true );

   }

   /**
    * Return the ejbql to used in a count query (for calculating number of
    * results)
    * @return String The ejbql query
    */
   protected String getCountEjbql()
   {
      String ejbql = getRenderedEjbql();
      
      Matcher fromMatcher = FROM_PATTERN.matcher(ejbql);
      if ( !fromMatcher.find() )
      {
         throw new IllegalArgumentException("no from clause found in query");
      }
      int fromLoc = fromMatcher.start(2);
      
      Matcher orderMatcher = ORDER_PATTERN.matcher(ejbql);
      int orderLoc = orderMatcher.find() ? orderMatcher.start(1) : ejbql.length();

      return "select count(*) " + ejbql.substring(fromLoc, orderLoc);
   }
   
   public String getEjbql()
   {
      return ejbql;
   }

   /**
    * Set the ejbql to use.  Calling this causes the ejbql to be reparsed and
    * the query to be refreshed
    */
   public void setEjbql(String ejbql)
   {
      this.ejbql = ejbql;
      parsedEjbql = null;
      refresh();
   }

   /**
    * Returns the index of the first result of the current page
    */
   public Integer getFirstResult()
   {
      return firstResult;
   }
   
   /**
    * Returns true if the previous page exists
    */
   public boolean isPreviousExists()
   {
      return getFirstResult()!=null && getFirstResult()!=0;
   }

   /**
    * Returns true if next page exists
    */
   public abstract boolean isNextExists();

   /**
    * Set the index at which the page to display should start
    */
   public void setFirstResult(Integer firstResult)
   {
      this.firstResult = firstResult;
      refresh();
   }

   /**
    * The page size
    */
   public Integer getMaxResults()
   {
       return maxResults;
   }

   public void setMaxResults(Integer maxResults)
   {
      this.maxResults = maxResults;
      refresh();
   }

   /**
    * List of restrictions to apply to the query.
    * 
    * For a query such as 'from Foo f' a restriction could be 
    * 'f.bar = #{foo.bar}'
    */
   public List<String> getRestrictions()
   {
      return restrictions;
   }

   /**
    * Calling setRestrictions causes the restrictions to be reparsed and the 
    * query refreshed
    */
   public void setRestrictions(List<String> restrictions)
   {
      this.restrictions = restrictions;
      parsedRestrictions = null;
      refresh();
   }

   /**
    * The order of the query
    */
   public String getOrder()
   {
      return order;
   }

   public void setOrder(String order)
   {      
       if (order!= null && !ORDER_CLAUSE_PATTERN.matcher(order).find()) {
          throw new IllegalArgumentException("invalid order column (\"" + order + "\" must match the regular expression \"" + ORDER_CLAUSE_PATTERN + "\")");
       }
       this.order = order;
       refresh();
   }
   
   protected List<ValueExpression> getQueryParameters()
   {
      return queryParameters;
   }
   
   protected List<ValueExpression> getRestrictionParameters()
   {
      return restrictionParameters;
   }
   
   private static boolean isAnyParameterDirty(List<ValueExpression> valueBindings, List<Object> lastParameterValues)
   {
      if (lastParameterValues==null) return true;
      for (int i=0; i<valueBindings.size(); i++)
      {
         Object parameterValue = valueBindings.get(i).getValue();
         Object lastParameterValue = lastParameterValues.get(i);
         //treat empty strings as null, for consistency with isRestrictionParameterSet()
         if ( "".equals(parameterValue) ) parameterValue = null;
         if ( "".equals(lastParameterValue) ) lastParameterValue = null;
         if ( parameterValue!=lastParameterValue && ( parameterValue==null || !parameterValue.equals(lastParameterValue) ) )
         {
            return true;
         }
      }
      return false;
   }
   
   private static List<Object> getParameterValues(List<ValueExpression> valueBindings)
   {
      List<Object> values = new ArrayList<Object>( valueBindings.size() );
      for (int i=0; i<valueBindings.size(); i++)
      {
         values.add( valueBindings.get(i).getValue() );
      }
      return values;
   }
   
   protected void evaluateAllParameters()
   {
      setQueryParameterValues( getParameterValues( getQueryParameters() ) );
      setRestrictionParameterValues( getParameterValues( getRestrictionParameters() ) );
   }
   
   protected boolean isAnyParameterDirty()
   {
      return isAnyParameterDirty( getQueryParameters(), getQueryParameterValues() )
            || isAnyParameterDirty( getRestrictionParameters(), getRestrictionParameterValues() );
   }
   
   protected List<Object> getQueryParameterValues()
   {
      return queryParameterValues;
   }
   
   protected void setQueryParameterValues(List<Object> queryParameterValues)
   {
      this.queryParameterValues = queryParameterValues;
   }
   
   protected List<Object> getRestrictionParameterValues()
   {
      return restrictionParameterValues;
   }
   
   protected void setRestrictionParameterValues(List<Object> restrictionParameterValues)
   {
      this.restrictionParameterValues = restrictionParameterValues;
   }
   protected List<E> truncResultList(List<E> results)
   {
      Integer mr = getMaxResults();
      if ( mr!=null && results.size() > mr )
      {
         return results.subList(0, mr);
      }
      else
      {
         return results;
      }
   }

}
