/*
 * JasperReports - Free Java Reporting Library.
 * Copyright (C) 2001 - 2009 Jaspersoft Corporation. All rights reserved.
 * http://www.jaspersoft.com
 *
 * Unless you have purchased a commercial license agreement from Jaspersoft,
 * the following license terms apply:
 *
 * This program is part of JasperReports.
 *
 * JasperReports is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * JasperReports 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 Lesser General Public License for more details.
 * 
 * You should have received a copy of the GNU Lesser General Public License
 * along with JasperReports. If not, see <http://www.gnu.org/licenses/>.
 */
package net.sf.jasperreports.engine.query;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;

import net.sf.jasperreports.engine.JRDataset;
import net.sf.jasperreports.engine.JRParameter;
import net.sf.jasperreports.engine.JRQuery;
import net.sf.jasperreports.engine.JRQueryChunk;
import net.sf.jasperreports.engine.JRRuntimeException;
import net.sf.jasperreports.engine.JRValueParameter;
import net.sf.jasperreports.engine.fill.JRFillParameter;
import net.sf.jasperreports.engine.util.JRQueryChunkHandler;
import net.sf.jasperreports.engine.util.JRQueryParser;

/**
 * Base abstract query executer.
 * 
 * @author Lucian Chirita (lucianc@users.sourceforge.net)
 * @version $Id: JRAbstractQueryExecuter.java 3034 2009-08-27 11:58:04Z teodord $
 */
public abstract class JRAbstractQueryExecuter implements JRQueryExecuter
{
	
	protected static final int CLAUSE_POSITION_ID = 0;

	/**
	 * A parameter present in the query.
	 */
	protected static class QueryParameter
	{
		protected static final int COUNT_SINGLE = -1;
		
		private final String name;
		private final int count;
		
		public QueryParameter(String name)
		{
			this(name, COUNT_SINGLE);
		}
		
		public QueryParameter(String name, int count)
		{
			this.name = name;
			this.count = count;
		}
		
		/**
		 * Decides whether the parameter has multiple values.
		 * 
		 * @return whether the parameter has multiple values.
		 */
		public boolean isMulti()
		{
			return count != COUNT_SINGLE;
		}
		
		/**
		 * Returns the number of parameter values.
		 * 
		 * @return the number of parameter values
		 * @see #isMulti()
		 */
		public int getCount()
		{
			return count;
		}
		
		/**
		 * Returns the name of the report parameter.
		 * 
		 * @return the name of the report parameter
		 */
		public String getName()
		{
			return name;
		}
	}
	
	/**
	 * Clause function registry.
	 */
	protected final Map clauseFunctions = new HashMap();
	
	protected final JRDataset dataset;
	private final Map parametersMap;
	
	private String queryString;
	
	/**
	 * List of {@link QueryParameter query parameters}.
	 */
	private List queryParameters;
	
	private Set parameterClauseStack;
	
	
	protected JRAbstractQueryExecuter(JRDataset dataset, Map parametersMap)
	{
		this.dataset = dataset;
		this.parametersMap = parametersMap;
		
		queryString = "";
		queryParameters = new ArrayList();
	}

	/**
	 * Registers a clause function.
	 * 
	 * @param id the function ID
	 * @param function the function
	 */
	protected void registerClauseFunction(String id, JRClauseFunction function)
	{
		clauseFunctions.put(id, function);
	}
	
	/**
	 * Unregisters a clause function.
	 * 
	 * @param id the function ID
	 */
	protected void unregisterClauseFunction(String id)
	{
		clauseFunctions.remove(id);
	}
	
	/**
	 * Resolves a clause function ID to a function instance.
	 * 
	 * @param id the function ID
	 * @return the clause function registered for the ID
	 * @throws JRRuntimeException if no function for the ID is found
	 */
	protected JRClauseFunction resolveFunction(String id)
	{
		JRClauseFunction function = (JRClauseFunction) clauseFunctions.get(id);
		if (function == null)
		{
			throw new JRRuntimeException("No clause function for id " + id + " found");
		}
		return function;
	}
	
	/**
	 * Parses the query and replaces the parameter clauses by the paramter values and
	 * the parameters by the return value of {@link #getParameterReplacement(String) getParameterReplacement}.
	 *
	 */
	protected void parseQuery()
	{
		parameterClauseStack = new HashSet();
		
		JRQuery query = dataset.getQuery();
		
		if (query != null)
		{
			JRQueryChunk[] chunks = query.getChunks();
			if (chunks != null && chunks.length > 0)
			{
				StringBuffer sbuffer = new StringBuffer();
				for(int i = 0; i < chunks.length; i++)
				{
					JRQueryChunk chunk = chunks[i];
					appendQueryChunk(sbuffer, chunk);
				}

				queryString = sbuffer.toString();
			}
		}
	}


