/*
 * 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.AbstractSelectStatement;
import org.eclipse.persistence.jpa.jpql.tools.spi.IEntity;
import org.eclipse.persistence.jpa.jpql.utility.iterable.ListIterable;
import static org.eclipse.persistence.jpa.jpql.parser.AbstractExpression.*;

/**
 * This state object represents the select statement, which has at least a <code><b>SELECT</b></code>
 * clause and a <code><b>FROM</b></code> clause.
 *
 * @see SelectStatementStateObject
 * @see FromClauseStateObject
 * @see GroupByClauseStateObject
 * @see HavingClauseStateObject
 * @see SelectClauseStateObject
 * @see WhereClauseStateObject
 *
 * @see AbstractSelectStatement
 *
 * @version 2.5
 * @since 2.4
 * @author Pascal Filion
 */
@SuppressWarnings("nls")
public abstract class AbstractSelectStatementStateObject extends AbstractStateObject {

    /**
     * The state object representing the <code><b>FROM</b></code> clause.
     */
    private AbstractFromClauseStateObject fromClause;

    /**
     * The state object representing the <code><b>GROUP BY</b></code> clause.
     */
    private GroupByClauseStateObject groupByClause;

    /**
     * The state object representing the <code><b>HAVING</b></code> clause.
     */
    private HavingClauseStateObject havingClause;

    /**
     * The state object representing the <code><b>SELECT</b></code> clause.
     */
    private AbstractSelectClauseStateObject selectClause;

    /**
     * The state object representing the <code><b>WHERE</b></code> clause.
     */
    private WhereClauseStateObject whereClause;

    /**
     * Notify the state object representing the <code><b>GROUP BY</b></code> clause has changed.
     */
    public static final String GROUP_BY_CLAUSE_PROPERTY = "groupByClause";

    /**
     * Notify the state object representing the <code><b>HAVING</b></code> clause has changed.
     */
    public static final String HAVING_CLAUSE_PROPERTY = "havingClause";

    /**
     * Notify the state object representing the <code><b>WHERE</b></code> clause has changed.
     */
    public static final String WHERE_CLAUSE_PROPERTY = "whereClause";

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

    /**
     * {@inheritDoc}
     */
    @Override
    protected void addChildren(List<StateObject> children) {

        super.addChildren(children);
        children.add(selectClause);
        children.add(fromClause);

        if (whereClause != null) {
            children.add(whereClause);
        }

        if (groupByClause != null) {
            children.add(groupByClause);
        }

        if (havingClause != null) {
            children.add(havingClause);
        }
    }

    /**
     * Adds a new collection declaration to the <code><b>FROM</b></code> clause.
     *
     * @return The {@link CollectionMemberDeclarationStateObject} representing the collection
     * declaration
     */
    public CollectionMemberDeclarationStateObject addCollectionDeclaration() {
        return getFromClause().addCollectionDeclaration();
    }

    /**
     * Adds a new collection declaration to the <code><b>FROM</b></code> clause.
     *
     * @param collectionValuedPath The collection-valued path expression
     * @param identificationVariable The variable defining the collection-valued path expression
     * @return The {@link CollectionMemberDeclarationStateObject} representing the collection
     * declaration
     */
    public CollectionMemberDeclarationStateObject addCollectionDeclaration(String collectionValuedPath,
                                                                           String identificationVariable) {

        return getFromClause().addCollectionDeclaration(collectionValuedPath, identificationVariable);
    }

    /**
     * Adds the <code><b>GROUP BY</b></code> clause. The clause is not added if it's already present.
     *
     * @return The {@link GroupByClauseStateObject}
     */
    public GroupByClauseStateObject addGroupByClause() {
        if (!hasGroupByClause()) {
            setGroupByClause(new GroupByClauseStateObject(this));
        }
        return groupByClause;
    }

    /**
     * Adds the <code><b>GROUP BY</b></code> clause and parses the given JPQL fragment. The clause is
     * not added if it's already present.
     *
     * @param jpqlFragment The fragment of the JPQL to parse that represents the group by items, the
     * fragment cannot start with <code><b>GROUP BY</b></code>
     * @return The {@link GroupByClauseStateObject}
     */
    public GroupByClauseStateObject addGroupByClause(String jpqlFragment) {
        GroupByClauseStateObject stateObject = addGroupByClause();
        stateObject.parse(jpqlFragment);
        return stateObject;
    }

    /**
     * Adds the <code><b>HAVING</b></code> clause. The clause is not added if it's already present.
     *
     * @return The {@link GroupByClauseStateObject}
     */
    public HavingClauseStateObject addHavingClause() {
        if (!hasHavingClause()) {
            setHavingClause(new HavingClauseStateObject(this));
        }
        return havingClause;
    }

    /**
     * Adds the <code><b>HAVING</b></code> clause and parses the given JPQL fragment. The clause is
     * not added if it's already present.
     *
     * @param jpqlFragment The fragment of the JPQL to parse that represents the conditional expression,
     * the fragment cannot start with <code><b>HAVING</b></code>
     * @return The {@link HavingClauseStateObject}
     */
    public HavingClauseStateObject addHavingClause(String jpqlFragment) {
        HavingClauseStateObject stateObject = addHavingClause();
        stateObject.parse(jpqlFragment);
        return stateObject;
    }

