/**
 * Entity.java
 * Created May 19, 2008
 * Copyright (c) TANDBERG Television 2007-2008
 */
package com.tandbergtv.watchpoint.search;

import static com.tandbergtv.watchpoint.search.HQLQuery.AND;
import static com.tandbergtv.watchpoint.search.HQLQuery.INNER_JOIN;
import static com.tandbergtv.watchpoint.search.HQLQuery.LEFT_JOIN;
import static com.tandbergtv.watchpoint.search.HQLQuery.OR;
import static com.tandbergtv.watchpoint.search.HQLQuery.PERIOD;

import java.util.ArrayList;
import java.util.List;

import com.tandbergtv.workflow.driver.search.SearchParameterBase;
import com.tandbergtv.workflow.driver.search.SortParameter;

/**
 * Search parameter for an entity. It contains other {@link SearchParameter}s which are just
 * plain properties of the entity. An entity may also contain other sub-entities which are merely
 * first class types that have a direct relationship with itself in the object model.
 * 
 * @author Sahil Verma
 */
public class Entity extends SearchParameter {
	
	protected List<SearchParameter> subparameters;
	
	protected Class<?> clazz;
	
	protected String property;
	
	protected boolean fetch;
	
	protected Join join;
	
	protected List<String> selectPropertyNames;
	
	/**
	 * Creates an Entity
	 * 
	 * @param name
	 * @param clazz
	 * @param alias
	 */
	public Entity(String name, Class<?> clazz, String alias) {
		super(name);
		this.clazz = clazz;
		this.subparameters = new ArrayList<SearchParameter>();
		this.alias = alias;
		this.fetch = true;
		this.join = Join.INNER;
		this.selectPropertyNames = new ArrayList<String>();
	}

	/**
	 * Creates an Entity
	 * 
	 * @param name
	 * @param property
	 * @param alias
	 */
	public Entity(String name, String property, String alias) {
		super(name);
		this.property = property;
		this.subparameters = new ArrayList<SearchParameter>();
		this.alias = alias;
		this.join = Join.INNER;
		this.selectPropertyNames = new ArrayList<String>();
	}
	
	/**
	 * @return the class
	 */
	public Class<?> getEntityClass() {
		return this.clazz;
	}

	/**
	 * Returns the name of the property of the parent entity that is used to refer to this entity
	 * 
	 * @return the property
	 */
	public String getProperty() {
		return this.property;
	}

	/**
	 * Determines if results of this type should be returned by the query
	 * 
	 * @return
	 */
	public boolean isFetch() {
		return this.fetch;
	}

	/**
	 * Sets a flag to indicate that this type is required in the query result
	 * 
	 * @param fetch
	 */
	public void setFetch(boolean fetch) {
		this.fetch = fetch;
	}

	/**
	 * Returns the join type
	 * 
	 * @return
	 */
	public Join getJoin() {
		return this.join;
	}

	/**
	 * Sets the join type
	 * 
	 * @param join
	 */
	public void setJoin(Join join) {
		this.join = join;
	}

	/**
	 * @return the subparameters
	 */
	public List<SearchParameter> getSubParameters() {
		return this.subparameters;
	}
	
	/**
	 * Returns the SELECT clause, or empty string if this entity is not needed in the query
	 * 
	 * @return
	 */
	public String getSelectClause() {
		if (!fetch)
			return "";
		
		String clause = getSelectedNames();
		
		/* Two levels of parameters are supported, any more and it'll get complicated */
		for (SearchParameter subparameter : getSubParameters()) {
			// No need to do anything on NestedQueryEntity coz it will be taken
			// care of in the sub select
			if(subparameter instanceof NestedQueryEntity)
				continue;
			if (subparameter instanceof Entity) {
				Entity e = Entity.class.cast(subparameter);
				
				if (e.isFetch())
					clause += ", " + e.getSelectedNames();
			}
		}
		
		return clause;
	}

	/**
	 * If there are specific fields that need to be selected, use those else
	 * simply return the complete entity (via its alias).
	 * 
	 */
	protected String getSelectedNames() {
		String alias = getCompleteAlias();
		if (this.selectPropertyNames != null && this.selectPropertyNames.size() > 0) {
			StringBuilder sb = new StringBuilder();
			for (String paramName : selectPropertyNames) {
				if (sb.length() > 0) {
					sb.append(", ");
				}
				sb.append(alias + "." + paramName);
			}
			return sb.toString();
		} else {
			return alias;
		}
	}
	
