/*******************************************************************************
 * Copyright (c) 2011, 2014 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.OrderByItem;
import org.eclipse.persistence.jpa.jpql.parser.OrderByItem.Ordering;
import org.eclipse.persistence.jpa.jpql.parser.OrderByItemBNF;
import static org.eclipse.persistence.jpa.jpql.parser.AbstractExpression.*;

/**
 * An <code><b>ORDER BY</b></code> item must be one of the following:
 * <ol>
 * <li> A {@link StateFieldPathExpressionStateObject state_field_path_expression} that evaluates to
 * an orderable state field of an entity or embeddable class abstract schema type designated in the
 * <code><b>SELECT</b></code> clause by one of the following:
 *   <ul>
 *   <li>A general_identification_variable
 *   <li>A single_valued_object_path_expression
 *   </ul>
 * <li>A {@link StateFieldPathExpressionStateObject state_field_path_expression} that evaluates to
 * the same state field of the same entity or embeddable abstract schema type as a {@link
 * StateFieldPathExpressionStateObject state_field_path_expression} in the <b>SELECT</b> clause
 * <li>A {@link ResultVariableStateObject result_variable} that refers to an orderable item in the
 * <code><b>SELECT</b></code> clause for which the same {@link ResultVariableStateObject result_variable}
 * has been specified. This may be the result of an aggregate_expression, a <code>scalar_expression</code>,
 * or a {@link StateFieldPathExpressionStateObject state_field_path_expression} in the
 * <code><b>SELECT</b></code> clause.
 * </ol>
 * <p>
 * The keyword <code><b>ASC</b></code> specifies that ascending ordering be used for the associated
 * <code><b>ORDER BY</b></code> item; the keyword <code><b>DESC</b></code> specifies that descending
 * ordering be used. Ascending ordering is the default.
 * <p>
 * JPA 1.0: <div><b>BNF:</b> <code>orderby_item ::= state_field_path_expression [ ASC | DESC ]</code></div>
 * <p>
 * JPA 2.0 <div><b>BNF:</b> <code>orderby_item ::= state_field_path_expression | result_variable [ ASC | DESC ]</code><p></div>
 *
 * @see OrderByItem
 *
 * @version 2.4
 * @since 2.4
 * @author Pascal Filion
 */
@SuppressWarnings("nls")
public class OrderByItemStateObject extends AbstractStateObject {

	/**
	 * The ascending ordering.
	 */
	private Ordering ordering;

	/**
	 * The {@link StateObject} representing the order by item.
	 */
	private StateObject stateObject;

	/**
	 * Notifies the ordering property has changed.
	 */
	public static final String ORDERING_PROPERTY = "ordering";

	/**
	 * Notifies the ordering property has changed.
	 */
	public static final String STATE_OBJECT_PROPERTY = "stateObject";

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

	/**
	 * Creates a new <code>OrderByItemStateObject</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>
	 */
	public OrderByItemStateObject(OrderByClauseStateObject parent, Ordering ordering) {
		super(parent);
		validateOrdering(ordering);
		this.ordering = ordering;
	}

	/**
	 * Creates a new <code>OrderByItemStateObject</code>.
	 *
	 * @param parent The parent of this state object, which cannot be <code>null</code>
	 * @param stateObject The {@link StateObject} of the item
	 * @param ordering One of the possible {@link Ordering} choice
	 * @exception NullPointerException The given parent cannot be <code>null</code>
	 */
	public OrderByItemStateObject(OrderByClauseStateObject parent,
	                              StateObject stateObject,
	                              Ordering ordering) {

		this(parent, ordering);
		this.stateObject = parent(stateObject);
	}

	/**
	 * Creates a new <code>OrderByItemStateObject</code>.
	 *
	 * @param parent The parent of this state object, which cannot be <code>null</code>
	 * @param jpqlFragment The JPQL fragment representing the ordering item
	 * @param ordering One of the possible {@link Ordering} choice
	 * @exception NullPointerException The given parent cannot be <code>null</code>
	 */
	public OrderByItemStateObject(OrderByClauseStateObject parent,
	                              String jpqlFragment,
	                              Ordering ordering) {

		this(parent, ordering);
		parse(jpqlFragment);
	}

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

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

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

