/*******************************************************************************
 * 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.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import java.util.ListIterator;
import java.util.StringTokenizer;
import org.eclipse.persistence.jpa.jpql.ExpressionTools;
import org.eclipse.persistence.jpa.jpql.parser.AbstractPathExpression;
import org.eclipse.persistence.jpa.jpql.parser.IdentificationVariableBNF;
import org.eclipse.persistence.jpa.jpql.tools.model.IListChangeEvent.EventType;
import org.eclipse.persistence.jpa.jpql.tools.model.IListChangeListener;
import org.eclipse.persistence.jpa.jpql.tools.model.ListChangeEvent;
import org.eclipse.persistence.jpa.jpql.tools.spi.IManagedType;
import org.eclipse.persistence.jpa.jpql.tools.spi.IManagedTypeProvider;
import org.eclipse.persistence.jpa.jpql.tools.spi.IMapping;
import org.eclipse.persistence.jpa.jpql.tools.spi.IType;
import org.eclipse.persistence.jpa.jpql.tools.spi.ITypeDeclaration;
import org.eclipse.persistence.jpa.jpql.utility.CollectionTools;
import org.eclipse.persistence.jpa.jpql.utility.iterable.ListIterable;
import org.eclipse.persistence.jpa.jpql.utility.iterable.SnapshotCloneListIterable;
import static org.eclipse.persistence.jpa.jpql.parser.AbstractExpression.*;

/**
 * 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 AbstractPathExpression
 *
 * @version 2.5
 * @since 2.4
 * @author Pascal Filion
 */
