/*******************************************************************************
 * 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 org.eclipse.persistence.jpa.jpql.Assert;
import org.eclipse.persistence.jpa.jpql.parser.FunctionsReturningStringsBNF;
import org.eclipse.persistence.jpa.jpql.parser.JPQLExpression;
import org.eclipse.persistence.jpa.jpql.parser.LiteralBNF;
import org.eclipse.persistence.jpa.jpql.parser.StringPrimaryBNF;
import org.eclipse.persistence.jpa.jpql.parser.TrimExpression;
import org.eclipse.persistence.jpa.jpql.parser.TrimExpression.Specification;
import static org.eclipse.persistence.jpa.jpql.parser.AbstractExpression.*;
import static org.eclipse.persistence.jpa.jpql.parser.Expression.*;

/**
 * The <code><b>TRIM</b></code> function trims the specified character from a string. If the
 * character to be trimmed is not specified, it is assumed to be space (or blank). The optional
 * <code>trim_character</code> is a single-character string literal or a character-valued input
 * parameter (i.e., char or <code>Character</code>). If a trim specification is not provided,
 * <code><b>BOTH</b></code> is assumed. The <code><b>TRIM</b></code> function returns the trimmed
 * string.
 *
 * <div><b>BNF:</b> <code>expression ::= TRIM([[trim_specification] [trim_character] FROM] string_primary)</code><p></div>
 *
 * @see TrimExpression
 *
 * @version 2.5
 * @since 2.4
 * @author Pascal Filion
 */
@SuppressWarnings({"nls", "unused"}) // unused used for the import statement: see bug 330740
public class TrimExpressionStateObject extends AbstractSingleEncapsulatedExpressionStateObject {

	/**
	 * Defines the way the string is trimmed.
	 */
	private Specification specification;

	/**
	 * The trim character, if specified.
	 */
	private StateObject trimCharacter;

	/**
	 * Notifies the visibility of the <code><b>FROM</b></code> identifier has changed.
	 */
	public static final String HAS_FROM_PROPERTY = "hasFrom";

	/**
	 * Notifies the specification property has changed.
	 */
	public static final String SPECIFICATION_PROPERTY = "specification";

	/**
	 * Notify the state object representing the trim character has changed.
	 */
	public static final String TRIM_CHARACTER_PROPERTY = "trimCharacterStateObject";

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

	/**
	 * Creates a new <code>TrimExpressionStateObject</code>.
	 *
	 * @param parent The parent of this state object, which cannot be <code>null</code>
	 * @param stateObject The {@link StateObject} representing the encapsulated expression
	 * @param specification Defines the way the string is trimmed, or {@link
     * org.eclipse.persistence.jpa.jpql.parser.TrimExpression.Specification#DEFAULT
     * Specification.DEFAULT} when it is not present
	 * @exception NullPointerException The given parent cannot be <code>null</code>
	 */
	public TrimExpressionStateObject(StateObject parent,
	                                 Specification specification,
	                                 StateObject stateObject) {

		this(parent, specification, null, stateObject);
	}

	/**
	 * Creates a new <code>TrimExpressionStateObject</code>.
	 *
	 * @param parent The parent of this state object, which cannot be <code>null</code>
	 * @param specification Defines the way the string is trimmed, or {@link
     * org.eclipse.persistence.jpa.jpql.parser.TrimExpression.Specification#DEFAULT
     * Specification.DEFAULT} when it is not present
	 * @param trimCharacter The trim character
	 * @param stateObject The {@link StateObject} representing the encapsulated expression
	 * @exception NullPointerException The given parent cannot be <code>null</code>
	 */
	public TrimExpressionStateObject(StateObject parent,
	                                 Specification specification,
	                                 StateObject trimCharacter,
	                                 StateObject stateObject) {

		super(parent, stateObject);
		this.specification = specification;
		this.trimCharacter = parent(trimCharacter);
	}

	/**
	 * Creates a new <code>TrimExpressionStateObject</code>.
	 *
	 * @param parent The parent of this state object, which cannot be <code>null</code>
	 * @param jpqlFragment The portion of the query representing the encapsulated expression
	 * @exception NullPointerException The given parent cannot be <code>null</code>
	 */
	public TrimExpressionStateObject(StateObject parent, String jpqlFragment) {
		super(parent, jpqlFragment);
	}

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

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

	/**
	 * {@inheritDoc}
	 */
	@Override
	public String getIdentifier() {
		return TRIM;
	}

	/**
	 * {@inheritDoc}
	 */
	@Override
	protected String getQueryBNFId() {
		return StringPrimaryBNF.ID;
	}

	/**
	 * Returns the new trim specification.
	 *
	 * @return The new trim specification; which is never <code>null</code>
	 */
	public Specification getSpecification() {
		return specification;
	}

