/*******************************************************************************
 * Copyright (c) 2006, 2014 Oracle and/or its affiliates. All rights reserved.
 * This program and the accompanying materials are made available under the
 * terms of the Eclipse protected License v1.0 and Eclipse Distribution License v. 1.0
 * which accompanies this distribution.
 * The Eclipse protected License is available at http://www.eclipse.org/legal/epl-v10.html
 * and the Eclipse Distribution License is available at
 * http://www.eclipse.org/org/documents/edl-v10.php.
 *
 * Contributors:
 *     Oracle - initial API and implementation
 *
 ******************************************************************************/
package org.eclipse.persistence.jpa.jpql.tools;

import java.util.Collection;
import java.util.Comparator;
import java.util.LinkedList;
import java.util.List;
import java.util.TreeSet;
import org.eclipse.persistence.jpa.jpql.AbstractGrammarValidator;
import org.eclipse.persistence.jpa.jpql.AbstractSemanticValidator;
import org.eclipse.persistence.jpa.jpql.Assert;
import org.eclipse.persistence.jpa.jpql.JPQLQueryProblem;
import org.eclipse.persistence.jpa.jpql.parser.Expression;
import org.eclipse.persistence.jpa.jpql.parser.InputParameter;
import org.eclipse.persistence.jpa.jpql.parser.JPQLExpression;
import org.eclipse.persistence.jpa.jpql.parser.JPQLGrammar;
import org.eclipse.persistence.jpa.jpql.tools.spi.IManagedTypeProvider;
import org.eclipse.persistence.jpa.jpql.tools.spi.IQuery;
import org.eclipse.persistence.jpa.jpql.tools.spi.IType;
import org.eclipse.persistence.jpa.jpql.tools.spi.ITypeRepository;

/**
 * This helper can perform the following operations over a JPQL query:
 * <ul>
 * <li>Calculates the result type of a query: {@link #getResultType()};</li>
 * <li>Calculates the type of an input parameter: {@link #getParameterType(String)}.</li>
 * <li>Calculates the possible choices to complete the query from a given
 *     position (used for content assist): {@link #buildContentAssistProposals(int)}.</li>
 * <li>Validates the query by introspecting it grammatically and semantically:
 *     <ul>
 *     <li>{@link #validate()},</li>
 *     <li>{@link #validateGrammar()},</li>
 *     <li>{@link #validateSemantic()}.</li>
 *     </ul>
 * </li>
 * <li>Refactoring support:
 *     <ul>
 *     <li>{@link #buildBasicRefactoringTool()} provides support for generating the delta of the
 *     refactoring operation through a collection of {@link TextEdit} objects.</li>
 *     <li>{@link #buildRefactoringTool()} provides support for refactoring the JPQL query through
 *     the editable {@link org.eclipse.persistence.jpa.jpql.tools.model.query.StateObject StateObject}
 *     and once all refactoring operations have been executed, the {@link
 *     org.eclipse.persistence.jpa.jpql.tools.model.IJPQLQueryFormatter IJPQLQueryFormatter} will
 *     generate a new string representation of the JPQL query.</li>
 *     </ul>
 * </li>
 * </ul>
 * <p>
 * Provisional API: This interface is part of an interim API that is still under development and
 * expected to change significantly before reaching stability. It is available at this early stage
 * to solicit feedback from pioneering adopters on the understanding that any code that uses this
 * API will almost certainly be broken (repeatedly) as the API evolves.<p>
 *
 * @version 2.5
 * @since 2.3
 * @author Pascal Filion
 */
@SuppressWarnings("nls")
public abstract class AbstractJPQLQueryHelper {

	/**
	 * This visitor is responsible to gather the possible proposals based on the position of the
	 * caret within the JPQL query.
	 */
	private AbstractContentAssistVisitor contentAssistVisitor;

	/**
	 * This visitor is responsible to visit the entire parsed tree representation of the JPQL query
	 * and to validate its content based on the JPQL grammar.
	 */
	private AbstractGrammarValidator grammarValidator;

	/**
	 * The {@link JPQLGrammar} that will determine how to parse JPQL queries.
	 */
	private JPQLGrammar jpqlGrammar;

	/**
	 * The context used to query information about the JPQL query.
	 */
	private JPQLQueryContext queryContext;

	/**
	 * This visitor is responsible to visit the entire parsed tree representation of the JPQL query
	 * and to validate the semantic of the information.
	 */
	private AbstractSemanticValidator semanticValidator;

	/**
	 * Creates a new <code>AbstractJPQLQueryHelper</code>.
	 *
	 * @param jpqlGrammar The {@link JPQLGrammar} that will determine how to parse JPQL queries
	 */
	public AbstractJPQLQueryHelper(JPQLGrammar jpqlGrammar) {
		super();
		Assert.isNotNull(jpqlGrammar, "The JPQLGrammar cannot be null");
		this.jpqlGrammar = jpqlGrammar;
	}