	protected void appendQueryChunk(StringBuffer sbuffer, JRQueryChunk chunk)
	{
		switch (chunk.getType())
		{
			case JRQueryChunk.TYPE_PARAMETER_CLAUSE :
			{
				appendParameterClauseChunk(sbuffer, chunk.getText());
				break;
			}
			case JRQueryChunk.TYPE_PARAMETER :
			{
				appendParameterChunk(sbuffer, chunk.getText());
				break;
			}
			case JRQueryChunk.TYPE_CLAUSE_TOKENS :
			{
				appendClauseChunk(sbuffer, chunk.getTokens());
				break;
			}
			case JRQueryChunk.TYPE_TEXT :
			default :
			{
				appendTextChunk(sbuffer, chunk.getText());
				break;
			}
		}
	}


	protected void appendTextChunk(StringBuffer sbuffer, String text)
	{
		sbuffer.append(text);
	}


	protected void appendParameterChunk(StringBuffer sbuffer, String chunkText)
	{
		String parameterName = chunkText;
		checkParameter(parameterName);

		sbuffer.append(getParameterReplacement(parameterName));
		addQueryParameter(chunkText);
	}


	/**
	 * Records a query parameter.
	 * 
	 * @param parameterName the parameter name
	 * @see #getCollectedParameters()
	 */
	protected void addQueryParameter(String parameterName)
	{
		QueryParameter param = new QueryParameter(parameterName);
		queryParameters.add(param);
	}


	/**
	 * Records a multi-valued query parameter.
	 * 
	 * @param parameterName the parameter name
	 * @param count the value count
	 * @see #getCollectedParameters()
	 * @see QueryParameter#isMulti()
	 */
	protected void addQueryMultiParameters(String parameterName, int count)
	{
		QueryParameter param = new QueryParameter(parameterName, count);
		queryParameters.add(param);
	}


	protected void appendParameterClauseChunk(final StringBuffer sbuffer, String chunkText)
	{
		String parameterName = chunkText;
		checkParameter(parameterName);
		
		if (!parameterClauseStack.add(parameterName))
		{
			throw new JRRuntimeException("The query contains circularly nested parameter clauses starting with " + parameterName);
		}
		
		try
		{
			Object parameterValue = getParameterValue(parameterName);
			String clauseText = String.valueOf(parameterValue);
			JRQueryChunkHandler nestedChunkHandler = new JRQueryChunkHandler()
			{
				public void handleParameterChunk(String text)
				{
					appendParameterChunk(sbuffer, text);
				}

				public void handleParameterClauseChunk(String text)
				{
					appendParameterClauseChunk(sbuffer, text);
				}

				public void handleTextChunk(String text)
				{
					appendTextChunk(sbuffer, text);
				}

				public void handleClauseChunk(String[] tokens)
				{
					appendClauseChunk(sbuffer, tokens);
				}
			};
			JRQueryParser.instance().parse(clauseText, nestedChunkHandler);
		}
		finally
		{
			parameterClauseStack.remove(parameterName);
		}
	}


