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

import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.eclipse.persistence.jpa.jpql.ExpressionTools;
import org.eclipse.persistence.jpa.jpql.WordParser;
import org.eclipse.persistence.jpa.jpql.parser.IdentifierRole;
import org.eclipse.persistence.jpa.jpql.parser.JPQLGrammar;
import org.eclipse.persistence.jpa.jpql.tools.spi.IEntity;
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.utility.XmlEscapeCharacterConverter;
import org.eclipse.persistence.jpa.jpql.tools.utility.iterable.SnapshotCloneIterable;
import static org.eclipse.persistence.jpa.jpql.parser.Expression.*;

/**
 * The default implementation of {@link ContentAssistProposals} which stores the valid proposals.
 * <p>
 * Provisional API: This interface is part of an interim API that is still under development and
 * expected to change significantly before reaching stability. It is available at this early stage
 * to solicit feedback from pioneering adopters on the understanding that any code that uses this
 * API will almost certainly be broken (repeatedly) as the API evolves.
 *
 * @version 2.5
 * @since 2.3
 * @author Pascal Filion
 */
@SuppressWarnings("nls")
public final class DefaultContentAssistProposals implements ContentAssistProposals {

	/**
	 * The prefix that is used to filter the list of class names.
	 *
	 * @since 2.5
	 */
	private String classNamePrefix;

	/**
	 * The class type helps to filter the various types of classes, i.e. anonymous, member, interface
	 * are never allowed.
	 *
	 * @since 2.5
	 */
	private ClassType classType;

	/**
	 * TODO
	 *
	 * @since 2.5
	 */
	private String columnNamePrefix;

	/**
	 * The set of possible abstract schema types.
	 */
	private Set<IEntity> entities;

	/**
	 * TODO
	 *
	 * @since 2.5
	 */
	private Map<IType, DefaultEnumProposals> enumProposals;

	/**
	 * This extension can be used to provide additional support to JPQL content assist that is
	 * outside the scope of providing proposals related to JPA metadata. It adds support for
	 * providing suggestions related to class names, enum constants, table names, column names.
	 *
	 * @since 2.5
	 */
	private ContentAssistExtension extension;

	/**
	 * The set of possible identification variables.
	 */
	private Set<String> identificationVariables;

	/**
	 * The set of possible JPQL identifiers.
	 */
	private Set<String> identifiers;

	/**
	 * The {@link JPQLGrammar} that defines how the JPQL query was parsed.
	 */
	private JPQLGrammar jpqlGrammar;

	/**
	 * The set of possible {@link IMapping mappings}, which can be state fields, association fields
	 * and/or collection fields.
	 */
	private Set<IMapping> mappings;

	/**
	 * The identification variables mapped to their abstract schema types.
	 */
	private Map<String, IEntity> rangeIdentificationVariables;

	/**
	 * TODO
	 *
	 * @since 2.5
	 */
	private String tableName;

	/**
	 * TODO
	 *
	 * @since 2.5
	 */
	private String tableNamePrefix;

	/**
	 * A JPQL identifier that is mapped to its longest counterpart.
	 */
	private static final Map<String, String> LONGUEST_IDENTIFIERS = buildLonguestIdentifiers();

	/**
	 * A JPQL identifier that is mapped to the list of counterparts used to find them in the query.
	 */
	private static final Map<String, List<String>> ORDERED_IDENTIFIERS = buildOrderedIdentifiers();

	/**
	 * Creates a new <code>DefaultContentAssistProposals</code>.
	 *
	 * @param jpqlGrammar The {@link JPQLGrammar} that defines how the JPQL query was parsed
	 * @param extension This extension can be used to provide additional support to JPQL content
	 * assist that is outside the scope of providing proposals related to JPA metadata. It adds
	 * support for providing suggestions related to class names, enum constants, table names, column
	 * names
	 */
	public DefaultContentAssistProposals(JPQLGrammar jpqlGrammar, ContentAssistExtension extension) {
		super();
		initialize(jpqlGrammar, extension);
	}

