/*
 * Copyright (c) 2006, 2019 Oracle and/or its affiliates. All rights reserved.
 * Copyright (c) 2019 IBM and/or its affiliates. All rights reserved.
 *
 * This program and the accompanying materials are made available under the
 * terms of the Eclipse Public License v. 2.0 which is available at
 * http://www.eclipse.org/legal/epl-2.0,
 * or the Eclipse Distribution License v. 1.0 which is available at
 * http://www.eclipse.org/org/documents/edl-v10.php.
 *
 * SPDX-License-Identifier: EPL-2.0 OR BSD-3-Clause
 */

// 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
                    //'queryExpression' will be altered pass-by-reference
                    if (expression.isLeftJoin()) {
                        baseExpression.leftJoin(queryExpression, onClause);
                    } else {
                        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;
        }
    }
}
