/*
 * This file is part of Nuts Framework.
 * Copyright (C) 2009 Nuts Develop Team.
 *
 * Nuts Framework is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License any later version.
 * 
 * Nuts Framework is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with Nuts Framework. If not, see <http://www.gnu.org/licenses/>.
 */
package nuts.gae.dao;

import nuts.core.dao.Conditions;
import nuts.core.lang.Arrays;
import nuts.core.lang.Exceptions;

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

import com.google.appengine.api.datastore.Query;
import com.google.appengine.api.datastore.Query.CompositeFilter;
import com.google.appengine.api.datastore.Query.CompositeFilterOperator;
import com.google.appengine.api.datastore.Query.Filter;
import com.google.appengine.api.datastore.Query.FilterOperator;

/**
 */
@SuppressWarnings("serial")
public class GaeConditions implements Conditions, Cloneable, Serializable {

	private final static char OPEN_PAREN = '(';
	private final static char CLOSE_PAREN = ')';
	
	private Query query;
	private Composite composite = new Composite();
	
	private String conjunction = AND;

	private static class Composite {
		private List<Object> filters = new ArrayList<Object>();
		private boolean closed = false;

		public Composite() {
		}
		
		private Composite getLastComposite() {
			if (!filters.isEmpty()) {
				Object o = filters.get(filters.size() - 1);
				if (o instanceof Composite) {
					Composite c = (Composite)o;
					return c.closed ? this : c;
				}
			}
			return this;
		}
		
		public void addFilter(String propertyName, FilterOperator operator, Object value) {
			Query.Filter filter = new Query.FilterPredicate(propertyName, operator, value);
			Composite c = getLastComposite();
			c.filters.add(filter);
		}
		
		public void addParen(char paren) {
			Composite c = getLastComposite();
			if (paren == OPEN_PAREN) {
				Composite n = new Composite();
				c.filters.add(n);
			}
			else {
				c.closed = true;
			}
		}
		
		public void addConjunction(String conjunction, boolean force) {
			Composite c = getLastComposite();
			if (c.filters.isEmpty()) {
				if (force) {
					throw new IllegalArgumentException("Explicitly appending logical operator " + conjunction + " to empty CompositeFilter is not allowed.");
				}
			}
			else {
				Object last = c.filters.get(c.filters.size() - 1); 
				if (last instanceof String) {
					if (force) {
						throw new IllegalArgumentException("Explicitly appending logical operator " + conjunction + " after '" + last + "' is not allowed.");
					}
					// 'and' 'or' already appended, so skip
				}
				else {
					c.filters.add(conjunction);
				}
			}
		}

		private CompositeFilterOperator buildFilterList(List<Filter> fs, int start) {
			CompositeFilterOperator op = CompositeFilterOperator.AND;
			List<Filter> cfs = new ArrayList<Filter>();
			for (int i = start; i < filters.size(); i++) {
				Object o = filters.get(i);
				if (o instanceof Composite) {
					Filter f = ((Composite)o).toQueryFilter();
					if (f != null) {
						cfs.add(f);
					}
				}
				else if (o instanceof Filter) {
					cfs.add((Filter)o);
				}
				else if (o instanceof String) {
					if (OR.equals(o)) {
						op = CompositeFilterOperator.OR;
						if (cfs.size() > 1) {
							Filter f = new CompositeFilter(CompositeFilterOperator.AND, cfs);
							cfs = new ArrayList<Filter>();
							cfs.add(f);
						}
						buildFilterList(cfs, i + 1);
						break;
					}
				}
			}
			fs.addAll(cfs);
			return op;
		}
		
		public Filter toQueryFilter() {
			if (filters.isEmpty()) {
				return null;
			}
			
			List<Filter> fs = new ArrayList<Filter>();
			CompositeFilterOperator op = buildFilterList(fs, 0);

			if (fs.size() == 1) {
				return fs.get(0); 
			}
			return new CompositeFilter(op, fs);
		}
	}
	
	/**
	 * Constructor
	 * 
	 * @param query query
	 */
	public GaeConditions(Query query) {
		this.query = query;
	}

	private void setQueryFilter() {
		Filter filter = composite.toQueryFilter();
		query.setFilter(filter);
	}
	
	private void addFilter(String propertyName, FilterOperator operator, Object value) {
		addConjunction(conjunction, false);
		composite.addFilter(propertyName, operator, value);
		setQueryFilter();
	}
	
	private GaeConditions addParen(char paren) {
		composite.addParen(paren);
		return this;
	}
	
	private GaeConditions addConjunction(String conjunction, boolean force) {
		composite.addConjunction(conjunction, force);
		return this;
	}

	/**
	 * @return conjunction
	 */
	public String getConjunction() {
		return conjunction;
	}

