/*
 * 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.Assert;
import org.eclipse.persistence.jpa.jpql.parser.JPQLExpression;
import org.eclipse.persistence.jpa.jpql.parser.JPQLGrammar;
import org.eclipse.persistence.jpa.jpql.tools.model.IJPQLQueryBuilder;
import org.eclipse.persistence.jpa.jpql.tools.spi.IManagedTypeProvider;

/**
 * This is the root of the {@link StateObject} hierarchy that represents a JPQL query. The only
 * child of this state object is one of the three possible query statements.
 *
 * @see DeleteStatementStateObject
 * @see SelectStatementStateObject
 * @see UpdateStatementStateObject
 * @see org.eclipse.persistence.jpa.jpql.parser.JPQLExpression JPQLExpression
 *
 * @version 2.4
 * @since 2.4
 * @author Pascal Filion
 */
@SuppressWarnings("nls")
public class JPQLQueryStateObject extends AbstractStateObject {

    /**
     * The external form of a provider that gives access to the JPA metadata.
     */
    private IManagedTypeProvider provider;

    /**
     * The builder that can create any {@link StateObject} for any given JPQL fragment.
     */
    private IJPQLQueryBuilder queryBuilder;

    /**
     * One of the possible three statements.
     */
    private StateObject queryStatement;

    /**
     * Notifies the query statement has changed.
     */
    public static final String QUERY_STATEMENT_PROPERTY = "statement";

    /**
     * Creates a new <code>JPQLQueryStateObject</code>.
     *
     * @param queryBuilder The builder that can create any {@link StateObject} for any given JPQL
     * fragment when using a parse method
     * @param provider The external form of a provider that gives access to the JPA metadata
     * @exception NullPointerException If one of the given arguments is <code>null</code>
     */
    public JPQLQueryStateObject(IJPQLQueryBuilder queryBuilder, IManagedTypeProvider provider) {
        super(null);
        initialize(queryBuilder, provider);
    }

    /**
     * {@inheritDoc}
     */
    public void accept(StateObjectVisitor visitor) {
        visitor.visit(this);
    }

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

    /**
     * Changes the query statement to be a <code><b>DELETE</b></code> statement.
     *
     * @return The new root object of the <code><b>DELETE</b></code> statement
     */
    public DeleteStatementStateObject addDeleteStatement() {
        DeleteStatementStateObject stateObject = new DeleteStatementStateObject(this);
        setQueryStatement(stateObject);
        return stateObject;
    }

    /**
     * Changes the query statement to be a <code><b>SELECT</b></code> statement. The <code><b>SELECT</b></code>
     * clause will have the <code><b>DISTINCT</b></code> identifier set.
     *
     * @return The new root object of the <code><b>SELECT</b></code> statement
     */
    public SelectStatementStateObject addDistinctSelectStatement() {
        SelectStatementStateObject stateObject = new SelectStatementStateObject(this);
        stateObject.getSelectClause().toggleDistinct();
        setQueryStatement(stateObject);
        return stateObject;
    }

    /**
     * Changes the query statement to be a <code><b>SELECT</b></code> statement.
     *
     * @return The new root object of the <code><b>SELECT</b></code> statement
     */
    public SelectStatementStateObject addSelectStatement() {
        SelectStatementStateObject stateObject = new SelectStatementStateObject(this);
        setQueryStatement(stateObject);
        return stateObject;
    }

    /**
     * Changes the query statement to be a <code><b>SELECT</b></code> statement.
     *
     * @param jpqlFragment The portion of the query representing the select items, excluding the
     * <code><b>SELECT</b></code> identifier
     * @return The new root object of the <code><b>SELECT</b></code> statement
     */
    public SelectStatementStateObject addSelectStatement(String jpqlFragment) {
        SelectStatementStateObject stateObject = new SelectStatementStateObject(this);
        stateObject.getSelectClause().parse(jpqlFragment);
        setQueryStatement(stateObject);
        return stateObject;
    }