@SuppressWarnings("nls")
public abstract class AbstractPathExpressionStateObject extends AbstractStateObject
                                                        implements ListHolderStateObject<String> {

	/**
	 * The {@link StateObject} that represents the identification variable portion of the path
	 * expression, which can be one of the following when the JPA version is 2.0 or later:
	 * <ul>
	 * <li>{@link IdentificationVariableStateObject}</li>
	 * <li>{@link KeyExpressionStateObject}</li>
	 * <li>{@link ValueExpressionStateObject}</li>
	 * </ul>
	 */
	private StateObject identificationVariable;

	/**
	 *
	 */
	private IManagedType managedType;

	/**
	 *
	 */
	private ArrayList<IMapping> mappings;

	/**
	 * The list of segments, including the general identification variable.
	 */
	private List<String> paths;

	/**
	 *
	 */
	private boolean resolved;

	/**
	 * The {@link IType} of the object being mapped to this identification variable.
	 */
	private IType type;

	/**
	 * The {@link ITypeDeclaration} of the object being mapped to this identification variable.
	 */
	private ITypeDeclaration typeDeclaration;

	/**
	 * Notifies the identification variable property has changed.
	 */
	public static final String IDENTIFICATION_VARIABLE_PROPERTY = "identificationVariable";

	/**
	 * Notifies the content of the paths list has changed.
	 */
	public static final String PATHS_LIST = "paths";

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

	/**
	 * Creates a new <code>AbstractPathExpressionStateObject</code>.
	 *
	 * @param parent The parent of this state object, which cannot be <code>null</code>
	 * @param path The path expression
	 * @exception NullPointerException The given parent cannot be <code>null</code>
	 */
	protected AbstractPathExpressionStateObject(StateObject parent, String path) {
		super(parent);
		setPath(path);
	}

	/**
	 * {@inheritDoc}
	 */
	@Override
	protected void addChildren(List<StateObject> children) {

		super.addChildren(children);

		StateObject stateObject = getIdentificationVariable();

		if (stateObject != null) {
			children.add(stateObject);
		}
	}

	/**
	 * {@inheritDoc}
	 */
	@SuppressWarnings("unchecked")
	public String addItem(String item) {
		getChangeSupport().addItem(this, paths, PATHS_LIST, item);
		return item;
	}

	/**
	 * {@inheritDoc}
	 */
	public void addItems(List<? extends String> items) {
		getChangeSupport().addItems(this, paths, PATHS_LIST, items);
	}

	/**
	 * {@inheritDoc}
	 */
	public void addListChangeListener(String listName, IListChangeListener<String> listener) {
		getChangeSupport().addListChangeListener(listName, listener);
	}

	/**
	 * Appends the given sequence of characters to the path expression. If the sequence does not
	 * begin with a dot, then the first segment will be appended to the last segment and then new
	 * segments will be created.
	 *
	 * @param text The sequence of characters to append to the path expression
	 */
	public void append(String text) {

		StringBuilder word = new StringBuilder();
		int pathCount = paths.size();
		boolean appendToLastSegment = true;
		int startIndex = pathCount;

		for (int index = 0, count = text.length(); index < count; index++) {
			char character = text.charAt(index);

			if (character == DOT) {
				if (word.length() > 0) {
					// Append the content of the buffer to the end of the last segment
					if (appendToLastSegment) {
						String currentPath = paths.get(pathCount - 1);
						paths.set(pathCount - 1, currentPath + word);
						startIndex = pathCount - 1;
					}
					// Add a new segment
					else {
						paths.add(word.toString());
						pathCount++;
					}

					// Clear the buffer
					word.delete(0, word.length());
				}

				appendToLastSegment = false;
				continue;
			}
			// Add the character to the buffer
			else {
				word.append(character);
			}
		}

		if (word.length() > 0) {
			// Append the content of the buffer to the end of the last segment
			if (appendToLastSegment) {
				String currentPath = paths.get(pathCount - 1);
				paths.set(pathCount - 1, currentPath + word);
				startIndex = pathCount - 1;
			}
			// Add a new segment
			else {
				paths.add(word.toString());
			}
		}

		ListChangeEvent<String> event = new ListChangeEvent<String>(this, paths, EventType.CHANGED, PATHS_LIST, paths, startIndex, itemsSize());
		getChangeSupport().fireListChangeEvent(event);
	}

	/**
	 * {@inheritDoc}
	 */
	public boolean canMoveDown(String item) {
		return false;
	}

	/**
	 * {@inheritDoc}
	 */
	public boolean canMoveUp(String item) {
		return false;
	}

	/**
	 * Clears the values related to the managed type and type.
	 */
	protected void clearResolvedObjects() {

		mappings.clear();

		resolved        = false;
		type            = null;
		typeDeclaration = null;
	}

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

	/**
	 * Returns the {@link StateObject} representing 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 StateObject getIdentificationVariable() {
		if ((identificationVariable == null) && hasItems()) {
			identificationVariable = buildStateObject(getItem(0), IdentificationVariableBNF.ID);
		}
		return identificationVariable;
	}

	/**
	 * {@inheritDoc}
	 */
	public String getItem(int index) {
		return paths.get(index);
	}

	/**
	 * Returns
	 *
	 * @return
	 */
	public IManagedType getManagedType() {
		if (managedType == null) {
			managedType = resolveManagedType();
		}
		return managedType;
	}

	/**
	 * Returns
	 *
	 * @return
	 */
	public IMapping getMapping() {
		resolveMappings();
		return mappings.get(itemsSize() - 1);
	}

	/**
	 * Retrieves the {@link IMapping} for the path at the given position.
	 *
	 * @param index The index of the path for which its {@link IMapping} should be retrieved, which
	 * should start at 1 to skip the identification variable
	 */
	public IMapping getMapping(int index) {
		resolveMappings();
		return mappings.get(index);
	}

	/**
	 * Returns the string representation of the path expression. If the identification variable is
	 * virtual, then it is not part of the result.
	 *
	 * @return The path expression, which is never <code>null</code>
	 */
	public String getPath() {
		return toString();
	}

	/**
	 * Returns the {@link IType} of the field handled by this object.
	 *
	 * @return Either the {@link IType} that was resolved by this state object or the {@link IType}
	 * for {@link IType#UNRESOLVABLE_TYPE} if it could not be resolved
	 */
	public IType getType() {
		if (type == null) {
			type = resolveType();
		}
		return type;
	}

	/**
	 * Returns the {@link ITypeDeclaration} of the field handled by this object.
	 *
	 * @return Either the {@link ITypeDeclaration} that was resolved by this object or the {@link
	 * ITypeDeclaration} for {@link IType#UNRESOLVABLE_TYPE} if it could not be resolved
	 */
	public ITypeDeclaration getTypeDeclaration() {
		if (typeDeclaration == null) {
			typeDeclaration = resolveTypeDeclaration();
		}
		return typeDeclaration;
	}

	/**
	 * Determines whether the identification variable is present.
	 *
	 * @return <code>true</code> the identification variable is present; <code>false</code> otherwise
	 */
	public boolean hasIdentificationVariable() {
		return getIdentificationVariable() != null;
	}

	/**
	 * {@inheritDoc}
	 */
	public boolean hasItems() {
		return !paths.isEmpty();
	}

	/**
	 * {@inheritDoc}
	 */
	@Override
	protected void initialize() {
		super.initialize();
		paths    = new ArrayList<String>();
		mappings = new ArrayList<IMapping>();
	}

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

		if (!super.isEquivalent(stateObject)) {
			return false;
		}

		AbstractPathExpressionStateObject path = (AbstractPathExpressionStateObject) stateObject;

		if (!areEquivalent(getIdentificationVariable(), path.getIdentificationVariable())) {
			return false;
		}

		int index = itemsSize();

		if (index != path.itemsSize()) {
			return false;
		}

		// Skip index 0, it is already tested and two identification
		// variables with different case are equivalent
		while (--index > 0) {
			String path1 = getItem(index);
			String path2 = path.getItem(index);

			if (ExpressionTools.valuesAreDifferent(path1, path2)) {
				return false;
			}
		}

		return true;
	}

	/**
	 * {@inheritDoc}
	 */
	public ListIterable<String> items() {
		return new SnapshotCloneListIterable<String>(paths);
	}

	/**
	 * {@inheritDoc}
	 */
	public int itemsSize() {
		return paths.size();
	}

	/**
	 * {@inheritDoc}
	 */
	public String moveDown(String item) {
		throw new RuntimeException(getClass().getName() + " does not support moveDown(String).");
	}

	/**
	 * {@inheritDoc}
	 */
	public String moveUp(String item) {
		throw new RuntimeException(getClass().getName() + " does not support moveUp(String).");
	}

	/**
	 * Removes the single path at the given index.
	 *
	 * @param index The position of the single path to remove. If the index is 0, then the
	 * identification variable is nullified
	 */
	public void removeItem(int index) {
		if (index == 0) {
			setIdentificationVariableInternally(null);
		}
		removeItem(getItem(index));
	}

	/**
	 * {@inheritDoc}
	 */
	public void removeItem(String item) {
		getChangeSupport().removeItem(this, paths, PATHS_LIST, item);
	}

	/**
	 * {@inheritDoc}
	 */
	public void removeItems(Collection<String> items) {
		getChangeSupport().removeItems(this, this.paths, PATHS_LIST, items);
	}

	/**
	 * {@inheritDoc}
	 */
	public void removeListChangeListener(String listName, IListChangeListener<String> listener) {
		getChangeSupport().removeListChangeListener(listName, listener);
	}

	/**
	 * Resolves
	 *
	 * @return
	 */
	protected abstract IManagedType resolveManagedType();

	/**
	 * Resolves the {@link IMapping} objects that constitutes the path expression.
	 */
	protected void resolveMappings() {

		if (!resolved) {
			resolved = true;
			IManagedTypeProvider provider = getManagedTypeProvider();
			IManagedType managedType = null;

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

				// Identification variable
				if (index == 0) {
					StateObject stateObject = getIdentificationVariable();

					// The identification variable is not set, which means the traversal can happen
					if (stateObject != null) {
						managedType = getDeclaration().findManagedType(stateObject);
					}

					mappings.add(null);
				}
				// Resolve the path expression after the identification variable
				else if (managedType != null) {

					String path = getItem(index);

					// Cache the mapping
					IMapping mapping = managedType.getMappingNamed(path);
					mappings.add(mapping);

					// Continue by retrieving the managed type
					if (mapping != null) {
						managedType = provider.getManagedType(mapping.getType());
					}
					else {
						managedType = null;
					}
				}
				else {
					mappings.add(null);
				}
			}
		}
	}

	/**
	 * Resolves the {@link IType} of the property handled by this object.
	 *
	 * @return Either the {@link IType} that was resolved by this object or the {@link IType} for
	 * {@link IType#UNRESOLVABLE_TYPE} if it could not be resolved
	 */
	protected abstract IType resolveType();

	/**
	 * Resolves the {@link ITypeDeclaration} of the property handled by this object.
	 *
	 * @return Either the {@link ITypeDeclaration} that was resolved by this object or the {@link
	 * ITypeDeclaration} for {@link IType#UNRESOLVABLE_TYPE} if it could not be resolved
	 */
	protected ITypeDeclaration resolveTypeDeclaration() {
		resolveMappings();
		return getMapping().getTypeDeclaration();
	}

	/**
	 * Sets the {@link StateObject} representing the identification variable that starts the path
	 * expression, which can be a sample identification variable, a map value, map key or map entry
	 * expression.
	 *
	 * @param identificationVariable The root of the path expression
	 */
	public void setIdentificationVariable(StateObject identificationVariable) {
		setIdentificationVariableInternally(identificationVariable);
		getChangeSupport().replaceItem(this, paths, PATHS_LIST, 0, identificationVariable.toString());
	}

	/**
	 * Sets the {@link StateObject} representing the identification variable that starts the path
	 * expression, which can be a sample identification variable, a map value, map key or map entry
	 * expression. This method does not replace the first path in the list of paths.
	 *
	 * @param identificationVariable The root of the path expression
	 */
	protected void setIdentificationVariableInternally(StateObject identificationVariable) {
		clearResolvedObjects();
		StateObject oldIdentificationVariable = this.identificationVariable;
		this.identificationVariable = parent(identificationVariable);
		firePropertyChanged(IDENTIFICATION_VARIABLE_PROPERTY, oldIdentificationVariable, identificationVariable);
	}

	/**
	 * Changes the path expression with the list of segments, the identification variable will also
	 * be updated with the first segment.
	 *
	 * @param path The new path expression
	 */
	public void setPath(CharSequence path) {

		List<String> paths = new ArrayList<String>();

		for (StringTokenizer tokenizer = new StringTokenizer(path.toString(), ".", true); tokenizer.hasMoreTokens(); ) {
			String token = tokenizer.nextToken();
			if (!token.equals(".")) {
				paths.add(token);
			}
			else if (!tokenizer.hasMoreTokens()) {
				paths.add(ExpressionTools.EMPTY_STRING);
			}
		}

		setPaths(paths.listIterator());
	}

	/**
	 * Replaces the existing path segment to become the given one.
	 *
	 * @param index The position of the path segment to replace
	 * @param path The replacement
	 */
	public void setPath(int index, String path) {

		if (index == 0) {
			setIdentificationVariableInternally(null);
		}
		else {
			clearResolvedObjects();
		}

		getChangeSupport().replaceItem(this, paths, PATHS_LIST, index, path);
	}

	/**
	 * Changes the path expression with the list of segments, the identification variable will also
	 * be updated with the first segment.
	 *
	 * @param paths The new path expression
	 */
	public void setPaths(List<String> paths) {
		setIdentificationVariableInternally(null);
		getChangeSupport().replaceItems(this, this.paths, PATHS_LIST, paths);
	}

	/**
	 * Changes the path expression with the list of segments, the identification variable will also
	 * be updated with the first segment.
	 *
	 * @param paths The new path expression
	 */
	public void setPaths(ListIterator<String> paths) {
		setPaths(CollectionTools.list(paths));
	}

	/**
	 * Changes the path expression with the list of segments, the identification variable will also
	 * be updated with the first segment.
	 *
	 * @param paths The new path expression
	 */
	public void setPaths(String... paths) {
		setPaths(Arrays.asList(paths));
	}

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

		StateObject stateObject = getIdentificationVariable();

		if (stateObject != null) {
			String variable = stateObject.toString();

			if (variable.length() > 0) {
				writer.append(variable);

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

		for (int index = 1, count = paths.size(); index < count; index++) {
			writer.append(paths.get(index));
			if (index < count - 1) {
				writer.append(DOT);
			}
		}
	}
}