	private static Map<String, String> buildLonguestIdentifiers() {

		Map<String, String> identifiers = new HashMap<String, String>();

		identifiers.put(IS_EMPTY,         IS_NOT_EMPTY);
		identifiers.put(IS_NULL,          IS_NOT_NULL);
		identifiers.put(IN,               NOT_IN);
		identifiers.put(BETWEEN,          NOT_BETWEEN);
		identifiers.put(MEMBER,           NOT_MEMBER_OF);
		identifiers.put(MEMBER_OF,        NOT_MEMBER_OF);
		identifiers.put(NOT_MEMBER,       NOT_MEMBER_OF);
		identifiers.put(JOIN,             LEFT_OUTER_JOIN_FETCH);
		identifiers.put(JOIN_FETCH,       LEFT_OUTER_JOIN_FETCH);
		identifiers.put(LEFT_JOIN,        LEFT_OUTER_JOIN_FETCH);
		identifiers.put(LEFT_JOIN_FETCH,  LEFT_OUTER_JOIN_FETCH);
		identifiers.put(LEFT_OUTER_JOIN,  LEFT_OUTER_JOIN_FETCH);
		identifiers.put(INNER_JOIN,       LEFT_OUTER_JOIN_FETCH);
		identifiers.put(INNER_JOIN_FETCH, LEFT_OUTER_JOIN_FETCH);

		identifiers.put(SELECT, DELETE_FROM);
		identifiers.put(UPDATE, DELETE_FROM);

		return identifiers;
	}

	private static Map<String, List<String>> buildOrderedIdentifiers() {

		Map<String, List<String>> identifiers = new HashMap<String, List<String>>();

		identifiers.put(IS_NOT_EMPTY, Collections.singletonList(IS_NOT_EMPTY));
		identifiers.put(IS_NOT_NULL,  Collections.singletonList(IS_NOT_NULL));
		identifiers.put(NOT_IN,       Collections.singletonList(NOT_IN));
		identifiers.put(NOT_BETWEEN,  Collections.singletonList(NOT_BETWEEN));

		List<String> members = new ArrayList<String>();
		members.add(MEMBER_OF);
		members.add(NOT_MEMBER);
		members.add(MEMBER);
		identifiers.put(NOT_MEMBER_OF, members);

		List<String> joins = new ArrayList<String>();
		joins.add(LEFT_OUTER_JOIN);
		joins.add(LEFT_JOIN_FETCH);
		joins.add(LEFT_JOIN);
		joins.add(INNER_JOIN_FETCH);
		joins.add(INNER_JOIN);
		joins.add(JOIN_FETCH);
		joins.add(JOIN);
		identifiers.put(LEFT_OUTER_JOIN_FETCH, joins);

		List<String> clauses = new ArrayList<String>();
		clauses.add(SELECT);
		clauses.add(UPDATE);
		identifiers.put(DELETE_FROM, clauses);

		return identifiers;
	}

	/**
	 * {@inheritDoc}
	 */
	public Iterable<IEntity> abstractSchemaTypes() {
		return new SnapshotCloneIterable<IEntity>(entities);
	}

	/**
	 * Adds the given {@link IEntity} as a possible abstract schema type.
	 *
	 * @param abstractSchemaType The abstract schema type that is a valid proposal
	 */
	public void addEntity(IEntity abstractSchemaType) {
		entities.add(abstractSchemaType);
	}

	/**
	 * Adds the constants of the given enum constant as a valid proposal.
	 *
	 * @param enumType The {@link IType} of the enum type
	 * @param enumConstant The enum constant to be added as a valid proposal
	 *
	 * @since 2.5
	 */
	public void addEnumConstant(IType enumType, String enumConstant) {

		DefaultEnumProposals proposal = enumProposals.get(enumType);

		if (proposal == null) {
			proposal = new DefaultEnumProposals(enumType);
			enumProposals.put(enumType, proposal);
		}

		proposal.constants.add(enumConstant);
	}

