/*
 * 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 java.io.IOException;
import java.util.List;
import org.eclipse.persistence.jpa.jpql.parser.ConditionalExpressionBNF;
import org.eclipse.persistence.jpa.jpql.tools.model.IConditionalExpressionStateObjectBuilder;
import static org.eclipse.persistence.jpa.jpql.parser.AbstractExpression.*;

/**
 * Conditional expressions are composed of other conditional expressions, comparison operations,
 * logical operations, path expressions that evaluate to boolean values, boolean literals, and
 * boolean input parameters. Arithmetic expressions can be used in comparison expressions.
 * Arithmetic expressions are composed of other arithmetic expressions, arithmetic operations, path
 * expressions that evaluate to numeric values, numeric literals, and numeric input parameters.
 * Arithmetic operations use numeric promotion. Standard bracketing () for ordering expression
 * evaluation is supported.
 *
 * <div><b>BNF:</b> <code>expression ::= identifier conditional_expression</code><p></div>
 *
 * @see org.eclipse.persistence.jpa.jpql.parser.AbstractConditionalClause AbstractConditionalClause
 *
 * @version 2.4
 * @since 2.4
 * @author Pascal Filion
 */
@SuppressWarnings("nls")
public abstract class AbstractConditionalClauseStateObject extends AbstractStateObject {

    /**
     * The builder is cached during the creation of the conditional expression.
     */
    private IConditionalExpressionStateObjectBuilder builder;

    /**
     * The state object representing the composition of the conditional expressions.
     */
    private StateObject conditionalStateObject;

    /**
     * Notifies the conditional expression property has changed.
     */
    public static final String CONDITIONAL_STATE_OBJECT_PROPERTY = "conditionalStateObject";

