/*******************************************************************************
 * Copyright (c) 2008, 2013 IBM Corporation and others.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v1.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/legal/epl-v10.html
 *
 * Contributors:
 *     IBM Corporation - initial API and implementation
 *******************************************************************************/
package org.eclipse.jdt.internal.compiler.parser;

import java.util.ArrayList;

import org.eclipse.jdt.core.Signature;
import org.eclipse.jdt.core.compiler.CharOperation;
import org.eclipse.jdt.internal.compiler.ast.ASTNode;
import org.eclipse.jdt.internal.compiler.ast.ArrayQualifiedTypeReference;
import org.eclipse.jdt.internal.compiler.ast.ArrayTypeReference;
import org.eclipse.jdt.internal.compiler.ast.ImportReference;
import org.eclipse.jdt.internal.compiler.ast.ParameterizedQualifiedTypeReference;
import org.eclipse.jdt.internal.compiler.ast.ParameterizedSingleTypeReference;
import org.eclipse.jdt.internal.compiler.ast.QualifiedTypeReference;
import org.eclipse.jdt.internal.compiler.ast.SingleTypeReference;
import org.eclipse.jdt.internal.compiler.ast.TypeParameter;
import org.eclipse.jdt.internal.compiler.ast.TypeReference;
import org.eclipse.jdt.internal.compiler.ast.Wildcard;
import org.eclipse.jdt.internal.compiler.classfmt.ClassFileConstants;
import org.eclipse.jdt.internal.compiler.lookup.TypeBinding;
import org.eclipse.jdt.internal.compiler.lookup.TypeConstants;
import org.eclipse.jdt.internal.compiler.problem.ProblemReporter;

@SuppressWarnings({ "rawtypes", "unchecked" })
public abstract class TypeConverter {

	int namePos;

	protected ProblemReporter problemReporter;
	protected boolean has1_5Compliance;
	private char memberTypeSeparator;

	protected TypeConverter(ProblemReporter problemReporter, char memberTypeSeparator) {
		this.problemReporter = problemReporter;
		this.has1_5Compliance = problemReporter.options.originalComplianceLevel >= ClassFileConstants.JDK1_5;
		this.memberTypeSeparator = memberTypeSeparator;
	}

	private void addIdentifiers(String typeSignature, int start, int endExclusive, int identCount, ArrayList fragments) {
		if (identCount == 1) {
			char[] identifier;
			typeSignature.getChars(start, endExclusive, identifier = new char[endExclusive-start], 0);
			fragments.add(identifier);
		} else
			fragments.add(extractIdentifiers(typeSignature, start, endExclusive-1, identCount));
	}

	/*
	 * Build an import reference from an import name, e.g. java.lang.*
	 */
	protected ImportReference createImportReference(
		String[] importName,
		int start,
		int end,
		boolean onDemand,
		int modifiers) {

		int length = importName.length;
		long[] positions = new long[length];
		long position = ((long) start << 32) + end;
		char[][] qImportName = new char[length][];
		for (int i = 0; i < length; i++) {
			qImportName[i] = importName[i].toCharArray();
			positions[i] = position; // dummy positions
		}
		return new ImportReference(
			qImportName,
			positions,
			onDemand,
			modifiers);
	}

	protected TypeParameter createTypeParameter(char[] typeParameterName, char[][] typeParameterBounds, int start, int end) {

		TypeParameter parameter = new TypeParameter();
		parameter.name = typeParameterName;
		parameter.sourceStart = start;
		parameter.sourceEnd = end;
		if (typeParameterBounds != null) {
			int length = typeParameterBounds.length;
			if (length > 0) {
				parameter.type = createTypeReference(typeParameterBounds[0], start, end);
				if (length > 1) {
					parameter.bounds = new TypeReference[length-1];
					for (int i = 1; i < length; i++) {
						TypeReference bound = createTypeReference(typeParameterBounds[i], start, end);
						bound.bits |= ASTNode.IsSuperType;
						parameter.bounds[i-1] = bound;
					}
				}
			}
		}
		return parameter;
	}

