/*******************************************************************************
 * Copyright (c) 2005, 2016 BEA Systems, Inc.
 *
 * This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License 2.0
 * which accompanies this distribution, and is available at
 * https://www.eclipse.org/legal/epl-2.0/
 *
 * SPDX-License-Identifier: EPL-2.0
 *
 * Contributors:
 *    tyeung@bea.com - initial API and implementation
 *    olivier_thomann@ca.ibm.com - add hashCode() and equals(..) methods
 *******************************************************************************/
package org.eclipse.jdt.internal.compiler.classfmt;

import java.util.Arrays;

import org.eclipse.jdt.core.compiler.CharOperation;
import org.eclipse.jdt.internal.compiler.ast.Annotation;
import org.eclipse.jdt.internal.compiler.codegen.ConstantPool;
import org.eclipse.jdt.internal.compiler.env.*;
import org.eclipse.jdt.internal.compiler.impl.*;
import org.eclipse.jdt.internal.compiler.lookup.TagBits;
import org.eclipse.jdt.internal.compiler.util.Util;

public class AnnotationInfo extends ClassFileStruct implements IBinaryAnnotation {
	/** The name of the annotation type */
	private char[] typename;
	/**
	 * null until this annotation is initialized
	 * @see #getElementValuePairs()
	 */
	private volatile ElementValuePairInfo[] pairs;

	long standardAnnotationTagBits = 0;
	int readOffset = 0;

	static Object[] EmptyValueArray = new Object[0];

