/*
 * Copyright (c) 2006, 2018 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 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.text.MessageFormat;
import java.util.Collection;
import java.util.LinkedList;
import java.util.Map;
import java.util.ResourceBundle;
import org.eclipse.persistence.config.ParserValidationType;
import org.eclipse.persistence.exceptions.JPQLException;
import org.eclipse.persistence.expressions.Expression;
import org.eclipse.persistence.internal.expressions.ParameterExpression;
import org.eclipse.persistence.internal.queries.JPQLCallQueryMechanism;
import org.eclipse.persistence.internal.sessions.AbstractSession;
import org.eclipse.persistence.jpa.jpql.EclipseLinkGrammarValidator;
import org.eclipse.persistence.jpa.jpql.JPQLQueryProblem;
import org.eclipse.persistence.jpa.jpql.JPQLQueryProblemResourceBundle;
import org.eclipse.persistence.jpa.jpql.parser.AbstractExpressionVisitor;
import org.eclipse.persistence.jpa.jpql.parser.ConditionalExpressionBNF;
import org.eclipse.persistence.jpa.jpql.parser.DefaultEclipseLinkJPQLGrammar;
import org.eclipse.persistence.jpa.jpql.parser.DeleteStatement;
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.parser.JPQLGrammar1_0;
import org.eclipse.persistence.jpa.jpql.parser.JPQLGrammar2_0;
import org.eclipse.persistence.jpa.jpql.parser.JPQLGrammar2_1;
import org.eclipse.persistence.jpa.jpql.parser.SelectStatement;
import org.eclipse.persistence.jpa.jpql.parser.UpdateStatement;
import org.eclipse.persistence.queries.DatabaseQuery;
import org.eclipse.persistence.queries.DatabaseQuery.ParameterType;
import org.eclipse.persistence.queries.DeleteAllQuery;
import org.eclipse.persistence.queries.JPAQueryBuilder;
import org.eclipse.persistence.queries.ObjectLevelReadQuery;
import org.eclipse.persistence.queries.ReadAllQuery;
import org.eclipse.persistence.queries.ReportQuery;
import org.eclipse.persistence.queries.UpdateAllQuery;
import static org.eclipse.persistence.jpa.jpql.JPQLQueryProblemMessages.*;

/**
 * This class compiles a JPQL query into a {@link DatabaseQuery}. If validation is not turned off,
 * then the JPQL query will be validated based on the grammar related to the validation level and
 * will also be validated based on the semantic (context).
 * <p>
 * The validation level determines how to validate the JPQL query. It checks if any specific feature
 * is allowed. For instance, if the JPQL query has functions defined for EclipseLink grammar but
 * the validation level is set for generic JPA, then an exception will be thrown indicating the
 * function cannot be used.
 *
 * @see JPQLExpression
 *
 * @version 2.5
 * @since 2.3
 * @author John Bracken
 * @author Pascal Filion
 */
@SuppressWarnings("nls")
public final class HermesParser implements JPAQueryBuilder {

    /**
     * Determines how to validate the JPQL query grammatically.
     */
    private String validationLevel;

    /**
     * Creates a new <code>HermesParser</code>.
     */
    public HermesParser() {
        super();
        validationLevel = ParserValidationType.DEFAULT;
    }

    /**
     * Registers the input parameters derived from the JPQL expression with the {@link DatabaseQuery}.
     *
     * @param queryContext The {@link JPQLQueryContext} containing the information about the JPQL query
     * @param databaseQuery The EclipseLink {@link DatabaseQuery} where the input parameter types are added
     */
    private void addArguments(JPQLQueryContext queryContext, DatabaseQuery databaseQuery) {

        if (queryContext.inputParameters != null) {

            for (Map.Entry<InputParameter, Expression> entry : queryContext.inputParameters.entrySet()) {
                ParameterExpression parameter = (ParameterExpression) entry.getValue();

                databaseQuery.addArgument(
                    parameter.getField().getName(),
                    (Class<?>) parameter.getType(),
                    entry.getKey().isPositional() ? ParameterType.POSITIONAL : ParameterType.NAMED
                );
            }
        }
    }

