/*
 * FindBugs - Find bugs in Java programs
 * Copyright (C) 2003-2005, 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;

import java.io.IOException;

import org.apache.bcel.Constants;
import org.apache.bcel.classfile.Field;
import org.apache.bcel.classfile.JavaClass;
import org.apache.bcel.generic.ConstantPoolGen;
import org.apache.bcel.generic.FieldInstruction;
import org.apache.bcel.generic.GETFIELD;
import org.apache.bcel.generic.GETSTATIC;
import org.apache.bcel.generic.Instruction;
import org.apache.bcel.generic.PUTFIELD;
import org.apache.bcel.generic.PUTSTATIC;

import edu.umd.cs.findbugs.ba.AnalysisContext;
import edu.umd.cs.findbugs.ba.SignatureConverter;
import edu.umd.cs.findbugs.ba.SourceInfoMap;
import edu.umd.cs.findbugs.ba.XField;
import edu.umd.cs.findbugs.classfile.FieldDescriptor;
import edu.umd.cs.findbugs.internalAnnotations.DottedClassName;
import edu.umd.cs.findbugs.visitclass.DismantleBytecode;
import edu.umd.cs.findbugs.visitclass.PreorderVisitor;
import edu.umd.cs.findbugs.xml.XMLAttributeList;
import edu.umd.cs.findbugs.xml.XMLOutput;

/**
 * A BugAnnotation specifying a particular field in particular class.
 *
 * @author David Hovemeyer
 * @see BugAnnotation
 */
public class FieldAnnotation extends PackageMemberAnnotation {
	private static final long serialVersionUID = 1L;

	public static final String DEFAULT_ROLE = "FIELD_DEFAULT";

	public static final String DID_YOU_MEAN_ROLE = "FIELD_DID_YOU_MEAN";
	public static final String VALUE_OF_ROLE="FIELD_VALUE_OF";
	public static final String LOADED_FROM_ROLE = VALUE_OF_ROLE;
	public static final String STORED_ROLE = "FIELD_STORED";
	public static final String INVOKED_ON_ROLE="FIELD_INVOKED_ON";
	public static final String ARGUMENT_ROLE="FIELD_ARGUMENT";

	private String fieldName;
	private String fieldSig;
	private String fieldSourceSig;
	private boolean isStatic;

	/**
	 * Constructor.
	 *
	 * @param className the name of the class containing the field
	 * @param fieldName the name of the field
	 * @param fieldSig  the type signature of the field
	 */
	public FieldAnnotation(@DottedClassName String className, String fieldName, String fieldSig, boolean isStatic) {
		super(className, DEFAULT_ROLE);
		if (fieldSig.indexOf(".") >= 0) {
			assert false : "signatures should not be dotted: " + fieldSig;
			fieldSig = fieldSig.replace('.','/');
		}
		this.fieldName = fieldName;
		this.fieldSig = fieldSig;
		this.isStatic = isStatic;
	}
	
	public FieldAnnotation(@DottedClassName String className, String fieldName, String fieldSig, String fieldSourceSig, boolean isStatic) {
		this(className, fieldName, fieldSig, isStatic);
		this.fieldSourceSig = fieldSourceSig;
	}
	/**
	 * Constructor.
	 *
	 * @param className the name of the class containing the field
	 * @param fieldName the name of the field
	 * @param fieldSig  the type signature of the field
	 * @param accessFlags accessFlags for the field
	 */
	public FieldAnnotation(@DottedClassName String className, String fieldName, String fieldSig, int accessFlags) {
		this(className, fieldName, fieldSig, (accessFlags & Constants.ACC_STATIC) != 0);
	}
	/**
	 * Factory method. Class name, field name, and field signatures are taken from
	 * the given visitor, which is visiting the field.
	 *
	 * @param visitor the visitor which is visiting the field
	 * @return the FieldAnnotation object
	 */
	public static FieldAnnotation fromVisitedField(PreorderVisitor visitor) {
		return new FieldAnnotation(visitor.getDottedClassName(),
				visitor.getFieldName(), visitor.getFieldSig(),
				visitor.getFieldIsStatic());
	}

