/*
 * Copyright (c) 1998, 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 from Oracle TopLink
package org.eclipse.persistence.internal.jpa.parsing;

import java.util.*;
import org.eclipse.persistence.descriptors.ClassDescriptor;
import org.eclipse.persistence.exceptions.JPQLException;

/**
 * INTERNAL
 * <p><b>Purpose</b>: The ParseTreeContext holds and manages context information for the parse tree for validation.
 * <p><b>Responsibilities</b>:<ul>
 * <li> Associate schema names with variables
 * <li> Associate identifier with nodes
 * <li> Answer an alias for a variable name
 * <li> Answer a class for a variable name
 * <li> Answer a class loader
 * <li> Answer true if there is a class for a variable name
 * <li> Answer a node for a given identifier
 * <li> Print the context on a string
 * </ul>
 * @see ParseTree
 *    @author Jon Driscoll and Joel Lucuik
 *    @since TopLink 4.0
 */
public class ParseTreeContext {
    private Map variableDecls;
    private String baseVariable;
    private int currentScope;
    private Set outerScopeVariables;
    private Map fetchJoins;
    private TypeHelper typeHelper;
    private Map parameterTypes;
    private List parameterNames;
    private NodeFactory nodeFactory;
    private String queryInfo;

    /**
     * INTERNAL
     * Return a new initialized ParseTreeContext
     */
    public ParseTreeContext(NodeFactory nodeFactory, String queryInfo) {
        super();
        variableDecls = new HashMap();
        currentScope = 0;
        fetchJoins = new HashMap();
        typeHelper = null;
        parameterTypes = new HashMap();
        parameterNames = new ArrayList();
        this.nodeFactory = nodeFactory;
        this.queryInfo = queryInfo;
    }

    /**
     * INTERNAL
     * Associate the given schema with the given variable.
     */
    public void registerSchema(String variable, String schema, int line, int column) {
        VariableDecl decl = (VariableDecl)variableDecls.get(variable);
        if (decl == null) {
            decl = new VariableDecl(variable, schema);
            variableDecls.put(variable, decl);
        } else {
            String text = decl.isRangeVariable ? decl.schema : decl.path.getAsString();
            throw JPQLException.multipleVariableDeclaration(
                getQueryInfo(), line, column, variable, text);
        }
    }

    /**
     * INTERNAL
     * Associate the given path with the given variable.
     */
    public void registerJoinVariable(String variable, Node path, int line, int column) {
        VariableDecl decl = (VariableDecl)variableDecls.get(variable);
        if (decl == null) {
            decl = new VariableDecl(variable, path);
            variableDecls.put(variable, decl);
        } else {
            String text = decl.isRangeVariable ? decl.schema : decl.path.getAsString();
            throw JPQLException.multipleVariableDeclaration(
                getQueryInfo(), line, column, variable, text);
        }
    }
    /**
     * INTERNAL
     */
    public void unregisterVariable(String variable) {
        variableDecls.remove(variable);
    }

    /**
     * INTERNAL
     * Returns true if the specified string denotes a variable.
     */
    public boolean isVariable(String variable) {
        VariableDecl decl = (VariableDecl)variableDecls.get(variable);
        return decl != null;
    }

    /**
     * INTERNAL
     * Returns true if the specified string denotes a range variable.
     */
    /** */
    public boolean isRangeVariable(String variable) {
        VariableDecl decl = (VariableDecl)variableDecls.get(variable);
        return (decl != null) && decl.isRangeVariable;
    }

    /**
     * INTERNAL
     * Returns the abstract schema name if the specified string denotes a
     * range variable.
     */
    public String schemaForVariable(String variable) {
        VariableDecl decl = (VariableDecl)variableDecls.get(variable);
        return (decl != null) ? decl.schema : null;
    }

    /**
     * INTERNAL
     * Answer the class associated with the provided schema name
     */
    public Class classForSchemaName(String schemaName, GenerationContext context) {
        ClassDescriptor descriptor = context.getSession().getDescriptorForAlias(schemaName);
        if (descriptor == null) {
            throw JPQLException.entityTypeNotFound(getQueryInfo(), schemaName);
        }
        Class theClass = descriptor.getJavaClass();
        if (theClass == null) {
            throw JPQLException.resolutionClassNotFoundException(getQueryInfo(), schemaName);
        }
        return theClass;
    }