	/**
	 * Creates a new <code>AbstractJPQLQueryHelper</code>.
	 *
	 * @param queryContext The context used to query information about the JPQL query
	 * @exception NullPointerException The JPQLQueryContext cannot be <code>null</code>
	 */
	protected AbstractJPQLQueryHelper(JPQLQueryContext queryContext) {
		super();

		Assert.isNotNull(queryContext, "The JPQLQueryContext cannot be null");

		this.queryContext = queryContext;
		this.jpqlGrammar  = queryContext.getGrammar();
	}

	/**
	 * Creates the concrete instance of the tool that can refactor the content of a JPQL query. This
	 * version simply provides the delta of the refactoring operations.
	 *
	 * @return The concrete instance of {@link RefactoringTool}
	 * @see #buildRefactoringTool
	 * @since 2.5
	 */
	public abstract BasicRefactoringTool buildBasicRefactoringTool();

	/**
	 * Retrieves the possibles choices that can complete the query from the given position within
	 * the query.
	 * <p>
	 * <b>Note:</b> Disposing of the internal data is not done automatically.
	 *
	 * @param position The position within the query for which a list of possible choices are created
	 * for completing the query
	 * @return The list of valid proposals regrouped by categories
	 */
	public ContentAssistProposals buildContentAssistProposals(int position) {
		return buildContentAssistProposals(position, ContentAssistExtension.NULL_HELPER);
	}

	/**
	 * Retrieves the possibles choices that can complete the query from the given position within
	 * the query.
	 * <p>
	 * <b>Note:</b> Disposing of the internal data is not done automatically.
	 *
	 * @param position The position within the query for which a list of possible choices are created
	 * for completing the query
	 * @param extension This extension can be used to provide additional information that is outside
	 * the scope of simply providing JPA metadata information, such as table names, column names,
	 * class names
	 * @return The list of valid proposals regrouped by categories
	 * @since 2.5
	 */
	public ContentAssistProposals buildContentAssistProposals(int position, ContentAssistExtension extension) {
		return getContentAssistVisitor().buildProposals(position, extension);
	}

	/**
	 * Creates the concrete instance of the content assist visitor that will give the possible
	 * choices based on the position of the cursor within the JPQL query.
	 *
	 * @param queryContext The context used to query information about the JPQL query
	 * @return A new concrete instance of {@link AbstractContentAssistVisitor}
	 */
	protected abstract AbstractContentAssistVisitor buildContentAssistVisitor(JPQLQueryContext queryContext);

	/**
	 * Creates the concrete instance of the validator that will grammatically validate the JPQL query.
	 *
	 * @param jpqlGrammar The context used to query information about the JPQL query
	 * @return A new concrete instance of {@link AbstractGrammarValidator}
	 */
	protected abstract AbstractGrammarValidator buildGrammarValidator(JPQLGrammar jpqlGrammar);

	/**
	 * Creates a context that will be used to store and retrieve information about the JPQL query.
	 *
	 * @param jpqlGrammar The JPQL grammar that is required for dictating how the JPQL query will be
	 * parsed. It is also used by validation and by the content assist
	 * @return A new {@link JPQLQueryContext}
	 */
	protected abstract JPQLQueryContext buildJPQLQueryContext(JPQLGrammar jpqlGrammar);

	/**
	 * Creates the {@link Comparator} that can sort {@link IType ITypes} based on the numerical
	 * priority.
	 *
	 * @return {@link NumericTypeComparator}
	 */
	protected Comparator<IType> buildNumericTypeComparator() {
		return new NumericTypeComparator(getTypeHelper());
	}

	/**
	 * Creates the concrete instance of the tool that can refactor the content of a JPQL query. This
	 * version provides a way to manipulate the editable version of the JPQL query ({@link
     * org.eclipse.persistence.jpa.jpql.tools.model.query.StateObject StateObject} and simply
     * outputs the result of the refactoring operations, i.e. the updated JPQL query).
	 *
	 * @return The concrete instance of {@link RefactoringTool}
	 * @see #buildBasicRefactoringTool()
	 * @since 2.4
	 */
	public abstract RefactoringTool buildRefactoringTool();

	/**
	 * Creates the concrete instance of the validator that will semantically validate the JPQL query.
	 *
	 * @param queryContext The context used to query information about the JPQL query
	 * @return A new concrete instance of {@link AbstractSemanticValidator}
	 */
	protected abstract AbstractSemanticValidator buildSemanticValidator(JPQLQueryContext queryContext);

