/*
 * FindBugs - Find Bugs in Java programs
 * Copyright (C) 2006, University of Maryland
 * 
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 * 
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 * 
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 */

package edu.umd.cs.findbugs.classfile.engine;

import java.io.DataInputStream;
import java.io.IOException;
import java.util.Collection;
import java.util.HashSet;
import java.util.Set;

import edu.umd.cs.findbugs.annotations.CheckForNull;
import edu.umd.cs.findbugs.classfile.ClassDescriptor;
import edu.umd.cs.findbugs.classfile.DescriptorFactory;
import edu.umd.cs.findbugs.classfile.FieldDescriptor;
import edu.umd.cs.findbugs.classfile.IClassConstants;
import edu.umd.cs.findbugs.classfile.ICodeBaseEntry;
import edu.umd.cs.findbugs.classfile.InvalidClassFileFormatException;
import edu.umd.cs.findbugs.classfile.MethodDescriptor;
import edu.umd.cs.findbugs.classfile.analysis.ClassInfo;
import edu.umd.cs.findbugs.classfile.analysis.ClassNameAndSuperclassInfo;
import edu.umd.cs.findbugs.internalAnnotations.SlashedClassName;
import edu.umd.cs.findbugs.io.IO;
import edu.umd.cs.findbugs.util.ClassName;

/**
 * Parse a class to extract symbolic information.
 * see <a href=http://java.sun.com/docs/books/vmspec/2nd-edition/html/ClassFile.doc.html">
   http://java.sun.com/docs/books/vmspec/2nd-edition/html/ClassFile.doc.html </a>
 * 
 * @author David Hovemeyer
 */
public class ClassParser implements ClassParserInterface {

	static class Constant {
		int tag;
		Object[] data;

		Constant(int tag, Object[] data) {
			this.tag = tag;
			this.data = data;
		}
	}

	private DataInputStream in;
	private ClassDescriptor expectedClassDescriptor;
	private ICodeBaseEntry codeBaseEntry;
	private Constant[] constantPool;
	private ClassDescriptor immediateEnclosingClass;

	/**
	 * Constructor.
	 * 
	 * @param in                       the DataInputStream to read class data from
	 * @param expectedClassDescriptor  ClassDescriptor expected: null if unknown
	 * @param codeBaseEntry            codebase entry class is loaded from
	 */
	public ClassParser(
			DataInputStream in,
			@CheckForNull ClassDescriptor expectedClassDescriptor,
			ICodeBaseEntry codeBaseEntry) {
		this.in = in;
		this.expectedClassDescriptor = expectedClassDescriptor;
		this.codeBaseEntry = codeBaseEntry;
	}

	/* (non-Javadoc)
     * @see edu.umd.cs.findbugs.classfile.engine.ClassParserInterface#parse(edu.umd.cs.findbugs.classfile.analysis.ClassNameAndSuperclassInfo.Builder)
     */
	public void parse(ClassNameAndSuperclassInfo.Builder builder) throws InvalidClassFileFormatException {
		try {
			int magic = in.readInt();
			if (magic != 0xcafebabe) throw new InvalidClassFileFormatException("Classfile header isn't 0xCAFEBABE", expectedClassDescriptor, codeBaseEntry);
			int major_version = in.readUnsignedShort();
			int minor_version = in.readUnsignedShort();
			int constant_pool_count = in.readUnsignedShort();

			constantPool = new Constant[constant_pool_count];
			for (int i = 1; i < constantPool.length; i++) {
				constantPool[i] = readConstant();
				if (constantPool[i].tag == IClassConstants.CONSTANT_Double ||
						constantPool[i].tag == IClassConstants.CONSTANT_Long) {
					// Double and Long constants take up two constant pool entries
					++i;
				}
			}

			int access_flags = in.readUnsignedShort();

			int this_class = in.readUnsignedShort();
			ClassDescriptor thisClassDescriptor =
				getClassDescriptor(this_class);

			int super_class = in.readUnsignedShort();
			ClassDescriptor superClassDescriptor =
				getClassDescriptor(super_class);

			int interfaces_count = in.readUnsignedShort();
			if (interfaces_count < 0) {
				throw new InvalidClassFileFormatException(expectedClassDescriptor, codeBaseEntry);
			}
			ClassDescriptor[] interfaceDescriptorList;
			if (interfaces_count == 0)
				interfaceDescriptorList = ClassDescriptor.EMPTY_ARRAY;
			else {
				interfaceDescriptorList = new ClassDescriptor[interfaces_count];
				for (int i = 0; i < interfaceDescriptorList.length; i++)
					interfaceDescriptorList[i] = getClassDescriptor(in.readUnsignedShort());
			}
			// Extract all references to other classes,
			// both CONSTANT_Class entries and also referenced method
			// signatures.
			Collection<ClassDescriptor> referencedClassDescriptorList = extractReferencedClasses();
			
			builder.setClassDescriptor(thisClassDescriptor);
			builder.setSuperclassDescriptor(superClassDescriptor);
			builder.setInterfaceDescriptorList(interfaceDescriptorList);
			builder.setCodeBaseEntry(codeBaseEntry);
			builder.setAccessFlags(access_flags);
			builder.setReferencedClassDescriptors(referencedClassDescriptorList);
			builder.setClassfileVersion(major_version, minor_version);
		} catch (IOException e) {
			throw new InvalidClassFileFormatException(expectedClassDescriptor, codeBaseEntry, e);
		}
	}