    /**
     * INTERNAL
     * getVariableNameForClass():
     *        Answer the name mapped to the specified class. Answer null if none found.
     * SELECT OBJECT (emp) FROM Employee emp
     *   getVariableNameForClass(Employee.class) => "emp"
     */
    public String getVariableNameForClass(Class theClass, GenerationContext context) {
        for (Iterator i = variableDecls.entrySet().iterator(); i.hasNext(); ) {
            Map.Entry entry = (Map.Entry)i.next();
            String nextVariable = (String)entry.getKey();
            VariableDecl decl = (VariableDecl)entry.getValue();
            if ((decl.schema != null) &&
                (theClass == this.classForSchemaName(decl.schema, context))) {
                return nextVariable;
            }
        }
        return null;
    }

    /**
     * INTERNAL
     * Returns the path if the specified string denotes a join or collection
     * member variable.
     */
    public Node pathForVariable(String variable) {
        VariableDecl decl = (VariableDecl)variableDecls.get(variable);
        return (decl != null) ? decl.path : null;
    }

    /** */
    public String getBaseVariable() {
        return baseVariable;
    }

    /** */
    public void setBaseVariable(String variable) {
        this.baseVariable = variable;
    }

    /** */
    public NodeFactory getNodeFactory() {
        return nodeFactory;
    }

    /** */
    public String getQueryInfo() {
        return queryInfo;
    }

    /**
     * INTERNAL
     * Returns true if the specified string denotes a variable declared in an
     * outer scope.
     */
    public boolean isDeclaredInOuterScope(String variable) {
        VariableDecl decl = (VariableDecl)variableDecls.get(variable);
        return (decl != null) ? (decl.scope < currentScope) : false;
    }

    /**
     * INTERNAL
     * Sets the scope of the specified variable to the current scope.
     */
    public void setScopeOfVariable(String variable) {
        VariableDecl decl = (VariableDecl)variableDecls.get(variable);
        if (decl != null) {
            decl.scope = currentScope;
        }
    }

    /**
     * INTERNAL
     * Enters a new scope. This initializes the set of outer scope variables.
     */
    public void enterScope() {
        currentScope++;
        resetOuterScopeVariables();
    }

    /**
     * INTERNAL
     * Leaves the current scope.
     */
    public void leaveScope() {
        currentScope--;
    }

    /**
     * INTERNAL
     * Adds the specified variable to the set of outer scope variables.
     */
    public void registerOuterScopeVariable(String variable) {
        outerScopeVariables.add(variable);
    }

    /**
     * INTERNAL
     * Returns the set of outer scope variables.
     */
    public Set getOuterScopeVariables() {
        return outerScopeVariables;
    }

    /**
     * INTERNAL
     * Resets the set of outer scope variables.
     */
    public void resetOuterScopeVariables() {
        outerScopeVariables = new HashSet();
    }

    /**
     * INTERNAL
     * Resets the set of outer scope variables.
     */
    public void resetOuterScopeVariables(Set variables) {
        outerScopeVariables = variables;
    }

    /**
     * Associate the given variableName with the given node representing a
     * JOIN FETCH node.
     */
    public void registerFetchJoin(String variableName, Node node) {
        List joins = (List)fetchJoins.get(variableName);
        if (joins == null) {
            joins = new ArrayList();
            fetchJoins.put(variableName, joins);
        }
        joins.add(node);
    }

    /** Returns a list of FETCH JOIN nodes for the specified attached to the
     * specified variable. */
    public List getFetchJoins(String variableName) {
        return (List)fetchJoins.get(variableName);
    }

    /** Mark the specified variable as used if it is declared in the current
     * scope. */
    public void usedVariable(String variable) {
        VariableDecl decl = (VariableDecl)variableDecls.get(variable);
        if ((decl != null) && (decl.scope == currentScope)) {
            decl.used = true;
        }
    }

    /** Returns s set of variables that are declared in the current scope,
     * but not used in the query. */
    public Set getUnusedVariables() {
        Set unused = new HashSet();
        for (Iterator i = variableDecls.entrySet().iterator(); i.hasNext();) {
            Map.Entry entry = (Map.Entry)i.next();
            String variable = (String)entry.getKey();
            VariableDecl decl = (VariableDecl)entry.getValue();
            if ((decl.scope == currentScope) && !decl.used) {
                unused.add(variable);
            }
        }
        return unused;
    }

