/*
 * Bytecode Analysis Framework
 * Copyright (C) 2003,2004 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.ba;

import java.util.HashMap;
import java.util.Map;

import org.apache.bcel.Constants;
import org.apache.bcel.Repository;
import org.apache.bcel.classfile.Code;
import org.apache.bcel.classfile.ConstantClass;
import org.apache.bcel.classfile.ConstantFieldref;
import org.apache.bcel.classfile.ConstantNameAndType;
import org.apache.bcel.classfile.ConstantPool;
import org.apache.bcel.classfile.JavaClass;
import org.apache.bcel.classfile.Method;
import org.apache.bcel.generic.ConstantPoolGen;
import org.apache.bcel.generic.INVOKESTATIC;

import edu.umd.cs.findbugs.SystemProperties;

/**
 * Determine which methods are accessors used
 * by inner classes to access fields in their enclosing classes.
 * This has been tested with javac from the Sun JDK 1.4.x,
 * but will probably not work with other source to bytecode compilers.
 * 
 * <p>
 * The instance of InnerClassAccessMap should be retrieved
 * from the AnalysisContext.
 * </p>
 *
 * @author David Hovemeyer
 * @see InnerClassAccess
 */
public class InnerClassAccessMap {
	private static final boolean DEBUG = SystemProperties.getBoolean("icam.debug");

	/* ----------------------------------------------------------------------
	 * Fields
	 * ---------------------------------------------------------------------- */

	/**
	 * Map of class names to maps of method names to InnerClassAccess objects
	 * representing access methods.
	 */
	private Map<String, Map<String, InnerClassAccess>> classToAccessMap;

	/* ----------------------------------------------------------------------
	 * Public interface
	 * ---------------------------------------------------------------------- */

	/**
	 * Create an instance.
	 * 
	 * @return a new instance of InnerClassAccessMap
	 */
	public static InnerClassAccessMap create() {
		return new InnerClassAccessMap();
	}

	/**
	 * Get the InnerClassAccess in given class with the given method name.
	 *
	 * @param className  the name of the class
	 * @param methodName the name of the access method
	 * @return the InnerClassAccess object for the method, or null if
	 *         the method doesn't seem to be an inner class access
	 */
	public InnerClassAccess getInnerClassAccess(String className, String methodName) throws ClassNotFoundException {
		Map<String, InnerClassAccess> map = getAccessMapForClass(className);
		return map.get(methodName);
	}

	/**
	 * Get the inner class access object for given invokestatic instruction.
	 * Returns null if the called method is not an inner class access.
	 *
	 * @param inv the invokestatic instruction
	 * @param cpg the ConstantPoolGen for the method
	 * @return the InnerClassAccess, or null if the call is not an inner class access
	 */
	public InnerClassAccess getInnerClassAccess(INVOKESTATIC inv, ConstantPoolGen cpg) throws ClassNotFoundException {
		String methodName = inv.getMethodName(cpg);
		if (methodName.startsWith("access$")) {
			String className = inv.getClassName(cpg);

			return getInnerClassAccess(className, methodName);
		}
		return null;
	}

	/**
	 * Clear the cache.
	 */
	public void clearCache() {
		classToAccessMap.clear();
	}

	/* ----------------------------------------------------------------------
	 * Implementation
	 * ---------------------------------------------------------------------- */

	/**
	 * Constructor.
	 */
	private InnerClassAccessMap() {
		this.classToAccessMap = new HashMap<String, Map<String, InnerClassAccess>>();
	}

	/**
	 * Convert byte to unsigned int.
	 */
	private static int toInt(byte b) {
		int value = b & 0x7F;
		if ((b & 0x80) != 0)
			value |= 0x80;
		return value;
	}

	/**
	 * Get an unsigned 16 bit constant pool index from a byte array.
	 */
	private static int getIndex(byte[] instructionList, int index) {
		return (toInt(instructionList[index + 1]) << 8) | toInt(instructionList[index + 2]);
	}

	private static class LookupFailure extends RuntimeException {
		/**
		 * 
		 */
		private static final long serialVersionUID = 1L;
		private final ClassNotFoundException exception;

		public LookupFailure(ClassNotFoundException exception) {
			this.exception = exception;
		}

		public ClassNotFoundException getException() {
			return exception;
		}
	}

	/**
	 * Callback to scan an access method to determine what
	 * field it accesses, and whether the field is loaded or stored.
	 */
	private static class InstructionCallback implements BytecodeScanner.Callback {
		private JavaClass javaClass;
		private String methodName;
		private String methodSig;
		private byte[] instructionList;
		private InnerClassAccess access;
		private int accessCount;

		/**
		 * Constructor.
		 *
		 * @param javaClass       the class containing the access method
		 * @param methodName      the name of the access method
		 * @param methodSig       the signature of the access method
		 * @param instructionList the bytecode of the method
		 */
		public InstructionCallback(JavaClass javaClass, String methodName, String methodSig, byte[] instructionList) {
			this.javaClass = javaClass;
			this.methodName = methodName;
			this.methodSig = methodSig;
			this.instructionList = instructionList;
			this.access = null;
			this.accessCount = 0;
		}

		public void handleInstruction(int opcode, int index) {
			switch (opcode) {
			case Constants.GETFIELD:
			case Constants.PUTFIELD:
				setField(getIndex(instructionList, index), false, opcode == Constants.GETFIELD);
				break;
			case Constants.GETSTATIC:
			case Constants.PUTSTATIC:
				setField(getIndex(instructionList, index), true, opcode == Constants.GETSTATIC);
				break;
			}
		}

