/*
 * Cobertura - http://cobertura.sourceforge.net/
 *
 * Copyright (C) 2011 Piotr Tabor
 *
 * Note: This file is dual licensed under the GPL and the Apache
 * Source License (so that it can be used from both the main
 * Cobertura classes and the ant tasks).
 *
 * Cobertura is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published
 * by the Free Software Foundation; either version 2 of the License,
 * or (at your option) any later version.
 *
 * Cobertura 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
 * General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with Cobertura; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
 * USA
 */

package net.sourceforge.cobertura.instrument.pass1;

import net.sourceforge.cobertura.CoverageIgnore;
import net.sourceforge.cobertura.instrument.ContextMethodAwareMethodAdapter;
import org.objectweb.asm.*;

import java.util.Set;
import java.util.concurrent.atomic.AtomicInteger;

public class DetectIgnoredCodeMethodVisitor
		extends
			ContextMethodAwareMethodAdapter {
	final String superName;
	final Set<Integer> ignoredLineIds;
	final Set<String> ignoredMethodNamesAndSignatures;

	final Set<String> ignoreMethodAnnotations;
	final boolean ignoreTrivial;

	enum IgnoredStatus {
		POSSIBLE_TRIVIAL_GETTER, POSSIBLE_TRIVIAL_SETTER, POSSIBLE_TRIVIAL_INIT, IGNORED_BY_ANNOTATION, NOT_IGNORED;

		boolean isTrivial() {
			return (this == POSSIBLE_TRIVIAL_GETTER)
					|| (this == POSSIBLE_TRIVIAL_SETTER)
					|| (this == POSSIBLE_TRIVIAL_INIT);
		}
	}

	public IgnoredStatus ignoredStatus;

	public DetectIgnoredCodeMethodVisitor(MethodVisitor mv,
			Set<Integer> ignoredLineIds,
			Set<String> ignoredMethodNamesAndSignatures, boolean ignoreTrivial,
			Set<String> ignoreMethodAnnotations, String className,
			String superName, String methodName, String description,
			AtomicInteger lineIdGenerator) {
		super(mv, className, methodName, description, lineIdGenerator);
		this.superName = superName;
		this.ignoredLineIds = ignoredLineIds;
		this.ignoredMethodNamesAndSignatures = ignoredMethodNamesAndSignatures;
		this.ignoreTrivial = ignoreTrivial;
		this.ignoredStatus = checkForTrivialSignature(methodName, description);
		this.ignoreMethodAnnotations = ignoreMethodAnnotations;
	}

	private static IgnoredStatus checkForTrivialSignature(String name,
			String desc) {
		Type[] args = Type.getArgumentTypes(desc);
		Type ret = Type.getReturnType(desc);
		if (name.equals("<init>")) {
			return IgnoredStatus.POSSIBLE_TRIVIAL_INIT;
		}

		// a "setter" method must:
		// - have a name starting with "set"
		// - take one arguments
		// - return nothing (void)
		if (name.startsWith("set") && args.length == 1
				&& ret.equals(Type.VOID_TYPE)) {
			return IgnoredStatus.POSSIBLE_TRIVIAL_SETTER;
		}

		// a "getter" method must:
		// - have a name starting with "get", "is", or "has"
		// - take no arguments
		// - return a value (non-void)
		if ((name.startsWith("get") || name.startsWith("is") || name
				.startsWith("has"))
				&& args.length == 0 && !ret.equals(Type.VOID_TYPE)) {
			return IgnoredStatus.POSSIBLE_TRIVIAL_GETTER;
		}

		return IgnoredStatus.NOT_IGNORED;
	}

	public AnnotationVisitor visitAnnotation(String desc, boolean visible) {
		// Check to see if this annotation is one of the ones that we use to
		// trigger us to ignore this method
		String clazz = Type.getObjectType(
				desc.substring(1).replace(';', ' ').trim()).getClassName();
		if (ignoreMethodAnnotations.contains(clazz)
				|| desc.equals(Type.getDescriptor(CoverageIgnore.class))) {
			ignoredStatus = IgnoredStatus.IGNORED_BY_ANNOTATION;
		}
		return super.visitAnnotation(desc, visible);
	}

	@Override
	public void visitJumpInsn(int arg0, Label arg1) {
		markNotTrivial();
		super.visitJumpInsn(arg0, arg1);
	}

	public void visitFieldInsn(int opcode, String string, String string1,
			String string2) {
		super.visitFieldInsn(opcode, string, string1, string2);
		if (ignoredStatus.isTrivial()) {
			// trivial opcodes for accessing class fields are:
			// - GETFIELD or PUTFIELD
			if ((ignoredStatus == IgnoredStatus.POSSIBLE_TRIVIAL_GETTER && opcode != Opcodes.GETFIELD)
					|| (ignoredStatus == IgnoredStatus.POSSIBLE_TRIVIAL_SETTER && opcode != Opcodes.PUTFIELD)
					|| (ignoredStatus == IgnoredStatus.POSSIBLE_TRIVIAL_INIT && opcode != Opcodes.PUTFIELD)) {
				markNotTrivial();
			}
		}
	}

	public void visitVarInsn(int opcode, int i1) {
		super.visitVarInsn(opcode, i1);
		if (ignoredStatus.isTrivial() && opcode != Opcodes.ILOAD
				&& opcode != Opcodes.LLOAD && opcode != Opcodes.FLOAD
				&& opcode != Opcodes.DLOAD && opcode != Opcodes.ALOAD) {
			markNotTrivial();
		}
	}

	@Override
	public void visitTypeInsn(int arg0, String arg1) {
		super.visitTypeInsn(arg0, arg1);
		markNotTrivial();
	}

	@Override
	public void visitLookupSwitchInsn(Label arg0, int[] arg1, Label[] arg2) {
		super.visitLookupSwitchInsn(arg0, arg1, arg2);
		markNotTrivial();
	}

	@Override
	public void visitTableSwitchInsn(int arg0, int arg1, Label arg2,
			Label[] arg3) {
		super.visitTableSwitchInsn(arg0, arg1, arg2, arg3);
		markNotTrivial();
	}

	@Override
	public void visitMultiANewArrayInsn(String arg0, int arg1) {
		super.visitMultiANewArrayInsn(arg0, arg1);
		markNotTrivial();
	}

	@Override
	public void visitIincInsn(int arg0, int arg1) {
		super.visitIincInsn(arg0, arg1);
		markNotTrivial();
	}

	@Override
	public void visitLdcInsn(Object arg0) {
		super.visitLdcInsn(arg0);
		markNotTrivial();
	}

	@Override
	public void visitIntInsn(int arg0, int arg1) {
		super.visitIntInsn(arg0, arg1);
		markNotTrivial();
	}

	@Override
	public void visitMethodInsn(int opcode, String owner, String name,
			String desc) {
		if (ignoredStatus.isTrivial()
				&& !(ignoredStatus == IgnoredStatus.POSSIBLE_TRIVIAL_INIT
						&& name.equals("<init>") && owner.equals(superName) && opcode == Opcodes.INVOKESPECIAL)) {
			markNotTrivial();
		}
		super.visitMethodInsn(opcode, owner, name, desc);
	}

	@Override
	public void visitEnd() {
		super.visitEnd();
		if ((ignoredStatus == IgnoredStatus.IGNORED_BY_ANNOTATION)
				|| (ignoreTrivial && ignoredStatus.isTrivial())) {
			ignoredMethodNamesAndSignatures.add(methodName + methodSignature);
		}
	}

	public void markNotTrivial() {
		if (ignoredStatus.isTrivial()) {
			ignoredStatus = IgnoredStatus.NOT_IGNORED;
		}
	}

}