	/* (non-Javadoc)
	 * @see com.tandbergtv.workflow.driver.search.SearchParameterBase#getPartialFromClause(int)
	 */
	public String getPartialFromClause(int count) {
		String clause = "";
		setObjectCount(count);
		
		for (SearchParameter subparameter : this.subparameters) {
			// nested entity should never show up in the from clause. It will
			// always be used for the sub query.
			if(subparameter instanceof NestedQueryEntity) 
				continue;
			if (subparameter instanceof Entity) {
				/* Compute joins in the FROM clause */
				Entity e = Entity.class.cast(subparameter);
				String name = getCompleteAlias() + PERIOD + e.getProperty();
				HQLQuery jointype = (e.getJoin() == Join.INNER) ? INNER_JOIN : LEFT_JOIN;
				
				clause += System.getProperty("line.separator");
				clause += jointype + name + " " + e.getCompleteAlias();
				
				/* If we're sorting using this entity, attach a WITH clause to filter correctly */
				if (e.getJoin() == Join.LEFT_OUTER) {
					String partialWhere = e.getPartialWhereClause();
					
					if (partialWhere.length() > 0)
						clause += HQLQuery.WITH + partialWhere;
				}
			}
		}
		
		return this.clazz.getName() + " " + this.alias + this.objectCount + clause;
	}

	/* (non-Javadoc)
	 * @see com.tandbergtv.workflow.driver.search.SearchParameterBase#getPartialWhereClause()
	 */
	public String getPartialWhereClause() {
		String clause = "";
		
		/* Add all the properties */
		SearchParameter prev = null;
		for (SearchParameter subparameter : subparameters) {
			if (subparameter.getDecoratedParameter() instanceof SortParameter)
				continue;
			
			if (subparameter instanceof Entity) {
				Entity e = Entity.class.cast(subparameter);
				
				/* We've already taken care of the criterion in the FROM clause, so skip the filters */
				if (e.getJoin() == Join.LEFT_OUTER)
					continue;
			}

			// if we are behind an open parentheses or in front of a close
			// parentheses or the clause is not empty, add a boolean operator.
			if (!((subparameter instanceof GroupingEntity
					&& ((GroupingEntity)subparameter).isClosed())
					|| (prev != null && prev instanceof GroupingEntity
							&& ((GroupingEntity)prev).isOpen()))
					&& clause.length() > 0) {
				if(subparameter.isConjunction()) {
					clause += System.getProperty("line.separator") + "\t" + AND;
				} else {
					clause += System.getProperty("line.separator") + "\t" + OR;
				}
			}
			
			prev = subparameter;
			clause += subparameter.getPartialWhereClause();
		}
		
		return clause;
	}
	
	/* (non-Javadoc)
	 * @see com.tandbergtv.watchpoint.pmm.search.SearchParameter#getPartialOrderByClause()
	 */
	public String getPartialOrderByClause() {
		String clause = "";
		
		/* There can be a sort parameter attached to this entity or to one of the child entities */
		for (SearchParameter parameter : getSubParameters()) {
			if (parameter instanceof Entity) {
				clause += parameter.getPartialOrderByClause();
			} else if (parameter.getDecoratedParameter() instanceof SortParameter) {
				if (clause.length() > 0)
					clause += ", ";

				clause += getCompleteAlias() + PERIOD + parameter.getPartialOrderByClause();
			}
		}
		
		return clause;
	}

	/* (non-Javadoc)
	 * @see com.tandbergtv.watchpoint.pmm.search.SearchParameter#setObjectCount(int)
	 */
	public void setObjectCount(int count) {
		this.objectCount = count;
		
		for (SearchParameter subparameter : subparameters) {
			// for nested entity, we want to use the parent's complete alias. so
			// no point in incrementing the count for this.
			if (subparameter instanceof NestedQueryEntity)
				subparameter.setObjectCount(this.objectCount);
			else if (subparameter instanceof Entity)
				subparameter.setObjectCount(count++);
			else
				subparameter.setObjectCount(this.objectCount);
		}
	}

	/**
	 * Adds a child parameter 
	 * 
	 * @param parameter
	 */
	public void addParameter(SearchParameterBase parameter) {
		/* We use legacy parameter type for backwards compatibility */
		SearchParameter subparameter = null;
		
		if (parameter instanceof SearchParameter) {
			subparameter = (SearchParameter)parameter;
		} else {
			subparameter = new SearchParameter(parameter);
			subparameter.setAlias(this.alias);
		}
		
		this.subparameters.add(subparameter);
	}

	/**
	 * Adds a child parameter 
	 * 
	 * @param parameter
	 */
	public void addParameter(SearchParameterBase parameter, boolean isConjunction) {
		/* We use legacy parameter type for backwards compatibility */
		SearchParameter subparameter = null;
		
		if (parameter instanceof SearchParameter) {
			subparameter = (SearchParameter)parameter;
		} else {
			subparameter = new SearchParameter(parameter);
			subparameter.setAlias(this.alias);
		}
		subparameter.setConjunction(isConjunction);		
		this.subparameters.add(subparameter);
	}
	
	/**
	 * @return the selectPropertyNames
	 */
	public List<String> getSelectPropertyNames() {
		return selectPropertyNames;
	}

	/**
	 * @param selectPropertyNames the selectPropertyNames to set
	 */
	public void setSelectPropertyNames(List<String> selectPropertyNames) {
		this.selectPropertyNames = selectPropertyNames;
	}
}
