/*******************************************************************************
 * 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.internal.jpa.jpql;

import java.util.Collection;
import org.eclipse.persistence.expressions.Expression;
import org.eclipse.persistence.expressions.ExpressionBuilder;
import org.eclipse.persistence.history.AsOfSCNClause;
import org.eclipse.persistence.jpa.jpql.LiteralType;
import org.eclipse.persistence.jpa.jpql.parser.AbstractEclipseLinkExpressionVisitor;
import org.eclipse.persistence.jpa.jpql.parser.AbstractFromClause;
import org.eclipse.persistence.jpa.jpql.parser.AbstractSelectClause;
import org.eclipse.persistence.jpa.jpql.parser.AbstractSelectStatement;
import org.eclipse.persistence.jpa.jpql.parser.AsOfClause;
import org.eclipse.persistence.jpa.jpql.parser.AvgFunction;
import org.eclipse.persistence.jpa.jpql.parser.CollectionExpression;
import org.eclipse.persistence.jpa.jpql.parser.CollectionMemberDeclaration;
import org.eclipse.persistence.jpa.jpql.parser.ConstructorExpression;
import org.eclipse.persistence.jpa.jpql.parser.CountFunction;
import org.eclipse.persistence.jpa.jpql.parser.EclipseLinkAnonymousExpressionVisitor;
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.Join;
import org.eclipse.persistence.jpa.jpql.parser.KeyExpression;
import org.eclipse.persistence.jpa.jpql.parser.MaxFunction;
import org.eclipse.persistence.jpa.jpql.parser.MinFunction;
import org.eclipse.persistence.jpa.jpql.parser.ObjectExpression;
import org.eclipse.persistence.jpa.jpql.parser.OrderByClause;
import org.eclipse.persistence.jpa.jpql.parser.OrderByItem;
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.StateFieldPathExpression;
import org.eclipse.persistence.jpa.jpql.parser.SumFunction;
import org.eclipse.persistence.jpa.jpql.parser.UnionClause;
import org.eclipse.persistence.jpa.jpql.parser.ValueExpression;
import org.eclipse.persistence.jpa.jpql.parser.WhereClause;
import org.eclipse.persistence.mappings.DatabaseMapping;
import org.eclipse.persistence.queries.ObjectLevelReadQuery;
import org.eclipse.persistence.queries.ReportQuery;

/**
 * This visitor is responsible to populate an {@link ObjectLevelReadQuery} by traversing a {@link
 * org.eclipse.persistence.jpa.jpql.parser.Expression JPQL Expression} representing a
 * <b>SELECT</b> query.
 *
 * @see ObjectLevelReadQueryVisitor
 * @see ReportQueryVisitor
 *
 * @version 2.5
 * @since 2.3
 * @author Pascal Filion
 * @author John Bracken
 */
abstract class AbstractObjectLevelReadQueryVisitor extends AbstractEclipseLinkExpressionVisitor {

	/**
	 * The {@link ObjectLevelReadQuery} to populate.
	 */
	ObjectLevelReadQuery query;

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

	/**
	 * Creates a new <code>ReadAllQueryVisitor</code>.
	 *
	 * @param queryContext The context used to query information about the application metadata and
	 * cached information
	 * @param query The {@link ObjectLevelReadQuery} to populate by using this visitor to visit the
	 * parsed tree representation of the JPQL query
	 */
	AbstractObjectLevelReadQueryVisitor(JPQLQueryContext queryContext, ObjectLevelReadQuery query) {
		super();
		this.query = query;
		this.queryContext = queryContext;
	}

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

		Expression queryExpression = queryContext.buildExpression(expression);
		org.eclipse.persistence.history.AsOfClause asOfClause;

		if (expression.hasScn()) {
			asOfClause = new AsOfSCNClause(queryExpression);
		}
		else {
			asOfClause = new org.eclipse.persistence.history.AsOfClause(queryExpression);
		}

