/*******************************************************************************
 * Copyright (c) 2006, 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.parser;

import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import org.eclipse.persistence.jpa.jpql.WordParser;
import org.eclipse.persistence.jpa.jpql.utility.iterable.ListIterable;
import org.eclipse.persistence.jpa.jpql.utility.iterable.SnapshotCloneListIterable;

/**
 * An identification variable followed by the navigation operator (.) and a state field or
 * association field is a path expression. The type of the path expression is the type computed as
 * the result of navigation; that is, the type of the state field or association field to which the
 * expression navigates.
 *
 * @see CollectionValuedPathExpression
 * @see IdentificationVariable
 *
 * @version 2.5
 * @since 2.3
 * @author Pascal Filion
 */
public abstract class AbstractPathExpression extends AbstractExpression {

	/**
	 * Determines whether the path ends with a dot or not.
	 */
	private boolean endsWithDot;

	/**
	 * The identification variable that starts the path expression, which can be a sample {@link
	 * IdentificationVariable identification variable}, an {@link EntryExpression entry expression},
	 * a {@link ValueExpression value expression} or a {@link KeyExpression key expression}.
	 */
	private AbstractExpression identificationVariable;

	/**
	 * The state field path in a ordered list of string segments.
	 */
	private List<String> paths;

	/**
	 * The cached number of segments representing the path expression.
	 */
	private int pathSize;

	/**
	 * Determines whether the path starts with a dot or not.
	 */
	private boolean startsWithDot;

	/**
	 * Creates a new <code>AbstractPathExpression</code>.
	 *
	 * @param parent The parent of this expression
	 * @param identificationVariable The identification variable that was already parsed, which means
	 * the beginning of the parsing should start with a dot
	 */
	protected AbstractPathExpression(AbstractExpression parent, AbstractExpression identificationVariable) {
		super(parent);
		this.pathSize = -1;
		this.identificationVariable = identificationVariable;
		this.identificationVariable.setParent(this);
	}

	/**
	 * Creates a new <code>AbstractPathExpression</code>.
	 *
	 * @param parent The parent of this expression
	 * @param identificationVariable The identification variable that was already parsed, which means
	 * the beginning of the parsing should start with a dot
	 * @param paths The path expression that is following the identification variable
	 */
	public AbstractPathExpression(AbstractExpression parent,
	                              AbstractExpression identificationVariable,
	                              String paths) {

		super(parent, paths);
		this.pathSize = -1;
		this.identificationVariable = identificationVariable;
		this.identificationVariable.setParent(this);
	}

	/**
	 * Creates a new <code>AbstractPathExpression</code>.
	 *
	 * @param parent The parent of this expression
	 * @param paths The path expression
	 */
	protected AbstractPathExpression(AbstractExpression parent, String paths) {
		super(parent, paths);
		this.pathSize = -1;
	}

	/**
	 * {@inheritDoc}
	 */
	public void acceptChildren(ExpressionVisitor visitor) {
		getIdentificationVariable().accept(visitor);
	}

	/**
	 * {@inheritDoc}
	 */
	@Override
	protected void addChildrenTo(Collection<Expression> children) {
		checkPaths();
		children.add(identificationVariable);
	}

	/**
	 * {@inheritDoc}
	 */
	@Override
	protected final void addOrderedChildrenTo(List<Expression> children) {

		checkPaths();

		if (!hasVirtualIdentificationVariable()) {
			children.add(identificationVariable);
		}

		children.add(buildStringExpression(getText()));
	}

	private void checkPaths() {

		// Nothing to do
		if (paths != null) {
			return;
		}

		paths = new ArrayList<String>();
		String text = getText();
		char character = '\0';
		StringBuilder singlePath = new StringBuilder();

		// Extract each path from the text
		for (int index = 0, count = text.length(); index < count; index++) {

			character = text.charAt(index);

			// Make sure the identification variable is handled
			// correctly if it was passed during instantiation
			if (index == 0) {

				// No identification variable was passed during instantiation
				if (identificationVariable == null) {

					// The path starts with '.'
					startsWithDot = (character == DOT);

					// Start appending to the current single path
					if (!startsWithDot) {
						singlePath.append(character);
					}
				}
				// The identification variable was passed during instantiation,
				// add its parsed text as a path, it's assume the character is a dot
				else if (!identificationVariable.isNull() &&
				         !identificationVariable.isVirtual()) {

					paths.add(identificationVariable.toParsedText());
				}
				// Start appending to the current single path
				else {
					singlePath.append(character);
				}
			}
			else {

				// Append the character and continue
				if (character != DOT) {
					singlePath.append(character);
				}
				// Scanning a '.'
				else {

					// Store the current single path
					paths.add(singlePath.toString());

					// Clean the buffer
					singlePath.setLength(0);
				}
			}
		}

		// Check if the last character is a '.'
		endsWithDot = (character == DOT);

		// Make sure the last path is added to the list
		if (singlePath.length() > 0) {
			paths.add(singlePath.toString());
		}

		// Cache the size
		pathSize = paths.size();

		// The identification variable can never be null
		if (identificationVariable == null) {
			if (startsWithDot || !endsWithDot && (pathSize == 1)) {
				identificationVariable = buildNullExpression();
			}
			else {
				identificationVariable = new IdentificationVariable(this, paths.get(0));
			}
		}
	}

