/*******************************************************************************
 * Copyright (c) 2011, 2013 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.internal.jpa.jpql;

import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;
import org.eclipse.persistence.jpa.jpql.ExpressionTools;
import org.eclipse.persistence.jpa.jpql.JPQLQueryDeclaration.Type;
import org.eclipse.persistence.jpa.jpql.LiteralType;
import org.eclipse.persistence.jpa.jpql.parser.AbstractEclipseLinkExpressionVisitor;
import org.eclipse.persistence.jpa.jpql.parser.AbstractSchemaName;
import org.eclipse.persistence.jpa.jpql.parser.CollectionExpression;
import org.eclipse.persistence.jpa.jpql.parser.CollectionMemberDeclaration;
import org.eclipse.persistence.jpa.jpql.parser.CollectionValuedPathExpression;
import org.eclipse.persistence.jpa.jpql.parser.DeleteClause;
import org.eclipse.persistence.jpa.jpql.parser.DeleteStatement;
import org.eclipse.persistence.jpa.jpql.parser.Expression;
import org.eclipse.persistence.jpa.jpql.parser.FromClause;
import org.eclipse.persistence.jpa.jpql.parser.IdentificationVariable;
import org.eclipse.persistence.jpa.jpql.parser.IdentificationVariableDeclaration;
import org.eclipse.persistence.jpa.jpql.parser.JPQLExpression;
import org.eclipse.persistence.jpa.jpql.parser.Join;
import org.eclipse.persistence.jpa.jpql.parser.RangeVariableDeclaration;
import org.eclipse.persistence.jpa.jpql.parser.ResultVariable;
import org.eclipse.persistence.jpa.jpql.parser.SelectClause;
import org.eclipse.persistence.jpa.jpql.parser.SelectStatement;
import org.eclipse.persistence.jpa.jpql.parser.SimpleFromClause;
import org.eclipse.persistence.jpa.jpql.parser.SimpleSelectClause;
import org.eclipse.persistence.jpa.jpql.parser.SimpleSelectStatement;
import org.eclipse.persistence.jpa.jpql.parser.SubExpression;
import org.eclipse.persistence.jpa.jpql.parser.TableVariableDeclaration;
import org.eclipse.persistence.jpa.jpql.parser.UpdateClause;
import org.eclipse.persistence.jpa.jpql.parser.UpdateStatement;

/**
 * This visitor visits the declaration clause of the JPQL query and creates the list of
 * {@link Declaration Declarations}.
 *
 * @version 2.5
 * @since 2.4
 * @author Pascal Filion
 */
@SuppressWarnings("nls")
final class DeclarationResolver {

	/**
	 * The first {@link Declaration} that was created when visiting the declaration clause.
	 */
	private Declaration baseDeclaration;

	/**
	 * The {@link Declaration} objects mapped to their identification variable.
	 */
	private List<Declaration> declarations;

	/**
	 * The parent {@link DeclarationResolver} which represents the superquery's declaration or
	 * <code>null</code> if this is used for the top-level query.
	 */
	private DeclarationResolver parent;

	/**
	 * Determines whether the {@link Declaration Declaration} objects were created after visiting the
	 * query's declaration clause.
	 */
	private boolean populated;

	/**
	 * The {@link JPQLQueryContext} is used to query information about the application metadata and
	 * cached information.
	 */
	private JPQLQueryContext queryContext;

	/**
	 * The result variables used to identify select expressions.
	 */
	private Collection<IdentificationVariable> resultVariables;

	/**
	 * Creates a new <code>DeclarationResolver</code>.
	 *
	 * @param queryContext The context used to query information about the application metadata and
	 * cached information
	 * @param parent The parent {@link DeclarationResolver} which represents the superquery's declaration
	 */
	DeclarationResolver(JPQLQueryContext queryContext, DeclarationResolver parent) {
		super();
		initialize(queryContext, parent);
	}

