/*******************************************************************************
 * Copyright (c) 2000, 2004 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.core.search.indexing;

import org.eclipse.jdt.core.Signature;
import org.eclipse.jdt.core.compiler.CharOperation;
import org.eclipse.jdt.core.search.SearchDocument;
import org.eclipse.jdt.internal.compiler.classfmt.ClassFileConstants;
import org.eclipse.jdt.internal.compiler.classfmt.ClassFileReader;
import org.eclipse.jdt.internal.compiler.classfmt.ClassFormatException;
import org.eclipse.jdt.internal.compiler.classfmt.FieldInfo;
import org.eclipse.jdt.internal.compiler.classfmt.MethodInfo;
import org.eclipse.jdt.internal.compiler.env.IGenericType;
import org.eclipse.jdt.internal.compiler.util.SuffixConstants;

public class BinaryIndexer extends AbstractIndexer implements SuffixConstants {
	private static final char[] BYTE = "byte".toCharArray(); //$NON-NLS-1$
	private static final char[] CHAR = "char".toCharArray(); //$NON-NLS-1$
	private static final char[] DOUBLE = "double".toCharArray(); //$NON-NLS-1$
	private static final char[] FLOAT = "float".toCharArray(); //$NON-NLS-1$
	private static final char[] INT = "int".toCharArray(); //$NON-NLS-1$
	private static final char[] LONG = "long".toCharArray(); //$NON-NLS-1$
	private static final char[] SHORT = "short".toCharArray(); //$NON-NLS-1$
	private static final char[] BOOLEAN = "boolean".toCharArray(); //$NON-NLS-1$
	private static final char[] VOID = "void".toCharArray(); //$NON-NLS-1$
	private static final char[] INIT = "<init>".toCharArray(); //$NON-NLS-1$

	public BinaryIndexer(SearchDocument document) {
		super(document);
	}
	public void addTypeReference(char[] typeName) {
		int length = typeName.length;
		if (length > 2 && typeName[length - 2] == '$') {
			switch (typeName[length - 1]) {
				case '0' :
				case '1' :
				case '2' :
				case '3' :
				case '4' :
				case '5' :
				case '6' :
				case '7' :
				case '8' :
				case '9' :
					return; // skip local type names
			}
		}

	 	// consider that A$B is a member type: so replace '$' with '.'
	 	// (see https://bugs.eclipse.org/bugs/show_bug.cgi?id=40116)
		typeName = CharOperation.replaceOnCopy(typeName, '$', '.'); // copy it so the original is not modified

		super.addTypeReference(typeName);
	}
	/**
	 * For example:
	 *   - int foo(String[]) is ([Ljava/lang/String;)I => java.lang.String[] in a char[][]
	 *   - void foo(int) is (I)V ==> int
	 */
	private void convertToArrayType(char[][] parameterTypes, int counter, int arrayDim) {
		int length = parameterTypes[counter].length;
		char[] arrayType = new char[length + arrayDim*2];
		System.arraycopy(parameterTypes[counter], 0, arrayType, 0, length);
		for (int i = 0; i < arrayDim; i++) {
			arrayType[length + (i * 2)] = '[';
			arrayType[length + (i * 2) + 1] = ']';
		}
		parameterTypes[counter] = arrayType;
	}
	/**
	 * For example:
	 *   - int foo(String[]) is ([Ljava/lang/String;)I => java.lang.String[] in a char[][]
	 *   - void foo(int) is (I)V ==> int
	 */
	private char[] convertToArrayType(char[] typeName, int arrayDim) {
		int length = typeName.length;
		char[] arrayType = new char[length + arrayDim*2];
		System.arraycopy(typeName, 0, arrayType, 0, length);
		for (int i = 0; i < arrayDim; i++) {
			arrayType[length + (i * 2)] = '[';
			arrayType[length + (i * 2) + 1] = ']';
		}
		return arrayType;
	}
	private char[] decodeFieldType(char[] signature) throws ClassFormatException {
		if (signature == null) return null;
		int arrayDim = 0;
		for (int i = 0, max = signature.length; i < max; i++) {
			switch(signature[i]) {
				case 'B':
					if (arrayDim > 0)
						return convertToArrayType(BYTE, arrayDim);
					return BYTE;
					
				case 'C':
					if (arrayDim > 0)
						return convertToArrayType(CHAR, arrayDim);
					return CHAR;
					
				case 'D':
					if (arrayDim > 0)
						return convertToArrayType(DOUBLE, arrayDim);
					return DOUBLE;
					
				case 'F':
					if (arrayDim > 0)
						return convertToArrayType(FLOAT, arrayDim);
					return FLOAT;
					
				case 'I':
					if (arrayDim > 0)
					return convertToArrayType(INT, arrayDim);
					return INT;
					
				case 'J':
					if (arrayDim > 0)
						return convertToArrayType(LONG, arrayDim);
					return LONG;
					
				case 'L':
					int indexOfSemiColon = CharOperation.indexOf(';', signature, i+1);
					if (indexOfSemiColon == -1) throw new ClassFormatException(ClassFormatException.ErrInvalidMethodSignature);
					if (arrayDim > 0) {
						return convertToArrayType(replace('/','.',CharOperation.subarray(signature, i + 1, indexOfSemiColon)), arrayDim);
					}
					return replace('/','.',CharOperation.subarray(signature, i + 1, indexOfSemiColon));
					
				case 'S':
					if (arrayDim > 0)
						return convertToArrayType(SHORT, arrayDim);
					return SHORT;
					
				case 'Z':
					if (arrayDim > 0)
						return convertToArrayType(BOOLEAN, arrayDim);
					return BOOLEAN;
					
				case 'V':
					return VOID;
					
				case '[':
					arrayDim++;
					break;
					
				default:
					throw new ClassFormatException(ClassFormatException.ErrInvalidMethodSignature);
			}
		}
		return null;
	}
	/**
	 * For example:
	 *   - int foo(String[]) is ([Ljava/lang/String;)I => java.lang.String[] in a char[][]
	 *   - void foo(int) is (I)V ==> int
	 */
	private char[][] decodeParameterTypes(char[] signature) throws ClassFormatException {
		if (signature == null) return null;
		int indexOfClosingParen = CharOperation.lastIndexOf(')', signature);
		if (indexOfClosingParen == 1) {
			// there is no parameter
			return null;
		}
		if (indexOfClosingParen == -1) {
			throw new ClassFormatException(ClassFormatException.ErrInvalidMethodSignature);
		}
		char[][] parameterTypes = new char[3][];
		int parameterTypesCounter = 0;
		int arrayDim = 0;
		for (int i = 1; i < indexOfClosingParen; i++) {
			if (parameterTypesCounter == parameterTypes.length) {
				// resize
				System.arraycopy(parameterTypes, 0, (parameterTypes = new char[parameterTypesCounter * 2][]), 0, parameterTypesCounter);
			}
			switch(signature[i]) {
				case 'B':
					parameterTypes[parameterTypesCounter++] = BYTE;
					if (arrayDim > 0)
						convertToArrayType(parameterTypes, parameterTypesCounter-1, arrayDim);
					arrayDim = 0;
					break;
					
				case 'C':
					parameterTypes[parameterTypesCounter++] = CHAR;
					if (arrayDim > 0)
						convertToArrayType(parameterTypes, parameterTypesCounter-1, arrayDim);
					arrayDim = 0;
					break;

				case 'D':
					parameterTypes[parameterTypesCounter++] = DOUBLE;
					if (arrayDim > 0)
						convertToArrayType(parameterTypes, parameterTypesCounter-1, arrayDim);
					arrayDim = 0;
					break;

				case 'F':
					parameterTypes[parameterTypesCounter++] = FLOAT;
					if (arrayDim > 0)
						convertToArrayType(parameterTypes, parameterTypesCounter-1, arrayDim);
					arrayDim = 0;
					break;
					
				case 'I':
					parameterTypes[parameterTypesCounter++] = INT;
					if (arrayDim > 0)
						convertToArrayType(parameterTypes, parameterTypesCounter-1, arrayDim);
					arrayDim = 0;
					break;
					
				case 'J':
					parameterTypes[parameterTypesCounter++] = LONG;
					if (arrayDim > 0)
						convertToArrayType(parameterTypes, parameterTypesCounter-1, arrayDim);
					arrayDim = 0;
					break;

				case 'L':
					int indexOfSemiColon = CharOperation.indexOf(';', signature, i+1);
					if (indexOfSemiColon == -1) throw new ClassFormatException(ClassFormatException.ErrInvalidMethodSignature);
					parameterTypes[parameterTypesCounter++] = replace('/','.',CharOperation.subarray(signature, i + 1, indexOfSemiColon));
					if (arrayDim > 0)
						convertToArrayType(parameterTypes, parameterTypesCounter-1, arrayDim);
					i = indexOfSemiColon;
					arrayDim = 0;
					break;

				case 'S':
					parameterTypes[parameterTypesCounter++] = SHORT;
					if (arrayDim > 0)
						convertToArrayType(parameterTypes, parameterTypesCounter-1, arrayDim);
					arrayDim = 0;
					break;

				case 'Z':
					parameterTypes[parameterTypesCounter++] = BOOLEAN;
					if (arrayDim > 0)
						convertToArrayType(parameterTypes, parameterTypesCounter-1, arrayDim);
					arrayDim = 0;
					break;

				case '[':
					arrayDim++;
					break;
					
				default:
					throw new ClassFormatException(ClassFormatException.ErrInvalidMethodSignature);
			}
		}
		if (parameterTypes.length != parameterTypesCounter) {
			System.arraycopy(parameterTypes, 0, parameterTypes = new char[parameterTypesCounter][], 0, parameterTypesCounter);
		}
		return parameterTypes;
	}
	private char[] decodeReturnType(char[] signature) throws ClassFormatException {
		if (signature == null) return null;
		int indexOfClosingParen = CharOperation.lastIndexOf(')', signature);
		if (indexOfClosingParen == -1) throw new ClassFormatException(ClassFormatException.ErrInvalidMethodSignature);
		int arrayDim = 0;
		for (int i = indexOfClosingParen + 1, max = signature.length; i < max; i++) {
			switch(signature[i]) {
				case 'B':
					if (arrayDim > 0)
						return convertToArrayType(BYTE, arrayDim);
					return BYTE;
					
				case 'C':
					if (arrayDim > 0)
						return convertToArrayType(CHAR, arrayDim);
					return CHAR;
					
				case 'D':
					if (arrayDim > 0)
						return convertToArrayType(DOUBLE, arrayDim);
					return DOUBLE;

				case 'F':
					if (arrayDim > 0)
						return convertToArrayType(FLOAT, arrayDim);
					return FLOAT;

				case 'I':
					if (arrayDim > 0)
						return convertToArrayType(INT, arrayDim);
					return INT;

				case 'J':
					if (arrayDim > 0)
						return convertToArrayType(LONG, arrayDim);
					return LONG;

				case 'L':
					int indexOfSemiColon = CharOperation.indexOf(';', signature, i+1);
					if (indexOfSemiColon == -1) throw new ClassFormatException(ClassFormatException.ErrInvalidMethodSignature);
					if (arrayDim > 0) {
						return convertToArrayType(replace('/','.',CharOperation.subarray(signature, i + 1, indexOfSemiColon)), arrayDim);
					}
					return replace('/','.',CharOperation.subarray(signature, i + 1, indexOfSemiColon));

				case 'S':
					if (arrayDim > 0)
						return convertToArrayType(SHORT, arrayDim);
					return SHORT;

				case 'Z':
					if (arrayDim > 0)
						return convertToArrayType(BOOLEAN, arrayDim);
					return BOOLEAN;

				case 'V':
					return VOID;

				case '[':
					arrayDim++;
					break;
					
				default:
					throw new ClassFormatException(ClassFormatException.ErrInvalidMethodSignature);
			}
		}
		return null;
	}
	private int extractArgCount(char[] signature) throws ClassFormatException {
		int indexOfClosingParen = CharOperation.lastIndexOf(')', signature);
		if (indexOfClosingParen == 1) {
			// there is no parameter
			return 0;
		}
		if (indexOfClosingParen == -1) {
			throw new ClassFormatException(ClassFormatException.ErrInvalidMethodSignature);
		}
		int parameterTypesCounter = 0;
		for (int i = 1; i < indexOfClosingParen; i++) {
			switch(signature[i]) {
				case 'B':
				case 'C':
				case 'D':
				case 'F':
				case 'I':
				case 'J':
				case 'S':
				case 'Z':
					parameterTypesCounter++;
					break;
				case 'L':
					int indexOfSemiColon = CharOperation.indexOf(';', signature, i+1);
					if (indexOfSemiColon == -1) throw new ClassFormatException(ClassFormatException.ErrInvalidMethodSignature);
					parameterTypesCounter++;
					i = indexOfSemiColon;
					break;
				case '[':
					break;
				default:
					throw new ClassFormatException(ClassFormatException.ErrInvalidMethodSignature);
			}
		}
		return parameterTypesCounter;
	}
	private char[] extractClassName(int[] constantPoolOffsets, ClassFileReader reader, int index) {
		// the entry at i has to be a field ref or a method/interface method ref.
		int class_index = reader.u2At(constantPoolOffsets[index] + 1);
		int utf8Offset = constantPoolOffsets[reader.u2At(constantPoolOffsets[class_index] + 1)];
		return reader.utf8At(utf8Offset + 3, reader.u2At(utf8Offset + 1));
	}
	private char[] extractName(int[] constantPoolOffsets, ClassFileReader reader, int index) {
		int nameAndTypeIndex = reader.u2At(constantPoolOffsets[index] + 3);
		int utf8Offset = constantPoolOffsets[reader.u2At(constantPoolOffsets[nameAndTypeIndex] + 1)];
		return reader.utf8At(utf8Offset + 3, reader.u2At(utf8Offset + 1));
	}
	private char[] extractClassReference(int[] constantPoolOffsets, ClassFileReader reader, int index) {
		// the entry at i has to be a class ref.
		int utf8Offset = constantPoolOffsets[reader.u2At(constantPoolOffsets[index] + 1)];
		return reader.utf8At(utf8Offset + 3, reader.u2At(utf8Offset + 1));
	}
	/**
	 * Extract all type, method, field and interface method references from the constant pool
	 */
	private void extractReferenceFromConstantPool(byte[] contents, ClassFileReader reader) throws ClassFormatException {
		int[] constantPoolOffsets = reader.getConstantPoolOffsets();
		int constantPoolCount = constantPoolOffsets.length;
		for (int i = 1; i < constantPoolCount; i++) {
			int tag = reader.u1At(constantPoolOffsets[i]);
			/**
			 * u1 tag
			 * u2 class_index
			 * u2 name_and_type_index
			 */
			char[] name = null;
			char[] type = null;
			switch (tag) {
				case ClassFileConstants.FieldRefTag :
					// add reference to the class/interface and field name and type
					name = extractName(constantPoolOffsets, reader, i);
					addFieldReference(name);
					break;
				case ClassFileConstants.MethodRefTag :
					// add reference to the class and method name and type
				case ClassFileConstants.InterfaceMethodRefTag :
					// add reference to the interface and method name and type
					name = extractName(constantPoolOffsets, reader, i);
					type = extractType(constantPoolOffsets, reader, i);
					if (CharOperation.equals(INIT, name)) {
						// add a constructor reference
						char[] className = replace('/', '.', extractClassName(constantPoolOffsets, reader, i)); // so that it looks like java.lang.String
						addConstructorReference(className, extractArgCount(type));
					} else {
						// add a method reference
						addMethodReference(name, extractArgCount(type));
					}
					break;
				case ClassFileConstants.ClassTag :
					// add a type reference
					name = extractClassReference(constantPoolOffsets, reader, i);
					if (name.length > 0 && name[0] == '[')
						break; // skip over array references
					name = replace('/', '.', name); // so that it looks like java.lang.String
					addTypeReference(name);
					
					// also add a simple reference on each segment of the qualification (see http://bugs.eclipse.org/bugs/show_bug.cgi?id=24741)
					char[][] qualification = CharOperation.splitOn('.', name);
					for (int j = 0, length = qualification.length; j < length; j++) {
						addNameReference(qualification[j]);
					}
					break;
			}
		}
	}
	private char[] extractType(int[] constantPoolOffsets, ClassFileReader reader, int index) {
		int constantPoolIndex = reader.u2At(constantPoolOffsets[index] + 3);
		int utf8Offset = constantPoolOffsets[reader.u2At(constantPoolOffsets[constantPoolIndex] + 3)];
		return reader.utf8At(utf8Offset + 3, reader.u2At(utf8Offset + 1));
	}
	public void indexDocument() {
		try {
			byte[] contents = this.document.getByteContents();
			ClassFileReader reader = new ClassFileReader(contents, this.document.getPath().toCharArray());
	
			// first add type references
			char[] className = replace('/', '.', reader.getName()); // looks like java/lang/String
			// need to extract the package name and the simple name
			int packageNameIndex = CharOperation.lastIndexOf('.', className);
			char[] packageName = null;
			char[] name = null;
			if (packageNameIndex >= 0) {
				packageName = CharOperation.subarray(className, 0, packageNameIndex);
				name = CharOperation.subarray(className, packageNameIndex + 1, className.length);
			} else {
				packageName = CharOperation.NO_CHAR;
				name = className;
			}
			char[] enclosingTypeName = null;
			if (reader.isNestedType()) {
				if (reader.isAnonymous()) {
					name = CharOperation.NO_CHAR;
				} else {
					name = reader.getInnerSourceName();
				}
				if (reader.isLocal() || reader.isAnonymous()) {
					enclosingTypeName = ONE_ZERO;
				} else {
					char[] fullEnclosingName = reader.getEnclosingTypeName();
					int nameLength = fullEnclosingName.length - packageNameIndex - 1;
					if (nameLength <= 0) {
						// See PR 1GIR345: ITPJCORE:ALL - Indexer: NegativeArraySizeException
						return;
					}
					enclosingTypeName = new char[nameLength]; 
					System.arraycopy(fullEnclosingName, packageNameIndex + 1, enclosingTypeName, 0, nameLength);
				}
			}
			// type parameters
			char[][] typeParameterSignatures = null;
			char[] genericSignature = reader.getGenericSignature();
			if (genericSignature != null) {
				CharOperation.replace(genericSignature, '/', '.');
				typeParameterSignatures = Signature.getTypeParameters(genericSignature);
			}
			
			// eliminate invalid innerclasses (1G4KCF7)
			if (name == null) return;
			
			char[][] superinterfaces = replace('/', '.', reader.getInterfaceNames());
			char[][] enclosingTypeNames = enclosingTypeName == null ? null : new char[][] {enclosingTypeName};
			switch (reader.getKind()) {
				case IGenericType.CLASS_DECL :
					char[] superclass = replace('/', '.', reader.getSuperclassName());
					addClassDeclaration(reader.getModifiers(), packageName, name, enclosingTypeNames, superclass, superinterfaces, typeParameterSignatures);
					break;
				case IGenericType.INTERFACE_DECL :
					addInterfaceDeclaration(reader.getModifiers(), packageName, name, enclosingTypeNames, superinterfaces, typeParameterSignatures);
					break;
				case IGenericType.ENUM_DECL :
					addEnumDeclaration(reader.getModifiers(), packageName, name, enclosingTypeNames, superinterfaces);
					break;
				case IGenericType.ANNOTATION_TYPE_DECL :
					addAnnotationTypeDeclaration(reader.getModifiers(), packageName, name, enclosingTypeNames);
					break;
			}			
	
			// first reference all methods declarations and field declarations
			MethodInfo[] methods = (MethodInfo[]) reader.getMethods();
			if (methods != null) {
				for (int i = 0, max = methods.length; i < max; i++) {
					MethodInfo method = methods[i];
					char[] descriptor = method.getMethodDescriptor();
					char[][] parameterTypes = decodeParameterTypes(descriptor);
					char[] returnType = decodeReturnType(descriptor);
					char[][] exceptionTypes = replace('/', '.', method.getExceptionTypeNames());
					if (method.isConstructor()) {
						addConstructorDeclaration(className, parameterTypes, exceptionTypes);
					} else {
						if (!method.isClinit()) {
							addMethodDeclaration(method.getSelector(), parameterTypes, returnType, exceptionTypes);
						}
					}
				}
			}
			FieldInfo[] fields = (FieldInfo[]) reader.getFields();
			if (fields != null) {
				for (int i = 0, max = fields.length; i < max; i++) {
					FieldInfo field = fields[i];
					char[] fieldName = field.getName();
					char[] fieldType = decodeFieldType(replace('/', '.', field.getTypeName()));
					addFieldDeclaration(fieldType, fieldName);
				}
			}
	
			// record all references found inside the .class file
			extractReferenceFromConstantPool(contents, reader);
		} catch (ClassFormatException e) {
			// ignore
		}
	}
	/*
	 * Modify the array by replacing all occurences of toBeReplaced with newChar
	 */
	private char[][] replace(char toBeReplaced, char newChar, char[][] array) {
		if (array == null) return null;
		for (int i = 0, max = array.length; i < max; i++) {
			replace(toBeReplaced, newChar, array[i]);
		}
		return array;
	}
	/*
	 * Modify the array by replacing all occurences of toBeReplaced with newChar
	 */
	private char[] replace(char toBeReplaced, char newChar, char[] array) {
		if (array == null) return null;
		for (int i = 0, max = array.length; i < max; i++) {
			if (array[i] == toBeReplaced) {
				array[i] = newChar;
			}
		}
		return array;
	}
}