	/**
	 * Disposes of the internal data.
	 */
	public void dispose() {

		if (queryContext != null) {
			queryContext.dispose();
		}

		// Not required but during debugging, this is important to be reset
		if (contentAssistVisitor != null) {
			contentAssistVisitor.dispose();
		}
	}

	/**
	 * Returns the visitor that can visit a JPQL query and based on the position of the cursor within
	 * the JPQL query and determines the valid proposals.
	 *
	 * @return A concrete instance of {@link AbstractContentAssistVisitor}
	 * @see #buildContentAssistVisitor(JPQLQueryContext)
	 */
	protected AbstractContentAssistVisitor getContentAssistVisitor() {
		if (contentAssistVisitor == null) {
			contentAssistVisitor = buildContentAssistVisitor(getQueryContext());
		}
		return contentAssistVisitor;
	}

	/**
	 * Returns the JPQL grammar that will be used to define how to parse a JPQL query.
	 *
	 * @return The grammar that was used to parse this {@link Expression}
	 * @since 2.4
	 */
	public JPQLGrammar getGrammar() {
		return jpqlGrammar;
	}

	protected AbstractGrammarValidator getGrammarValidator() {
		if (grammarValidator == null) {
			grammarValidator = buildGrammarValidator(jpqlGrammar);
		}
		return grammarValidator;
	}

	/**
	 * Returns the root of the parsed tree representation of the JPQL query.
	 *
	 * @return The parsed JPQL query
	 */
	public JPQLExpression getJPQLExpression() {
		return getQueryContext().getJPQLExpression();
	}

	/**
	 * Retrieves, if it can be determined, the type of the given input parameter with the given name.
	 * The type will be guessed based on its location within expression.
	 * <p>
	 * Note: Both named and positional input parameter can be used.
	 *
	 * @param parameterName The name of the input parameter to retrieve its type, which needs to be
	 * prepended by ':' or '?'
	 * @return Either the closest type of the input parameter or <code>null</code> if the type
	 * couldn't be determined
	 */
	public IType getParameterType(String parameterName) {

		// Retrieve the input parameter's qualifier (':' or '?')
		char character = parameterName.length() > 0 ? parameterName.charAt(0) : '\0';

		// Does not begin with either ':' or '?'
		if ((character != ':') && (character != '?')) {
			return getTypeHelper().objectType();
		}

		// Find the InputParameters with the given parameter name
		Collection<InputParameter> inputParameters = getQueryContext().findInputParameters(parameterName);

		// No InputParameter was found
		if (inputParameters.isEmpty()) {
			return getTypeHelper().objectType();
		}

		// Now find the closest type for each location
		TreeSet<IType> types = new TreeSet<IType>(buildNumericTypeComparator());

		for (InputParameter inputParameter : inputParameters) {
			IType type = queryContext.getParameterType(inputParameter);

			// A type is ignored if it cannot be determined and it can't affect the calculation
			// if the same input parameter is used elsewhere. Example:
			// SELECT e FROM Employee e WHERE :name IS NOT NULL AND e.name = 'JPQL'
			// The first :name cannot be used to calculate the type
			if (type.isResolvable()) {
				types.add(type);
			}
		}

		return types.isEmpty() ? getTypeHelper().objectType() : types.first();
	}

	/**
	 * Returns the string representation of the parsed tree.
	 *
	 * @return The string created from the parsed tree representation of the original JPQL query
	 */
	public String getParsedJPQLQuery() {
		return getJPQLExpression().toParsedText();
	}

	/**
	 * Returns the provider for managed types (entities, embeddables, mapped superclasses).
	 *
	 * @return The container of managed types
	 */
	public IManagedTypeProvider getProvider() {
		return getQuery().getProvider();
	}

	/**
	 * Returns the external form representing a named query.
	 *
	 * @return The external form representing a named query
	 */
	public IQuery getQuery() {
		return getQueryContext().getQuery();
	}

	/**
	 * Returns the {@link JPQLQueryContext} that contains information about the JPQL query.
	 *
	 * @return The {@link JPQLQueryContext} that contains information contained in the JPQL query
	 */
	public JPQLQueryContext getQueryContext() {
		if (queryContext == null) {
			queryContext = buildJPQLQueryContext(jpqlGrammar);
		}
		return queryContext;
	}

	/**
	 * Calculates the type of the query result of the JPQL query.
	 * <p>
	 * See {@link org.eclipse.persistence.jpa.jpql.tools.resolver.Resolver Resolver}
     * to understand how the type is calculated.
	 *
	 * @return The result type of the JPQL query if it could accurately be calculated or the
	 * {@link IType} for <code>Object</code> if it could not be calculated
	 */
	public IType getResultType() {

		IType type = queryContext.getType(getJPQLExpression());

		if (!type.isResolvable()) {
			type = getTypeHelper().objectType();
		}

		return type;
	}