	/**
	 * Adds a "virtual" range variable declaration that will be used when parsing a JPQL fragment.
	 *
	 * @param entityName The name of the entity to be accessible with the given variable name
	 * @param variableName The identification variable used to navigate to the entity
	 */
	void addRangeVariableDeclaration(String entityName, String variableName) {

		// This method should only be used by HermesParser.buildSelectionCriteria(),
		// initializes these variables right away since this method should only be
		// called by HermesParser.buildSelectionCriteria()
		populated = true;
		resultVariables = Collections.emptySet();

		// Create the "virtual" range variable declaration
		RangeVariableDeclaration rangeVariableDeclaration = new RangeVariableDeclaration(
			entityName,
			variableName
		);

		// Make sure the identification variable was not declared more than once,
		// this could cause issues when trying to resolve it
		RangeDeclaration declaration = new RangeDeclaration(queryContext);
		declaration.rootPath               = entityName;
		declaration.baseExpression         = rangeVariableDeclaration;
		declaration.identificationVariable = (IdentificationVariable) rangeVariableDeclaration.getIdentificationVariable();

		declarations.add(declaration);

		// Make sure it is marked as the base declaration and the base Expression is created
		if (baseDeclaration == null) {
			baseDeclaration = declaration;

			// Make sure the base Expression is initialized, which will cache it
			// into the right context as well (the top-level context)
			declaration.getQueryExpression();
		}
	}

	/**
	 * Converts the given {@link Declaration} from being set as a range variable declaration to
	 * a path expression declaration.
	 * <p>
	 * In this query "<code>UPDATE Employee SET firstName = 'MODIFIED' WHERE (SELECT COUNT(m) FROM
	 * managedEmployees m) > 0</code>" <em>managedEmployees</em> is an unqualified collection-valued
	 * path expression (<code>employee.managedEmployees</code>).
	 *
	 * @param declaration The {@link Declaration} that was parsed to range over an abstract schema
	 * name but is actually ranging over a path expression
	 * @param outerVariableName The identification variable coming from the parent identification
	 * variable declaration
	 */
	void convertUnqualifiedDeclaration(RangeDeclaration declaration, String outerVariableName) {

		QualifyRangeDeclarationVisitor visitor = new QualifyRangeDeclarationVisitor();

		// Convert the declaration expression into a derived declaration
		visitor.declaration       = declaration;
		visitor.outerVariableName = outerVariableName;
		visitor.queryContext      = queryContext.getCurrentContext();

		declaration.declarationExpression.accept(visitor);

		// Now replace the old declaration with the new one
		int index = declarations.indexOf(declaration);
		declarations.set(index, visitor.declaration);

		// Update the base declaration
		if (baseDeclaration == declaration) {
			baseDeclaration = visitor.declaration;
		}
	}

	/**
	 * Retrieves the {@link Declaration} for which the given variable name is used to navigate to the
	 * "root" object.
	 *
	 * @param variableName The name of the identification variable that is used to navigate a "root"
	 * object
	 * @return The {@link Declaration} containing the information about the identification variable
	 * declaration
	 */
	Declaration getDeclaration(String variableName) {

		for (Declaration declaration : declarations) {
			if (declaration.getVariableName().equalsIgnoreCase(variableName)) {
				return declaration;
			}
		}

		return null;
	}

	/**
	 * Returns the ordered list of {@link Declaration Declarations}.
	 *
	 * @return The {@link Declaration Declarations} of the current query that was parsed
	 */
	List<Declaration> getDeclarations() {
		return declarations;
	}

	/**
	 * Returns the first {@link Declaration} that was created after visiting the declaration clause.
	 *
	 * @return The first {@link Declaration} object
	 */
	Declaration getFirstDeclaration() {
		return baseDeclaration;
	}

	/**
	 * Returns the parsed representation of a <b>JOIN FETCH</b> that were defined in the same
	 * declaration than the given range identification variable name.
	 *
	 * @param variableName The name of the identification variable that should be used to define an entity
	 * @return The <b>JOIN FETCH</b> expressions used in the same declaration or an empty collection
	 * if none was defined
	 */
	Collection<Join> getJoinFetches(String variableName) {

		Declaration declaration = getDeclaration(variableName);

		if ((declaration != null) && (declaration.getType() == Type.RANGE)) {
			RangeDeclaration rangeDeclaration = (RangeDeclaration) declaration;
			if (rangeDeclaration.hasJoins()) {
				return rangeDeclaration.getJoinFetches();
			}
		}

		return null;
	}

	/**
	 * Returns the parent of this {@link DeclarationResolver}.
	 *
	 * @return The parent of this {@link DeclarationResolver} if this is used for a subquery or
	 * <code>null</code> if this is used for the top-level query
	 */
	DeclarationResolver getParent() {
		return parent;
	}

