/*******************************************************************************
 * 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 Public License v1.0 and Eclipse Distribution License v. 1.0
 * which accompanies this distribution.
 * The Eclipse Public 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;

import org.eclipse.persistence.jpa.jpql.parser.AbstractEclipseLinkExpressionVisitor;
import org.eclipse.persistence.jpa.jpql.parser.AnonymousExpressionVisitor;
import org.eclipse.persistence.jpa.jpql.parser.AsOfClause;
import org.eclipse.persistence.jpa.jpql.parser.BadExpression;
import org.eclipse.persistence.jpa.jpql.parser.CastExpression;
import org.eclipse.persistence.jpa.jpql.parser.CollectionExpression;
import org.eclipse.persistence.jpa.jpql.parser.CollectionValuedPathExpression;
import org.eclipse.persistence.jpa.jpql.parser.ConnectByClause;
import org.eclipse.persistence.jpa.jpql.parser.DatabaseType;
import org.eclipse.persistence.jpa.jpql.parser.EclipseLinkExpressionVisitor;
import org.eclipse.persistence.jpa.jpql.parser.Expression;
import org.eclipse.persistence.jpa.jpql.parser.ExtractExpression;
import org.eclipse.persistence.jpa.jpql.parser.FunctionExpression;
import org.eclipse.persistence.jpa.jpql.parser.HierarchicalQueryClause;
import org.eclipse.persistence.jpa.jpql.parser.InExpression;
import org.eclipse.persistence.jpa.jpql.parser.NullExpression;
import org.eclipse.persistence.jpa.jpql.parser.OrderSiblingsByClause;
import org.eclipse.persistence.jpa.jpql.parser.RangeVariableDeclaration;
import org.eclipse.persistence.jpa.jpql.parser.RegexpExpression;
import org.eclipse.persistence.jpa.jpql.parser.SimpleSelectClause;
import org.eclipse.persistence.jpa.jpql.parser.SimpleSelectStatement;
import org.eclipse.persistence.jpa.jpql.parser.StartWithClause;
import org.eclipse.persistence.jpa.jpql.parser.StateFieldPathExpression;
import org.eclipse.persistence.jpa.jpql.parser.TableExpression;
import org.eclipse.persistence.jpa.jpql.parser.TableVariableDeclaration;
import org.eclipse.persistence.jpa.jpql.parser.UnionClause;

import static org.eclipse.persistence.jpa.jpql.JPQLQueryProblemMessages.*;

/**
 * This validator is responsible to gather the problems found in a JPQL query by validating the
 * content to make sure it is semantically valid for EclipseLink. The grammar is not validated by
 * this visitor.
 * <p>
 * For instance, the function <b>AVG</b> accepts a state field path. The property it represents has
 * to be of numeric type. <b>AVG(e.name)</b> is parsable but is not semantically valid because the
 * type of name is a string (the property signature is: "<code>private String name</code>").
 * <p>
 * <b>Note:</b> EclipseLink does not validate types, but leaves it to the database. This is because
 * some databases such as Oracle allow different types to different functions and perform implicit
 * type conversion. i.e. <code>CONCAT('test', 2)</code> returns <code>'test2'</code>. Also the
 * <b>FUNC</b> function has an unknown type, so should be allowed with any function.
 * <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.
 *
 * @see EclipseLinkGrammarValidator
 *
 * @version 2.5.1
 * @since 2.4
 * @author Pascal Filion
 */