		/**
		 * Get the InnerClassAccess object representing the method.
		 *
		 * @return the InnerClassAccess, or null if the method
		 *         was not found to be a simple load or store in the
		 *         expected form
		 */
		public InnerClassAccess getAccess() {
			return access;
		}

		/**
		 * Called to indicate that a field load or store was encountered.
		 *
		 * @param cpIndex  the constant pool index of the fieldref
		 * @param isStatic true if it is a static field access
		 * @param isLoad   true if the access is a load
		 */
		private void setField(int cpIndex, boolean isStatic, boolean isLoad) {
			// We only allow one field access for an accessor method.
			accessCount++;
			if (accessCount != 1) {
				access = null;
				return;
			}

			ConstantPool cp = javaClass.getConstantPool();
			ConstantFieldref fieldref = (ConstantFieldref) cp.getConstant(cpIndex);

			ConstantClass cls = (ConstantClass) cp.getConstant(fieldref.getClassIndex());
			String className = cls.getBytes(cp).replace('/', '.');

			ConstantNameAndType nameAndType = (ConstantNameAndType) cp.getConstant(fieldref.getNameAndTypeIndex());
			String fieldName = nameAndType.getName(cp);
			String fieldSig = nameAndType.getSignature(cp);

			try {
				XField xfield = Hierarchy.findXField(className, fieldName, fieldSig, isStatic);
				if (xfield != null && xfield.isStatic() == isStatic && isValidAccessMethod(methodSig, xfield, isLoad)) {
					access = new InnerClassAccess(methodName, methodSig, xfield, isLoad);
				}
			} catch (ClassNotFoundException e) {
				throw new LookupFailure(e);
			}
		}

		/**
		 * Determine if the method appears to be an accessor of the expected form.
		 * This has only been tested with the Sun JDK 1.4 javac (definitely)
		 * and jikes 1.18 (I think).
		 *
		 * @param methodSig the method's signature
		 * @param field     the field accessed by the method
		 * @param isLoad    true if the access is a load
		 */
		private boolean isValidAccessMethod(String methodSig, XField field, boolean isLoad) {

			// Get the method parameters and return type
			// (as they appear in the method signature).
			int paramsEnd = methodSig.indexOf(')');
			if (paramsEnd < 0)
				return false;
			String methodParams = methodSig.substring(0, paramsEnd + 1);
			String methodReturnType = methodSig.substring(paramsEnd + 1);

			// Figure out what the expected method parameters should be
			String classSig = "L" + javaClass.getClassName().replace('.', '/') + ";";
			StringBuilder buf = new StringBuilder();
			buf.append('(');
			if (!field.isStatic())
				buf.append(classSig); // the OuterClass.this reference
			if (!isLoad)
				buf.append(field.getSignature()); // the value being stored
			buf.append(')');
			String expectedMethodParams = buf.toString();

			// See if params match
			if (!methodParams.equals(expectedMethodParams)) {
				if (DEBUG) {
					System.out.println("In " + javaClass.getClassName() + "." + methodName +
						" expected params " +
						expectedMethodParams + ", saw " + methodParams);
					System.out.println(isLoad ? "LOAD" : "STORE");
				}
				return false;
			}

			// Return type can be either the type of the field, or void.
			if (!methodReturnType.equals("V") && !methodReturnType.equals(field.getSignature())) {
				if (DEBUG) {
					System.out.println("In " + javaClass.getClassName() + "." + methodName +
						" expected return type V or " + field.getSignature() +
						", saw " + methodReturnType);
					System.out.println(isLoad ? "LOAD" : "STORE");
				}
				return false;
			}

			return true;
		}
	}

	private static final Map<String, InnerClassAccess> emptyMap = new HashMap<String, InnerClassAccess>();

	/**
	 * Return a map of inner-class member access method names to
	 * the fields that they access for given class name.
	 *
	 * @param className the name of the class
	 * @return map of access method names to the fields they access
	 */
	private Map<String, InnerClassAccess> getAccessMapForClass(String className)
			throws ClassNotFoundException {

		Map<String, InnerClassAccess> map = classToAccessMap.get(className);
		if (map == null) {
			map = new HashMap<String, InnerClassAccess>();

			if (!className.startsWith("[")) {
				JavaClass javaClass = Repository.lookupClass(className);

				Method[] methodList = javaClass.getMethods();
				for (Method method : methodList) {
					String methodName = method.getName();
					if (!methodName.startsWith("access$"))
						continue;

					Code code = method.getCode();
					if (code == null)
						continue;

					if (DEBUG) System.out.println("Analyzing " + className + "." +
							method.getName() + " as an inner-class access method...");

					byte[] instructionList = code.getCode();
					String methodSig = method.getSignature();
					InstructionCallback callback = new InstructionCallback(javaClass, methodName, methodSig, instructionList);
					try {
						new BytecodeScanner().scan(instructionList, callback);
					} catch (LookupFailure lf) {
						throw lf.getException();
					}
					InnerClassAccess access = callback.getAccess();
					if (DEBUG) System.out.println((access != null ? "IS" : "IS NOT") + " an inner-class access method");
					if (access != null)
						map.put(methodName, access);
				}
			}

			if (map.size() == 0)
				map = emptyMap;

			classToAccessMap.put(className, map);
		}

		return map;
	}

}

// vim:ts=4