	/**
	 * Returns the variables that got defined in the select expression. This only applies to JPQL
	 * queries built for JPA 2.0 or later.
	 *
	 * @return The variables identifying the select expressions, if any was defined or an empty set
	 * if none were defined
	 */
	Collection<IdentificationVariable> getResultVariables() {

		if (parent != null) {
			return parent.getResultVariables();
		}

		if (resultVariables == null) {
			ResultVariableVisitor visitor = new ResultVariableVisitor();
			queryContext.getJPQLExpression().accept(visitor);
			resultVariables = visitor.resultVariables;
		}

		return resultVariables;
	}

	/**
	 * Initializes this <code>DeclarationResolver</code>.
	 *
	 * @param queryContext The context used to query information about the query
	 * @param parent The parent {@link DeclarationResolver}, which is not <code>null</code> when this
	 * resolver is created for a subquery
	 */
	private void initialize(JPQLQueryContext queryContext, DeclarationResolver parent) {
		this.parent       = parent;
		this.queryContext = queryContext;
		this.declarations = new LinkedList<Declaration>();
	}

	/**
	 * Determines whether the given identification variable is defining a <b>JOIN</b> expression or
	 * in a <code>IN</code> expressions for a collection-valued field. If the search didn't find the
	 * identification in this resolver, then it will traverse the parent hierarchy.
	 *
	 * @param variableName The identification variable to check for what it maps
	 * @return <code>true</code> if the given identification variable maps a collection-valued field
	 * defined in a <code>JOIN</code> or <code>IN</code> expression; <code>false</code> otherwise
	 */
	boolean isCollectionIdentificationVariable(String variableName) {

		boolean result = isCollectionIdentificationVariableImp(variableName);

		if (!result && (parent != null)) {
			result = parent.isCollectionIdentificationVariableImp(variableName);
		}

		return result;
	}

	/**
	 * Determines whether the given identification variable is defining a <b>JOIN</b> expression or
	 * in a <code>IN</code> expressions for a collection-valued field. The search does not traverse
	 * the parent hierarchy.
	 *
	 * @param variableName The identification variable to check for what it maps
	 * @return <code>true</code> if the given identification variable maps a collection-valued field
	 * defined in a <code>JOIN</code> or <code>IN</code> expression; <code>false</code> otherwise
	 */
	boolean isCollectionIdentificationVariableImp(String variableName) {

		for (Declaration declaration : declarations) {

			switch (declaration.getType()) {

				case COLLECTION: {
					if (declaration.getVariableName().equalsIgnoreCase(variableName)) {
						return true;
					}
					return false;
				}

				case RANGE:
				case DERIVED: {

					AbstractRangeDeclaration rangeDeclaration = (AbstractRangeDeclaration) declaration;

					// Check the JOIN expressions
					for (Join join : rangeDeclaration.getJoins()) {

						String joinVariableName = queryContext.literal(
							join.getIdentificationVariable(),
							LiteralType.IDENTIFICATION_VARIABLE
						);

						if (joinVariableName.equalsIgnoreCase(variableName)) {
							// Make sure the JOIN expression maps a collection mapping
							Declaration joinDeclaration = queryContext.getDeclaration(joinVariableName);
							return joinDeclaration.getMapping().isCollectionMapping();
						}
					}
				}
			}
		}

		return false;
	}

	/**
	 * Determines whether the given variable name is an identification variable name used to define
	 * an abstract schema name.
	 *
	 * @param variableName The name of the variable to verify if it's defined in a range variable
	 * declaration in the current query or any parent query
	 * @return <code>true</code> if the variable name is mapping an abstract schema name; <code>false</code>
	 * if it's defined in a collection member declaration
	 */
	boolean isRangeIdentificationVariable(String variableName) {
		boolean result = isRangeIdentificationVariableImp(variableName);
		if (!result && (parent != null)) {
			result = parent.isRangeIdentificationVariableImp(variableName);
		}
		return result;
	}

	private boolean isRangeIdentificationVariableImp(String variableName) {
		Declaration declaration = getDeclaration(variableName);
		return (declaration != null) && declaration.getType().isRange();
	}

	/**
	 * Determines whether the given variable is a result variable or not.
	 *
	 * @param variableName The variable to check if it used to identify a select expression
	 * @return <code>true</code> if the given variable is defined as a result variable;
	 * <code>false</code> otherwise
	 */
	boolean isResultVariable(String variableName) {

		// Only the top-level SELECT query has result variables
		if (parent != null) {
			return parent.isResultVariable(variableName);
		}

		for (IdentificationVariable resultVariable : getResultVariables()) {
			if (resultVariable.getText().equalsIgnoreCase(variableName)) {
				return true;
			}
		}

		return false;
	}