	/**
	 * @param conjunction the conjunction to set
	 */
	public void setConjunction(String conjunction) {
		if (conjunction == null) {
			throw new IllegalArgumentException(
					"the conjunction is required; it can not be null");
		}

		conjunction = conjunction.toUpperCase();
		if (!AND.equals(conjunction) && !OR.equals(conjunction)) {
			throw new IllegalArgumentException("Invalid conjunction ["
					+ conjunction + "], must be AND/OR");
		}

		this.conjunction = conjunction;
	}

	/**
	 * setConjunctionToAnd
	 */
	public void setConjunctionToAnd() {
		this.conjunction = AND;
	}

	/**
	 * setConjunctionToOr
	 */
	public void setConjunctionToOr() {
		this.conjunction = OR;
	}

	/**
	 * add AND expression
	 * 
	 * @return this
	 */
	public GaeConditions and() {
		return addConjunction(AND, true);
	}

	/**
	 * add OR expression
	 * 
	 * @return this
	 */
	public GaeConditions or() {
		return addConjunction(OR, true);
	}

	/**
	 * add open paren (
	 * 
	 * @return this
	 */
	public GaeConditions open() {
		return addParen(OPEN_PAREN);
	}

	/**
	 * add close paren )
	 * 
	 * @return this
	 */
	public GaeConditions close() {
		return addParen(CLOSE_PAREN);
	}

	/**
	 * add "column IS NULL" expression
	 * 
	 * @param column column
	 * @return this
	 */
	public GaeConditions isNull(String column) {
		addFilter(column, FilterOperator.EQUAL, null);
		return this;
	}

	/**
	 * add "column IS NOT NULL" expression
	 * 
	 * @param column column
	 * @return this
	 */
	public GaeConditions isNotNull(String column) {
		addFilter(column, FilterOperator.NOT_EQUAL, null);
		return this;
	}

	/**
	 * add "column = value" expression
	 * 
	 * @param column column
	 * @param value value
	 * @return this
	 */
	public GaeConditions equalTo(String column, Object value) {
		addFilter(column, FilterOperator.EQUAL, value);
		return this;
	}

	/**
	 * add "column &lt;&gt; value" expression
	 * 
	 * @param column column
	 * @param value value
	 * @return this
	 */
	public GaeConditions notEqualTo(String column, Object value) {
		addFilter(column, FilterOperator.NOT_EQUAL, value);
		return this;
	}

	/**
	 * add "column &gt; value" expression
	 * 
	 * @param column column
	 * @param value value
	 * @return this
	 */
	public GaeConditions greaterThan(String column, Object value) {
		addFilter(column, FilterOperator.GREATER_THAN, value);
		return this;
	}

	/**
	 * add "column &gt;= value" expression
	 * 
	 * @param column column
	 * @param value value
	 * @return this
	 */
	public GaeConditions greaterThanOrEqualTo(String column, Object value) {
		addFilter(column, FilterOperator.GREATER_THAN_OR_EQUAL, value);
		return this;
	}

	/**
	 * add "column &lt; value" expression
	 * 
	 * @param column column
	 * @param value value
	 * @return this
	 */
	public GaeConditions lessThan(String column, Object value) {
		addFilter(column, FilterOperator.LESS_THAN, value);
		return this;
	}

	/**
	 * add "column &lt;= value" expression
	 * 
	 * @param column column
	 * @param value value
	 * @return this
	 */
	public GaeConditions lessThanOrEqualTo(String column, Object value) {
		addFilter(column, FilterOperator.LESS_THAN_OR_EQUAL, value);
		return this;
	}

	/**
	 * add "column LIKE value" expression
	 * 
	 * @param column column
	 * @param value value
	 * @return this
	 */
	public GaeConditions like(String column, Object value) {
		throw Exceptions.unsupported("LIKE is not supported by Google App Engine.");
	}

	/**
	 * add "column LIKE %value%" expression
	 * 
	 * @param column column
	 * @param value value
	 * @return this
	 */
	public GaeConditions match(String column, Object value) {
		throw Exceptions.unsupported("LIKE is not supported by Google App Engine.");
	}

	/**
	 * add "column LIKE value%" expression
	 * 
	 * @param column column
	 * @param value value
	 * @return this
	 */
	public GaeConditions leftMatch(String column, Object value) {
		throw Exceptions.unsupported("LIKE is not supported by Google App Engine.");
	}

	/**
	 * add "column LIKE %value" expression
	 * 
	 * @param column column
	 * @param value value
	 * @return this
	 */
	public GaeConditions rightMatch(String column, Object value) {
		throw Exceptions.unsupported("LIKE is not supported by Google App Engine.");
	}

	/**
	 * add "column NOT LIKE value" expression
	 * 
	 * @param column column
	 * @param value value
	 * @return this
	 */
	public GaeConditions notLike(String column, Object value) {
		throw Exceptions.unsupported("LIKE is not supported by Google App Engine.");
	}

	/**
	 * add "column = compareColumn" expression
	 * 
	 * @param column column
	 * @param compareColumn column to compare
	 * @return this
	 */
	public GaeConditions equalToColumn(String column, String compareColumn) {
		throw Exceptions.unsupported("Column compare is not supported by Google App Engine.");
	}