	/**
	 * Handles a {@link JRQueryChunk#TYPE_CLAUSE_TOKENS clause query chunk}.
	 * <p>
	 * The default implementation considers the first token as a
	 * {@link JRClauseFunction clause function} ID and delegates the call to the
	 * function.
	 * </p>
	 * <p>
	 * Extending query executers can override this to implement custom query clause handling.
	 * </p>
	 * 
	 * @param sbuffer the query text buffer
	 * @param clauseTokens clause tokens
	 * @see #registerClauseFunction(String, JRClauseFunction)
	 * @throws JRRuntimeException if there is no first token or no clause function is found for the ID
	 */
	protected void appendClauseChunk(final StringBuffer sbuffer, String[] clauseTokens)
	{
		JRClauseTokens tokens = new JRClauseTokens(clauseTokens);
		String id = tokens.getToken(CLAUSE_POSITION_ID);
		if (id == null)
		{
			throw new JRRuntimeException("Query clause ID/first token missing");
		}
		
		JRClauseFunction function = resolveFunction(id);
		applyClause(function, tokens, sbuffer);
	}

	
	protected void applyClause(JRClauseFunction function, JRClauseTokens tokens, final StringBuffer sbuffer)
	{
		function.apply(tokens, new JRQueryClauseContext()
		{
			public void addQueryMultiParameters(String parameterName, int count)
			{
				JRAbstractQueryExecuter.this.addQueryMultiParameters(parameterName, count);
			}

			public void addQueryParameter(String parameterName)
			{
				JRAbstractQueryExecuter.this.addQueryParameter(parameterName);
			}

			public JRValueParameter getValueParameter(String parameterName)
			{
				return JRAbstractQueryExecuter.this.getValueParameter(parameterName);
			}

			public StringBuffer queryBuffer()
			{
				return sbuffer;
			}
		});
	}

	
	/**
	 * Returns the parsed query string with the paramter clauses replaced by the paramter values and 
	 * the parameters replaced by {@link #getParameterReplacement(String) getParameterReplacement}.
	 * 
	 * @return the parsed query string
	 */
	protected String getQueryString()
	{
		return queryString;
	}
	
	
	/**
	 * Returns the list of parameter names in the order in which they appear in the query.
	 * 
	 * @return the list of parameter names
	 */
	protected List getCollectedParameterNames()
	{
		List parameterNames = new ArrayList(queryParameters.size());
		for (Iterator it = queryParameters.iterator(); it.hasNext();)
		{
			QueryParameter param = (QueryParameter) it.next();
			parameterNames.add(param.getName());
		}
		return parameterNames;
	}
	
	
	/**
	 * Returns the list of {@link QueryParameter query parameters} in the order in which they appear in the query.
	 * 
	 * @return the list of query parameters
	 */
	protected List getCollectedParameters()
	{
		return queryParameters;
	}
	
	
	/**
	 * Returns the value of a fill paramter.
	 * @param parameterName the paramter name
	 * @param ignoreMissing if <code>true</code>, the method will return null for non existing parameters;
	 * 		otherwise, an exception will be thrown if the parameter does not exist
	 * @return the parameter value
	 */
	protected Object getParameterValue(String parameterName, boolean ignoreMissing)
	{
		if (ignoreMissing)
		{
			JRValueParameter parameter = getValueParameter(JRParameter.REPORT_PARAMETERS_MAP, false);
			return ((Map)parameter.getValue()).get(parameterName);
		}
		
		JRValueParameter parameter = getValueParameter(parameterName, ignoreMissing);
		return parameter == null ? null : parameter.getValue();
	}
	
	
	/**
	 * Returns the value of a fill paramter.
	 * @param parameterName the paramter name
	 * @return the parameter value
	 */
	protected Object getParameterValue(String parameterName)
	{
		return getParameterValue(parameterName, false);
	}
	
	
	/**
	 * Return a fill parameter from the paramter map.
	 * 
	 * @param parameterName the paramter name
	 * @return the parameter
	 * @deprecated {@link #getValueParameter(String) getValueParameter(String)} should be used instead
	 */
	protected JRFillParameter getParameter(String parameterName)
	{
		JRFillParameter parameter = (JRFillParameter) parametersMap.get(parameterName);
		
		if (parameter == null)
		{
			throw new JRRuntimeException("Parameter \"" + parameterName + "\" does not exist.");
		}
		
		return parameter;
	}

	
	protected void checkParameter(String parameterName)
	{
		if (!parametersMap.containsKey(parameterName))
		{
			throw new JRRuntimeException("Parameter \"" + parameterName + "\" does not exist.");
		}
	}
	
	
	/**
	 * Return a value parameter from the paramters map.
	 * 
	 * @param parameterName the paramter name
	 * @param ignoreMissing if <code>true</code>, the method will return null for non existing parameters;
	 * 		otherwise, an exception will be thrown if the parameter does not exist
	 * @return the parameter
	 */
	protected JRValueParameter getValueParameter(String parameterName, boolean ignoreMissing)
	{
		JRValueParameter parameter = (JRValueParameter) parametersMap.get(parameterName);
		
		if (parameter == null && !ignoreMissing)
		{
			throw new JRRuntimeException("Parameter \"" + parameterName + "\" does not exist.");
		}
		
		return parameter;
	}

	
	/**
	 * Return a value parameter from the parameters map.
	 * 
	 * @param parameterName the parameter name
	 * @return the parameter
	 */
	protected JRValueParameter getValueParameter(String parameterName)
	{
		return getValueParameter(parameterName, false);
	}

	
	/**
	 * Returns the replacement text for a query paramter.
	 * 
	 * @param parameterName the paramter name
	 * @return the replacement text
	 * @see JRQueryChunk#TYPE_PARAMETER
	 */
	protected abstract String getParameterReplacement(String parameterName);
}