    /**
     * Changes the query statement to be a <code><b>UPDATE</b></code> statement.
     *
     * @return The new root object of the <code><b>UPDTE</b></code> statement
     */
    public UpdateStatementStateObject addUpdateStatement() {
        UpdateStatementStateObject stateObject = new UpdateStatementStateObject(this);
        setQueryStatement(stateObject);
        return stateObject;
    }

    /**
     * Changes the query statement to be a <code><b>UPDATE</b></code> statement.
     *
     * @param jpqlFragment The portion of the query representing the select items, excluding the
     * <code><b>UPDATE</b></code> identifier
     * @return The new root object of the <code><b>UPDATE</b></code> statement
     */
    public UpdateStatementStateObject addUpdateStatement(String jpqlFragment) {
        UpdateStatementStateObject stateObject = new UpdateStatementStateObject(this);
        stateObject.getModifyClause().parse(jpqlFragment);
        setQueryStatement(stateObject);
        return stateObject;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    protected StateObject checkParent(StateObject parent) {
        return parent;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public DeclarationStateObject getDeclaration() {
        return (queryStatement == null) ? null : queryStatement.getDeclaration();
    }

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

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

    /**
     * {@inheritDoc}
     */
    @Override
    public IManagedTypeProvider getManagedTypeProvider() {
        return provider;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public StateObject getParent() {
        return null;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public IJPQLQueryBuilder getQueryBuilder() {
        return queryBuilder;
    }

    /**
     * Returns the only child of this state object, which represents one of the three query statement.
     *
     * @return The state object representing the query statement, which was created based on the
     * query type
     */
    public StateObject getQueryStatement() {
        return queryStatement;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public JPQLQueryStateObject getRoot() {
        return this;
    }

    /**
     * Determines whether a query statement has been defined.
     *
     * @return <code>true</code> if there is a query statement defined; <code>false</code> otherwise
     */
    public boolean hasQueryStatement() {
        return queryStatement != null;
    }

    /**
     * Initializes this <code>JPQLQueryStateObject</code>.
     *
     * @param queryBuilder The builder that can create any {@link StateObject} for any given JPQL fragment
     * @param provider The external form that gives access to the JPA metadata
     * @exception NullPointerException If one of the given arguments is <code>null</code>
     */
    protected void initialize(IJPQLQueryBuilder queryBuilder, IManagedTypeProvider provider) {

        Assert.isNotNull(queryBuilder, "IJPQLQueryBuilder cannot be null");
        Assert.isNotNull(provider,     "IManagedTypeProvider cannot be null");

        this.provider     = provider;
        this.queryBuilder = queryBuilder;
    }

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

        if (super.isEquivalent(stateObject)) {
            JPQLQueryStateObject jpqlStateObject = (JPQLQueryStateObject) stateObject;
            return areEquivalent(queryStatement, jpqlStateObject.queryStatement);
        }

        return false;
    }

    /**
     * Parses the given JPQL fragment as this state object's query statement.
     *
     * @param jpqlFragment The portion of the query to parse
     * @param queryBNFId The unique identifier of the BNF that determines how to parse the fragment
     */
    public void parse(CharSequence jpqlFragment, String queryBNFId) {
        StateObject stateObject = buildStateObject(jpqlFragment, queryBNFId);
        setQueryStatement(stateObject);
    }

    /**
     * Keeps a reference of the {@link JPQLExpression parsed object} object, which should only be
     * done when this object is instantiated during the conversion of a parsed JPQL query into
     * {@link StateObject StateObjects}.
     *
     * @param expression The {@link JPQLExpression parsed object} representing the JPQL query
     */
    public void setExpression(JPQLExpression expression) {
        super.setExpression(expression);
    }

    /**
     * Sets the only child of this state object, which represents one of the three query statement.
     *
     * @param queryStatement The state object representing the query statement, which was created
     * based on the query type
     */
    public void setQueryStatement(StateObject queryStatement) {
        StateObject oldStatement = this.queryStatement;
        this.queryStatement = parent(queryStatement);
        firePropertyChanged(QUERY_STATEMENT_PROPERTY, oldStatement, queryStatement);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    protected void toTextInternal(Appendable writer) throws IOException {
        if (queryStatement != null) {
            queryStatement.toString(writer);
        }
    }
}