    /**
     * Adds a new range variable declaration to the <code><b>FROM</b></code> clause.
     *
     * @return The {@link StateObject} representing the new range variable declaration
     */
    public IdentificationVariableDeclarationStateObject addRangeDeclaration() {
        return getFromClause().addRangeDeclaration();
    }

    /**
     * Adds to this select statement a new range variable declaration.
     *
     * @param entity The external form of the entity to add to the declaration list
     * @param identificationVariable The unique identifier identifying the given {@link IEntity}
     * @return The {@link StateObject} representing the new range variable declaration
     */
    public IdentificationVariableDeclarationStateObject addRangeDeclaration(IEntity entity,
                                                                            String identificationVariable) {

        return getFromClause().addRangeDeclaration(entity, identificationVariable);
    }

    /**
     * Adds to this select statement a new range variable declaration.
     *
     * @param entityName The name of the entity
     * @param identificationVariable The unique identifier identifying the entity
     * @return The {@link StateObject} representing the range variable declaration
     */
    public IdentificationVariableDeclarationStateObject addRangeDeclaration(String entityName,
                                                                            String identificationVariable) {

        return getFromClause().addRangeDeclaration(entityName, identificationVariable);
    }

    /**
     * Adds the <code><b>WHERE</b></code> clause. The clause is not added if it's already present.
     *
     * @return The {@link GroupByClauseStateObject}
     */
    public WhereClauseStateObject addWhereClause() {
        if (!hasWhereClause()) {
            setWhereClause(new WhereClauseStateObject(this));
        }
        return whereClause;
    }

    /**
     * Adds the <code><b>WHERE</b></code> clause and parses the given JPQL fragment. The clause is
     * not added if it's already present.
     *
     * @param jpqlFragment The fragment of the JPQL to parse that represents the conditional expression,
     * the fragment cannot start with <code><b>WHERE</b></code>
     * @return The {@link WhereClauseStateObject}
     */
    public WhereClauseStateObject addWhereClause(String jpqlFragment) {
        WhereClauseStateObject stateObject = addWhereClause();
        stateObject.parse(jpqlFragment);
        return stateObject;
    }

    /**
     * Creates the state object representing the <code><b>FROM</b></code> clause.
     *
     * @return A concrete instance of {@link AbstractFromClauseStateObject}
     */
    protected abstract AbstractFromClauseStateObject buildFromClause();

    /**
     * Creates the state object representing the <code><b>SELECT</b></code> clause.
     *
     * @return A concrete instance of {@link AbstractSelectClauseStateObject}
     */
    protected abstract AbstractSelectClauseStateObject buildSelectClause();