	/**
	 * Adds the given identification variable as a proposal.
	 *
	 * @param identificationVariable The identification variable that is a valid proposal
	 */
	public void addIdentificationVariable(String identificationVariable) {
		identificationVariables.add(identificationVariable);
	}

	/**
	 * Adds the given JPQL identifier as a proposal.
	 *
	 * @param identifier The JPQL identifier that is a valid proposal
	 */
	public void addIdentifier(String identifier) {
		identifiers.add(identifier);
	}

	/**
	 * Adds the given {@link IMapping mapping} (state field, association field or collection field)
	 * as a valid proposal.
	 *
	 * @param mapping The {@link IMapping} of the state field, association field or collection field
	 */
	public void addMapping(IMapping mapping) {
		mappings.add(mapping);
	}

	/**
	 * Adds the given {@link IMapping mappings} (state fields, association fields or collection fields)
	 * as valid proposals.
	 *
	 * @param mappings The {@link IMapping mappings} of the state fields, association fields or
	 * collection fields
	 */
	public void addMappings(Collection<IMapping> mappings) {
		this.mappings.addAll(mappings);
	}

	/**
	 * Adds the given range identification variable that is mapping the given abstract schema type.
	 *
	 * @param identificationVariable The range identification variable mapping the abstract schema name
	 * @param entity The abstract type name that identifies the type of the variable
	 */
	public void addRangeIdentificationVariable(String identificationVariable, IEntity entity) {
		rangeIdentificationVariables.put(identificationVariable, entity);
	}

	/**
	 * {@inheritDoc}
	 */
	public Result buildEscapedQuery(String jpqlQuery, String proposal, int position, boolean insert) {

		Result result = buildQuery(jpqlQuery, proposal, position, insert);

		// Escape the JPQL query and adjust the position accordingly
		int[] positions = { result.position };
		result.jpqlQuery = ExpressionTools.escape(result.jpqlQuery, positions);
		result.position  = positions[0];

		return result;
	}

	/**
	 * Calculates the start and end position for correctly inserting the proposal into the query.
	 *
	 * @param wordParser This parser can be used to retrieve words from the cursor position
	 * @param proposal The proposal to be inserted into the query
	 * @param insert Flag that determines if the proposal is simply inserted or it should also replace
	 * the partial word following the position of the cursor
	 * @return The start and end positions
	 */
	public int[] buildPositions(WordParser wordParser, String proposal, boolean insert) {

		int index;
		String wordsToReplace = null;

		// Special case for arithmetic symbol (<, <=, =, <>, >, >=, +, -, *, /)
		if (wordParser.isArithmeticSymbol(proposal.charAt(0))) {
			int startIndex = wordParser.position();
			int endIndex   = startIndex;
			char character;

			// Now look for the start index
			do {
				character = wordParser.character(--startIndex);
			}
			while (wordParser.isArithmeticSymbol(character));

			// Now look for the end index
			do {
				character = wordParser.character(endIndex++);
			}
			while (wordParser.isArithmeticSymbol(character));

			// Increment the start index because the loop always decrements it by one before stopping
			// and decrement the end position because it's always incremented by one before stopping
			index = ++startIndex;
			wordsToReplace = wordParser.substring(index, --endIndex);
		}
		else {
			// Always try to find the start position using the longest JPQL identifier
			// if the proposal has more than one word
			wordsToReplace = longuestIdentifier(proposal);
			index = startPosition(wordParser, wordsToReplace);

			// Now check with the other possibilities
			if ((index == -1) && ORDERED_IDENTIFIERS.containsKey(wordsToReplace)) {
				for (String identifier : ORDERED_IDENTIFIERS.get(wordsToReplace)) {
					wordsToReplace = identifier;
					index = startPosition(wordParser, wordsToReplace);
					if (index > -1) {
						break;
					}
				}
			}
		}

		// Now calculate the start position
		int startPosition;

		if (index > -1) {
			startPosition = index;
		}
		else {
			// Check for path expression (which uses a dot) and update the wordToReplace
			// and startPosition accordingly
			String partialWord = wordParser.partialWord();
			String entireWord = wordParser.entireWord();
			int dotIndex = -1;

			if (isMappingName(proposal)  ||
			    isEnumConstant(proposal) ||
			    isColumnName(proposal)) {

				dotIndex = partialWord.lastIndexOf(".");
			}

			if (dotIndex > 0) {
				wordsToReplace = entireWord.substring(dotIndex + 1);
				startPosition = wordParser.position() - partialWord.length() + dotIndex + 1;
			}
			else if (insert) {
				wordsToReplace = wordParser.word();
				startPosition = wordParser.position() - partialWord.length();
			}
			else {
				wordsToReplace = entireWord;
				startPosition = wordParser.position() - partialWord.length();
			}
		}

		// Now calculate the end position
		int length;

		// Only the partial word to the left of the cursor will be replaced and
		// the partial word to the right of the cursor will stay
		if (insert) {
			String partialWord = wordParser.substring(startPosition, wordParser.position());
			length = partialWord.length();
		}
		else if (proposal != wordsToReplace) {
			length = wordsToReplace.length();
		}
		else {
			index = 0;

			for (int charIndex = 0, wordLength = wordParser.length(), count = wordsToReplace.length();
			     startPosition + index < wordLength && charIndex < count;
			     index++, charIndex++) {

				// Find the end position by matching the proposal with the content of the query by
				// scanning every character starting at the start position, the end position will be
				// the position of the first character that is different or at the end of the query
				if (wordParser.character(startPosition + charIndex) !=
					 wordsToReplace.charAt(charIndex)) {

					break;
				}
			}

			length = index;
		}

		return new int[] { startPosition, startPosition + length };
	}