	/*
	 * Build a type reference from a readable name, e.g. java.lang.Object[][]
	 */
	protected TypeReference createTypeReference(
		char[] typeName,
		int start,
		int end,
		boolean includeGenericsAnyway) {

		int length = typeName.length;
		this.namePos = 0;
		return decodeType(typeName, length, start, end, true);
	}

	/*
	 * Build a type reference from a readable name, e.g. java.lang.Object[][]
	 */
	protected TypeReference createTypeReference(
		char[] typeName,
		int start,
		int end) {

		int length = typeName.length;
		this.namePos = 0;
		return decodeType(typeName, length, start, end, false);
	}

	/*
	 * Build a type reference from a type signature, e.g. Ljava.lang.Object;
	 */
	protected TypeReference createTypeReference(
			String typeSignature,
			int start,
			int end) {

		int length = typeSignature.length();
		this.namePos = 0;
		return decodeType(typeSignature, length, start, end);
	}

	private TypeReference decodeType(String typeSignature, int length, int start, int end) {
		int identCount = 1;
		int dim = 0;
		int nameFragmentStart = this.namePos, nameFragmentEnd = -1;
		boolean nameStarted = false;
		ArrayList fragments = null;
		typeLoop: while (this.namePos < length) {
			char currentChar = typeSignature.charAt(this.namePos);
			switch (currentChar) {
				case Signature.C_BOOLEAN :
					if (!nameStarted) {
						this.namePos++;
						if (dim == 0)
							return new SingleTypeReference(TypeBinding.BOOLEAN.simpleName, ((long) start << 32) + end);
						else
							return new ArrayTypeReference(TypeBinding.BOOLEAN.simpleName, dim, ((long) start << 32) + end);
					}
					break;
				case Signature.C_BYTE :
					if (!nameStarted) {
						this.namePos++;
						if (dim == 0)
							return new SingleTypeReference(TypeBinding.BYTE.simpleName, ((long) start << 32) + end);
						else
							return new ArrayTypeReference(TypeBinding.BYTE.simpleName, dim, ((long) start << 32) + end);
					}
					break;
				case Signature.C_CHAR :
					if (!nameStarted) {
						this.namePos++;
						if (dim == 0)
							return new SingleTypeReference(TypeBinding.CHAR.simpleName, ((long) start << 32) + end);
						else
							return new ArrayTypeReference(TypeBinding.CHAR.simpleName, dim, ((long) start << 32) + end);
					}
					break;
				case Signature.C_DOUBLE :
					if (!nameStarted) {
						this.namePos++;
						if (dim == 0)
							return new SingleTypeReference(TypeBinding.DOUBLE.simpleName, ((long) start << 32) + end);
						else
							return new ArrayTypeReference(TypeBinding.DOUBLE.simpleName, dim, ((long) start << 32) + end);
					}
					break;
				case Signature.C_FLOAT :
					if (!nameStarted) {
						this.namePos++;
						if (dim == 0)
							return new SingleTypeReference(TypeBinding.FLOAT.simpleName, ((long) start << 32) + end);
						else
							return new ArrayTypeReference(TypeBinding.FLOAT.simpleName, dim, ((long) start << 32) + end);
					}
					break;
				case Signature.C_INT :
					if (!nameStarted) {
						this.namePos++;
						if (dim == 0)
							return new SingleTypeReference(TypeBinding.INT.simpleName, ((long) start << 32) + end);
						else
							return new ArrayTypeReference(TypeBinding.INT.simpleName, dim, ((long) start << 32) + end);
					}
					break;
				case Signature.C_LONG :
					if (!nameStarted) {
						this.namePos++;
						if (dim == 0)
							return new SingleTypeReference(TypeBinding.LONG.simpleName, ((long) start << 32) + end);
						else
							return new ArrayTypeReference(TypeBinding.LONG.simpleName, dim, ((long) start << 32) + end);
					}
					break;
				case Signature.C_SHORT :
					if (!nameStarted) {
						this.namePos++;
						if (dim == 0)
							return new SingleTypeReference(TypeBinding.SHORT.simpleName, ((long) start << 32) + end);
						else
							return new ArrayTypeReference(TypeBinding.SHORT.simpleName, dim, ((long) start << 32) + end);
					}
					break;
				case Signature.C_VOID :
					if (!nameStarted) {
						this.namePos++;
						return new SingleTypeReference(TypeBinding.VOID.simpleName, ((long) start << 32) + end);
					}
					break;
				case Signature.C_RESOLVED :
				case Signature.C_UNRESOLVED :
				case Signature.C_TYPE_VARIABLE :
					if (!nameStarted) {
						nameFragmentStart = this.namePos+1;
						nameStarted = true;
					}
					break;
				case Signature.C_STAR:
					this.namePos++;
					Wildcard result = new Wildcard(Wildcard.UNBOUND);
					result.sourceStart = start;
					result.sourceEnd = end;
					return result;
				case Signature.C_EXTENDS:
					this.namePos++;
					result = new Wildcard(Wildcard.EXTENDS);
					result.bound = decodeType(typeSignature, length, start, end);
					result.sourceStart = start;
					result.sourceEnd = end;
					return result;
				case Signature.C_SUPER:
					this.namePos++;
					result = new Wildcard(Wildcard.SUPER);
					result.bound = decodeType(typeSignature, length, start, end);
					result.sourceStart = start;
					result.sourceEnd = end;
					return result;
				case Signature.C_ARRAY :
					dim++;
					break;
				case Signature.C_GENERIC_END :
				case Signature.C_SEMICOLON :
					nameFragmentEnd = this.namePos-1;
					this.namePos++;
					break typeLoop;
				case Signature.C_DOLLAR:
					if (this.memberTypeSeparator != Signature.C_DOLLAR)
						break;
					// $FALL-THROUGH$
				case Signature.C_DOT :
					if (!nameStarted) {
						nameFragmentStart = this.namePos+1;
						nameStarted = true;
					} else if (this.namePos > nameFragmentStart) // handle name starting with a $ (see https://bugs.eclipse.org/bugs/show_bug.cgi?id=91709)
						identCount ++;
					break;
				case Signature.C_GENERIC_START :
					nameFragmentEnd = this.namePos-1;
					// convert 1.5 specific constructs only if compliance is 1.5 or above
					if (!this.has1_5Compliance)
						break typeLoop;
					if (fragments == null) fragments = new ArrayList(2);
					addIdentifiers(typeSignature, nameFragmentStart, nameFragmentEnd + 1, identCount, fragments);
					this.namePos++; // skip '<'
					TypeReference[] arguments = decodeTypeArguments(typeSignature, length, start, end); // positionned on '>' at end
					fragments.add(arguments);
					identCount = 1;
					nameStarted = false;
					// next increment will skip '>'
					break;
			}
			this.namePos++;
		}
		if (fragments == null) { // non parameterized
			/* rebuild identifiers and dimensions */
			if (identCount == 1) { // simple type reference
				if (dim == 0) {
					char[] nameFragment = new char[nameFragmentEnd - nameFragmentStart + 1];
					typeSignature.getChars(nameFragmentStart, nameFragmentEnd +1, nameFragment, 0);
					return new SingleTypeReference(nameFragment, ((long) start << 32) + end);
				} else {
					char[] nameFragment = new char[nameFragmentEnd - nameFragmentStart + 1];
					typeSignature.getChars(nameFragmentStart, nameFragmentEnd +1, nameFragment, 0);
					return new ArrayTypeReference(nameFragment, dim, ((long) start << 32) + end);
				}
			} else { // qualified type reference
				long[] positions = new long[identCount];
				long pos = ((long) start << 32) + end;
				for (int i = 0; i < identCount; i++) {
					positions[i] = pos;
				}
				char[][] identifiers = extractIdentifiers(typeSignature, nameFragmentStart, nameFragmentEnd, identCount);
				if (dim == 0) {
					return new QualifiedTypeReference(identifiers, positions);
				} else {
					return new ArrayQualifiedTypeReference(identifiers, dim, positions);
				}
			}
		} else { // parameterized
			// rebuild type reference from available fragments: char[][], arguments, char[][], arguments...
			// check trailing qualified name
			if (nameStarted) {
				addIdentifiers(typeSignature, nameFragmentStart, nameFragmentEnd + 1, identCount, fragments);
			}
			int fragmentLength = fragments.size();
			if (fragmentLength == 2) {
				Object firstFragment = fragments.get(0);
				if (firstFragment instanceof char[]) {
					// parameterized single type
					return new ParameterizedSingleTypeReference((char[]) firstFragment, (TypeReference[]) fragments.get(1), dim, ((long) start << 32) + end);
				}
			}
			// parameterized qualified type
			identCount = 0;
			for (int i = 0; i < fragmentLength; i ++) {
				Object element = fragments.get(i);
				if (element instanceof char[][]) {
					identCount += ((char[][])element).length;
				} else if (element instanceof char[])
					identCount++;
			}
			char[][] tokens = new char[identCount][];
			TypeReference[][] arguments = new TypeReference[identCount][];
			int index = 0;
			for (int i = 0; i < fragmentLength; i ++) {
				Object element = fragments.get(i);
				if (element instanceof char[][]) {
					char[][] fragmentTokens = (char[][]) element;
					int fragmentTokenLength = fragmentTokens.length;
					System.arraycopy(fragmentTokens, 0, tokens, index, fragmentTokenLength);
					index += fragmentTokenLength;
				} else if (element instanceof char[]) {
					tokens[index++] = (char[]) element;
				} else {
					arguments[index-1] = (TypeReference[]) element;
				}
			}
			long[] positions = new long[identCount];
			long pos = ((long) start << 32) + end;
			for (int i = 0; i < identCount; i++) {
				positions[i] = pos;
			}
			return new ParameterizedQualifiedTypeReference(tokens, arguments, dim, positions);
		}
	}

