/*
 * Copyright (c) 2012, 2021 Oracle, IBM Corporation, 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.lang.reflect.Constructor;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import org.eclipse.persistence.descriptors.ClassDescriptor;
import org.eclipse.persistence.jpa.jpql.ExpressionTools;
import org.eclipse.persistence.jpa.jpql.ITypeHelper;
import org.eclipse.persistence.jpa.jpql.JPQLQueryDeclaration;
import org.eclipse.persistence.jpa.jpql.SemanticValidatorHelper;
import org.eclipse.persistence.jpa.jpql.parser.Expression;
import org.eclipse.persistence.jpa.jpql.parser.IdentificationVariable;
import org.eclipse.persistence.jpa.jpql.parser.JPQLGrammar;
import org.eclipse.persistence.jpa.jpql.parser.SimpleSelectStatement;
import org.eclipse.persistence.mappings.DatabaseMapping;
import org.eclipse.persistence.mappings.querykeys.QueryKey;

/**
 * The EclipseLink implementation of {@link SemanticValidatorHelper}, which directly accesses
 * EclipseLink objects without using Hermes SPI.
 *
 * @version 2.5
 * @since 2.4
 * @author Pascal Filion
 */
final class EclipseLinkSemanticValidatorHelper implements SemanticValidatorHelper {

    /**
     * The context used to query information about the JPQL query.
     */
    private final JPQLQueryContext queryContext;

    /**
     * Creates a new <code>EclipseLinkSemanticValidatorHelper</code>.
     *
     * @param queryContext The context used to query information about the JPQL query
     */
    EclipseLinkSemanticValidatorHelper(JPQLQueryContext queryContext) {
        super();
        this.queryContext = queryContext;
    }