	/**
	 * {@inheritDoc}
	 */
	public Result buildQuery(String jpqlQuery, String proposal, int position, boolean insert) {

		// Nothing to replace
		if (ExpressionTools.stringIsEmpty(proposal)) {
			return new Result(jpqlQuery, position);
		}

		WordParser wordParser = new WordParser(jpqlQuery);
		wordParser.setPosition(position);

		// Calculate the start and end positions
		int[] positions = buildPositions(wordParser, proposal, insert);

		// Create the new query
		StringBuilder sb = new StringBuilder(jpqlQuery);
		sb.replace(positions[0], positions[1], proposal);

		// Return the result
		return new Result(sb.toString(), positions[0] + proposal.length());
	}

	/**
	 * {@inheritDoc}
	 */
	public ResultQuery buildXmlQuery(String jpqlQuery, String proposal, int position, boolean insert) {

		// Nothing to replace
		if (ExpressionTools.stringIsEmpty(proposal)) {
			return new Result(jpqlQuery, position);
		}

		int[] positions = { position };

		// First convert the escape characters into their unicode characters
		String query = XmlEscapeCharacterConverter.unescape(jpqlQuery, positions);

		// Calculate the start and end positions
		WordParser wordParser = new WordParser(query);
		wordParser.setPosition(positions[0]);
		int[] proposalPositions = buildPositions(wordParser, proposal, insert);

		// Escape the proposal
		proposal = XmlEscapeCharacterConverter.escape(proposal, new int[1]);

		// Adjust the positions so it's in the original JPQL query, which may contain escaped characters
		XmlEscapeCharacterConverter.reposition(jpqlQuery, proposalPositions);

		// Create the new JPQL query
		StringBuilder sb = new StringBuilder(jpqlQuery);
		sb.replace(proposalPositions[0], proposalPositions[1], proposal);

		// Return the result
		return new Result(sb.toString(), proposalPositions[0] + proposal.length());
	}

	/**
	 * {@inheritDoc}
	 */
	public Iterable<String> classNames() {
		if (classNamePrefix == null) {
			return Collections.emptyList();
		}
		return extension.classNames(classNamePrefix, classType);
	}

	/**
	 * {@inheritDoc}
	 */
	public Iterable<String> columnNames() {
		if (tableName == null) {
			return Collections.emptyList();
		}
		return extension.columnNames(tableName, columnNamePrefix);
	}