    /**
     * Returns the list of {@link VariableDeclarationStateObject} defining the variable declarations,
     * which are mapping an entity to a variable or a collection-valued member to a variable.
     * <p>
     * Example:
     * <ul>
     * <li><code>Employee e</code></li>
     * <li><code>IN (e.employees) AS emps</code></li>
     * </ul>
     *
     * @return The list of {@link VariableDeclarationStateObject}
     */
    public ListIterable<? extends VariableDeclarationStateObject> declarations() {
        return fromClause.items();
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public IdentificationVariableStateObject findIdentificationVariable(String identificationVariable) {
        return fromClause.findIdentificationVariable(identificationVariable);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public DeclarationStateObject getDeclaration() {
        return fromClause;
    }

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

    /**
     * Returns the state object representing the <code><b>FROM</b></code> clause.
     *
     * @return The state object representing the <code><b>FROM</b></code> clause, which is never
     * <code>null</code>
     */
    public AbstractFromClauseStateObject getFromClause() {
        return fromClause;
    }

    /**
     * Returns the state object representing the <code><b>GROUP BY</b></code> clause.
     *
     * @return Either the actual state object representing the <code><b>GROUP BY</b></code> clause or
     * <code>null</code> if it's not present
     */
    public GroupByClauseStateObject getGroupByClause() {
        return groupByClause;
    }

    /**
     * Returns the state object representing the <code><b>HAVING</b></code> clause.
     *
     * @return Either the actual state object representing the <code><b>HAVING</b></code> clause or
     * <code>null</code> if it's not present
     */
    public HavingClauseStateObject getHavingClause() {
        return havingClause;
    }

    /**
     * Returns the state object representing the <code><b>SELECT</b></code> clause.
     *
     * @return Either the actual state object representing the <code><b>SELECT</b></code> clause,
     * which is never <code>null</code>
     */
    public AbstractSelectClauseStateObject getSelectClause() {
        return selectClause;
    }

    /**
     * Returns the state object representing the <code><b>WHERE</b></code> clause.
     *
     * @return Either the actual state object representing the <code><b>WHERE</b></code> clause or
     * the <code>null</code> state object since <code>null</code> is never returned
     */
    public WhereClauseStateObject getWhereClause() {
        return whereClause;
    }

    /**
     * Returns the state object representing the <code><b>GROUP BY</b></code> clause.
     *
     * @return Either the actual state object representing the <code><b>GROUP BY</b></code> clause or
     * <code>null</code> if it's not present
     */
    public boolean hasGroupByClause() {
        return groupByClause != null;
    }

    /**
     * Returns the state object representing the <code><b>HAVING</b></code> clause.
     *
     * @return Either the actual state object representing the <code><b>HAVING</b></code> clause  or
     * <code>null</code> if it's not present
     */
    public boolean hasHavingClause() {
        return havingClause != null;
    }

    /**
     * Returns the state object representing the <code><b>WHERE</b></code> clause.
     *
     * @return Either the actual state object representing the <code><b>WHERE</b></code> clause or
     * <code>null</code> if it's not present
     */
    public boolean hasWhereClause() {
        return whereClause != null;
    }

    /**
     * Returns the {@link IdentificationVariableStateObject IdentificationVariableStateObjects}
     * holding onto the identification variables, which are the variables defined in the
     * <code><b>FROM</b></code> clause.
     * <p>
     * Example:
     * <ul>
     * <li><code>Employee e</code>; <i>e</i> is returned</li>
     * <li><code>IN (e.employees) AS emps</code>; <i>emps</i> is returned</li>
     * <li><code>Manager m JOIN m.employees emps</code>; <i>m</i> and <i>emps</i> are returned</li>
     * </ul>
     *
     * @return The list of {@link IdentificationVariableStateObject IdentificationVariableStateObjects}
     */
    public Iterable<IdentificationVariableStateObject> identificationVariables() {
        return fromClause.identificationVariables();
    }

    /**
     * {@inheritDoc}
     */
    @Override
    protected void initialize() {
        super.initialize();
        selectClause = buildSelectClause();
        fromClause   = buildFromClause();
    }

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

        if (super.isEquivalent(stateObject)) {
            AbstractSelectStatementStateObject select = (AbstractSelectStatementStateObject) stateObject;
            return areEquivalent(selectClause,  select.selectClause)  &&
                   areEquivalent(fromClause,    select.fromClause)    &&
                   areEquivalent(fromClause,    select.fromClause)    &&
                   areEquivalent(whereClause,   select.whereClause)   &&
                   areEquivalent(groupByClause, select.groupByClause) &&
                   areEquivalent(havingClause,  select.havingClause);
        }

        return false;
    }

    /**
     * Parses the given JPQL fragment and create the select item. For the top-level query, the
     * fragment can contain several select items but for a subquery, it can represent only one.
     *
     * @param jpqlFragment The portion of the query representing one or several select items
     */
    public void parseSelect(String jpqlFragment) {
        getSelectClause().parse(jpqlFragment);
    }

    /**
     * Removes the <code><b>GROUP BY</b></code> clause.
     */
    public void removeGroupByClause() {
        setGroupByClause(null);
    }

    /**
     * Removes the <code><b>HAVING</b></code> clause.
     */
    public void removeHavingClause() {
        setHavingClause(null);
    }

    /**
     * Removes the <code><b>WHERE</b></code> clause.
     */
    public void removeWhereClause() {
        setWhereClause(null);
    }

    private void setGroupByClause(GroupByClauseStateObject groupByClause) {
        GroupByClauseStateObject oldGroupByClause = this.groupByClause;
        this.groupByClause = groupByClause;
        firePropertyChanged(GROUP_BY_CLAUSE_PROPERTY, oldGroupByClause, groupByClause);
    }

    private void setHavingClause(HavingClauseStateObject havingClause) {
        HavingClauseStateObject oldHavingClause = this.havingClause;
        this.havingClause = havingClause;
        firePropertyChanged(HAVING_CLAUSE_PROPERTY, oldHavingClause, havingClause);
    }

    private void setWhereClause(WhereClauseStateObject whereClause) {
        WhereClauseStateObject oldWhereClause = this.whereClause;
        this.whereClause = whereClause;
        firePropertyChanged(WHERE_CLAUSE_PROPERTY, oldWhereClause, whereClause);
    }

    /**
     * Either adds or removes the state object representing the <code><b>GROUP BY</b></code> clause.
     */
    public void toggleGroupByClause() {
        if (hasGroupByClause()) {
            removeGroupByClause();
        }
        else {
            addGroupByClause();
        }
    }

    /**
     * Either adds or removes the state object representing the <code><b>HAVING</b></code> clause.
     */
    public void toggleHavingClause() {
        if (hasHavingClause()) {
            removeHavingClause();
        }
        else {
            addHavingClause();
        }
    }

    /**
     * Either adds or removes the state object representing the <code><b>WHERE</b></code> clause.
     */
    public void toggleWhereClause() {
        if (hasWhereClause()) {
            removeWhereClause();
        }
        else {
            addWhereClause();
        }
    }

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

        selectClause.toString(writer);
        writer.append(SPACE);
        fromClause.toString(writer);

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

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

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