	private TypeReference decodeType(char[] typeName, int length, int start, int end, boolean includeGenericsAnyway) {
		int identCount = 1;
		int dim = 0;
		int nameFragmentStart = this.namePos, nameFragmentEnd = -1;
		ArrayList fragments = null;
		typeLoop: while (this.namePos < length) {
			char currentChar = typeName[this.namePos];
			switch (currentChar) {
				case '?' :
					this.namePos++; // skip '?'
					while (typeName[this.namePos] == ' ') this.namePos++;
					switch(typeName[this.namePos]) {
						case 's' :
							checkSuper: {
								int max = TypeConstants.WILDCARD_SUPER.length-1;
								for (int ahead = 1; ahead < max; ahead++) {
									if (typeName[this.namePos+ahead] != TypeConstants.WILDCARD_SUPER[ahead+1]) {
										break checkSuper;
									}
								}
								this.namePos += max;
								Wildcard result = new Wildcard(Wildcard.SUPER);
								result.bound = decodeType(typeName, length, start, end, includeGenericsAnyway);
								result.sourceStart = start;
								result.sourceEnd = end;
								return result;
							}
							break;
						case 'e' :
							checkExtends: {
								int max = TypeConstants.WILDCARD_EXTENDS.length-1;
								for (int ahead = 1; ahead < max; ahead++) {
									if (typeName[this.namePos+ahead] != TypeConstants.WILDCARD_EXTENDS[ahead+1]) {
										break checkExtends;
									}
								}
								this.namePos += max;
								Wildcard result = new Wildcard(Wildcard.EXTENDS);
								result.bound = decodeType(typeName, length, start, end, includeGenericsAnyway);
								result.sourceStart = start;
								result.sourceEnd = end;
								return result;
							}
							break;
					}
					Wildcard result = new Wildcard(Wildcard.UNBOUND);
					result.sourceStart = start;
					result.sourceEnd = end;
					return result;
				case '[' :
					if (dim == 0 && nameFragmentEnd < 0) nameFragmentEnd = this.namePos-1;
					dim++;
					break;
				case ']' :
					break;
				case '>' :
				case ',' :
					break typeLoop;
				case '.' :
					if (nameFragmentStart < 0) nameFragmentStart = this.namePos+1; // member type name
					identCount ++;
					break;
				case '<' :
					/* We need to convert and preserve 1.5 specific constructs either if compliance is 1.5 or above,
					   or the caller has explicitly requested generics to be included. The parameter includeGenericsAnyway
					   should be used by the caller to signal that in the calling context generics information must be 
					   internalized even when the requesting project is 1.4. But in all cases, we must skip over them to
					   see if there are any applicable type fragments after the type parameters: i.e we just aren't done
					   having seen a '<' in 1.4 mode. 
					   
					   Because of the way type signatures are encoded, TypeConverter.decodeType(String, int, int, int) is immune
					   to this problem. See https://bugs.eclipse.org/bugs/show_bug.cgi?id=325633
					 */
					if (this.has1_5Compliance || includeGenericsAnyway) {
						if (fragments == null) fragments = new ArrayList(2);
					}
					nameFragmentEnd = this.namePos-1;
					if (this.has1_5Compliance || includeGenericsAnyway) {
						char[][] identifiers = CharOperation.splitOn('.', typeName, nameFragmentStart, this.namePos);
						fragments.add(identifiers);
					}
					this.namePos++; // skip '<'
					TypeReference[] arguments = decodeTypeArguments(typeName, length, start, end, includeGenericsAnyway); // positionned on '>' at end
					if (this.has1_5Compliance || includeGenericsAnyway) {
						fragments.add(arguments);
						identCount = 0;
						nameFragmentStart = -1;
						nameFragmentEnd = -1;
					}
					// next increment will skip '>'
					break;
			}
			this.namePos++;
		}
		if (nameFragmentEnd < 0) nameFragmentEnd = this.namePos-1;
		if (fragments == null) { // non parameterized
			/* rebuild identifiers and dimensions */
			if (identCount == 1) { // simple type reference
				if (dim == 0) {
					char[] nameFragment;
					if (nameFragmentStart != 0 || nameFragmentEnd >= 0) {
						int nameFragmentLength = nameFragmentEnd - nameFragmentStart + 1;
						System.arraycopy(typeName, nameFragmentStart, nameFragment = new char[nameFragmentLength], 0, nameFragmentLength);
					} else {
						nameFragment = typeName;
					}
					return new SingleTypeReference(nameFragment, ((long) start << 32) + end);
				} else {
					int nameFragmentLength = nameFragmentEnd - nameFragmentStart + 1;
					char[] nameFragment = new char[nameFragmentLength];
					System.arraycopy(typeName, nameFragmentStart, nameFragment, 0, nameFragmentLength);
					return new ArrayTypeReference(nameFragment, dim, ((long) start << 32) + end);
				}
			} else { // qualified type reference
				long[] positions = new long[identCount];
				long pos = ((long) start << 32) + end;
				for (int i = 0; i < identCount; i++) {
					positions[i] = pos;
				}
				char[][] identifiers = CharOperation.splitOn('.', typeName, nameFragmentStart, nameFragmentEnd+1);
				if (dim == 0) {
					return new QualifiedTypeReference(identifiers, positions);
				} else {
					return new ArrayQualifiedTypeReference(identifiers, dim, positions);
				}
			}
		} else { // parameterized
			// rebuild type reference from available fragments: char[][], arguments, char[][], arguments...
			// check trailing qualified name
			if (nameFragmentStart > 0 && nameFragmentStart < length) {
				char[][] identifiers = CharOperation.splitOn('.', typeName, nameFragmentStart, nameFragmentEnd+1);
				fragments.add(identifiers);
			}
			int fragmentLength = fragments.size();
			if (fragmentLength == 2) {
				char[][] firstFragment = (char[][]) fragments.get(0);
				if (firstFragment.length == 1) {
					// parameterized single type
					return new ParameterizedSingleTypeReference(firstFragment[0], (TypeReference[]) fragments.get(1), dim, ((long) start << 32) + end);
				}
			}
			// parameterized qualified type
			identCount = 0;
			for (int i = 0; i < fragmentLength; i ++) {
				Object element = fragments.get(i);
				if (element instanceof char[][]) {
					identCount += ((char[][])element).length;
				}
			}
			char[][] tokens = new char[identCount][];
			TypeReference[][] arguments = new TypeReference[identCount][];
			int index = 0;
			for (int i = 0; i < fragmentLength; i ++) {
				Object element = fragments.get(i);
				if (element instanceof char[][]) {
					char[][] fragmentTokens = (char[][]) element;
					int fragmentTokenLength = fragmentTokens.length;
					System.arraycopy(fragmentTokens, 0, tokens, index, fragmentTokenLength);
					index += fragmentTokenLength;
				} else {
					arguments[index-1] = (TypeReference[]) element;
				}
			}
			long[] positions = new long[identCount];
			long pos = ((long) start << 32) + end;
			for (int i = 0; i < identCount; i++) {
				positions[i] = pos;
			}
			return new ParameterizedQualifiedTypeReference(tokens, arguments, dim, positions);
		}
	}