	/**
	 * {@inheritDoc}
	 */
	public Iterable<EnumProposals> enumConstant() {
		return new SnapshotCloneIterable<EnumProposals>(enumProposals.values());
	}

	/**
	 * {@inheritDoc}
	 */
	public IEntity getAbstractSchemaType(String identificationVariable) {
		return rangeIdentificationVariables.get(identificationVariable);
	}

	/**
	 * Returns the prefix that will be used to filter the list of possible class names.
	 *
	 * @return The prefix that is used to filter the list of class names or <code>null</code> if it
	 * was not set for the cursor position within the JPQL query
	 * @since 2.5
	 */
	public String getClassNamePrefix() {
		return classNamePrefix;
	}

	/**
	 * {@inheritDoc}
	 */
	public ClassType getClassType() {
		return classType;
	}

	/**
	 * Returns the prefix that will be used by {@link ContentAssistExtension} to filter the column
	 * names if the table name is not <code>null</code>.
	 *
	 * @return The prefix that is used to filter the list of columns names, which is <code>null</code>
	 * if it has not been set along with the table name
	 * @since 2.5
	 */
	public String getColumnNamePrefix() {
		return columnNamePrefix;
	}

	/**
	 * Returns the {@link JPQLGrammar} that defines how the JPQL query was parsed.
	 *
	 * @return The {@link JPQLGrammar} that was used to parse this {@link org.eclipse.persistence.jpa.jpql.parser.Expression Expression}
	 */
	public JPQLGrammar getGrammar() {
		return jpqlGrammar;
	}

	/**
	 * {@inheritDoc}
	 */
	public IdentifierRole getIdentifierRole(String identifier) {
		return jpqlGrammar.getExpressionRegistry().getIdentifierRole(identifier);
	}

	/**
	 * Returns the table name that will be used by {@link ContentAssistExtension} to retrieve the
	 * column names.
	 *
	 * @return The name of the table for which its column names should be retrieve as possible proposals
	 * @since 2.5
	 */
	public String getTableName() {
		return tableName;
	}

	/**
	 * Returns the prefix that will be used to filter the list of possible table names.
	 *
	 * @return The prefix that is used to filter the list of table names or <code>null</code> if it
	 * was not set for the cursor position within the JPQL query
	 * @since 2.5
	 */
	public String getTableNamePrefix() {
		return tableNamePrefix;
	}

	/**
	 * {@inheritDoc}
	 */
	public boolean hasProposals() {
		return !mappings.isEmpty()                ||
		       !entities.isEmpty()                ||
		       !identifiers.isEmpty()             ||
		       !enumProposals.isEmpty()           ||
		       !identificationVariables.isEmpty() ||
		        classNames().iterator().hasNext() ||
		        tableNames().iterator().hasNext() ||
		       columnNames().iterator().hasNext() ||
		       !rangeIdentificationVariables.isEmpty();
	}

	/**
	 * {@inheritDoc}
	 */
	public Iterable<String> identificationVariables() {
		List<String> variables = new ArrayList<String>(identificationVariables.size() + rangeIdentificationVariables.size());
		variables.addAll(identificationVariables);
		variables.addAll(rangeIdentificationVariables.keySet());
		return new SnapshotCloneIterable<String>(variables);
	}

	/**
	 * {@inheritDoc}
	 */
	public Iterable<String> identifiers() {
		return new SnapshotCloneIterable<String>(identifiers);
	}

	protected void initialize(JPQLGrammar jpqlGrammar, ContentAssistExtension extension) {

		this.extension                    = extension;
		this.jpqlGrammar                  = jpqlGrammar;
		this.mappings                     = new HashSet<IMapping>();
		this.identifiers                  = new HashSet<String>();
		this.entities                     = new HashSet<IEntity>();
		this.identificationVariables      = new HashSet<String>();
		this.rangeIdentificationVariables = new HashMap<String, IEntity>();
		this.enumProposals                = new HashMap<IType, DefaultEnumProposals>();
	}