	/**
	 * Factory method. Class name, field name, and field signatures are taken from
	 * the given visitor, which is visiting a reference to the field
	 * (i.e., a getfield or getstatic instruction).
	 *
	 * @param visitor the visitor which is visiting the field reference
	 * @return the FieldAnnotation object
	 */
	public static FieldAnnotation fromReferencedField(DismantleBytecode visitor) {
		String className = visitor.getDottedClassConstantOperand();
		return new FieldAnnotation(className,
				visitor.getNameConstantOperand(),
				visitor.getSigConstantOperand(), visitor.getRefFieldIsStatic());
	}

	/**
	 * Factory method. Construct from class name and BCEL Field object.
	 *
	 * @param className the name of the class which defines the field
	 * @param field     the BCEL Field object
	 * @return the FieldAnnotation
	 */
	public static FieldAnnotation fromBCELField(@DottedClassName String className, Field field) {
		return new FieldAnnotation(className, field.getName(), field.getSignature(), field.isStatic());
	}
	/**
	 * Factory method. Construct from class name and BCEL Field object.
	 *
	 * @param jClass the class which defines the field
	 * @param field     the BCEL Field object
	 * @return the FieldAnnotation
	 */
	public static FieldAnnotation fromBCELField(JavaClass jClass, Field field) {
		return new FieldAnnotation(jClass.getClassName(), field.getName(), field.getSignature(), field.isStatic());
	}

	/**
	 * Factory method.  Construct from a FieldDescriptor.
	 * 
	 * @param fieldDescriptor the FieldDescriptor
	 * @return the FieldAnnotation
	 */
	public static FieldAnnotation fromFieldDescriptor(FieldDescriptor fieldDescriptor) {
		return new FieldAnnotation(
				fieldDescriptor.getClassDescriptor().getDottedClassName(),
				fieldDescriptor.getName(),
				fieldDescriptor.getSignature(),
				fieldDescriptor.isStatic());
	}

	public static FieldAnnotation fromXField(XField fieldDescriptor) {
		return new FieldAnnotation(
				fieldDescriptor.getClassName(),
				fieldDescriptor.getName(),
				fieldDescriptor.getSignature(),
				fieldDescriptor.getSourceSignature(),
				fieldDescriptor.isStatic());
	}
	/**
	 * Get the field name.
	 */
	public String getFieldName() {
		return fieldName;
	}

	/**
	 * Get the type signature of the field.
	 */
	public String getFieldSignature() {
		return fieldSig;
	}

	/**
	 * Return whether or not the field is static.
	 */
	public boolean isStatic() {
		return isStatic;
	}

	/**
	 * Is the given instruction a read of a field?
	 *
	 * @param ins the Instruction to check
	 * @param cpg ConstantPoolGen of the method containing the instruction
	 * @return the Field if the instruction is a read of a field, null otherwise
	 */
	public static FieldAnnotation isRead(Instruction ins, ConstantPoolGen cpg) {
		if (ins instanceof GETFIELD || ins instanceof GETSTATIC) {
			FieldInstruction fins = (FieldInstruction) ins;
			String className = fins.getClassName(cpg);
			return new FieldAnnotation(className, fins.getName(cpg), fins.getSignature(cpg), fins instanceof GETSTATIC);
		} else
			return null;
	}

	/**
	 * Is the instruction a write of a field?
	 *
	 * @param ins the Instruction to check
	 * @param cpg ConstantPoolGen of the method containing the instruction
	 * @return the Field if instruction is a write of a field, null otherwise
	 */
	public static FieldAnnotation isWrite(Instruction ins, ConstantPoolGen cpg) {
		if (ins instanceof PUTFIELD || ins instanceof PUTSTATIC) {
			FieldInstruction fins = (FieldInstruction) ins;
			String className = fins.getClassName(cpg);
			return new FieldAnnotation(className, fins.getName(cpg), fins.getSignature(cpg), fins instanceof PUTSTATIC);
		} else
			return null;
	}

	public void accept(BugAnnotationVisitor visitor) {
		visitor.visitFieldAnnotation(this);
	}

	@Override
	protected String formatPackageMember(String key, ClassAnnotation primaryClass) {
		if (key.equals("") || key.equals("hash"))
			return className + "." + fieldName;
		else if (key.equals("givenClass")) {
			String primaryClassName = primaryClass.getClassName();
			if (className.equals(primaryClassName)) 
				return getNameInClass(primaryClass);
			else return shorten(primaryClass.getPackageName(), className) + "." + fieldName;
		}
		else if (key.equals("name"))
			return fieldName;
		else if (key.equals("fullField")) {
			SignatureConverter converter = new SignatureConverter(fieldSig);
			StringBuilder result = new StringBuilder();
			if (isStatic)
				result.append("static ");
			result.append(converter.parseNext());
			result.append(' ');
			result.append(className);
			result.append('.');
			result.append(fieldName);
			return result.toString();
		} else
			throw new IllegalArgumentException("unknown key " + key);
	}