	private TypeReference[] decodeTypeArguments(char[] typeName, int length, int start, int end, boolean includeGenericsAnyway) {
		ArrayList argumentList = new ArrayList(1);
		int count = 0;
		argumentsLoop: while (this.namePos < length) {
			TypeReference argument = decodeType(typeName, length, start, end, includeGenericsAnyway);
			count++;
			argumentList.add(argument);
			if (this.namePos >= length) break argumentsLoop;
			if (typeName[this.namePos] == '>') {
				break argumentsLoop;
			}
			this.namePos++; // skip ','
		}
		TypeReference[] typeArguments = new TypeReference[count];
		argumentList.toArray(typeArguments);
		return typeArguments;
	}

	private TypeReference[] decodeTypeArguments(String typeSignature, int length, int start, int end) {
		ArrayList argumentList = new ArrayList(1);
		int count = 0;
		argumentsLoop: while (this.namePos < length) {
			TypeReference argument = decodeType(typeSignature, length, start, end);
			count++;
			argumentList.add(argument);
			if (this.namePos >= length) break argumentsLoop;
			if (typeSignature.charAt(this.namePos) == Signature.C_GENERIC_END) {
				break argumentsLoop;
			}
		}
		TypeReference[] typeArguments = new TypeReference[count];
		argumentList.toArray(typeArguments);
		return typeArguments;
	}

	private char[][] extractIdentifiers(String typeSignature, int start, int endInclusive, int identCount) {
		char[][] result = new char[identCount][];
		int charIndex = start;
		int i = 0;
		while (charIndex < endInclusive) {
			char currentChar;
			if ((currentChar = typeSignature.charAt(charIndex)) == this.memberTypeSeparator || currentChar == Signature.C_DOT) {
				typeSignature.getChars(start, charIndex, result[i++] = new char[charIndex - start], 0);
				start = ++charIndex;
			} else
				charIndex++;
		}
		typeSignature.getChars(start, charIndex + 1, result[i++] = new char[charIndex - start + 1], 0);
		return result;
	}
}