	public RuntimeException exceptionDuringDecode;

AnnotationInfo(byte[] classFileBytes, int[] contantPoolOffsets, int offset) {
	super(classFileBytes, contantPoolOffsets, offset);
}
/**
 * @param offset the offset into <code>classFileBytes</code> for the "type_index" of the annotation attribute.
 * @param populate <code>true</code> to indicate to build out the annotation structure.
 */
AnnotationInfo(byte[] classFileBytes, int[] contantPoolOffsets, int offset, boolean runtimeVisible, boolean populate) {
	this(classFileBytes, contantPoolOffsets, offset);
	if (populate)
		decodeAnnotation();
	else
		this.readOffset = scanAnnotation(0, runtimeVisible, true);
}
private void decodeAnnotation() {
	this.readOffset = 0;
	int utf8Offset = this.constantPoolOffsets[u2At(0)] - this.structOffset;
	this.typename = utf8At(utf8Offset + 3, u2At(utf8Offset + 1));
	int numberOfPairs = u2At(2);
	// u2 type_index + u2 num_member_value_pair
	this.readOffset += 4;
	ElementValuePairInfo[] decodedPairs = numberOfPairs == 0 ? ElementValuePairInfo.NoMembers : new ElementValuePairInfo[numberOfPairs];
	int i = 0;
	try {
		while (i < numberOfPairs) {
			// u2 member_name_index;
			utf8Offset = this.constantPoolOffsets[u2At(this.readOffset)] - this.structOffset;
			char[] membername = utf8At(utf8Offset + 3, u2At(utf8Offset + 1));
			this.readOffset += 2;
			Object value = decodeDefaultValue();
			decodedPairs[i++] = new ElementValuePairInfo(membername, value);
		}
		this.pairs = decodedPairs;
	} catch (RuntimeException any) {
		sanitizePairs(decodedPairs);
		StringBuilder newMessage = new StringBuilder(any.getMessage());
		newMessage.append(" while decoding pair #").append(i).append(" of annotation @").append(this.typename); //$NON-NLS-1$ //$NON-NLS-2$
		newMessage.append(", bytes at structOffset ").append(this.structOffset).append(":"); //$NON-NLS-1$ //$NON-NLS-2$
		int offset = this.structOffset;
		while (offset <= this.structOffset+this.readOffset && offset < this.reference.length) {
			newMessage.append(' ').append(Integer.toHexString(this.reference[offset++] & 0xFF));
		}
		throw new IllegalStateException(newMessage.toString(), any);
	}
}
private void sanitizePairs(ElementValuePairInfo[] oldPairs) {
	if (oldPairs != null) {
		ElementValuePairInfo[] newPairs = new ElementValuePairInfo[oldPairs.length];
		int count = 0;
		for (int i = 0; i < oldPairs.length; i++) {
			ElementValuePairInfo evpInfo = oldPairs[i];
			if (evpInfo != null)
				newPairs[count++] = evpInfo;
		}
		if (count < oldPairs.length) {
			this.pairs = Arrays.copyOf(newPairs, count);
		} else {
			this.pairs = newPairs;
		}
	} else {
		this.pairs = ElementValuePairInfo.NoMembers;
	}
}
Object decodeDefaultValue() {
	Object value = null;
	// u1 tag;
	int tag = u1At(this.readOffset);
	this.readOffset++;
	int constValueOffset = -1;
	switch (tag) {
		case 'Z': // boolean constant
			constValueOffset = this.constantPoolOffsets[u2At(this.readOffset)] - this.structOffset;
			value = BooleanConstant.fromValue(i4At(constValueOffset + 1) == 1);
			this.readOffset += 2;
			break;
		case 'I': // integer constant
			constValueOffset = this.constantPoolOffsets[u2At(this.readOffset)] - this.structOffset;
			value = IntConstant.fromValue(i4At(constValueOffset + 1));
			this.readOffset += 2;
			break;
		case 'C': // char constant
			constValueOffset = this.constantPoolOffsets[u2At(this.readOffset)] - this.structOffset;
			value = CharConstant.fromValue((char) i4At(constValueOffset + 1));
			this.readOffset += 2;
			break;
		case 'B': // byte constant
			constValueOffset = this.constantPoolOffsets[u2At(this.readOffset)] - this.structOffset;
			value = ByteConstant.fromValue((byte) i4At(constValueOffset + 1));
			this.readOffset += 2;
			break;
		case 'S': // short constant
			constValueOffset = this.constantPoolOffsets[u2At(this.readOffset)] - this.structOffset;
			value = ShortConstant.fromValue((short) i4At(constValueOffset + 1));
			this.readOffset += 2;
			break;
		case 'D': // double constant
			constValueOffset = this.constantPoolOffsets[u2At(this.readOffset)] - this.structOffset;
			value = DoubleConstant.fromValue(doubleAt(constValueOffset + 1));
			this.readOffset += 2;
			break;
		case 'F': // float constant
			constValueOffset = this.constantPoolOffsets[u2At(this.readOffset)] - this.structOffset;
			value = FloatConstant.fromValue(floatAt(constValueOffset + 1));
			this.readOffset += 2;
			break;
		case 'J': // long constant
			constValueOffset = this.constantPoolOffsets[u2At(this.readOffset)] - this.structOffset;
			value = LongConstant.fromValue(i8At(constValueOffset + 1));
			this.readOffset += 2;
			break;
		case 's': // String
			constValueOffset = this.constantPoolOffsets[u2At(this.readOffset)] - this.structOffset;
			value = StringConstant.fromValue(String.valueOf(utf8At(constValueOffset + 3, u2At(constValueOffset + 1))));
			this.readOffset += 2;
			break;
		case 'e':
			constValueOffset = this.constantPoolOffsets[u2At(this.readOffset)] - this.structOffset;
			char[] typeName = utf8At(constValueOffset + 3, u2At(constValueOffset + 1));
			this.readOffset += 2;
			constValueOffset = this.constantPoolOffsets[u2At(this.readOffset)] - this.structOffset;
			char[] constName = utf8At(constValueOffset + 3, u2At(constValueOffset + 1));
			this.readOffset += 2;
			value = new EnumConstantSignature(typeName, constName);
			break;
		case 'c':
			constValueOffset = this.constantPoolOffsets[u2At(this.readOffset)] - this.structOffset;
			char[] className = utf8At(constValueOffset + 3, u2At(constValueOffset + 1));
			value = new ClassSignature(className);
			this.readOffset += 2;
			break;
		case '@':
			value = new AnnotationInfo(this.reference, this.constantPoolOffsets, this.readOffset + this.structOffset, false, true);
			this.readOffset += ((AnnotationInfo) value).readOffset;
			break;
		case '[':
			int numberOfValues = u2At(this.readOffset);
			this.readOffset += 2;
			if (numberOfValues == 0) {
				value = EmptyValueArray;
			} else {
				Object[] arrayElements = new Object[numberOfValues];
				value = arrayElements;
				for (int i = 0; i < numberOfValues; i++)
					arrayElements[i] = decodeDefaultValue();
			}
			break;
		default:
			String tagDisplay = tag == 0 ? "0x00" : (char) tag + " ("+Integer.toHexString(tag&0xFF)+')';  //$NON-NLS-1$//$NON-NLS-2$
			throw new IllegalStateException("Unrecognized tag " + tagDisplay); //$NON-NLS-1$
	}
	return value;
}
@Override
public IBinaryElementValuePair[] getElementValuePairs() {
	if (this.pairs == null)
		lazyInitialize();
	return this.pairs;
}
@Override
public char[] getTypeName() {
	return this.typename;
}
@Override
public boolean isDeprecatedAnnotation() {
	return (this.standardAnnotationTagBits & (TagBits.AnnotationDeprecated | TagBits.AnnotationTerminallyDeprecated)) != 0;
}
void initialize() {
	if (this.pairs == null)
		decodeAnnotation();
}
synchronized void lazyInitialize() {
	if (this.pairs == null)
		decodeAnnotation();
}
private int readRetentionPolicy(int offset) {
	int currentOffset = offset;
	int tag = u1At(currentOffset);
	currentOffset++;
	switch (tag) {
		case 'e':
			int utf8Offset = this.constantPoolOffsets[u2At(currentOffset)] - this.structOffset;
			char[] typeName = utf8At(utf8Offset + 3, u2At(utf8Offset + 1));
			currentOffset += 2;
			if (typeName.length == 38 && CharOperation.equals(typeName, ConstantPool.JAVA_LANG_ANNOTATION_RETENTIONPOLICY)) {
				utf8Offset = this.constantPoolOffsets[u2At(currentOffset)] - this.structOffset;
				char[] constName = utf8At(utf8Offset + 3, u2At(utf8Offset + 1));
				this.standardAnnotationTagBits |= Annotation.getRetentionPolicy(constName);
			}
			currentOffset += 2;
			break;
		case 'B':
		case 'C':
		case 'D':
		case 'F':
		case 'I':
		case 'J':
		case 'S':
		case 'Z':
		case 's':
		case 'c':
			currentOffset += 2;
			break;
		case '@':
			// none of the supported standard annotation are in the nested
			// level.
			currentOffset = scanAnnotation(currentOffset, false, false);
			break;
		case '[':
			int numberOfValues = u2At(currentOffset);
			currentOffset += 2;
			for (int i = 0; i < numberOfValues; i++)
				currentOffset = scanElementValue(currentOffset);
			break;
		default:
			throw new IllegalStateException();
	}
	return currentOffset;
}
private int readTargetValue(int offset) {
	int currentOffset = offset;
	int tag = u1At(currentOffset);
	currentOffset++;
	switch (tag) {
		case 'e':
			int utf8Offset = this.constantPoolOffsets[u2At(currentOffset)] - this.structOffset;
			char[] typeName = utf8At(utf8Offset + 3, u2At(utf8Offset + 1));
			currentOffset += 2;
			if (typeName.length == 34 && CharOperation.equals(typeName, ConstantPool.JAVA_LANG_ANNOTATION_ELEMENTTYPE)) {
				utf8Offset = this.constantPoolOffsets[u2At(currentOffset)] - this.structOffset;
				char[] constName = utf8At(utf8Offset + 3, u2At(utf8Offset + 1));
				this.standardAnnotationTagBits |= Annotation.getTargetElementType(constName);
			}
			currentOffset += 2;
			break;
		case 'B':
		case 'C':
		case 'D':
		case 'F':
		case 'I':
		case 'J':
		case 'S':
		case 'Z':
		case 's':
		case 'c':
			currentOffset += 2;
			break;
		case '@':
			// none of the supported standard annotation are in the nested
			// level.
			currentOffset = scanAnnotation(currentOffset, false, false);
			break;
		case '[':
			int numberOfValues = u2At(currentOffset);
			currentOffset += 2;
			if (numberOfValues == 0) {
				this.standardAnnotationTagBits |= TagBits.AnnotationTarget;
			} else {
				for (int i = 0; i < numberOfValues; i++)
					currentOffset = readTargetValue(currentOffset);
			}
			break;
		default:
			throw new IllegalStateException();
	}
	return currentOffset;
}
/**
 * Read through this annotation in order to figure out the necessary tag
 * bits and the length of this annotation. The data structure will not be
 * flushed out.
 *
 * The tag bits are derived from the following (supported) standard
 * annotation. java.lang.annotation.Documented,
 * java.lang.annotation.Retention, java.lang.annotation.Target, and
 * java.lang.Deprecated
 *
 * @param expectRuntimeVisibleAnno
 *            <code>true</cod> to indicate that this is a runtime-visible annotation
 * @param toplevel <code>false</code> to indicate that an nested annotation is read.
 * 		<code>true</code> otherwise
 * @return the next offset to read.
 */
private int scanAnnotation(int offset, boolean expectRuntimeVisibleAnno, boolean toplevel) {
	int currentOffset = offset;
	int utf8Offset = this.constantPoolOffsets[u2At(offset)] - this.structOffset;
	char[] typeName = utf8At(utf8Offset + 3, u2At(utf8Offset + 1));
	if (toplevel)
		this.typename = typeName;
	int numberOfPairs = u2At(offset + 2);
	// u2 type_index + u2 number_member_value_pair
	currentOffset += 4;
	if (expectRuntimeVisibleAnno && toplevel) {
		switch (typeName.length) {
			case 22:
				if (CharOperation.equals(typeName, ConstantPool.JAVA_LANG_DEPRECATED)) {
					this.standardAnnotationTagBits |= TagBits.AnnotationDeprecated;
				}
				break;
			case 23:
				if (CharOperation.equals(typeName, ConstantPool.JAVA_LANG_SAFEVARARGS)) {
					this.standardAnnotationTagBits |= TagBits.AnnotationSafeVarargs;
					return currentOffset;
				}
				break;
			case 29:
				if (CharOperation.equals(typeName, ConstantPool.JAVA_LANG_ANNOTATION_TARGET)) {
					currentOffset += 2;
					return readTargetValue(currentOffset);
				}
				break;
			case 32:
				if (CharOperation.equals(typeName, ConstantPool.JAVA_LANG_ANNOTATION_RETENTION)) {
					currentOffset += 2;
					return readRetentionPolicy(currentOffset);
				}
				if (CharOperation.equals(typeName, ConstantPool.JAVA_LANG_ANNOTATION_INHERITED)) {
					this.standardAnnotationTagBits |= TagBits.AnnotationInherited;
					return currentOffset;
				}
				break;
			case 33:
				if (CharOperation.equals(typeName, ConstantPool.JAVA_LANG_ANNOTATION_DOCUMENTED)) {
					this.standardAnnotationTagBits |= TagBits.AnnotationDocumented;
					return currentOffset;
				}
				break;
			case 52:
				if (CharOperation.equals(typeName, ConstantPool.JAVA_LANG_INVOKE_METHODHANDLE_POLYMORPHICSIGNATURE)) {
					this.standardAnnotationTagBits |= TagBits.AnnotationPolymorphicSignature;
					return currentOffset;
				}
				break;
		}
	}
	for (int i = 0; i < numberOfPairs; i++) {
		// u2 member_name_index
		currentOffset += 2;
		currentOffset = scanElementValue(currentOffset);
	}
	return currentOffset;
}
/**
 * @param offset
 *            the offset to start reading.
 * @return the next offset to read.
 */
private int scanElementValue(int offset) {
	int currentOffset = offset;
	int tag = u1At(currentOffset);
	currentOffset++;
	switch (tag) {
		case 'Z':
			if ((this.standardAnnotationTagBits & TagBits.AnnotationDeprecated) != 0) {
				// assume member_name is 'since', because @Deprecated has only one boolean member
				int constantOffset = this.constantPoolOffsets[u2At(currentOffset)] - this.structOffset + 1;
				if (i4At(constantOffset) == 1) {
					this.standardAnnotationTagBits |= TagBits.AnnotationTerminallyDeprecated;
				}
			}
			currentOffset += 2;
			break;
		case 'B':
		case 'C':
		case 'D':
		case 'F':
		case 'I':
		case 'J':
		case 'S':
		case 's':
		case 'c':
			currentOffset += 2;
			break;
		case 'e':
			currentOffset += 4;
			break;
		case '@':
			// none of the supported standard annotation are in the nested
			// level.
			currentOffset = scanAnnotation(currentOffset, false, false);
			break;
		case '[':
			int numberOfValues = u2At(currentOffset);
			currentOffset += 2;
			for (int i = 0; i < numberOfValues; i++)
				currentOffset = scanElementValue(currentOffset);
			break;
		default:
			throw new IllegalStateException();
	}
	return currentOffset;
}
@Override
public String toString() {
	return BinaryTypeFormatter.annotationToString(this);
}
@Override
public int hashCode() {
	final int prime = 31;
	int result = 1;
	result = prime * result + Util.hashCode(this.pairs);
	result = prime * result + CharOperation.hashCode(this.typename);
	return result;
}
@Override
public boolean equals(Object obj) {
	if (this == obj) {
		return true;
	}
	if (obj == null) {
		return false;
	}
	if (getClass() != obj.getClass()) {
		return false;
	}
	AnnotationInfo other = (AnnotationInfo) obj;
	if (!Arrays.equals(this.pairs, other.pairs)) {
		return false;
	}
	if (!Arrays.equals(this.typename, other.typename)) {
		return false;
	}
	return true;
}
}