	/**
	 * Determines whether the path ends with a dot or not.
	 *
	 * @return <code>true</code> if the path ends with a dot; <code>false</code> otherwise
	 */
	public final boolean endsWithDot() {
		checkPaths();
		return endsWithDot;
	}

	/**
	 * {@inheritDoc}
	 */
	@Override
	public final JPQLQueryBNF findQueryBNF(Expression expression) {

		if ((identificationVariable != null) && identificationVariable.isAncestor(expression)) {
			return getQueryBNF(GeneralIdentificationVariableBNF.ID);
		}

		return super.findQueryBNF(expression);
	}

	/**
	 * Returns the identification variable that starts the path expression, which can be a sample
	 * identification variable, a map value, map key or map entry expression.
	 *
	 * @return The root of the path expression
	 */
	public final Expression getIdentificationVariable() {
		checkPaths();
		return identificationVariable;
	}

	/**
	 * Returns the specified segment of the state field path.
	 *
	 * @param index The 0-based segment index
	 * @return The specified segment
	 */
	public final String getPath(int index) {
		checkPaths();
		return paths.get(index);
	}

	/**
	 * Determines whether the identification variable was parsed.
	 *
	 * @return <code>true</code> the identification variable was parsed; <code>false</code> otherwise
	 */
	public final boolean hasIdentificationVariable() {
		checkPaths();
		return !identificationVariable.isNull() &&
		       !identificationVariable.isVirtual();
	}

	/**
	 * Determines whether the path's identification variable is virtual or not, meaning it's not part
	 * of the query but is required for proper navigability.
	 *
	 * @return <code>true</code> if this identification variable was virtually created to fully
	 * qualify path expression; <code>false</code> if it was parsed
	 */
	public final boolean hasVirtualIdentificationVariable() {
		checkPaths();
		return identificationVariable.isVirtual();
	}

	/**
	 * {@inheritDoc}
	 */
	@Override
	protected final void parse(WordParser wordParser, boolean tolerant) {
		wordParser.moveForward(getText());
	}

	/**
	 * Returns the segments in the state field path in order.
	 *
	 * @return An <code>Iterator</code> over the segments of the state field path
	 */
	public final ListIterable<String> paths() {
		checkPaths();
		return new SnapshotCloneListIterable<String>(paths);
	}

	/**
	 * Returns the number of segments in the state field path.
	 *
	 * @return The number of segments
	 */
	public final int pathSize() {
		checkPaths();
		return pathSize;
	}

	/**
	 * Sets a virtual identification variable because the abstract schema name was parsed without
	 * one. This is valid in an <b>UPDATE</b> and <b>DELETE</b> queries.
	 *
	 * @param variableName The identification variable that was generated to identify the "root" object
	 */
	protected final void setVirtualIdentificationVariable(String variableName) {

		identificationVariable = new IdentificationVariable(this, variableName, true);

		rebuildActualText();
		rebuildParsedText();
	}

	/**
	 * Determines whether the path starts with a dot or not.
	 *
	 * @return <code>true</code> if the path starts with a dot; <code>false</code> otherwise
	 */
	public final boolean startsWithDot() {
		return startsWithDot;
	}

	/**
	 * Returns a string representation from the given range.
	 *
	 * @param startIndex The beginning of the range to create the string representation
	 * @param stopIndex When to stop creating the string representation, which is exclusive
	 * @return The string representation of this path expression contained in the given range
	 * @since 2.4
	 */
	public String toParsedText(int startIndex, int stopIndex) {

		checkPaths();
		StringBuilder writer = new StringBuilder();

		for (int index = startIndex; index < stopIndex; index++) {
			writer.append(paths.get(index));

			if (index < stopIndex - 1) {
				writer.append(DOT);
			}
		}

		return writer.toString();
	}

	/**
	 * {@inheritDoc}
	 */
	@Override
	protected final void toParsedText(StringBuilder writer, boolean actual) {

		checkPaths();

		if (startsWithDot) {
			writer.append(DOT);
		}

		for (int index = 0, count = pathSize(); index < count; index++) {

			if (index > 0) {
				writer.append(DOT);
			}

			// Make sure to use the identification variable for proper formatting
			if ((index == 0) && hasIdentificationVariable()) {
				identificationVariable.toParsedText(writer, actual);
			}
			// Append a single path
			else {
				writer.append(paths.get(index));
			}
		}

		if (endsWithDot) {
			writer.append(DOT);
		}
	}
}