    //answer true if two or more variables are mapped to the same type name in variableTypes
    //true => "SELECT OBJECT (emp1) FROM Employee emp1, Employee emp2 WHERE ..."
    //false => "SELECT OBJECT (emp) FROM Employee emp WHERE ..."
    public boolean hasMoreThanOneVariablePerType() {
        Map typeNamesToVariables = new HashMap();
        int nrOfRangeVariables = 0;
        //Map the Aliases to the variable names, then check the count
        for (Iterator i = variableDecls.entrySet().iterator(); i.hasNext(); ) {
            Map.Entry entry = (Map.Entry)i.next();
            String variable = (String)entry.getKey();
            VariableDecl decl = (VariableDecl)entry.getValue();
            if (decl.isRangeVariable) {
                nrOfRangeVariables++;
                typeNamesToVariables.put(decl.schema, variable);
            }
        }
        return typeNamesToVariables.size() != nrOfRangeVariables;
    }

    //answer true if two or more aliases are involved in the FROM (different types)
    //true => "SELECT OBJECT (emp1) FROM Employee emp1, Address addr1 WHERE ..."
    //false => "SELECT OBJECT (emp) FROM Employee emp WHERE ..."
    //false => "SELECT OBJECT (emp1) FROM Employee emp1, Employee emp2 WHERE ..."
    public boolean hasMoreThanOneAliasInFrom() {
        Map typeNamesToVariables = new HashMap();
        for (Iterator i = variableDecls.entrySet().iterator(); i.hasNext(); ) {
            Map.Entry entry = (Map.Entry)i.next();
            String variable = (String)entry.getKey();
            VariableDecl decl = (VariableDecl)entry.getValue();
            if (decl.isRangeVariable) {
                typeNamesToVariables.put(decl.schema, variable);
            }
        }
        return typeNamesToVariables.size() > 1;
    }

    /**
     * INTERNAL
     * Returns the type helper stored in this context.
     */
    public TypeHelper getTypeHelper() {
        return typeHelper;
    }

    /**
     * INTERNAL
     * Stores the specified type helper in this context.
     */
    public void setTypeHelper(TypeHelper typeHelper) {
        this.typeHelper = typeHelper;
    }

    /**
     * INTERNAL
     * Add a parameter.
     */
    public void addParameter(String parameterName) {
        if (!parameterNames.contains(parameterName)){
            parameterNames.add(parameterName);
        }
    }

    /**
     * INTERNAL
     * Defines the type of the parameter with the specified name.
     */
    public void defineParameterType(String parameterName, Object parameterType,
        int line, int column) {
        if (parameterTypes.containsKey(parameterName)) {
            // existing entry
            Object oldType = parameterTypes.get(parameterName);
            if (typeHelper.isAssignableFrom(oldType, parameterType)) {
                // OK
            } else if (typeHelper.isAssignableFrom(parameterType, oldType)) {
                // new parameter type is more general
                parameterTypes.put(parameterName, parameterType);
            } else {
                // error case old usage and new usage do not match type
                throw JPQLException.invalidMultipleUseOfSameParameter(
                    getQueryInfo(), line, column, parameterName,
                    typeHelper.getTypeName(oldType),
                    typeHelper.getTypeName(parameterType));
            }
        } else {
            // new entry
            parameterTypes.put(parameterName, parameterType);
        }
    }

    /**
     * INTERNAL
     * Returns true if the query has at least one parameter.
     */
    public boolean hasParameters() {
        return !parameterNames.isEmpty();
    }

    /**
     * INTERNAL
     * Return the type of the specified parameter.
     */
    public Object getParameterType(String parameter) {
        return parameterTypes.get(parameter);
    }

    /**
     * INTERNAL
     * Return the parameter names.
     */
    public List getParameterNames() {
        return parameterNames;
    }

    /**
     * INTERNAL
     * Class defining the type of the values the variableDecls map.
     * It holds the following values:
     * variable - the name of the variable
     * isRangeVariable - true if the variable is declared as range variable
     * schema - the abstract for a range variable
     * path - the path for join or collection member variable
     * scope - the scope of the variable
     * used - true if the variable is used in any of the clauses
     */
    static class VariableDecl {
        public final String variable;
        public final boolean isRangeVariable;
        public final String schema;
        public final Node path;
        public int scope;
        public boolean used;
        public VariableDecl(String variable, String schema) {
            this.variable = variable;
            this.isRangeVariable = true;
            this.schema = schema;
            this.path = null;
            this.used = false;
        }
        public VariableDecl(String variable, Node path) {
            this.variable = variable;
            this.isRangeVariable = false;
            this.schema = null;
            this.path = path;
            this.used = false;
        }
    }
}