	/**
	 * @param primaryClass
	 * @return
	 */
	private String getNameInClass(ClassAnnotation primaryClass) {
		if (primaryClass == null)
			return   className + "." + fieldName;
		String givenPackageName = primaryClass.getPackageName();
		String thisPackageName = this.getPackageName();
		if (thisPackageName.equals(givenPackageName))
			if (thisPackageName.length() == 0) return fieldName;
			else return className.substring(thisPackageName.length() + 1) +"." + fieldName;
		return   className + "." + fieldName;
	}

	@Override
	public int hashCode() {
		return className.hashCode() + fieldName.hashCode() + fieldSig.hashCode();
	}

	@Override
	public boolean equals(Object o) {
		if (!(o instanceof FieldAnnotation))
			return false;
		FieldAnnotation other = (FieldAnnotation) o;
		return className.equals(other.className)
				&& fieldName.equals(other.fieldName)
				&& fieldSig.equals(other.fieldSig)
				&& isStatic == other.isStatic;
	}

	public int compareTo(BugAnnotation o) {
		if (!(o instanceof FieldAnnotation)) // BugAnnotations must be Comparable with any type of BugAnnotation
			return this.getClass().getName().compareTo(o.getClass().getName());
		FieldAnnotation other = (FieldAnnotation) o;
		int cmp;
		cmp = className.compareTo(other.className);
		if (cmp != 0)
			return cmp;
		cmp = fieldName.compareTo(other.fieldName);
		if (cmp != 0)
			return cmp;
		return fieldSig.compareTo(other.fieldSig);
	}

	/* (non-Javadoc)
	 * @see edu.umd.cs.findbugs.PackageMemberAnnotation#getSourceLines()
	 */
	@Override
	public SourceLineAnnotation getSourceLines() {
		if (sourceLines == null) {
			// Create source line annotation for field on demand
			AnalysisContext currentAnalysisContext = AnalysisContext.currentAnalysisContext();
			if (currentAnalysisContext == null)
				sourceLines = new SourceLineAnnotation(className, sourceFileName, -1, -1, -1, -1);
			else {
			SourceInfoMap.SourceLineRange fieldLine = currentAnalysisContext
				.getSourceInfoMap()
				.getFieldLine(className, fieldName);
			if (fieldLine == null) 	sourceLines = new SourceLineAnnotation(
					className, sourceFileName, -1, -1, -1, -1);
			else sourceLines = new SourceLineAnnotation(
					className, sourceFileName, fieldLine.getStart(), fieldLine.getEnd(), -1, -1);
			}
		}
		return sourceLines;
	}

	/* ----------------------------------------------------------------------
	 * XML Conversion support
	 * ---------------------------------------------------------------------- */

	private static final String ELEMENT_NAME = "Field";

	public void writeXML(XMLOutput xmlOutput) throws IOException {
		writeXML(xmlOutput, false, false);
	}

	public void writeXML(XMLOutput xmlOutput, boolean addMessages, boolean isPrimary) throws IOException {
		XMLAttributeList attributeList = new XMLAttributeList()
			.addAttribute("classname", getClassName())
			.addAttribute("name", getFieldName())
			.addAttribute("signature", getFieldSignature());
		if (fieldSourceSig != null)
			attributeList.addAttribute("sourceSignature", fieldSourceSig);
		attributeList.addAttribute("isStatic", String.valueOf(isStatic()));
		if (isPrimary) attributeList.addAttribute("primary", "true");


		String role = getDescription();
		if (!role.equals(DEFAULT_ROLE))
			attributeList.addAttribute("role", role);

		xmlOutput.openTag(ELEMENT_NAME, attributeList);
		getSourceLines().writeXML(xmlOutput, addMessages, false);
		if (addMessages) {
			xmlOutput.openTag(BugAnnotation.MESSAGE_TAG);
			xmlOutput.writeText(this.toString());
			xmlOutput.closeTag(BugAnnotation.MESSAGE_TAG);
		}
		xmlOutput.closeTag(ELEMENT_NAME);
	}
}

// vim:ts=4