    /**
     * Creates a new <code>AbstractConditionalClauseStateObject</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 AbstractConditionalClauseStateObject(StateObject parent) {
        super(parent);
    }

    /**
     * Creates a new <code>AbstractConditionalClauseStateObject</code>.
     *
     * @param parent The parent of this state object, which cannot be <code>null</code>
     * @param conditionalStateObject The {@link StateObject} representing the conditional expression
     * @exception NullPointerException The given parent cannot be <code>null</code>
     */
    protected AbstractConditionalClauseStateObject(StateObject parent,
                                                   StateObject conditionalStateObject) {

        super(parent);
        this.conditionalStateObject = parent(conditionalStateObject);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    protected void addChildren(List<StateObject> children) {
        super.addChildren(children);
        if (conditionalStateObject != null) {
            children.add(conditionalStateObject);
        }
    }

    /**
     * Parses the given JPQL fragment as the right side of an <code><b>AND</b></code> expression. The
     * current conditional expression will become the left side of the <code><b>AND</b></code>
     * expression.
     *
     * @param jpqlFragment The portion of the query representing the right side of the
     * <code><b>AND</b></code> expression
     * @return The newly created {@link AndExpressionStateObject}
     */
    public AndExpressionStateObject andParse(String jpqlFragment) {

        StateObject stateObject = buildStateObject(jpqlFragment, ConditionalExpressionBNF.ID);

        // Make sure the current conditional expression is encapsulated if it's an OR expression in
        // order to preserve logical operator precedence.
        // Example: A or B and we're adding C, it has to become (A or B) and C
        if (shouldEncapsulateORExpression(conditionalStateObject)) {
            conditionalStateObject = new SubExpressionStateObject(this, conditionalStateObject);
        }

        // Make sure the right side of the AND expression is encapsulated in order to preserve logical
        // operator precedence in the case it's an OR expression.
        // Example: A and we're adding B or C, it has to become A and (B or C)
        if (shouldEncapsulateORExpression(stateObject)) {
            stateObject = new SubExpressionStateObject(this, stateObject);
        }

        AndExpressionStateObject andStateObject = new AndExpressionStateObject(
            this,
            conditionalStateObject,
            stateObject
        );

        setConditional(andStateObject);
        return andStateObject;
    }

    /**
     * Creates and returns a new {@link IConditionalExpressionStateObjectBuilder} that can be used to
     * programmatically create a conditional expression and once the expression is complete,
     * {@link IConditionalExpressionStateObjectBuilder#commit()} will push the {@link StateObject}
     * representation of that expression as this clause's conditional expression.
     *
     * @return A new builder that can be used to quickly create a conditional expression
     */
    public IConditionalExpressionStateObjectBuilder getBuilder() {
        if (builder == null) {
            builder = getQueryBuilder().buildStateObjectBuilder(this);
        }
        return builder;
    }

    /**
     * Returns the state object representing the composition of the conditional expressions.
     *
     * @return The actual conditional expression
     */
    public StateObject getConditional() {
        return conditionalStateObject;
    }

    /**
     * Returns the JPQL identifier of this clause.
     *
     * @return The JPQL identifier of this conditional clause
     */
    public abstract String getIdentifier();

    /**
     * Determines whether the {@link StateObject} representing the conditional expression is present
     * or not.
     *
     * @return <code>true</code> if the conditional expression is not <code>null</code>;
     * <code>false</code> otherwise
     */
    public boolean hasConditional() {
        return conditionalStateObject != null;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public boolean isEquivalent(StateObject stateObject) {

        if (super.isEquivalent(stateObject)) {
            AbstractConditionalClauseStateObject clause = (AbstractConditionalClauseStateObject) stateObject;
            return areEquivalent(conditionalStateObject, clause.conditionalStateObject);
        }

        return false;
    }

    /**
     * Parses the given JPQL fragment as the right side of an <code><b>OR</b></code> expression. The
     * current conditional expression will become the left side of the <code><b>OR</b></code>
     * expression.
     *
     * @param jpqlFragment The portion of the query representing the right side of the
     * <code><b>OR</b></code> expression
     * @return The newly created {@link OrExpressionStateObject}
     */
    public OrExpressionStateObject orParse(String jpqlFragment) {

        OrExpressionStateObject orStateObject = new OrExpressionStateObject(
            this,
            conditionalStateObject,
            buildStateObject(jpqlFragment, ConditionalExpressionBNF.ID)
        );

        setConditional(orStateObject);
        return orStateObject;
    }

    /**
     * Parses the given JPQL fragment, which represents a conditional expression, and creates the
     * {@link StateObject}.
     *
     * @param jpqlFragment The portion of the query representing a conditional expression
     */
    public void parse(String jpqlFragment) {
        StateObject stateObject = buildStateObject(jpqlFragment, ConditionalExpressionBNF.ID);
        setConditional(stateObject);
    }

    /**
     * Sets the given {@link StateObject} to be the conditional expression of this clause.
     *
     * @param conditionalStateObject The new {@link StateObject} representing the conditional
     * expression
     */
    public void setConditional(StateObject conditionalStateObject) {

        builder = null;

        StateObject oldConditionalStateObject = this.conditionalStateObject;
        this.conditionalStateObject = parent(conditionalStateObject);
        firePropertyChanged(CONDITIONAL_STATE_OBJECT_PROPERTY, oldConditionalStateObject, conditionalStateObject);
    }

    protected boolean shouldEncapsulateORExpression(StateObject stateObject) {

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

        final boolean[] encapsulate = { false };

        StateObjectVisitor visitor = new AbstractStateObjectVisitor() {
            @Override
            public void visit(OrExpressionStateObject stateObject) {
                encapsulate[0] = true;
            }
        };

        stateObject.accept(visitor);
        return encapsulate[0];
    }

    /**
     * {@inheritDoc}
     */
    @Override
    protected void toTextInternal(Appendable writer) throws IOException {

        writer.append(getIdentifier());

        if (conditionalStateObject != null) {
            writer.append(SPACE);
            conditionalStateObject.toString(writer);
        }
    }
}