    private void addIdentificationVariable(IdentificationVariable identificationVariable,
                                           Map<String, List<IdentificationVariable>> identificationVariables) {

        String variableName = (identificationVariable != null) ? identificationVariable.getVariableName() : null;

        if (ExpressionTools.stringIsNotEmpty(variableName)) {

            // Add the IdentificationVariable to the list
            List<IdentificationVariable> variables = identificationVariables.get(variableName);

            if (variables == null) {
                variables = new ArrayList<IdentificationVariable>();
                identificationVariables.put(variableName, variables);
            }

            variables.add(identificationVariable);
        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void collectAllDeclarationIdentificationVariables(Map<String, List<IdentificationVariable>> identificationVariables) {

        JPQLQueryContext currentContext = queryContext.getCurrentContext();

        while (currentContext != null) {
            collectLocalDeclarationIdentificationVariables(currentContext, identificationVariables);
            currentContext = currentContext.getActualParent();
        }
    }

    private void collectLocalDeclarationIdentificationVariables(JPQLQueryContext queryContext,
                                                                Map<String, List<IdentificationVariable>> identificationVariables) {

        DeclarationResolver declarationResolver = queryContext.getDeclarationResolverImp();

        // Collect the identification variables from the declarations
        for (Declaration declaration : declarationResolver.getDeclarations()) {
            IdentificationVariable identificationVariable = declaration.identificationVariable;
            addIdentificationVariable(identificationVariable, identificationVariables);
        }

        // Collect the result variables
        for (IdentificationVariable identificationVariable : declarationResolver.getResultVariables()) {
            addIdentificationVariable(identificationVariable, identificationVariables);
        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void collectLocalDeclarationIdentificationVariables(Map<String, List<IdentificationVariable>> identificationVariables) {
        collectLocalDeclarationIdentificationVariables(queryContext, identificationVariables);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void disposeSubqueryContext() {
        queryContext.disposeSubqueryContext();
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public String[] entityNames() {

        List<String> names = new ArrayList<String>();

        for (ClassDescriptor descriptor : queryContext.getSession().getDescriptors().values()) {
            if (!descriptor.isAggregateDescriptor()) {
                String name = descriptor.getAlias();
                if (ExpressionTools.stringIsEmpty(name)) {
                    name = descriptor.getJavaClass().getSimpleName();
                }
                names.add(name);
            }
        }

        return names.toArray(new String[names.size()]);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public List<JPQLQueryDeclaration> getAllDeclarations() {

        List<JPQLQueryDeclaration> declarations = new ArrayList<JPQLQueryDeclaration>();
        JPQLQueryContext context = queryContext.getCurrentContext();

        while (context != null) {
            declarations.addAll(context.getDeclarationResolverImp().getDeclarations());
            context = context.getActualParent();
        }

        return declarations;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public Object[] getConstructors(Object type) {
        return (type != null) ? ((Class<?>) type).getDeclaredConstructors() : ExpressionTools.EMPTY_ARRAY;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    @SuppressWarnings({ "rawtypes", "unchecked" })
    public List getDeclarations() {
        return queryContext.getDeclarations();
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public ClassDescriptor getEmbeddable(Object type) {

        ClassDescriptor descriptor = queryContext.getDescriptor((Class<?>) type);

        if ((descriptor != null) && descriptor.isAggregateDescriptor()) {
            return descriptor;
        }

        return null;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public ClassDescriptor getEntityNamed(String entityName) {
        return queryContext.getDescriptor(entityName);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    @SuppressWarnings("unchecked")
    public String[] getEnumConstants(Object type) {

        Enum<?>[] constants = ((Class<Enum<?>>) type).getEnumConstants();
        String[] names = new String[constants.length];

        for (int index = constants.length; --index >= 0; ) {
            names[index] = constants[index].name();
        }

        return names;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public JPQLGrammar getGrammar() {
        return queryContext.getGrammar();
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public Object getManagedType(Expression expression) {
        return queryContext.resolveDescriptor(expression);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public Object getReferenceManagedType(Object mapping) {

        if (mapping == null) {
            return null;
        }

        return ((DatabaseMapping) mapping).getReferenceDescriptor();
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public Object getMappingNamed(Object entity, String name) {

        ClassDescriptor descriptor = (ClassDescriptor) entity;
        Object mapping = descriptor.getObjectBuilder().getMappingForAttributeName(name);

        if (mapping == null) {
            mapping = descriptor.getQueryKeyNamed(name);
        }

        return mapping;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public Class<?> getMappingType(Object mapping) {

        if (mapping == null) {
            return null;
        }

        try {
            // We do a try/catch, it should be faster than doing an instance of
            // since a QueryKey is used a lot less than a DatabaseMapping
            return queryContext.calculateMappingType((DatabaseMapping) mapping);
        }
        catch (ClassCastException e) {
            return queryContext.calculateQueryKeyType((QueryKey) mapping);
        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public Type[] getMethodParameterTypeDeclarations(Object constructor) {
        return ((Constructor<?>) constructor).getGenericParameterTypes();
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public Class<?> getType(Expression expression) {
        return queryContext.getType(expression);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public Object getType(Object typeDeclaration) {
        // Not used
        return null;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public Class<?> getType(String className) {
        return queryContext.getType(className);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public Object getTypeDeclaration(Expression expression) {
        // Not used
        return null;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public ITypeHelper getTypeHelper() {
        // Not used
        return null;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public String getTypeName(Object type) {
        return ((Class<?>) type).getName();
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public boolean isAssignableTo(Object type1, Object type2) {
        return ((Class<?>) type2).isAssignableFrom((Class<?>) type1) ;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public boolean isCollectionIdentificationVariable(String variableName) {
        return queryContext.isCollectionIdentificationVariable(variableName);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public boolean isCollectionMapping(Object mapping) {

        if (mapping == null) {
            return false;
        }

        try {
            // We do a try/catch, it should be faster than doing an instance of
            // since a QueryKey is used a lot less than a DatabaseMapping
            return ((DatabaseMapping) mapping).isCollectionMapping();
        }
        catch (ClassCastException e) {
            return ((QueryKey) mapping).isCollectionQueryKey();
        }
    }

    @Override
    public boolean isEmbeddableMapping(Object mapping) {
        return (mapping instanceof org.eclipse.persistence.mappings.EmbeddableMapping);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public boolean isEnumType(Object type) {
        return (type != null) && ((Class<?>) type).isEnum();
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public boolean isIdentificationVariableValidInComparison(IdentificationVariable expression) {

        Declaration declaration = queryContext.findDeclaration(expression.getVariableName());

        if (declaration == null) {
            return false;
        }

        DatabaseMapping mapping = declaration.getMapping();

        if (mapping == null) {
            return false;
        }

        // Direct collection is not an object so it's valid
        return mapping.isDirectCollectionMapping() ||
               mapping.isAbstractCompositeDirectCollectionMapping();
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public boolean isManagedTypeResolvable(Object managedType) {
        return managedType != null;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public boolean isPropertyMapping(Object mapping) {

        if (mapping == null) {
            return false;
        }

        try {
            // We do a try/catch, it should be faster than doing an instance of
            // since a QueryKey is used a lot less than a DatabaseMapping
            return ((DatabaseMapping) mapping).isDirectToFieldMapping();
        }
        catch (ClassCastException e) {
            return ((QueryKey) mapping).isDirectQueryKey();
        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public boolean isRelationshipMapping(Object mapping) {

        if (mapping == null) {
            return false;
        }

        if (mapping instanceof DatabaseMapping) {

            DatabaseMapping databaseMapping = (DatabaseMapping) mapping;

            return databaseMapping.isForeignReferenceMapping()            ||
                   databaseMapping.isAbstractCompositeCollectionMapping() ||
                   databaseMapping.isAbstractCompositeDirectCollectionMapping();
        }

        return ((QueryKey) mapping).isForeignReferenceQueryKey();
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public boolean isResultVariable(String variableName) {
        return queryContext.isResultVariable(variableName);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public boolean isTransient(Object mapping) {
        return mapping == null;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public boolean isTypeDeclarationAssignableTo(Object typeDeclaration1, Object typeDeclaration2) {
        // Not used
        return false;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public boolean isTypeResolvable(Object type) {
        return type != null;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void newSubqueryContext(SimpleSelectStatement expression) {
        queryContext.newSubQueryContext(expression, null);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public Object resolveMapping(Expression expression) {
        return queryContext.resolveMappingObject(expression);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public Object resolveMapping(String variableName, String name) {

        // Find the declaration associated with the identification variable
        Declaration declaration = queryContext.findDeclaration(variableName);

        if (declaration == null) {
            return null;
        }

        // Retrieve the associated descriptor
        ClassDescriptor descriptor = declaration.getDescriptor();

        if (descriptor == null) {
            return null;
        }

        // Now, retrieve the mapping
        Object mapping = descriptor.getObjectBuilder().getMappingForAttributeName(name);

        // No mapping was found, look for a query key
        if (mapping == null) {
            mapping = descriptor.getQueryKeyNamed(name);
        }

        return mapping;
    }
}