	protected AbstractSemanticValidator getSemanticValidator() {
		if (semanticValidator == null) {
			semanticValidator = buildSemanticValidator(getQueryContext());
		}
		return semanticValidator;
	}

	/**
	 * Returns the {@link IType} representing the given Java type.
	 *
	 * @param type The Java type for which its external form is requested
	 * @return The external form for the given Java type
	 */
	public IType getType(Class<?> type) {
		return getTypeRepository().getType(type);
	}

	/**
	 * Returns a helper that gives access to the most common {@link IType types}.
	 *
	 * @return A helper containing a collection of methods related to {@link IType}
	 */
	public TypeHelper getTypeHelper() {
		return getTypeRepository().getTypeHelper();
	}

	/**
	 * Returns the repository that gives access to the application's types.
	 *
	 * @return The repository for classes, interfaces, enum types and annotations
	 */
	public ITypeRepository getTypeRepository() {
		return getProvider().getTypeRepository();
	}

	/**
	 * Sets the parsed tree representation of the JPQL query. If the expression was parsed outside of
	 * the scope of this context, then this method has to be invoked before {@link #setQuery(IQuery)}
	 * because the JPQL query is automatically parsed by that method.
	 *
	 * @param jpqlExpression The parsed representation of the JPQL query to manipulate
	 * @see #setQuery(IQuery)
	 */
	public void setJPQLExpression(JPQLExpression jpqlExpression) {
		getQueryContext().setJPQLExpression(jpqlExpression);
	}

	/**
	 * Sets the external form of the JPQL query, which will be parsed and information will be
	 * extracted for later access.
	 *
	 * @param query The external form of the JPQL query
	 */
	public void setQuery(IQuery query) {
		getQueryContext().setQuery(query);
	}

	/**
	 * Validates the query by introspecting it grammatically and semantically.
	 *
	 * @return The non-<code>null</code> list that will be used to store the {@link JPQLQueryProblem
	 * problems} if any was found
	 */
	public List<JPQLQueryProblem> validate() {
		List<JPQLQueryProblem> problems = new LinkedList<JPQLQueryProblem>();
		validate(getJPQLExpression(), problems);
		return problems;
	}

	/**
	 * Validates the query by introspecting it grammatically and semantically.
	 *
	 * @param expression The parsed tree representation of the JPQL fragment to validate
	 * @param problems A non-<code>null</code> list that will be used to store the {@link
	 * JPQLQueryProblem problems} if any was found
	 */
	public void validate(Expression expression, List<JPQLQueryProblem> problems) {
		validateGrammar(expression, problems);
		validateSemantic(expression, problems);
	}

	/**
	 * Validates the query by only introspecting it grammatically.
	 *
	 * @return The non-<code>null</code> list that will be used to store the {@link JPQLQueryProblem
	 * problems} if any was found
	 */
	public List<JPQLQueryProblem> validateGrammar() {
		List<JPQLQueryProblem> problems = new LinkedList<JPQLQueryProblem>();
		validateGrammar(getJPQLExpression(), problems);
		return problems;
	}

	/**
	 * Validates the query by only introspecting it grammatically.
	 *
	 * @param expression The parsed tree representation of the query
	 * @param problems A non-<code>null</code> list that will be used to store the {@link
	 * JPQLQueryProblem problems} if any was found
	 */
	public void validateGrammar(Expression expression, List<JPQLQueryProblem> problems) {
		AbstractGrammarValidator visitor = getGrammarValidator();
		try {
			visitor.setProblems(problems);
			expression.accept(visitor);
		}
		finally {
			visitor.dispose();
		}
	}

	/**
	 * Validates the query by only introspecting it semantically.
	 *
	 * @return The non-<code>null</code> list that will be used to store the {@link JPQLQueryProblem
	 * problems} if any was found
	 */
	public List<JPQLQueryProblem> validateSemantic() {
		List<JPQLQueryProblem> problems = new LinkedList<JPQLQueryProblem>();
		validateSemantic(getJPQLExpression(), problems);
		return problems;
	}

	/**
	 * Validates the query by only introspecting it semantically.
	 *
	 * @param expression The parsed tree representation of the query
	 * @param problems A non-<code>null</code> list that will be used to store the {@link
	 * JPQLQueryProblem problems} if any was found
	 */
	public void validateSemantic(Expression expression, List<JPQLQueryProblem> problems) {
		AbstractSemanticValidator visitor = getSemanticValidator();
		try {
			visitor.setProblems(problems);
			expression.accept(visitor);
		}
		finally {
			visitor.dispose();
		}
	}
}