	/**
	 * Determines whether the given proposal is a column name (which should be unqualified).
	 *
	 * @param proposal The proposal that is being inserted into the JPQL query
	 * @return <code>true</code> if the given proposal is a column name; <code>false</code> otherwise
	 * @since 2.5
	 */
	public boolean isColumnName(String proposal) {

		for (String columName : columnNames()) {
			if (columName.equals(proposal)) {
				return true;
			}
		}

		return false;
	}

	/**
	 * Determines whether the given proposal is an enum constant name (which should be unqualified).
	 *
	 * @param proposal The proposal that is being inserted into the JPQL query
	 * @return <code>true</code> if the given proposal is a unqualified enum constant name;
	 * <code>false</code> otherwise
	 * @since 2.5
	 */
	public boolean isEnumConstant(String proposal) {

		for (EnumProposals enumProposal : enumProposals.values()) {
			for (String enumConstant : enumProposal.enumConstants()) {
				if (enumConstant.equals(proposal)) {
					return true;
				}
			}
		}

		return false;
	}

	/**
	 * Determines whether the given proposal is a mapping name.
	 *
	 * @param proposal The proposal that is being inserted into the JPQL query
	 * @return <code>true</code> if the given proposal is a mapping name; <code>false</code> otherwise
	 * @since 2.5
	 */
	public boolean isMappingName(String proposal) {

		for (IMapping mapping : mappings) {
			if (mapping.getName().equals(proposal)) {
				return true;
			}
		}

		return false;
	}

	/**
	 * Returns the longest possible JPQL identifier that is related to the given proposal if the
	 * proposal is a JPQL identifier and contains multiple words. For instance, the longest form of
	 * <code>JOIN</code> or <code>JOIN FETCH</code> is <code>LEFT OUTER JOIN FETCH</code>.
	 *
	 * @param proposal The proposal to retrieve its longest form if one is associated with it
	 * @return Either the given proposal if it's not a JPQL identifier or it does not have a longer
	 * form or the longest version of the JPQL identifier
	 */
	public String longuestIdentifier(String proposal) {
		return LONGUEST_IDENTIFIERS.containsKey(proposal) ? LONGUEST_IDENTIFIERS.get(proposal) : proposal;
	}

	/**
	 * {@inheritDoc}
	 */
	public Iterable<IMapping> mappings() {
		return new SnapshotCloneIterable<IMapping>(mappings);
	}

	/**
	 * Removes the given JPQL identifier.
	 *
	 * @param identifier The identifier that was added but actually needs to be removed
	 */
	protected void removeIdentifier(String identifier) {
		identifiers.remove(identifier);
	}

	/**
	 * Adds the given prefix that will be used to filter the list of possible class names.
	 *
	 * @param prefix The prefix that is used to filter the list of class names
	 * @param classType Determines how to filter the various types of classes
	 * @since 2.5
	 */
	public void setClassNamePrefix(String prefix, ClassType classType) {
		this.classNamePrefix = prefix;
		this.classType = classType;
	}

	/**
	 * Sets the table name and a prefix that will be used to filter the names of the table's columns.
	 *
	 * @param tableName The name of the table for which its column names should be retrieve as
	 * possible proposals
	 * @param prefix The prefix that is used to filter the list of columns names, which is never
	 * <code>null</code> but can be an empty string
	 * @since 2.5
	 */
	public void setTableName(String tableName, String prefix) {
		this.tableName = tableName;
		this.columnNamePrefix = prefix;
	}

	/**
	 * Adds the given prefix that will be used to filter the list of possible columns names.
	 *
	 * @param tableNamePrefix The prefix that is used to filter the list of columns names
	 * @since 2.5
	 */
	public void setTableNamePrefix(String tableNamePrefix) {
		this.tableNamePrefix = tableNamePrefix;
	}

	public int startPosition(WordParser wordParser, String proposal) {

		int index = wordParser.position();
		int maxMoveLength = proposal.length();

		while (index >= 0 && maxMoveLength > 0) {
			if (wordParser.startsWithIgnoreCase(proposal, index)) {
				return index;
			}
			index--;
			maxMoveLength--;
		}

		return -1;
	}

