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;
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 SUBJECT_PATTERN = Pattern.compile("^select\\s+((distinct)?\\s?\\w+(?:\\s*\\.\\s*\\w+)?)(?:\\s,\\s*(\\w+(?:\\s*\\.\\s*\\w+)?))?\\s+from", Pattern.CASE_INSENSITIVE); 
   private static final Pattern FROM_PATTERN = Pattern.compile("(^|\\s)(from)\\s",       Pattern.CASE_INSENSITIVE);
   private static final Pattern WHERE_PATTERN = Pattern.compile("\\s(where)\\s",         Pattern.CASE_INSENSITIVE);
   private static final Pattern ORDER_PATTERN = Pattern.compile("\\s(order)(\\s)+by\\s", Pattern.CASE_INSENSITIVE);
   private static final Pattern GROUP_PATTERN = Pattern.compile("\\s(group)(\\s)+by\\s", Pattern.CASE_INSENSITIVE);
   private static final Pattern JOIN_FETCH_PATTERN = Pattern.compile("join fetch",       Pattern.CASE_INSENSITIVE);
   private static final String  JOIN_OPERATOR="join";

   private static final Pattern ORDER_COLUMN_PATTERN = Pattern.compile("^\\w+(\\.\\w+)*$");

   private static final String DIR_ASC = "asc";
   private static final String DIR_DESC = "desc";
   private static final String LOGIC_OPERATOR_AND = "and";
   private static final String LOGIC_OPERATOR_OR = "or";

   private String ejbql;
   private Integer firstResult;
   private Integer maxResults;
   private List<ValueExpression> restrictions = new ArrayList<ValueExpression>(0);
   private String order;
   private String orderColumn;
   private String orderDirection;
   private String restrictionLogicOperator;
   private String groupBy;
   
   private boolean useWildcardAsCountQuerySubject = true;
   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<ValueExpression> restrictionFragments = getRestrictions();
         parsedRestrictions = new ArrayList<String>( restrictionFragments.size() );
         restrictionParameters = new ArrayList<ValueExpression>( restrictionFragments.size() );         
         for ( ValueExpression restriction: restrictionFragments )
         {
            QueryParser rqp = new QueryParser( restriction.getExpressionString(), 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(" ").append(getRestrictionLogicOperator()).append(" ");
            }
            else
            {
               builder.append(" where ");
            }
            builder.append( parsedRestrictions.get(i) );
         }
      }
      
      if (getGroupBy()!=null) {
          builder.append(" group by ").append(getGroupBy());
      }

      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);
      
      // TODO can we just create a protected method that builds the query w/o the order by and group by clauses?
      Matcher orderMatcher = ORDER_PATTERN.matcher(ejbql);
      int orderLoc = orderMatcher.find() ? orderMatcher.start(1) : ejbql.length();

      Matcher groupMatcher = GROUP_PATTERN.matcher(ejbql);
      int groupLoc = groupMatcher.find() ? groupMatcher.start(1) : orderLoc;

      Matcher whereMatcher = WHERE_PATTERN.matcher(ejbql);
      int whereLoc = whereMatcher.find() ? whereMatcher.start(1) : groupLoc;

      String subject;
      if (useWildcardAsCountQuerySubject) {
         subject = "*";
      }
      else if (getGroupBy() != null) {
          subject = "distinct " + getGroupBy();
       }
      // to be JPA-compliant, we need to make this query like "select count(u) from User u"
      // however, Hibernate produces queries some databases cannot run when the primary key is composite
      else {
          Matcher subjectMatcher = SUBJECT_PATTERN.matcher(ejbql);
          if ( subjectMatcher.find() )
          {
             subject = subjectMatcher.group(1);
          }
          else
          {
             throw new IllegalStateException("invalid select clause for query");
          }
      }
      String fromToWherePart=JOIN_FETCH_PATTERN.matcher(ejbql.substring(fromLoc, whereLoc)).replaceAll(JOIN_OPERATOR);
      return new StringBuilder(ejbql.length() + 15).append("select count(").append(subject).append(") ").
         append(fromToWherePart).
         append(ejbql.substring(whereLoc, groupLoc)).toString().trim();
   }
   
   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();

   /**
    * Returns true if the query is paginated, revealing
    * whether navigation controls are needed.
    */
   public boolean isPaginated() {
      return isNextExists() || isPreviousExists();
   }
   
   /**
    * 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<ValueExpression> getRestrictions()
   {
      return restrictions;
   }

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

   /**
    * A convenience method for registering the restrictions from Strings. This
    * method is primarily intended to be used from Java, not to expose a bean
    * property for component configuration. Use setRestrictions() for the later.
    */
   public void setRestrictionExpressionStrings(List<String> expressionStrings)
   {
      Expressions expressions = new Expressions();
      List<ValueExpression> restrictionVEs = new ArrayList<ValueExpression>(expressionStrings.size());
      for (String expressionString : expressionStrings)
      {
         restrictionVEs.add(expressions.createValueExpression(expressionString));
      }
      setRestrictions(restrictionVEs);
   }
   
   public List<String> getRestrictionExpressionStrings()
   {
      List<String> expressionStrings = new ArrayList<String>();
      for (ValueExpression restriction : getRestrictions())
      {
         expressionStrings.add(restriction.getExpressionString());
      }
      return expressionStrings;
   }
   
    public String getGroupBy() {
        return groupBy;
    }
    
    public void setGroupBy(String groupBy) {
        this.groupBy = groupBy;
    }
    
   /**
    * The order clause of the query
    */
   
   public String getOrder() {
       String column = getOrderColumn();

       if (column == null) {
           return order;
       }
       
       String direction = getOrderDirection();
       
       if (direction == null) {
           return column;
       } else {
           return column + ' ' + direction;
       }   
   }

   public void setOrder(String order)
   {      
       this.order = order;
       refresh();
   }
   
   public String getOrderDirection() {
       return orderDirection;
   }

   public void setOrderDirection(String orderDirection) {
       this.orderDirection = sanitizeOrderDirection(orderDirection);
   }
   
   private String sanitizeOrderDirection(String direction) {
       if (direction == null || direction.length()==0) {
           return null;
       } else if (direction.equalsIgnoreCase(DIR_ASC)) {
           return DIR_ASC;
       } else if (direction.equalsIgnoreCase(DIR_DESC)) {
           return DIR_DESC;
       } else {
           throw new IllegalArgumentException("invalid order direction");
       }
   }

   public String getOrderColumn() {
       return orderColumn;
   }

   public void setOrderColumn(String orderColumn) {
       this.orderColumn = sanitizeOrderColumn(orderColumn);
   }

   private String sanitizeOrderColumn(String columnName) {
       if (columnName == null || columnName.trim().length() == 0) {
           return null;
       } else if (ORDER_COLUMN_PATTERN.matcher(columnName).find()) {
           return columnName;
       } else {
           throw new IllegalArgumentException("invalid order column (\"" + columnName + "\" must match the regular expression \"" + ORDER_COLUMN_PATTERN + "\")");
       }
   }
   
   public String getRestrictionLogicOperator()
   {
      return restrictionLogicOperator != null ? restrictionLogicOperator : LOGIC_OPERATOR_AND;
   }
   
   public void setRestrictionLogicOperator(String operator)
   {
      restrictionLogicOperator = sanitizeRestrictionLogicOperator(operator);
   }
   
   private String sanitizeRestrictionLogicOperator(String operator) {
      if (operator == null || operator.trim().length() == 0)
      {
         return LOGIC_OPERATOR_AND;
      }
      if (!(LOGIC_OPERATOR_AND.equals(operator) || LOGIC_OPERATOR_OR.equals(operator)))
      {
         throw new IllegalArgumentException("Invalid restriction logic operator: " + operator);
      }
      else
      {
         return operator;
      }  
   }
   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;
      }
   }

   protected boolean isUseWildcardAsCountQuerySubject() {
      return useWildcardAsCountQuerySubject;
   }

   protected void setUseWildcardAsCountQuerySubject(boolean useCompliantCountQuerySubject) {
       this.useWildcardAsCountQuerySubject = useCompliantCountQuerySubject;
   }

}