	/**
	 * Visits the given {@link Expression} (which is either the top-level query or a subquery) and
	 * retrieve the information from its declaration clause.
	 *
	 * @param expression The {@link Expression} to visit in order to retrieve the information
	 * contained in the given query's declaration
	 */
	void populate(Expression expression) {
		if (!populated) {
			populated = true;
			populateImp(expression);
		}
	}

	private void populateImp(Expression expression) {

		DeclarationVisitor visitor = new DeclarationVisitor();
		visitor.queryContext = queryContext;
		visitor.declarations = declarations;

		expression.accept(visitor);
		baseDeclaration = visitor.baseDeclaration;
	}

	private static class DeclarationVisitor extends AbstractEclipseLinkExpressionVisitor {

		/**
		 * The first {@link Declaration} that was created when visiting the declaration clause.
		 */
		private Declaration baseDeclaration;

		/**
		 * This flag is used to determine what to do in {@link #visit(SimpleSelectStatement)}.
		 */
		private boolean buildingDeclaration;

		/**
		 * The {@link Declaration} being populated.
		 */
		private Declaration currentDeclaration;

		/**
		 * The list of {@link Declaration} objects to which new ones will be added by traversing the
		 * declaration clause.
		 */
		List<Declaration> declarations;

		/**
		 * The {@link JPQLQueryContext} is used to query information about the application metadata and
		 * cached information.
		 */
		JPQLQueryContext queryContext;

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

			String rootPath = expression.getText();

			// Abstract schema name (entity name)
			if (rootPath.indexOf('.') == -1) {
				currentDeclaration = new RangeDeclaration(queryContext);
			}
			else {

				// Check to see if the "root" path is a class name before assuming it's a derived path
				Class<?> type = queryContext.getType(rootPath);

				// Fully qualified class name
				if (type != null) {
					RangeDeclaration declaration = new RangeDeclaration(queryContext);
					declaration.type = type;
					currentDeclaration = declaration;
				}
				// Derived path expression (for subqueries)
				else {
					currentDeclaration = new DerivedDeclaration(queryContext);
				}
			}

			currentDeclaration.rootPath = rootPath;
		}

		/**
		 * {@inheritDoc}
		 */
		@Override
		public void visit(CollectionExpression expression) {
			expression.acceptChildren(this);
		}

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

			Declaration declaration = new CollectionDeclaration(queryContext);
			declaration.baseExpression        = expression.getCollectionValuedPathExpression();
			declaration.rootPath              = declaration.baseExpression.toActualText();
			declaration.declarationExpression = expression;
			declarations.add(declaration);

			// A derived collection member declaration does not have an identification variable
			if (!expression.isDerived()) {
				IdentificationVariable identificationVariable = (IdentificationVariable) expression.getIdentificationVariable();
				declaration.identificationVariable = identificationVariable;
			}