	/* (non-Javadoc)
     * @see edu.umd.cs.findbugs.classfile.engine.ClassParserInterface#parse(edu.umd.cs.findbugs.classfile.analysis.ClassInfo.Builder)
     */
	public void parse(ClassInfo.Builder builder) throws InvalidClassFileFormatException {
		throw new UnsupportedOperationException("Need to use a ClassParserUsingASM to build ClassInfo");
	}

	/**
	 * Extract references to other classes.
	 * 
	 * @return array of ClassDescriptors of referenced classes
	 * @throws InvalidClassFileFormatException
	 */
	private Collection<ClassDescriptor> extractReferencedClasses() throws InvalidClassFileFormatException {
		Set<ClassDescriptor> referencedClassSet = new HashSet<ClassDescriptor>();
		for (Constant constant : constantPool) {
			if (constant == null) {
				continue;
			}
			if (constant.tag == IClassConstants.CONSTANT_Class) {
				@SlashedClassName String className = getUtf8String((Integer)constant.data[0]);
				if (className.indexOf('[') >= 0) {
					extractReferencedClassesFromSignature(referencedClassSet, className);
				} else if (ClassName.isValidClassName(className)) {
					referencedClassSet.add(DescriptorFactory.instance().getClassDescriptor(className));
				}
			} else if (constant.tag == IClassConstants.CONSTANT_Methodref
					|| constant.tag == IClassConstants.CONSTANT_Fieldref
					|| constant.tag == IClassConstants.CONSTANT_InterfaceMethodref) {
				// Get the target class name
				String className = getClassName((Integer) constant.data[0]);
				extractReferencedClassesFromSignature(referencedClassSet, className);

				// Parse signature to extract class names
				String signature = getSignatureFromNameAndType((Integer)constant.data[1]);
				extractReferencedClassesFromSignature(referencedClassSet, signature);
			}
		}
		return referencedClassSet;
	}

	/**
	 * @param referencedClassSet
	 * @param signature
	 */
	public static void extractReferencedClassesFromSignature(Set<ClassDescriptor> referencedClassSet, String signature) {
		while (signature.length() > 0) {
			int start = signature.indexOf('L');
			if (start < 0) {
				break;
			}
			int end = signature.indexOf(';', start);
			if (end < 0) {
				break;
			}
			@SlashedClassName String className = signature.substring(start+1, end);
			if (ClassName.isValidClassName(className)) {
				referencedClassSet.add(DescriptorFactory.instance().getClassDescriptor(className));
			}
			signature = signature.substring(end + 1);
		}
	}

	// 8: UTF-8 string
	// I: int
	// F: float
	// L: long
	// D: double
	// i: 2-byte constant pool index
	private static final String[] CONSTANT_FORMAT_MAP = {
		null,
		"8", // 1: CONSTANT_Utf8
		null,
		"I", // 3: CONSTANT_Integer
		"F", // 4: CONSTANT_Float
		"L", // 5: CONSTANT_Long
		"D", // 6: CONSTANT_Double
		"i", // 7: CONSTANT_Class
		"i", // 8: CONSTANT_String 
		"ii", // 9: CONSTANT_Fieldref
		"ii", // 10: CONSTANT_Methodref
		"ii", // 11: CONSTANT_InterfaceMethodref
		"ii", // 12: CONSTANT_NameAndType
	};