	/**
	 * add "column &lt;&gt; compareColumn" expression
	 * 
	 * @param column column
	 * @param compareColumn column to compare
	 * @return this
	 */
	public GaeConditions notEqualToColumn(String column, String compareColumn) {
		throw Exceptions.unsupported("Column compare is not supported by Google App Engine.");
	}

	/**
	 * add "column %gt; compareColumn" expression
	 * 
	 * @param column column
	 * @param compareColumn column to compare
	 * @return this
	 */
	public GaeConditions greaterThanColumn(String column, String compareColumn) {
		throw Exceptions.unsupported("Column compare is not supported by Google App Engine.");
	}

	/**
	 * add "column &gt;= compareColumn" expression
	 * 
	 * @param column column
	 * @param compareColumn column to compare
	 * @return this
	 */
	public GaeConditions greaterThanOrEqualToColumn(String column,
			String compareColumn) {
		throw Exceptions.unsupported("Column compare is not supported by Google App Engine.");
	}

	/**
	 * add "column &lt; compareColumn" expression
	 * 
	 * @param column column
	 * @param compareColumn column to compare
	 * @return this
	 */
	public GaeConditions lessThanColumn(String column, String compareColumn) {
		throw Exceptions.unsupported("Column compare is not supported by Google App Engine.");
	}

	/**
	 * add "column %lt;= compareColumn" expression
	 * 
	 * @param column column
	 * @param compareColumn column to compare
	 * @return this
	 */
	public GaeConditions lessThanOrEqualToColumn(String column,
			String compareColumn) {
		throw Exceptions.unsupported("Column compare is not supported by Google App Engine.");
	}

	/**
	 * add "column IN (value1, value2 ...)" expression
	 * 
	 * @param column column
	 * @param values values
	 * @return this
	 */
	public GaeConditions in(String column, Object[] values) {
		addFilter(column, FilterOperator.IN, Arrays.asList(values));
		return this;
	}

	/**
	 * add "column IN (value1, value2 ...)" expression
	 * 
	 * @param column column
	 * @param values values
	 * @return this
	 */
	public GaeConditions in(String column, Collection values) {
		addFilter(column, FilterOperator.IN, values);
		return this;
	}

	/**
	 * add "column NOT IN (value1, value2 ...)" expression
	 * 
	 * @param column column
	 * @param values values
	 * @return this
	 */
	public GaeConditions notIn(String column, Object[] values) {
		throw Exceptions.unsupported("NOT IN is not supported by Google App Engine.");
	}

	/**
	 * add "column NOT IN (value1, value2 ...)" expression
	 * 
	 * @param column column
	 * @param values values
	 * @return this
	 */
	public GaeConditions notIn(String column, Collection values) {
		throw Exceptions.unsupported("NOT IN is not supported by Google App Engine.");
	}

	/**
	 * add "column BETWEEN (value1, value2)" expression
	 * 
	 * @param column column
	 * @param value1 value from
	 * @param value2 value to
	 * @return this
	 */
	public GaeConditions between(String column, Object value1, Object value2) {
		addFilter(column, FilterOperator.GREATER_THAN_OR_EQUAL, value1);
		addFilter(column, FilterOperator.LESS_THAN_OR_EQUAL, value2);
		return this;
	}

	/**
	 * add "column NOT BETWEEN (value1, value2)" expression
	 * 
	 * @param column column
	 * @param value1 value from
	 * @param value2 value to
	 * @return this
	 */
	public GaeConditions notBetween(String column, Object value1,
			Object value2) {
		throw Exceptions.unsupported("NOT BETWEEN is not supported by Google App Engine.");
	}

	/**
	 * @see java.lang.Object#hashCode()
	 */
	@Override
	public int hashCode() {
		final int prime = 31;
		int result = 1;
		result = prime * result
				+ ((query == null) ? 0 : query.hashCode());
		return result;
	}

	/**
	 * @see java.lang.Object#equals(java.lang.Object)
	 */
	@Override
	public boolean equals(Object obj) {
		if (this == obj)
			return true;
		if (obj == null)
			return false;
		if (getClass() != obj.getClass())
			return false;
		GaeConditions other = (GaeConditions)obj;
		if (query == null) {
			if (other.query != null)
				return false;
		}
		else if (!query.equals(other.query))
			return false;
		return true;
	}

	/**
	 * Clone
	 * 
	 * @throws CloneNotSupportedException if clone not supported
	 * @return Clone Object
	 */
	public Object clone() throws CloneNotSupportedException {
		return super.clone();
	}

	/**
	 * @return a string representation of the object.
	 */
	@Override
	public String toString() {
		StringBuilder sb = new StringBuilder();

		sb.append("{ ");
		sb.append("query: ").append(query);
		sb.append(" }");

		return sb.toString();
	}

}