			// This collection member declaration is the first defined,
			// it is then the base Declaration
			if (baseDeclaration == null) {
				baseDeclaration = declaration;
			}
		}

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

			String rootPath = expression.toParsedText();

			// Check to see if the "root" path is a class name before assuming it's a derived path
			Class<?> type = queryContext.getType(rootPath);

			// Fully qualified class name
			if (type != null) {
				RangeDeclaration declaration = new RangeDeclaration(queryContext);
				declaration.type = type;
				currentDeclaration = declaration;
			}
			// Derived path expression (for subqueries)
			else {
				currentDeclaration = new DerivedDeclaration(queryContext);
			}

			currentDeclaration.rootPath = rootPath;
		}

		/**
		 * {@inheritDoc}
		 */
		@Override
		public void visit(DeleteClause expression) {
			try {
				expression.getRangeVariableDeclaration().accept(this);
			}
			finally {
				currentDeclaration = null;
			}
		}

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

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

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

			try {
				// Visit the RangeVariableDeclaration, it will create the right Declaration
				expression.getRangeVariableDeclaration().accept(this);
				currentDeclaration.declarationExpression = expression;

				// Now visit the JOIN expressions
				expression.getJoins().accept(this);
			}
			finally {
				currentDeclaration = null;
			}
		}

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

			((AbstractRangeDeclaration) currentDeclaration).addJoin(expression);

			if (!expression.hasFetch() || expression.hasIdentificationVariable()) {
				IdentificationVariable identificationVariable = (IdentificationVariable) expression.getIdentificationVariable();

				JoinDeclaration declaration = new JoinDeclaration(queryContext);
				declaration.baseExpression = expression;
				declaration.identificationVariable = identificationVariable;
				declarations.add(declaration);
			}
		}

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

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

			// Traverse the "root" object, it will create the right Declaration
			buildingDeclaration = true;
			expression.getRootObject().accept(this);
			buildingDeclaration = false;

			// Cache more information
			currentDeclaration.identificationVariable = (IdentificationVariable) expression.getIdentificationVariable();
			currentDeclaration.baseExpression = expression;
			declarations.add(currentDeclaration);

			// This range variable declaration is the first defined,
			// it is then the base declaration
			if (baseDeclaration == null) {
				baseDeclaration = currentDeclaration;
			}
		}

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

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

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

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

			// The parent query is using a subquery in the FROM clause
			if (buildingDeclaration) {
				currentDeclaration = new SubqueryDeclaration(queryContext);
				currentDeclaration.rootPath = ExpressionTools.EMPTY_STRING;
			}
			// Simply traversing the tree to create the declarations
			else {
				expression.getFromClause().accept(this);
			}
		}

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

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

			TableDeclaration declaration = new TableDeclaration(queryContext);
			declaration.declarationExpression  = expression;
			declaration.baseExpression         = expression.getTableExpression();
			declaration.rootPath               = declaration.baseExpression.toParsedText();
			declaration.identificationVariable = (IdentificationVariable) expression.getIdentificationVariable();
			declarations.add(declaration);
		}

		/**
		 * {@inheritDoc}
		 */
		@Override
		public void visit(UpdateClause expression) {
			try {
				expression.getRangeVariableDeclaration().accept(this);
			}
			finally {
				currentDeclaration = null;
			}
		}

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

	private static class QualifyRangeDeclarationVisitor extends AbstractEclipseLinkExpressionVisitor {

		/**
		 * The {@link Declaration} being modified.
		 */
		AbstractRangeDeclaration declaration;

		/**
		 * The identification variable coming from the parent identification variable declaration.
		 */
		String outerVariableName;

		/**
		 * The {@link JPQLQueryContext} is used to query information about the application metadata and
		 * cached information.
		 */
		JPQLQueryContext queryContext;

		/**
		 * {@inheritDoc}
		 */
		@Override
		public void visit(CollectionValuedPathExpression expression) {
			// Create the path because CollectionValuedPathExpression.toParsedText()
			// does not contain the virtual identification variable
			StringBuilder rootPath = new StringBuilder();
			rootPath.append(outerVariableName);
			rootPath.append(".");
			rootPath.append(expression.toParsedText());
			declaration.rootPath = rootPath.toString();
		}

		/**
		 * {@inheritDoc}
		 */
		@Override
		public void visit(IdentificationVariableDeclaration expression) {
			expression.getRangeVariableDeclaration().accept(this);
			declaration.declarationExpression = expression;
		}

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

			DerivedDeclaration derivedDeclaration = new DerivedDeclaration(queryContext);
			derivedDeclaration.joins                       = declaration.joins;
			derivedDeclaration.rootPath                    = declaration.rootPath;
			derivedDeclaration.baseExpression              = declaration.baseExpression;
			derivedDeclaration.identificationVariable      = declaration.identificationVariable;
			declaration = derivedDeclaration;

			expression.setVirtualIdentificationVariable(outerVariableName, declaration.rootPath);
			expression.getRootObject().accept(this);
		}
	}

	/**
	 * This visitor traverses the <code><b>SELECT</b></code> clause and retrieves the result variables.
	 */
	private static class ResultVariableVisitor extends AbstractEclipseLinkExpressionVisitor {

		Set<IdentificationVariable> resultVariables;

		/**
		 * Creates a new <code>ResultVariableVisitor</code>.
		 */
		public ResultVariableVisitor() {
			super();
			resultVariables = new HashSet<IdentificationVariable>();
		}

		/**
		 * {@inheritDoc}
		 */
		@Override
		public void visit(CollectionExpression expression) {
			expression.acceptChildren(this);
		}

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

		/**
		 * {@inheritDoc}
		 */
		@Override
		public void visit(ResultVariable expression) {
			IdentificationVariable identificationVariable = (IdentificationVariable) expression.getResultVariable();
			resultVariables.add(identificationVariable);
		}

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

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