		query.setAsOfClause(asOfClause);
		query.setShouldMaintainCache(false);
	}

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

	/**
	 * {@inheritDoc}
	 */
	@Override
	public void visit(FromClause expression) {
		visitAbstractFromClause(expression);
	}

	/**
	 * {@inheritDoc}
	 */
	@Override
	public void visit(IdentificationVariable expression) {
		visitIdentificationVariable(expression);
	}

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

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

	/**
	 * {@inheritDoc}
	 */
	@Override
	public void visit(OrderByItem expression) {
		Expression queryExpression = queryContext.buildExpression(expression);
		query.addOrdering(queryExpression);
	}

	/**
	 * {@inheritDoc}
	 */
	@Override
	public void visit(SelectClause expression) {
		// Select couple flags
		visitAbstractSelectClause(expression);
	}

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

		// Handle SELECT/FROM/WHERE clauses
		visitAbstractSelectStatement(expression);

		// ORDER BY clause
		if (expression.hasOrderByClause()) {
			expression.getOrderByClause().accept(this);
		}

		// UNION clauses
		if (expression.hasUnionClauses()) {
			expression.getUnionClauses().accept(this);
		}
	}

	/**
	 * {@inheritDoc}
	 */
	@Override
	public void visit(SimpleFromClause expression) {
		visitAbstractFromClause(expression);
	}

	/**
	 * {@inheritDoc}
	 */
	@Override
	public void visit(SimpleSelectClause expression) {
		// Select couple flags
		visitAbstractSelectClause(expression);
	}

	/**
	 * {@inheritDoc}
	 */
	@Override
	public void visit(SimpleSelectStatement expression) {
		// Handle SELECT/FROM/WHERE
		visitAbstractSelectStatement(expression);
	}

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

		ReportQuery subquery = queryContext.buildSubquery((SimpleSelectStatement) expression.getQuery());
		Expression union = null;

		if (expression.isUnion()) {
			if (expression.hasAll()) {
				union = query.getExpressionBuilder().unionAll(subquery);
			}
			else {
				union = query.getExpressionBuilder().union(subquery);
			}
		}
		else if (expression.isIntersect()) {
			if (expression.hasAll()) {
				union = query.getExpressionBuilder().intersectAll(subquery);
			}
			else {
				union = query.getExpressionBuilder().intersect(subquery);
			}
		}
		else if (expression.isExcept()) {
			if (expression.hasAll()) {
				union = query.getExpressionBuilder().exceptAll(subquery);
			}
			else {
				union = query.getExpressionBuilder().except(subquery);
			}
		}

		query.addUnionExpression(union);
	}

	/**
	 * {@inheritDoc}
	 */
	@Override
	public void visit(WhereClause expression) {
		query.setSelectionCriteria(queryContext.buildExpression(expression));
	}

	void visitAbstractFromClause(AbstractFromClause expression) {

		// Set the ExpressionBuilder
		Expression baseExpression = queryContext.getBaseExpression();
		ExpressionBuilder expressionBuilder = baseExpression.getBuilder();
		query.setExpressionBuilder(expressionBuilder);

		// Set the reference class if it's not set
		if (query.getReferenceClass() == null) {
			query.setReferenceClass(expressionBuilder.getQueryClass());
			query.changeDescriptor(queryContext.getSession());
		}

		// Add join expressions to the query (but not the join fetch expressions)
		JoinVisitor visitor = new JoinVisitor();
		expression.accept(visitor);

		// Visit the AS OF clause
		if (expression.hasAsOfClause()) {
			expression.getAsOfClause().accept(this);
		}

		// Visit the hierarchical clause
		if (expression.hasHierarchicalQueryClause()) {
			expression.getHierarchicalQueryClause().accept(this);
		}
	}

	void visitAbstractSelectClause(AbstractSelectClause expression) {

		// DISTINCT
		if (expression.hasDistinct()) {

			CountFunctionVisitor visitor = new CountFunctionVisitor();
			expression.accept(visitor);

			if (!visitor.hasCountFunction) {
				query.useDistinct();
			}
		}

		// Indicate on the query if "return null if primary key null".
		// This means we want nulls returned if we expect an outer join
		// True:  SELECT employee.address FROM ..... // Simple 1:1
		// True:  SELECT a.b.c.d FROM ..... // where a->b, b->c and c->d are all 1:1.
		// False: SELECT OBJECT(employee) FROM ..... // simple SELECT
		// False: SELECT phoneNumber.areaCode FROM ..... // direct-to-field
		OneToOneSelectedVisitor visitor = new OneToOneSelectedVisitor();
		expression.accept(visitor);
		query.setShouldBuildNullForNullPk(visitor.oneToOneSelected);

		// Now visit the select expression
		expression.getSelectExpression().accept(this);
	}

	void visitAbstractSelectStatement(AbstractSelectStatement expression) {

		// First visit the FROM clause in order to retrieve the reference classes and
		// create an ExpressionBuilder for each abstract schema name
		expression.getFromClause().accept(this);
		expression.getSelectClause().accept(this);

		if (expression.hasWhereClause()) {
			expression.getWhereClause().accept(this);
		}
	}

	void visitIdentificationVariable(IdentificationVariable expression) {

		String variableName = expression.getVariableName();

		// Retrieve the join fetches that were defined in the same identification variable
		// declaration, if the identification variable is mapped to a join, then there will
		// not be any join fetch associated with it
		Collection<Join> joinFetches = queryContext.getJoinFetches(variableName);

		if (joinFetches != null ) {

			for (Join joinFetch : joinFetches) {
			    

				// Retrieve the join association path expression's identification variable
				String joinFetchVariableName = queryContext.literal(
					joinFetch,
					LiteralType.PATH_EXPRESSION_IDENTIFICATION_VARIABLE
				);

				// Both identification variables are the same.
				// Then add the join associated path expression as a joined attribute
				// Example: FROM Employee e JOIN FETCH e.employees
				if (variableName.equals(joinFetchVariableName)) {
	                org.eclipse.persistence.expressions.Expression queryExpression = null;
	                if (joinFetch.hasIdentificationVariable()){
	                    String identificationVariable = queryContext.literal(joinFetch, LiteralType.IDENTIFICATION_VARIABLE);
	                    queryExpression =queryContext.findQueryExpression(identificationVariable);
	                        //Join expression has identification variable and was processed in the From clause
	                }
				    if (queryExpression == null){
				        queryExpression = queryContext.buildExpression(joinFetch);
				    }
					query.addJoinedAttribute(queryExpression);
				}
			}
		}
	}

	private static class CountFunctionVisitor extends EclipseLinkAnonymousExpressionVisitor {

		/**
		 * Determines whether the single {@link org.eclipse.persistence.jpa.jpql.parser.Expression
		 * Expression} is the <b>COUNT</b> expression.
		 */
		boolean hasCountFunction;

		/**
		 * {@inheritDoc}
		 */
		@Override
		public void visit(CountFunction expression) {
			hasCountFunction = true;
		}

		/**
		 * {@inheritDoc}
		 */
		@Override
		protected void visit(org.eclipse.persistence.jpa.jpql.parser.Expression expression) {
			hasCountFunction = false;
		}

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

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

	private class JoinVisitor extends AbstractEclipseLinkExpressionVisitor {

		/**
		 * Cache the Expression associated with the identification variable so it can be used in the
		 * visit(Join) in order to properly create the ON clause expression.
		 */
		private Expression baseExpression;

		private Expression addNonFetchJoinedAttribute(org.eclipse.persistence.jpa.jpql.parser.Expression expression,
		                                              IdentificationVariable identificationVariable) {

			String variableName = identificationVariable.getVariableName();

			// Always add the expression, as it may not be defined elsewhere,
			// unless it has already been defined as the builder.
			Expression queryExpression = queryContext.getQueryExpression(variableName);

			if (queryExpression == null) {
				queryExpression = queryContext.buildExpression(expression);
				queryContext.addQueryExpression(variableName, queryExpression);
			}

			ObjectLevelReadQuery query = (ObjectLevelReadQuery) queryContext.getDatabaseQuery();

			if (query.getExpressionBuilder() != queryExpression) {
				query.addNonFetchJoinedAttribute(queryExpression);
			}

			return queryExpression;
		}

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

		/**
		 * {@inheritDoc}
		 */
		@Override
		public void visit(CollectionMemberDeclaration expression) {
			addNonFetchJoinedAttribute(
				expression,
				(IdentificationVariable) expression.getIdentificationVariable()
			);
		}

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

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

			expression.getRangeVariableDeclaration().accept(this);

			if (expression.hasJoins()) {
				expression.getJoins().accept(this);
			}
		}

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

			if (expression.hasIdentificationVariable()) {

				IdentificationVariable identificationVariable = (IdentificationVariable) expression.getIdentificationVariable();
				Expression queryExpression = null;
				
				if (expression.hasFetch()){
                    String variableName = identificationVariable.getVariableName();
                    queryExpression = queryContext.getQueryExpression(variableName);

                    if (queryExpression == null) {
                        queryExpression = queryContext.buildExpression(expression);
                        queryContext.addQueryExpression(variableName, queryExpression);
                    }
				}else{
				    queryExpression = addNonFetchJoinedAttribute(expression, identificationVariable);
				}

				// Add the ON clause to the expression
				if (expression.hasOnClause()) {
					Expression onClause = queryContext.buildExpression(expression.getOnClause());

					// Create the JOIN expression using the base Expression
					if (expression.isLeftJoin()) {
						queryExpression = baseExpression.leftJoin(queryExpression, onClause);
					}
					else {
						queryExpression = baseExpression.join(queryExpression, onClause);
					}
				}
			}
		}

		/**
		 * {@inheritDoc}
		 */
		@Override
		public void visit(RangeVariableDeclaration expression) {
			baseExpression = addNonFetchJoinedAttribute(
				expression,
				(IdentificationVariable) expression.getIdentificationVariable()
			);
		}

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

	private class OneToOneSelectedVisitor extends EclipseLinkAnonymousExpressionVisitor {

		/**
		 * Determines whether the visited {@link org.eclipse.persistence.jpa.jpql.parser.Expression
		 * Expression} represents a relationship.
		 */
		boolean oneToOneSelected;

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

		/**
		 * {@inheritDoc}
		 */
		@Override
		public void visit(CollectionExpression expression) {
			for (org.eclipse.persistence.jpa.jpql.parser.Expression child : expression.children()) {
				child.accept(this);
				if (oneToOneSelected) {
					break;
				}
			}
		}

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

		/**
		 * {@inheritDoc}
		 */
		@Override
		public void visit(CountFunction expression) {
			oneToOneSelected = false;
		}

		/**
		 * {@inheritDoc}
		 */
		@Override
		public void visit(IdentificationVariable expression) {
			oneToOneSelected = !queryContext.isRangeIdentificationVariable(expression.getVariableName());
		}

		/**
		 * {@inheritDoc}
		 */
		@Override
		public void visit(KeyExpression expression) {
			oneToOneSelected = true;
		}

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

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

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

		/**
		 * {@inheritDoc}
		 */
		@Override
		protected void visit(org.eclipse.persistence.jpa.jpql.parser.Expression expression) {
			oneToOneSelected = true;
		}

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

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

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

		/**
		 * {@inheritDoc}
		 */
		@Override
		public void visit(StateFieldPathExpression expression) {
			DatabaseMapping mapping = queryContext.resolveMapping(expression);
			oneToOneSelected = (mapping != null) && !mapping.isDirectToFieldMapping();
		}

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

		/**
		 * {@inheritDoc}
		 */
		@Override
		public void visit(ValueExpression expression) {
			oneToOneSelected = true;
		}
	}
}