	/**
	 * Read a constant from the constant pool.
	 * 
	 * @return a Constant
	 * @throws InvalidClassFileFormatException
	 * @throws IOException 
	 */
	private Constant readConstant()
	throws InvalidClassFileFormatException, IOException {
		int tag = in.readUnsignedByte();
		if (tag < 0 || tag >= CONSTANT_FORMAT_MAP.length || CONSTANT_FORMAT_MAP[tag] == null) {
			throw new InvalidClassFileFormatException(expectedClassDescriptor, codeBaseEntry);
		}
		String format = CONSTANT_FORMAT_MAP[tag];
		Object[] data = new Object[format.length()];
		for (int i = 0; i < format.length(); i++) {
			char spec = format.charAt(i);
			switch (spec) {
			case '8':
				data[i] = in.readUTF();
				break;
			case 'I':
				data[i] = in.readInt();
				break;
			case 'F':
				data[i] = new Float(in.readFloat());
				break;
			case 'L':
				data[i] = in.readLong();
				break;
			case 'D':
				data[i] = new Double(in.readDouble());
				break;
			case 'i':
				data[i] = in.readUnsignedShort();
				break;
			default: throw new IllegalStateException();
			}
		}

		return new Constant(tag, data);
	}

	/**
	 * Get a class name from a CONSTANT_Class.
	 * Note that this may be an array (e.g., "[Ljava/lang/String;").
	 * 
	 * @param index index of the constant
	 * @return the class name
	 * @throws InvalidClassFileFormatException
	 */
	private @SlashedClassName String getClassName(int index) throws InvalidClassFileFormatException {
		if (index == 0) {
			return null;
		}

		checkConstantPoolIndex(index);
		Constant constant = constantPool[index];
		checkConstantTag(constant, IClassConstants.CONSTANT_Class);

		int refIndex = ((Integer)constant.data[0]).intValue();
		String stringValue = getUtf8String(refIndex);

		return stringValue;
	}

	/**
	 * Get the ClassDescriptor of a class referenced in the constant pool.
	 * 
	 * @param index        index of the referenced class in the constant pool
	 * @return the ClassDescriptor of the referenced class
	 * @throws InvalidClassFileFormatException 
	 */
	private ClassDescriptor getClassDescriptor(int index) throws InvalidClassFileFormatException {
		@SlashedClassName String className = getClassName(index);
		return className != null ? DescriptorFactory.instance().getClassDescriptor(className) : null;
	}

	/**
	 * Get the UTF-8 string constant at given constant pool index.
	 * 
	 * @param refIndex the constant pool index
	 * @return the String at that index
	 * @throws InvalidClassFileFormatException
	 */
	private String getUtf8String(int refIndex) throws InvalidClassFileFormatException {
		checkConstantPoolIndex(refIndex);
		Constant refConstant = constantPool[refIndex];
		checkConstantTag(refConstant, IClassConstants.CONSTANT_Utf8);
		return (String) refConstant.data[0];
	}

	/**
	 * Check that a constant pool index is valid.
	 * 
	 * @param expectedClassDescriptor   class descriptor
	 * @param constantPool the constant pool
	 * @param index        the index to check
	 * @throws InvalidClassFileFormatException if the index is not valid
	 */
	private void checkConstantPoolIndex(int index) throws InvalidClassFileFormatException {
		if (index < 0 || index >= constantPool.length || constantPool[index] == null) {
			throw new InvalidClassFileFormatException(expectedClassDescriptor, codeBaseEntry);
		}
	}

	/**
	 * Check that a constant has the expected tag.
	 * 
	 * @param constant     the constant to check
	 * @param expectedTag the expected constant tag
	 * @throws InvalidClassFileFormatException if the constant's tag does not match the expected tag
	 */
	private void checkConstantTag(Constant constant, int expectedTag)
	throws InvalidClassFileFormatException {
		if (constant.tag != expectedTag) {
			throw new InvalidClassFileFormatException(expectedClassDescriptor, codeBaseEntry);
		}
	}

	interface FieldOrMethodDescriptorCreator<E> {
		public E create(String className, String name, String signature, int accessFlags);
	}

	/**
	 * Read field_info, return FieldDescriptor.
	 * 
	 * @param thisClassDescriptor the ClassDescriptor of this class (being parsed) 
	 * @return the FieldDescriptor
	 * @throws IOException 
	 * @throws InvalidClassFileFormatException 
	 */
	private FieldDescriptor readField(ClassDescriptor thisClassDescriptor)
	throws IOException, InvalidClassFileFormatException {
		return readFieldOrMethod(thisClassDescriptor, new FieldOrMethodDescriptorCreator<FieldDescriptor>() {
			/* (non-Javadoc)
			 * @see edu.umd.cs.findbugs.classfile.engine.ClassParser.FieldOrMethodDescriptorCreator#create(java.lang.String, java.lang.String, java.lang.String, int)
			 */
			public FieldDescriptor create(String className, String name, String signature, int accessFlags) {
				return DescriptorFactory.instance().getFieldDescriptor(className, name, signature, (accessFlags & IClassConstants.ACC_STATIC) != 0);
			}
		});
	}

