/*
 * Copyright (c) 2012, 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.util.Collections;
import java.util.LinkedList;
import java.util.List;
import org.eclipse.persistence.config.PersistenceUnitProperties;
import org.eclipse.persistence.dynamic.DynamicClassLoader;
import org.eclipse.persistence.jpa.jpql.parser.AbstractEclipseLinkExpressionVisitor;
import org.eclipse.persistence.jpa.jpql.parser.AbstractPathExpression;
import org.eclipse.persistence.jpa.jpql.parser.CollectionExpression;
import org.eclipse.persistence.jpa.jpql.parser.CollectionValuedPathExpression;
import org.eclipse.persistence.jpa.jpql.parser.ConstructorExpression;
import org.eclipse.persistence.jpa.jpql.parser.JPQLExpression;
import org.eclipse.persistence.jpa.jpql.parser.JPQLGrammar;
import org.eclipse.persistence.jpa.jpql.parser.SelectClause;
import org.eclipse.persistence.jpa.jpql.parser.SelectStatement;
import org.eclipse.persistence.jpa.jpql.parser.StateFieldPathExpression;
import org.eclipse.persistence.mappings.DatabaseMapping;
import org.eclipse.persistence.queries.DatabaseQuery;

/**
 * This object scans a {@link DatabaseQuery} and checks if it's a constructor query. If it is one,
 * then the fully qualified class name and the persistent fields will be available.
 *
 * @version 2.5
 * @since 2.5
 * @author Pascal Filion
 */
public final class ConstructorQueryMappings {

    /**
     * The fully qualified class name defined in the constructor expression if one is defined,
     * otherwise <code>null</code>.
     */
    private String className;

    /**
     * The list of {@link DatabaseMapping} representing the parameter arguments if the JPQL query
     * does contain a constructor expression.
     */
    private List<DatabaseMapping> mappings;

    /**
     * The {@link DatabaseQuery} for which its JPQL query was scanned to check if it's constructor
     * query and if so, to gather the information.
     */
    private DatabaseQuery query;

    /**
     * Creates a new <code>ConstructorQueryMappings</code>.
     *
     * @param query The {@link DatabaseQuery} for which its JPQL query was scanned to check if it's
     * constructor query and if so, to gather the information.
     */
    ConstructorQueryMappings(DatabaseQuery query) {
        super();
        this.query = query;
        this.mappings = new LinkedList<DatabaseMapping>();
    }

    /**
     * Returns the class name used to define the constructor expression.
     *
     * @return Either the fully qualified class name used in the constructor expression or
     * <code>null</code> if the query is not a constructor query
     */
    public String getClassName() {
        return className;
    }

    /**
     * Returns the {@link DatabaseQuery} for which its JPQL query was scanned to check if it's
     * constructor query and if so, to gather the information.
     *
     * @return The {@link DatabaseQuery} that was scanned
     */
    public DatabaseQuery getQuery() {
        return query;
    }

    /**
     * Determines whether the JPQL query is a constructor query, i.e. the select expression is a
     * constructor expression.
     * <p>
     * Example: <code>SELECT new test.example.Employee(e.name, e.id) FROM Employee e</code>
     *
     * @return <code>true</code> if the <code><b>SELECT</b></code> clause has a single select item
     * and it's a constructor expression; <code>false</code> otherwise
     */
    public boolean isConstructorQuery() {
        return className != null;
    }

    /**
     * Returns a non-<code>null</code> {@link Iterable} over the ordered list of {@link DatabaseMapping}
     * objects that represents the parameter arguments defined in the constructor expression.
     *
     * @return The list contains the persistent fields that were passed to the constructor
     */
    public Iterable<DatabaseMapping> mappings() {

        if (mappings.isEmpty()) {
            return Collections.emptyList();
        }

        return new LinkedList<DatabaseMapping>(mappings);
    }

    /**
     * Populates this object by scanning the JPQL query using the given JPQL grammar.
     *
     * @param jpqlGrammar The {@link JPQLGrammar} how the JPQL query is parsed
     */
    void populate(JPQLGrammar jpqlGrammar) {

        JPQLQueryContext queryContext = new JPQLQueryContext(query, jpqlGrammar);

        ConstructorVisitor visitor = new ConstructorVisitor(queryContext);
        queryContext.getJPQLExpression().accept(visitor);
    }

    /**
     * This visitor visits the constructor items and adds the attribute type mapped to the name.
     * <p/>
     * Example:<br/>
     * e.name -> "name" : String<br/>
     * e.address.zipcode -> "zipcode" : int
     */
    private class ConstructorItemVisitor extends AbstractEclipseLinkExpressionVisitor {

        private final JPQLQueryContext queryContext;

        ConstructorItemVisitor(JPQLQueryContext queryContext) {
            super();
            this.queryContext = queryContext;
        }

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

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

        /**
         * {@inheritDoc}
         */
        @Override
        public void visit(StateFieldPathExpression expression) {
            visitPathExpression(expression);
        }

        private void visitPathExpression(AbstractPathExpression expression) {

            DatabaseMapping mapping = queryContext.resolveMapping(expression);

            if (mapping != null) {
                mappings.add(mapping);
            }
        }
    }

    /**
     * Visits the JPQL query and gather the information contained in the constructor expressions.
     */
    private class ConstructorVisitor extends AbstractEclipseLinkExpressionVisitor {

        private final JPQLQueryContext queryContext;

        ConstructorVisitor(JPQLQueryContext queryContext) {
            super();
            this.queryContext = queryContext;
        }

        private DynamicClassLoader classLoader() {
            return (DynamicClassLoader) queryContext.getSession().getProperty(PersistenceUnitProperties.CLASSLOADER);
        }

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

            ConstructorQueryMappings.this.className = expression.getClassName();

            // Visit the constructor items and retrieve the persistent fields
            ConstructorItemVisitor visitor = new ConstructorItemVisitor(queryContext);
            expression.getConstructorItems().accept(visitor);
        }

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

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

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