/*******************************************************************************
 * Copyright (c) 2011, 2013 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 v1.0 and Eclipse Distribution License v. 1.0
 * which accompanies this distribution.
 * The Eclipse Public License is available at http://www.eclipse.org/legal/epl-v10.html
 * and the Eclipse Distribution License is available at
 * http://www.eclipse.org/org/documents/edl-v10.php.
 *
 * 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);
		}
	}
}