	/**
	 * Read method_info, read method descriptor.
	 * 
	 * @param thisClassDescriptor
	 * @return
	 * @throws IOException 
	 * @throws InvalidClassFileFormatException 
	 */
	private MethodDescriptor readMethod(ClassDescriptor thisClassDescriptor)
	throws InvalidClassFileFormatException, IOException {
		return readFieldOrMethod(thisClassDescriptor, new FieldOrMethodDescriptorCreator<MethodDescriptor>(){
			/* (non-Javadoc)
			 * @see edu.umd.cs.findbugs.classfile.engine.ClassParser.FieldOrMethodDescriptorCreator#create(java.lang.String, java.lang.String, java.lang.String, int)
			 */
			public MethodDescriptor create(String className, String name, String signature, int accessFlags) {
				return DescriptorFactory.instance().getMethodDescriptor(className, name, signature, (accessFlags & IClassConstants.ACC_STATIC) != 0);
			}
		});
	}

	/**
	 * Read field_info or method_info.
	 * They have the same format.
	 * 
	 * @param <E>                 descriptor type to return
	 * @param thisClassDescriptor class descriptor of class being parsed
	 * @param creator             callback to create the FieldDescriptor or MethodDescriptor
	 * @return the parsed descriptor
	 * @throws IOException
	 * @throws InvalidClassFileFormatException
	 */
	private<E> E readFieldOrMethod(
			ClassDescriptor thisClassDescriptor, FieldOrMethodDescriptorCreator<E> creator)
	throws IOException, InvalidClassFileFormatException {
		int access_flags = in.readUnsignedShort();
		int name_index = in.readUnsignedShort();
		int descriptor_index = in.readUnsignedShort();
		int attributes_count = in.readUnsignedShort();

		String name = getUtf8String(name_index);
		String signature = getUtf8String(descriptor_index);
		if (attributes_count < 0) {
			throw new InvalidClassFileFormatException(expectedClassDescriptor, codeBaseEntry);
		}
		for (int i = 0; i < attributes_count; i++) {
			readAttribute();
		}

		return creator.create(
				thisClassDescriptor.getClassName(), name, signature, access_flags);
	}

	/**
	 * Read an attribute.
	 * 
	 * @throws IOException 
	 * @throws InvalidClassFileFormatException 
	 */
	private void readAttribute() throws IOException, InvalidClassFileFormatException {
		int attribute_name_index = in.readUnsignedShort();
		String attrName = getUtf8String(attribute_name_index);

		int attribute_length = in.readInt();
		if (attribute_length < 0) {
			throw new InvalidClassFileFormatException(expectedClassDescriptor, codeBaseEntry);
		}

		if (attrName.equals("InnerClasses")) {
			readInnerClassesAttribute(attribute_length);
		} else {
			IO.skipFully(in, attribute_length);
		}
	}

	/**
	 * Read an InnerClasses attribute.
	 * 
	 * @param attribute_length length of attribute (excluding first 6 bytes)
	 * @throws InvalidClassFileFormatException
	 * @throws IOException
	 */
	private void readInnerClassesAttribute(int attribute_length) throws InvalidClassFileFormatException, IOException {
		int number_of_classes = in.readUnsignedShort();
		if (attribute_length != number_of_classes * 8) {
			throw new InvalidClassFileFormatException(expectedClassDescriptor, codeBaseEntry);
		}
		
		for (int i = 0; i < number_of_classes; i++) {
			int inner_class_info_index = in.readUnsignedShort();
			int outer_class_info_index = in.readUnsignedShort();
			int inner_name_index = in.readUnsignedShort();
			int inner_class_access_flags = in.readUnsignedShort();
			
			if (outer_class_info_index != 0) {
				// Record which class this class is a member of.
				this.immediateEnclosingClass = getClassDescriptor(outer_class_info_index);
			}
		}
	}

	/**
	 * Get the signature from a CONSTANT_NameAndType.
	 * 
	 * @param index the index of the CONSTANT_NameAndType
	 * @return the signature
	 * @throws InvalidClassFileFormatException 
	 */
	private String getSignatureFromNameAndType(int index) throws InvalidClassFileFormatException {
		checkConstantPoolIndex(index);
		Constant constant = constantPool[index];
		checkConstantTag(constant, IClassConstants.CONSTANT_NameAndType);
		return getUtf8String((Integer) constant.data[1]);
	}
}