	/**
	 * {@inheritDoc}
	 */
	public Iterable<String> tableNames() {
		if (tableNamePrefix == null) {
			return Collections.emptyList();
		}
		return extension.tableNames(tableNamePrefix);
	}

	/**
	 * {@inheritDoc}
	 */
	@Override
	public String toString() {

		StringBuilder sb = new StringBuilder();

		// JPQL identifiers
		if (!identifiers.isEmpty()) {
			sb.append(identifiers);
		}

		// Entity names
		if (!entities.isEmpty()) {
			if (sb.length() > 0) {
				sb.append(", ");
			}
			sb.append(entities);
		}

		// Identification variables
		if (!identificationVariables.isEmpty()) {
			if (sb.length() > 0) {
				sb.append(", ");
			}
			sb.append(identificationVariables);
		}

		// Range identification variables
		if (!rangeIdentificationVariables.isEmpty()) {
			if (sb.length() > 0) {
				sb.append(", ");
			}
			sb.append(rangeIdentificationVariables);
		}

		// Mappings
		if (!mappings.isEmpty()) {
			if (sb.length() > 0) {
				sb.append(", ");
			}
			sb.append(mappings);
		}

		// Class names
		for (String className : classNames()) {
			if (sb.length() > 0) {
				sb.append(", ");
			}
			sb.append(className);
		}

		// Table names
		for (String tableName : tableNames()) {
			if (sb.length() > 0) {
				sb.append(", ");
			}
			sb.append(tableName);
		}

		// Column names
		for (String columnName : columnNames()) {
			if (sb.length() > 0) {
				sb.append(", ");
			}
			sb.append(columnName);
		}

		// Enum constant names
		for (EnumProposals enumProposals : enumConstant()) {
			for (String enumConstant : enumProposals.enumConstants()) {
				if (sb.length() > 0) {
					sb.append(", ");
				}
				sb.append(enumConstant);
			}
		}

		// No proposals
		if (sb.length() == 0) {
			sb.append("<No Default Proposals>");
		}

		return sb.toString();
	}

	private class DefaultEnumProposals implements EnumProposals {

		private Set<String> constants;
		private IType enumType;

		DefaultEnumProposals(IType enumType) {
			super();
			this.enumType  = enumType;
			this.constants = new HashSet<String>();
		}

		/**
		 * {@inheritDoc}
		 */
		public Iterable<String> enumConstants() {
			return new SnapshotCloneIterable<String>(constants);
		}

		/**
		 * {@inheritDoc}
		 */
		public IType enumType() {
			return enumType;
		}
	}

	/**
	 * This contains the result of inserting a proposal into a JPQL query at a given position.
	 *
	 * @see ContentAssistProposals#buildEscapedQuery(String, String, int, boolean)
	 * @see ContentAssistProposals#buildQuery(String, String, int, boolean)
	 */
	private final class Result implements ResultQuery {

		/**
		 * The new JPQL query after insertion of the proposal.
		 */
		private String jpqlQuery;

		/**
		 * The position of the cursor within the new query.
		 */
		private int position;

		/**
		 * Creates a new <code>Result</code>.
		 *
		 * @param jpqlQuery The new JPQL query after insertion of the proposal
		 * @param position The position of the cursor within the new query
		 */
		public Result(String jpqlQuery, int position) {
			super();
			this.jpqlQuery = jpqlQuery;
			this.position  = position;
		}

		/**
		 * {@inheritDoc}
		 */
		public int getPosition() {
			return position;
		}

		/**
		 * {@inheritDoc}
		 */
		public String getQuery() {
			return jpqlQuery;
		}

		/**
		 * {@inheritDoc}
		 */
		@Override
		public String toString() {
			StringBuilder sb = new StringBuilder();
			sb.append("Query=[").append(jpqlQuery);
			sb.append("], position=").append(position);
			return sb.toString();
		}
	}
}