    /**
     * Creates a {@link JPQLException} indicating the problems with the JPQL query.
     *
     * @param queryContext The {@link JPQLQueryContext} containing the information about the JPQL query
     * @param problems The {@link JPQLQueryProblem problems} found in the JPQL query that are
     * translated into an exception
     * @param messageKey The key used to retrieve the localized message
     * @return The {@link JPQLException} indicating the problems with the JPQL query
     */
    private JPQLException buildException(JPQLQueryContext queryContext,
                                         Collection<JPQLQueryProblem> problems,
                                         String messageKey) {

        ResourceBundle bundle = resourceBundle();
        StringBuilder sb = new StringBuilder();

        for (JPQLQueryProblem problem : problems) {

            // Retrieve the localized message
            String message;

            try {
                message = bundle.getString(problem.getMessageKey());
            }
            catch (NullPointerException e) {
                // In case the resource bundle was not updated
                message = problem.getMessageKey();
            }

            // Now format the localized message
            String[] arguments = problem.getMessageArguments();

            if (arguments.length > 0) {
                message = MessageFormat.format(message, (Object[]) arguments);
            }

            // Append the description
            sb.append("\n");
            sb.append("[");
            sb.append(problem.getStartPosition());
            sb.append(", ");
            sb.append(problem.getEndPosition());
            sb.append("] ");
            sb.append(message);
        }

        String errorMessage = bundle.getString(messageKey);
        errorMessage = MessageFormat.format(errorMessage, queryContext.getJPQLQuery(), sb);
        return new JPQLException(errorMessage);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public DatabaseQuery buildQuery(CharSequence jpqlQuery, AbstractSession session) {
        return populateQueryImp(jpqlQuery, null, session);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public Expression buildSelectionCriteria(String entityName,
                                             String selectionCriteria,
                                             AbstractSession session) {

        try {
            // Create the parsed tree representation of the selection criteria
            JPQLExpression jpqlExpression = new JPQLExpression(
                selectionCriteria,
                DefaultEclipseLinkJPQLGrammar.instance(),
                ConditionalExpressionBNF.ID,
                isTolerant()
            );

            // Caches the info and add a virtual range variable declaration
            JPQLQueryContext queryContext = new JPQLQueryContext(jpqlGrammar());
            queryContext.cache(session, null, jpqlExpression, selectionCriteria);
            queryContext.addRangeVariableDeclaration(entityName, "this");

            // Validate the JPQL query, which will use the JPQL grammar matching the validation
            // level, for now, only validate the query statement because there could be an unknown
            // ending that is an order by clause
            validate(queryContext, jpqlExpression.getQueryStatement());

            // Create the Expression representing the selection criteria
            return queryContext.buildExpression(jpqlExpression.getQueryStatement());
        }
        catch (JPQLException exception) {
            throw exception;
        }
        catch (Exception exception) {
            throw buildUnexpectedException(selectionCriteria, exception);
        }
    }

    private JPQLException buildUnexpectedException(CharSequence jpqlQuery, Exception exception) {
        String errorMessage = resourceBundle().getString(HermesParser_UnexpectedException_ErrorMessage);
        errorMessage = MessageFormat.format(errorMessage, jpqlQuery);
        return new JPQLException(errorMessage, exception);
    }

    /**
     * Determines whether the JPQL query should be parsed with tolerance turned on or off, i.e. if
     * validation is turned off, then it's assumed the JPQL query is grammatically valid and complete.
     * In this case, it will be parsed with tolerance turned off resulting in better performance.
     *
     * @return <code>true</code> if the query might be incomplete or invalid; <code>false</code> if
     * the query is complete and grammatically valid
     */
    private boolean isTolerant() {
        return validationLevel != ParserValidationType.None;
    }

    /**
     * Returns the {@link JPQLGrammar} that will help to validate the JPQL query grammatically and
     * semantically (contextually). It will also checks if any specific feature added to that grammar
     * is allowed. For instance, if the JPQL query has functions defined for EclipseLink grammar but
     * the validation level is set for generic JPA, then an exception will be thrown.
     *
     * @return The {@link JPQLGrammar} written for a specific JPA version or for the current version
     * of EclipseLink
     */
    private JPQLGrammar jpqlGrammar() {

        if (validationLevel == ParserValidationType.EclipseLink) {
            return DefaultEclipseLinkJPQLGrammar.instance();
        }

        if (validationLevel == ParserValidationType.JPA10) {
            return JPQLGrammar1_0.instance();
        }

        if (validationLevel == ParserValidationType.JPA20) {
            return JPQLGrammar2_0.instance();
        }

        if (validationLevel == ParserValidationType.JPA21) {
            return JPQLGrammar2_1.instance();
        }

        return DefaultEclipseLinkJPQLGrammar.instance();
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void populateQuery(CharSequence jpqlQuery, DatabaseQuery query, AbstractSession session) {
        populateQueryImp(jpqlQuery, query, session);
    }

    private DatabaseQuery populateQueryImp(CharSequence jpqlQuery,
                                           DatabaseQuery query,
                                           AbstractSession session) {

        try {
            // Parse the JPQL query with the most recent JPQL grammar
            JPQLExpression jpqlExpression = new JPQLExpression(
                jpqlQuery,
                DefaultEclipseLinkJPQLGrammar.instance(),
                isTolerant()
            );

            // Create a context that caches the information contained in the JPQL query
            // (especially from the FROM clause)
            JPQLQueryContext queryContext = new JPQLQueryContext(jpqlGrammar());
            queryContext.cache(session, query, jpqlExpression, jpqlQuery);

            // Validate the JPQL query, which will use the JPQL grammar matching the validation level
            validate(queryContext, jpqlExpression);

            // Create the DatabaseQuery by visiting the parsed tree
            DatabaseQueryVisitor visitor = new DatabaseQueryVisitor(queryContext, jpqlQuery);
            jpqlExpression.accept(visitor);

            // Add the input parameter types to the DatabaseQuery
            if (query == null) {
                query = queryContext.getDatabaseQuery();
                addArguments(queryContext, query);
            }

            return query;
        }
        catch (JPQLException exception) {
            throw exception;
        }
        catch (Exception exception) {
            throw buildUnexpectedException(jpqlQuery, exception);
        }
    }

    private ResourceBundle resourceBundle() {
        return ResourceBundle.getBundle(JPQLQueryProblemResourceBundle.class.getName());
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void setValidationLevel(String validationLevel) {
        this.validationLevel = validationLevel;
    }

    /**
     * Grammatically and semantically validates the JPQL query. If the query is not valid, then an
     * exception will be thrown.
     *
     * @param queryContext The context used to query information about the application metadata and
     * cached information
     * @param expression The {@link org.eclipse.persistence.jpa.jpql.parser.Expression Expression} to
     * validate grammatically and semantically
     */
    private void validate(JPQLQueryContext queryContext,
                          org.eclipse.persistence.jpa.jpql.parser.Expression expression) {

        if (validationLevel != ParserValidationType.None) {

            Collection<JPQLQueryProblem> problems = new LinkedList<JPQLQueryProblem>();

            // Validate the JPQL query grammatically (based on the JPQL grammar)
            EclipseLinkGrammarValidator grammar = new EclipseLinkGrammarValidator(jpqlGrammar());
            grammar.setProblems(problems);
            expression.accept(grammar);

            if (!problems.isEmpty()) {
                throw buildException(
                    queryContext,
                    problems,
                    HermesParser_GrammarValidator_ErrorMessage
                );
            }

            // Validate the JPQL query semantically (contextually)
            EclipseLinkSemanticValidator semantic = new EclipseLinkSemanticValidator(queryContext);
            semantic.setProblems(problems);
            expression.accept(semantic);

            if (!problems.isEmpty()) {
                throw buildException(
                    queryContext,
                    problems,
                    HermesParser_SemanticValidator_ErrorMessage
                );
            }
        }
    }

    /**
     * This visitor traverses the parsed tree and create the right EclipseLink query and populates it.
     */
    private static class DatabaseQueryVisitor extends AbstractExpressionVisitor {

        private final String jpqlQuery;
        private final JPQLQueryContext queryContext;

        DatabaseQueryVisitor(JPQLQueryContext queryContext, CharSequence jpqlQuery) {
            super();
            this.jpqlQuery = jpqlQuery.toString();
            this.queryContext = queryContext;
        }

        private ReadAllQuery buildReadAllQuery(SelectStatement expression) {
            ReadAllQueryBuilder visitor = new ReadAllQueryBuilder(queryContext);
            expression.accept(visitor);
            return visitor.query;
        }

        private AbstractObjectLevelReadQueryVisitor buildVisitor(ObjectLevelReadQuery query) {

            if (query.isReportQuery()) {
                return new ReportQueryVisitor(queryContext, (ReportQuery) query);
            }

            if (query.isReadAllQuery()) {
                return new ReadAllQueryVisitor(queryContext, (ReadAllQuery) query);
            }

            return new ObjectLevelReadQueryVisitor(queryContext, query);
        }

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

            DeleteAllQuery query = queryContext.getDatabaseQuery();

            // Create and prepare the query
            if (query == null) {
                query = new DeleteAllQuery();
                queryContext.setDatabasQuery(query);
                query.setJPQLString(jpqlQuery);
                ((JPQLCallQueryMechanism) query.getQueryMechanism()).getJPQLCall().setIsParsed(true);
            }

            query.setSession(queryContext.getSession());
            query.setShouldDeferExecutionInUOW(false);

            // Now populate it
            DeleteQueryVisitor visitor = new DeleteQueryVisitor(queryContext, query);
            expression.accept(visitor);
        }

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

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

            ObjectLevelReadQuery query = queryContext.getDatabaseQuery();

            // Create and prepare the query
            if (query == null) {
                query = buildReadAllQuery(expression);
                queryContext.setDatabasQuery(query);
                query.setJPQLString(jpqlQuery);
                ((JPQLCallQueryMechanism) query.getQueryMechanism()).getJPQLCall().setIsParsed(true);
            }

            // Now populate it
            expression.accept(buildVisitor(query));
        }

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

            UpdateAllQuery query = queryContext.getDatabaseQuery();

            // Create and prepare the query
            if (query == null) {
                query = new UpdateAllQuery();
                queryContext.setDatabasQuery(query);
                query.setJPQLString(jpqlQuery);
                ((JPQLCallQueryMechanism) query.getQueryMechanism()).getJPQLCall().setIsParsed(true);
            }

            query.setSession(queryContext.getSession());
            query.setShouldDeferExecutionInUOW(false);

            // Now populate it
            UpdateQueryVisitor visitor = new UpdateQueryVisitor(queryContext, query);
            expression.accept(visitor);
        }
    }
}