public class AbstractEclipseLinkSemanticValidator extends AbstractSemanticValidator
                                                  implements EclipseLinkExpressionVisitor {

	/**
	 * The following extension can be used to give access to non-JPA metadata artifacts, such as
	 * database tables and columns.
	 */
	private EclipseLinkSemanticValidatorExtension extension;

	/**
	 * This visitor calculates the number of items the subquery <code><b>SELECT</b></code> clause has.
	 */
	private SubquerySelectItemCalculator subquerySelectItemCalculator;

	/**
	 * This visitor determines if the {@link Expression} being visited is a {@link TableExpression}.
	 */
	private TableExpressionVisitor tableExpressionVisitor;

	/**
	 * Creates a new <code>AbstractEclipseLinkSemanticValidator</code>.
	 *
	 * @param helper The given helper allows this validator to access the JPA artifacts without using
	 * Hermes SPI
	 * @param extension The following extension can be used to give access to non-JPA metadata
	 * artifacts, such as database tables and columns
	 * @exception NullPointerException The given {@link SemanticValidatorHelper} cannot be <code>null</code>
	 */
	protected AbstractEclipseLinkSemanticValidator(SemanticValidatorHelper helper,
	                                               EclipseLinkSemanticValidatorExtension extension) {

		super(helper);
		this.extension = extension;
	}

	/**
	 * {@inheritDoc}
	 */
	@Override
	protected LiteralVisitor buildLiteralVisitor() {
		return new EclipseLinkLiteralVisitor();
	}

	/**
	 * {@inheritDoc}
	 */
	@Override
	protected OwningClauseVisitor buildOwningClauseVisitor() {
		return new EclipseLinkOwningClauseVisitor();
	}

	protected SubquerySelectItemCalculator buildSubquerySelectItemCalculator() {
		return new SubquerySelectItemCalculator();
	}

	protected TableExpressionVisitor buildTableExpressionVisitor() {
		return new TableExpressionVisitor();
	}

	/**
	 * {@inheritDoc}
	 */
	@Override
	protected TopLevelFirstDeclarationVisitor buildTopLevelFirstDeclarationVisitor() {
		return new TopLevelFirstDeclarationVisitor(this);
	}

	protected JPQLQueryDeclaration getDeclaration(String variableName) {

		for (JPQLQueryDeclaration declaration : helper.getAllDeclarations()) {

			if (declaration.getVariableName().equalsIgnoreCase(variableName)) {
				return declaration;
			}
		}

		return null;
	}

	/**
	 * Returns the extension that gives access to non-JPA metadata artifacts, such as database tables
	 * and columns.
	 *
	 * @return An extension giving access to non-JPA metadata artifacts or {@link
	 * EclipseLinkSemanticValidatorExtension#NULL_EXTENSION} if no extension was provided
	 */
	protected EclipseLinkSemanticValidatorExtension getExtension() {
		return extension;
	}

	protected SubquerySelectItemCalculator getSubquerySelectItemCalculator() {
		if (subquerySelectItemCalculator == null) {
			subquerySelectItemCalculator = buildSubquerySelectItemCalculator();
		}
		return subquerySelectItemCalculator;
	}

	protected TableExpressionVisitor getTableExpressionVisitor() {
		if (tableExpressionVisitor == null) {
			tableExpressionVisitor = buildTableExpressionVisitor();
		}
		return tableExpressionVisitor;
	}

	protected boolean isTableExpression(Expression expression) {
		TableExpressionVisitor visitor = getTableExpressionVisitor();
		try {
			visitor.expression = expression;
			expression.accept(visitor);
			return visitor.valid;
		}
		finally {
			visitor.valid = false;
			visitor.expression = null;
		}
	}

	/**
	 * {@inheritDoc}
	 */
	@Override
	protected PathType selectClausePathExpressionPathType() {
		return PathType.ANY_FIELD_INCLUDING_COLLECTION;
	}

	/**
	 * Retrieves the number of select items the given subquery has.
	 *
	 * @param subquery The {@link Expression} to visit, which should represents a subquery
	 * @return The number of select items or 0 if the subquery is malformed or incomplete
	 */
	protected int subquerySelectItemCount(Expression subquery) {
		SubquerySelectItemCalculator visitor = getSubquerySelectItemCalculator();
		try {
			visitor.count = 0;
			subquery.accept(visitor);
			return visitor.count;
		}
		finally {
			visitor.count = 0;
		}
	}

	/**
	 * {@inheritDoc}
	 */
	@Override
	protected void validateFunctionExpression(FunctionExpression expression) {
		super.validateFunctionExpression(expression);

		// No need to validate if no extension was provided
		// Validate the column name
		if (extension != EclipseLinkSemanticValidatorExtension.NULL_EXTENSION &&
		   (expression.getIdentifier() == Expression.COLUMN) &&
		    expression.hasExpression()) {

			String columnName = expression.getUnquotedFunctionName();
			String variableName = literal(expression.getExpression(), LiteralType.IDENTIFICATION_VARIABLE);

			if (ExpressionTools.stringIsNotEmpty(variableName) &&
			    ExpressionTools.stringIsNotEmpty(columnName)) {

				// Retrieve the declaration associated with the identification variable
				JPQLQueryDeclaration declaration = getDeclaration(variableName);

				// Only a range variable declaration can be used
				if (declaration.getType().isRange()) {
					String entityName = literal(declaration.getBaseExpression(), LiteralType.ABSTRACT_SCHEMA_NAME);

					if (entityName != ExpressionTools.EMPTY_STRING) {

						// Retrieve the primary table of the entity
						String tableName = extension.getEntityTable(entityName);

						// The column is not on the primary table
						if (ExpressionTools.stringIsNotEmpty(tableName) &&
						    !extension.columnExists(tableName, expression.getUnquotedFunctionName())) {

							int startPosition = position(expression) +
							                    Expression.COLUMN.length() +
							                    (expression.hasLeftParenthesis() ? 1 : 0);

							int endPosition = startPosition +
							                  expression.getFunctionName().length();

							addProblem(
								expression,
								startPosition,
								endPosition,
								FunctionExpression_UnknownColumn,
								columnName,
								tableName
							);
						}
					}
				}
			}
		}
	}

	/**
	 * {@inheritDoc}
	 */
	@Override
	protected void validateInExpression(InExpression expression) {
		super.validateInExpression(expression);

		// Make sure the number of items matches if the left expression
		// is a nested array and the IN items expression is a subquery
		Expression inItems = expression.getInItems();

		if (isSubquery(inItems)) {
			int nestedArraySize = nestedArraySize(expression.getExpression());
			int subquerySelectItemsSize = subquerySelectItemCount(inItems);

			if ((nestedArraySize  > -1) && (subquerySelectItemsSize != nestedArraySize) ||
			    (nestedArraySize == -1) && (subquerySelectItemsSize  > 1)) {

				addProblem(expression, InExpression_InvalidItemCount);
			}
		}
	}

	/**
	 * {@inheritDoc}
	 */
	@Override
	protected void validateRangeVariableDeclarationRootObject(RangeVariableDeclaration expression) {

		Expression rootObject = expression.getRootObject();

		// Special case, the path expression could be a fully qualified class name,
		// make sure to not validate it as collection-valued path expression
		CollectionValuedPathExpression pathExpression = getCollectionValuedPathExpression(rootObject);

		if (pathExpression != null) {
			String path = pathExpression.toActualText();

			// The path expression is not a fully qualified class name
			if (helper.getType(path) == null) {
				pathExpression.accept(this);
			}
		}
		else {
			rootObject.accept(this);
		}
	}

	/**
	 * {@inheritDoc}
	 */
	@Override
	protected PathType validPathExpressionTypeForCountFunction() {
		return PathType.ANY_FIELD_INCLUDING_COLLECTION;
	}

	/**
	 * {@inheritDoc}
	 */
	@Override
	protected PathType validPathExpressionTypeForInExpression() {
		// Loosen up the JPA spec restriction because ANTLR parser used to allow it
		return PathType.ANY_FIELD_INCLUDING_COLLECTION;
	}

	/**
	 * {@inheritDoc}
	 */
	@Override
	protected PathType validPathExpressionTypeForInItem() {
		return PathType.ANY_FIELD_INCLUDING_COLLECTION;
	}

	/**
	 * {@inheritDoc}
	 */
	@Override
	protected Boolean validateThirdPartyStateFieldPathExpression(StateFieldPathExpression expression) {

		Boolean valid = null;

		// Retrieve the identification variable
		String variableName = literal(expression.getIdentificationVariable(), LiteralType.IDENTIFICATION_VARIABLE);

		if (variableName != ExpressionTools.EMPTY_STRING) {

			// Now find the associated declaration
			JPQLQueryDeclaration declaration = getDeclaration(variableName);

			if (declaration != null) {

				// The path expression is composed with an identification variable mapping a subquery
				if (declaration.getType() == JPQLQueryDeclaration.Type.SUBQUERY) {
					valid = Boolean.TRUE;
				}
				else {
					Expression baseExpression = declaration.getBaseExpression();

					// If the base expression is a TableExpression, then we can
					// continue to resolve the table and column names
					if ((baseExpression != null) && isTableExpression(baseExpression)) {

						// No need to validate if no extension was provided,
						// but mark valid to true so it's not validated otherwise
						if (extension == EclipseLinkSemanticValidatorExtension.NULL_EXTENSION) {
							valid = Boolean.TRUE;
						}
						else {
							valid = Boolean.FALSE;

							// Retrieve the table name
							String tableName = literal(baseExpression, LiteralType.STRING_LITERAL);

							if (tableName != ExpressionTools.EMPTY_STRING) {
								tableName = ExpressionTools.unquote(tableName);

								// If the table name can be resolved, then validate the column name
								if ((tableName != null) && extension.tableExists(tableName)) {
									String columnName = expression.getPath(1);

									// The column cannot be found on the table
									if (!extension.columnExists(tableName, columnName)) {
										addProblem(
											expression,
											StateFieldPathExpression_UnknownColumn,
											columnName,
											tableName
										);
									}
									else {
										valid = Boolean.TRUE;
									}
								}
							}
						}
					}
				}
			}
		}

		return valid;
	}

	/**
	 * {@inheritDoc}
	 */
	@Override
	protected PathType validPathExpressionTypeForStringExpression() {
		// Loosen up the JPA spec restriction because ANTLR parser used to allow it
		return PathType.ANY_FIELD_INCLUDING_COLLECTION;
	}

	/**
	 * {@inheritDoc}
	 */
	public void visit(AsOfClause expression) {
		super.visit(expression);
		// Nothing to validate semantically
	}

	/**
	 * {@inheritDoc}
	 */
	public void visit(CastExpression expression) {
		super.visit(expression);
		// Nothing to validate semantically
	}

	/**
	 * {@inheritDoc}
	 */
	public void visit(ConnectByClause expression) {
		super.visit(expression);
		// Nothing to validate semantically
	}

	/**
	 * {@inheritDoc}
	 */
	public void visit(DatabaseType expression) {
		super.visit(expression);
		// Nothing to validate semantically
	}

	/**
	 * {@inheritDoc}
	 */
	public void visit(ExtractExpression expression) {
		super.visit(expression);
		// Nothing to validate semantically
	}

	/**
	 * {@inheritDoc}
	 */
	public void visit(HierarchicalQueryClause expression) {
		super.visit(expression);
		// Nothing to validate semantically
	}

	/**
	 * {@inheritDoc}
	 */
	public void visit(OrderSiblingsByClause expression) {
		super.visit(expression);
		// TODO
	}

	/**
	 * {@inheritDoc}
	 */
	public void visit(RegexpExpression expression) {
		super.visit(expression);
		// Nothing to validate semantically
	}

	/**
	 * {@inheritDoc}
	 */
	public void visit(StartWithClause expression) {
		super.visit(expression);
		// Nothing to validate semantically
	}

	/**
	 * {@inheritDoc}
	 */
	public void visit(TableExpression expression) {
		super.visit(expression);

		// No need to validate if no extension was defined
		if (extension != EclipseLinkSemanticValidatorExtension.NULL_EXTENSION) {
			Expression tableNameExpression = expression.getExpression();
			String tableName = literal(tableNameExpression, LiteralType.STRING_LITERAL);

			if (tableName != ExpressionTools.EMPTY_STRING) {
				tableName = ExpressionTools.unquote(tableName);

				if ((tableName.length() > 0) && !extension.tableExists(tableName)) {
					addProblem(tableNameExpression, JPQLQueryProblemMessages.TableExpression_InvalidTableName);
				}
			}
		}
	}

	/**
	 * {@inheritDoc}
	 */
	public void visit(TableVariableDeclaration expression) {
		super.visit(expression);
		// Nothing to validate semantically
	}

	/**
	 * {@inheritDoc}
	 */
	public void visit(UnionClause expression) {
		super.visit(expression);
		// Nothing to validate semantically
	}

    // Made static final for performance reasons.
	/**
	 * This visitor retrieves the clause owning the visited {@link Expression}.
	 */
	public static final class EclipseLinkOwningClauseVisitor extends OwningClauseVisitor {

		public UnionClause unionClause;

		/**
		 * Creates a new <code>EclipseLinkOwningClauseVisitor</code>.
		 */
		public EclipseLinkOwningClauseVisitor() {
			super();
		}

		/**
		 * {@inheritDoc}
		 */
		@Override
		public void dispose() {
			super.dispose();
			unionClause = null;
		}

		public void visit(UnionClause expression) {
			this.unionClause = expression;
		}
	}

    // Made static final for performance reasons.
	protected static final class SubquerySelectItemCalculator extends AnonymousExpressionVisitor {

		public int count;

		/**
		 * {@inheritDoc}
		 */
		@Override
		public void visit(BadExpression expression) {
			count = 0;
		}

		/**
		 * {@inheritDoc}
		 */
		@Override
		public void visit(CollectionExpression expression) {
			count = expression.childrenSize();
		}

		/**
		 * {@inheritDoc}
		 */
		@Override
		protected void visit(Expression expression) {
			count = 1;
		}

		/**
		 * {@inheritDoc}
		 */
		@Override
		public void visit(NullExpression expression) {
			count = 0;
		}

		/**
		 * {@inheritDoc}
		 */
		@Override
		public void visit(SimpleSelectClause expression) {
			expression.getSelectExpression().accept(this);
		}

		/**
		 * {@inheritDoc}
		 */
		@Override
		public void visit(SimpleSelectStatement expression) {
			expression.getSelectClause().accept(this);
		}
	}

    // Made static final for performance reasons.
	protected static final class TableExpressionVisitor extends AbstractEclipseLinkExpressionVisitor {

		/**
		 * The {@link Expression} being visited.
		 */
		protected Expression expression;

		/**
		 * <code>true</code> if the {@link Expression} being visited is a {@link TableExpression}.
		 */
		protected boolean valid;

		/**
		 * {@inheritDoc}
		 */
		@Override
		public void visit(TableExpression expression) {
			valid = (this.expression == expression);
		}
	}

    // Made static final for performance reasons.
	protected static final class TopLevelFirstDeclarationVisitor extends AbstractSemanticValidator.TopLevelFirstDeclarationVisitor {

        private final AbstractEclipseLinkSemanticValidator validator;

        private TopLevelFirstDeclarationVisitor(AbstractEclipseLinkSemanticValidator validator) {
            this.validator = validator;
        }

		/**
		 * {@inheritDoc}
		 */
		@Override
		public void visit(CollectionValuedPathExpression expression) {

			// Derived path is not allowed, this could although be a fully
			// qualified class name, which was added to EclipseLink 2.4
			EclipseLinkVersion version = EclipseLinkVersion.value(validator.getProviderVersion());
			valid = version.isNewerThanOrEqual(EclipseLinkVersion.VERSION_2_4);

			if (valid) {
				Object type = validator.helper.getType(expression.toActualText());
				valid = validator.helper.isTypeResolvable(type);
			}
		}
	}
}