	/**
	 * Returns the {@link StateObject} representing the trim character.
	 *
	 * @return The {@link StateObject} representing the trim character or <code>null</code> if it is
	 * not present
	 */
	public StateObject getTrimCharacter() {
		return trimCharacter;
	}

	/**
	 * Determines whether the way the trim is trimmed was parsed.
	 *
	 * @return <code>true</code> if the query contained the way the trim needs to be trimmed;
	 * <code>false</code> otherwise
	 */
	public boolean hasSpecification() {
		return (specification != Specification.DEFAULT);
	}

	/**
	 * Determines whether the character used to trim the string was specified.
	 *
	 * @return <code>true</code> if the character used for trimming was specified; <code>false</code>
	 * otherwise
	 */
	public boolean hasTrimCharacter() {
		return trimCharacter != null;
	}

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

		if (super.isEquivalent(stateObject)) {
			TrimExpressionStateObject trim = (TrimExpressionStateObject) stateObject;
			return specification == trim.specification &&
			       areEquivalent(trimCharacter, trim.trimCharacter);
		}

		return false;
	}

	/**
	 * {@inheritDoc}
	 */
	@Override
	public void parse(String jpqlFragment) {

		StringBuilder sb = new StringBuilder();
		sb.append(TRIM);
		sb.append(LEFT_PARENTHESIS);
		sb.append(jpqlFragment);
		sb.append(RIGHT_PARENTHESIS);

		JPQLExpression jpqlExpression = new JPQLExpression(
			sb,
			getGrammar(),
			FunctionsReturningStringsBNF.ID,
			true
		);

		TrimExpression trimExpression = (TrimExpression) jpqlExpression.getQueryStatement();
		setSpecification(trimExpression.getSpecification());
		parseTrimCharacter(trimExpression.getTrimCharacter().toParsedText());
		super.parse(trimExpression.getExpression().toParsedText());

		// The trim character is actually the string primary
		if (!hasStateObject() && hasTrimCharacter()) {
			setStateObject(new StringLiteralStateObject(this, trimCharacter.toString()));
			trimCharacter = null;
		}
	}

	/**
	 * Parses the given JPQL fragment, which represents either a single-character string literal or a
	 * character-valued input parameter, the fragment will be parsed and converted into a {@link
	 * StateObject}.
	 *
	 * @param jpqlFragment The portion of the query to parse
	 */
	public void parseTrimCharacter(CharSequence jpqlFragment) {
		StateObject stateObject = buildStateObject(jpqlFragment, LiteralBNF.ID);
		setTrimCharacter(stateObject);
	}

	/**
	 * Removes the trim specification.
	 */
	public void removeSpecification() {
		setSpecification(Specification.DEFAULT);
	}

	/**
	 * Removes the trim character if it is defined.
	 */
	public void removeTrimCharacter() {
		setTrimCharacter(null);
	}

	/**
	 * Keeps a reference of the {@link TrimExpression 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 TrimExpression parsed object} representing a <code><b>TRIM</b></code>
	 * expression
	 */
	public void setExpression(TrimExpression expression) {
		super.setExpression(expression);
	}

	/**
	 * Sets the new trim specification.
	 *
	 * @param specification The new trim specification; <code>null</code> is not valid
	 */
	public void setSpecification(Specification specification) {
		Assert.isNotNull(specification, "The Specification cannot be null");
		Specification oldSpecification = this.specification;
		this.specification = specification;
		firePropertyChanged(SPECIFICATION_PROPERTY, oldSpecification, specification);
	}

	/**
	 * {@inheritDoc}
	 */
	@Override
	public void setStateObject(StateObject stateObject) {
		super.setStateObject(stateObject);
	}

	/**
	 * Sets the character to trim from the string. If the character to be trimmed is not specified,
	 * it is assumed to be space (or blank). It is a single-character string literal or a character-
	 * valued input parameter (i.e., char or <code>Character</code>).
	 *
	 * @param trimCharacter The trim character or <code>null</code> to remove it
	 */
	public void setTrimCharacter(StateObject trimCharacter) {
		StateObject oldTrimCharacter = this.trimCharacter;
		this.trimCharacter = parent(trimCharacter);
		firePropertyChanged(TRIM_CHARACTER_PROPERTY, oldTrimCharacter, trimCharacter);
	}

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

		// Specification
		if (specification != Specification.DEFAULT) {
			writer.append(specification.name());
			writer.append(SPACE);
		}

		// Trim character
		if (hasTrimCharacter()) {
			trimCharacter.toString(writer);
			writer.append(SPACE);
		}

		// FROM
		if ((specification != Specification.DEFAULT) || hasTrimCharacter()) {
			writer.append(FROM);
			writer.append(SPACE);
		}

		// String primary
		super.toTextEncapsulatedExpression(writer);
	}
}