/*
 * Copyright (c) 2011, 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.jpa.jpql.tools.model.query;

import org.eclipse.persistence.jpa.jpql.ExpressionTools;
import org.eclipse.persistence.jpa.jpql.parser.EncapsulatedIdentificationVariableExpression;
import org.eclipse.persistence.jpa.jpql.parser.IdentificationVariableBNF;
import org.eclipse.persistence.jpa.jpql.tools.spi.IManagedType;
import org.eclipse.persistence.jpa.jpql.tools.spi.IType;
import org.eclipse.persistence.jpa.jpql.tools.spi.ITypeDeclaration;

/**
 * This object represents an identification variable that is wrapped by a function.
 *
 * <div><b>BNF:</b> <code>&lt;identifier&gt;(identification_variable)</code><p></div>
 *
 * @see EncapsulatedIdentificationVariableExpression
 *
 * @version 2.4
 * @since 2.4
 * @author Pascal Filion
 */
@SuppressWarnings("nls")
public abstract class EncapsulatedIdentificationVariableExpressionStateObject extends AbstractSingleEncapsulatedExpressionStateObject {

    /**
     * The cached identification variable that is used to make sure it's the same as the one stored
     * in the {@link IdentificationVariableStateObject} itself.
     */
    private String identificationVariable;

    /**
     * The {@link IManagedType} mapped to the identification variable if the identification variable
     * maps to an entity name.
     */
    private IManagedType managedType;

    /**
     * The cached {@link IType} of the value to resolve.
     */
    private IType type;

    /**
     * The cached {@link ITypeDeclaration} of the value to resolve.
     */
    private ITypeDeclaration typeDeclaration;

    /**
     * Notifies the identification variable property has changed.
     */
    public static final String IDENTIFICATION_VARIABLE_PROPERTY = "identificationVariable";

    /**
     * Creates a new <code>EncapsulatedIdentificationVariableExpressionStateObject</code>.
     *
     * @param parent The parent of this state object, which cannot be <code>null</code>
     * @exception NullPointerException The given parent cannot be <code>null</code>
     */
    protected EncapsulatedIdentificationVariableExpressionStateObject(StateObject parent) {
        super(parent);
    }

    /**
     * Creates a new <code>EncapsulatedIdentificationVariableExpressionStateObject</code>.
     *
     * @param parent The parent of this state object, which cannot be <code>null</code>
     * @param identificationVariable The identification variable
     * @exception NullPointerException The given parent cannot be <code>null</code>
     */
    protected EncapsulatedIdentificationVariableExpressionStateObject(StateObject parent,
                                                                      String identificationVariable) {

        super(parent);
        setIdentificationVariable(identificationVariable);
    }

    /**
     * Makes sure the cached identification variable and the one owned by {@link
     * IdentificationVariableStateObject} are the same. If they are not, then clears the cached
     * values related to the type.
     */
    protected void checkIntegrity() {
        if (ExpressionTools.stringsAreDifferentIgnoreCase(identificationVariable, getStateObject().getText())) {
            clearResolvedObjects();
            identificationVariable = getStateObject().getText();
        }
    }

    /**
     * Clears the values related to the managed type and type.
     */
    protected void clearResolvedObjects() {
        type            = null;
        managedType     = null;
        typeDeclaration = null;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public EncapsulatedIdentificationVariableExpression getExpression() {
        return (EncapsulatedIdentificationVariableExpression) super.getExpression();
    }

    /**
     * Returns the identification variable.
     *
     * @return The name of the identification variable
     */
    public String getIdentificationVariable() {
        identificationVariable = getStateObject().getText();
        return identificationVariable;
    }

    /**
     * Returns the {@link IManagedType} associated with the field handled by this object. If this
     * object does not handle a field that has a {@link IManagedType}, then <code>null</code> should
     * be returned.
     * <p>
     * For example: "<code><b>SELECT</b> e <b>FROM</b> Employee e</code>", the object for <i>e</i>
     * would be returning the {@link IManagedType} for <i>Employee</i>.
     *
     * @return Either the {@link IManagedType}, if it could be resolved; <code>null</code> otherwise
     */
    public IManagedType getManagedType() {

        if (managedType != null) {
            checkIntegrity();
        }

        if (managedType == null) {
            managedType = resolveManagedType();
        }

        return managedType;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    protected String getQueryBNFId() {
        return IdentificationVariableBNF.ID;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public IdentificationVariableStateObject getStateObject() {
        return (IdentificationVariableStateObject) super.getStateObject();
    }

    /**
     * Returns the {@link IType} of the field handled by this object.
     *
     * @return Either the {@link IType} of the identification variable or the {@link IType} for
     * {@link IType#UNRESOLVABLE_TYPE} if it could not be resolved
     */
    public IType getType() {

        if (type != null) {
            checkIntegrity();
        }

        if (type == null) {
            type = resolveType();
        }

        return type;
    }

    /**
     * Returns the {@link ITypeDeclaration} of the field handled by this object.
     *
     * @return Either the {@link ITypeDeclaration} that was resolved by this object or the {@link
     * ITypeDeclaration} for {@link IType#UNRESOLVABLE_TYPE} if it could not be resolved
     */
    public ITypeDeclaration getTypeDeclaration() {

        if (typeDeclaration != null) {
            checkIntegrity();
        }

        if (typeDeclaration == null) {
            typeDeclaration = resolveTypeDeclaration();
        }

        return typeDeclaration;
    }

    /**
     * Determines whether the identification variable has been defined or not.
     *
     * @return <code>true</code> if the identification variable has been defined;
     * <code>false</code> otherwise
     */
    public boolean hasIdentificationVariable() {
        return ExpressionTools.stringIsNotEmpty(getIdentificationVariable());
    }

    /**
     * {@inheritDoc}
     */
    @Override
    protected void initialize() {
        super.initialize();
        setStateObject(new IdentificationVariableStateObject(this));
    }

    /**
     * Retrieves the {@link IManagedType} that is mapped to the identification variable, if and only
     * if the identification variable is used to declare an entity.
     *
     * @return Either the {@link IManagedType} declared by the identification variable or <code>null</code>
     * if it could not be resolved
     */
    protected IManagedType resolveManagedType() {
        return getManagedTypeProvider().getManagedType(getType());
    }

    /**
     * Resolves the {@link IType} of the property handled by this object.
     *
     * @return Either the {@link IType} that was resolved by this object or the {@link IType} for
     * {@link IType#UNRESOLVABLE_TYPE} if it could not be resolved
     */
    protected IType resolveType() {
        return getTypeDeclaration().getType();
    }

    /**
     * Resolves the {@link ITypeDeclaration} of the property handled by this object.
     *
     * @return Either the {@link ITypeDeclaration} that was resolved by this object or the {@link
     * ITypeDeclaration} for {@link IType#UNRESOLVABLE_TYPE} if it could not be resolved
     */
    protected ITypeDeclaration resolveTypeDeclaration() {
        return getStateObject().getTypeDeclaration();
    }

    /**
     * Sets the identification variable of this encapsulated expression.
     *
     * @param identificationVariable The name of the identification variable
     */
    public void setIdentificationVariable(String identificationVariable) {

        clearResolvedObjects();

        String oldIdentificationVariable = this.identificationVariable;
        this.identificationVariable = identificationVariable;
        getStateObject().setText(identificationVariable);
        firePropertyChanged(IDENTIFICATION_VARIABLE_PROPERTY, oldIdentificationVariable, identificationVariable);
    }
}