	/**
	 * Returns the enum constant representing the ordering type.
	 *
	 * @return The constant representing the ordering, in the case the ordering was not parsed, then
	 * {@link Ordering#DEFAULT} is returned
	 */
	public Ordering getOrdering() {
		return ordering;
	}

	/**
	 * {@inheritDoc}
	 */
	@Override
	public OrderByClauseStateObject getParent() {
		return (OrderByClauseStateObject) super.getParent();
	}

	/**
	 * Returns the {@link StateObject} representing the value used for ordering.
	 *
	 * @return The {@link StateObject} representing the value used for ordering
	 */
	public StateObject getStateObject() {
		return stateObject;
	}

	/**
	 * Determines whether the {@link StateObject} representing the value used for ordering has been
	 * defined.
	 *
	 * @return <code>true</code> if the ordering value is defined; <code>false</code> otherwise
	 */
	public boolean hasStateObject() {
		return stateObject != null;
	}

	/**
	 * Determines whether the ordering was specified as being ascendant.
	 *
	 * @return <code>true</code> if <b>ASC</b> was parsed; <code>false</code> otherwise
	 */
	public boolean isAscending() {
		return ordering == Ordering.ASC;
	}

	/**
	 * Determines whether the ordering was not specified.
	 *
	 * @return <code>true</code> if no ordering was parsed; <code>false</code> otherwise
	 */
	public boolean isDefault() {
		return ordering == Ordering.DEFAULT;
	}

	/**
	 * Determines whether the ordering was specified as being descendant.
	 *
	 * @return <code>true</code> if <b>DESC</b> was parsed; <code>false</code> otherwise
	 */
	public boolean isDescending() {
		return ordering == Ordering.DESC;
	}

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

		if (super.isEquivalent(stateObject)) {
			OrderByItemStateObject orderByItem = (OrderByItemStateObject) stateObject;
			return ordering == orderByItem.ordering &&
			       areEquivalent(stateObject, orderByItem.stateObject);
		}

		return false;
	}

	/**
	 * Parses the given JPQL fragment, which represents either a state-field pathe expression or a
	 * result variable, and creates the {@link StateObject}.
	 *
	 * @param jpqlFragment The portion of the query representing a state-field path expression or
	 * result variable
	 */
	public void parse(String jpqlFragment) {
		StateObject stateObject = buildStateObject(jpqlFragment, OrderByItemBNF.ID);
		setStateObject(stateObject);
	}

	/**
	 * Removes the ordering if it's specified, otherwise do nothing.
	 */
	public void removeOrdering() {
		setOrdering(Ordering.DEFAULT);
	}

	/**
	 * Keeps a reference of the {@link OrderByItem 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 OrderByItem parsed object} representing an order by item
	 */
	public void setExpression(OrderByItem expression) {
		super.setExpression(expression);
	}

	/**
	 * Sets the enum constant representing the ordering type.
	 *
	 * @param ordering The constant representing the ordering, in the case the ordering was not
	 * parsed, then {@link Ordering#DEFAULT} should be used
	 */
	public void setOrdering(Ordering ordering) {

		validateOrdering(ordering);

		Ordering oldOrdering = this.ordering;
		this.ordering = ordering;
		firePropertyChanged(ORDERING_PROPERTY, oldOrdering, ordering);
	}

	/**
	 * Sets the {@link StateObject} representing the value used for ordering.
	 *
	 * @param stateObject The {@link StateObject} representing the value used for ordering
	 */
	public void setStateObject(StateObject stateObject) {
		StateObject oldStateObject = this.stateObject;
		this.stateObject = parent(stateObject);
		firePropertyChanged(STATE_OBJECT_PROPERTY, oldStateObject, stateObject);
	}

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

		if (stateObject != null) {
			stateObject.toString(writer);
		}

		if (!isDefault()) {
			writer.append(SPACE);
			writer.append(ordering.name());
		}
	}

	private void validateOrdering(Ordering ordering) {
		Assert.isNotNull(ordering, "The Ordering cannot be null");
	}
}