/*
 * Cobertura - http://cobertura.sourceforge.net/
 *
 * Copyright (C) 2005 Mark Doliner
 * Copyright (C) 2006 Jiri Mares
 * Copyright (C) 2010 Piotr Tabor
 *
 * 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;

import net.sourceforge.cobertura.util.RegexUtil;

import org.objectweb.asm.Label;
import org.objectweb.asm.Opcodes;

/*
 * TODO: If class is abstract then do not count the "public abstract class bleh" line as a SLOC.
 */
public class SecondPassMethodInstrumenter extends NewLocalVariableMethodAdapter implements Opcodes
{
	private String TOUCH_COLLECTOR_CLASS="net/sourceforge/cobertura/coveragedata/TouchCollector";
	
	private int currentLine;
   
	private int currentJump;
	
	private boolean methodStarted;
	
	private int myVariableIndex;

	private Label startLabel;
	
	private Label endLabel;
	
	private JumpHolder lastJump;
   
	private FirstPassMethodInstrumenter firstPass;
	
	private static final int BOOLEAN_TRUE = ICONST_0;
	private static final int BOOLEAN_FALSE = ICONST_1;

	public SecondPassMethodInstrumenter(FirstPassMethodInstrumenter firstPass)
	{
		super(firstPass.getWriterMethodVisitor(), firstPass.getMyAccess(), firstPass.getMyDescriptor(), 2);
		this.firstPass = firstPass;
		this.currentLine = 0;
	}

	public void visitJumpInsn(int opcode, Label label)
	{
		//to touch the previous branch (when there is such)
		touchBranchFalse();
		
		// Ignore any jump instructions in the "class init" method.
		// When initializing static variables, the JVM first checks
		// that the variable is null before attempting to set it.
		// This check contains an IFNONNULL jump instruction which
		// would confuse people if it showed up in the reports.
		if ((opcode != GOTO) && (opcode != JSR) && (currentLine != 0)
				&& (!this.firstPass.getMyName().equals("<clinit>")))
		{
			lastJump = new JumpHolder(currentLine, currentJump++); 
			mv.visitIntInsn(SIPUSH, currentLine);
			mv.visitVarInsn(ISTORE, myVariableIndex);
			mv.visitIntInsn(SIPUSH, lastJump.getJumpNumber());
			mv.visitVarInsn(ISTORE, myVariableIndex + 1);
		}
		
		super.visitJumpInsn(opcode, label);
	}

	public void visitLineNumber(int line, Label start)
	{
		// Record initial information about this line of code
		currentLine = line;
		currentJump = 0;

		instrumentOwnerClass();

		// Mark the current line number as covered:
		// classData.touch(line)
		mv.visitIntInsn(SIPUSH, line);
		mv.visitMethodInsn(INVOKESTATIC,
				TOUCH_COLLECTOR_CLASS, "touch",
				"(Ljava/lang/String;I)V");

		super.visitLineNumber(line, start);
	}

	public void visitMethodInsn(int opcode, String owner, String name,
			String desc)
	{
		//to touch the previous branch (when there is such)
		touchBranchFalse();
		
		super.visitMethodInsn(opcode, owner, name, desc);

		// If any of the ignore patterns match this line
		// then remove it from our data
		if (RegexUtil.matches(firstPass.getIgnoreRegexs(), owner)) 
		{
			firstPass.removeLine(currentLine);
		}
	}

	public void visitFieldInsn(int opcode, String owner, String name, String desc)
	{
		//to touch the previous branch (when there is such)
		touchBranchFalse();
		
		super.visitFieldInsn(opcode, owner, name, desc);
	}

	public void visitIincInsn(int var, int increment)
	{
		//to touch the previous branch (when there is such)
		touchBranchFalse();
		
		super.visitIincInsn(var, increment);
	}

	public void visitInsn(int opcode)
	{
		//to touch the previous branch (when there is such)
		touchBranchFalse();
		
		super.visitInsn(opcode);
	}

	public void visitIntInsn(int opcode, int operand)
	{
		//to touch the previous branch (when there is such)
		touchBranchFalse();
		
		super.visitIntInsn(opcode, operand);
	}

	public void visitLabel(Label label)
	{
		//When this is the first method's label ... create the 2 new local variables (lineNumber and branchNumber)
		if (methodStarted) 
		{
			methodStarted = false;
			myVariableIndex = getFirstStackVariable();
			mv.visitInsn(ICONST_0);
			mv.visitVarInsn(ISTORE, myVariableIndex);
			mv.visitIntInsn(SIPUSH, -1); 
			mv.visitVarInsn(ISTORE, myVariableIndex + 1);
			startLabel = label;
		}
		//to have the last label for visitLocalVariable
		endLabel = label;
		
		super.visitLabel(label);
		
		//instrument the branch coverage collection
		if (firstPass.getJumpTargetLabels().keySet().contains(label)) 
		{ //this label is the true branch label
			if (lastJump != null) 
			{ //this is also label after jump - we have to check the branch number whether this is the true or false branch
				Label newLabelX = instrumentIsLastJump();
				instrumentOwnerClass();
				instrumentPutLineAndBranchNumbers();
				mv.visitInsn(BOOLEAN_FALSE);
				instrumentInvokeTouchJump();
				Label newLabelY = new Label();
				mv.visitJumpInsn(GOTO, newLabelY);
				mv.visitLabel(newLabelX);
				mv.visitVarInsn(ILOAD, myVariableIndex + 1);
				mv.visitJumpInsn(IFLT, newLabelY);
				instrumentOwnerClass();
				instrumentPutLineAndBranchNumbers();
				mv.visitInsn(BOOLEAN_TRUE);
				instrumentInvokeTouchJump();
				mv.visitLabel(newLabelY);
			}
			else
			{ //just hit te true branch
				//just check whether the jump has been invoked or the label has been touched other way 
				mv.visitVarInsn(ILOAD, myVariableIndex + 1);
				Label newLabelX = new Label();
				mv.visitJumpInsn(IFLT, newLabelX);
				instrumentJumpHit(true);
				mv.visitLabel(newLabelX);
			}
		} 
		else if (lastJump != null) 
		{ //this is "only" after jump label, hit the false branch only if the lastJump is same as stored stack lineNumber and jumpNumber
			Label newLabelX = instrumentIsLastJump();
			instrumentJumpHit(false); 
			mv.visitLabel(newLabelX);
		}
		lastJump = null;
		
		SwitchHolder sh = (SwitchHolder) firstPass.getSwitchTargetLabels().get(label);
		if (sh != null)
		{
			instrumentSwitchHit(sh.getLineNumber(), sh.getSwitchNumber(), sh.getBranch());
		}
		
		//we have to manually invoke the visitLineNumber because of not correct MedthodNode's handling
		Integer line = (Integer) firstPass.getLineLabels().get(label);
		if (line != null) {
			visitLineNumber(line.intValue(), label);
		}
	}

	public void visitLdcInsn(Object cst)
	{
		//to touch the previous branch (when there is such)
		touchBranchFalse();
		
		super.visitLdcInsn(cst);
	}

	public void visitMultiANewArrayInsn(String desc, int dims)
	{
		//to touch the previous branch (when there is such)
		touchBranchFalse();
		
		super.visitMultiANewArrayInsn(desc, dims);
	}

	public void visitLookupSwitchInsn(Label dflt, int[] keys, Label[] labels)
	{
		//to touch the previous branch (when there is such)
		touchBranchFalse();
		
		super.visitLookupSwitchInsn(dflt, keys, labels);
	}

	public void visitTableSwitchInsn(int min, int max, Label dflt, Label[] labels)
	{
		//to touch the previous branch (when there is such)
		touchBranchFalse();
		
		super.visitTableSwitchInsn(min, max, dflt, labels);
	}

	public void visitTryCatchBlock(Label start, Label end, Label handler, String type)
	{
		//to touch the previous branch (when there is such)
		touchBranchFalse();
		
		super.visitTryCatchBlock(start, end, handler, type);
	}

	public void visitTypeInsn(int opcode, String desc)
	{
		//to touch the previous branch (when there is such)
		touchBranchFalse();
		
		super.visitTypeInsn(opcode, desc);
	}

	public void visitVarInsn(int opcode, int var)
	{
		//to touch the previous branch (when there is such)
		touchBranchFalse();
		
		//this is to change the variable instructions to conform to 2 new variables
		super.visitVarInsn(opcode, var);
	}

	public void visitCode()
	{
		methodStarted = true;
		super.visitCode();
	}
	
	private void touchBranchFalse() {
		if (lastJump != null) {
			lastJump = null;
			instrumentJumpHit(false);
		}
	}

	private void instrumentOwnerClass()
	{
		// OwnerClass is the name of the class being instrumented
		mv.visitLdcInsn(firstPass.getOwnerClass());
	}
	
	private void instrumentSwitchHit(int lineNumber, int switchNumber, int branch)
	{
		instrumentOwnerClass();
		
		//Invoke the touchSwitch(lineNumber, switchNumber, branch)
		mv.visitIntInsn(SIPUSH, lineNumber);
		mv.visitIntInsn(SIPUSH, switchNumber);
		mv.visitIntInsn(SIPUSH, branch);
		instrumentInvokeTouchSwitch();
	}
	
	private void instrumentJumpHit(boolean branch)
	{
		instrumentOwnerClass();
		
		//Invoke the touchJump(lineNumber, branchNumber, branch)
		instrumentPutLineAndBranchNumbers();
		mv.visitInsn(branch ? BOOLEAN_TRUE : BOOLEAN_FALSE);
		instrumentInvokeTouchJump();
	}

	private void instrumentInvokeTouchJump()
	{
		mv.visitMethodInsn(INVOKESTATIC, TOUCH_COLLECTOR_CLASS, "touchJump", "(Ljava/lang/String;IIZ)V");
		mv.visitIntInsn(SIPUSH, -1); //is important to reset current branch, because we have to know that the branch info on stack has already been used and can't be used
		mv.visitVarInsn(ISTORE, myVariableIndex + 1);
	}

	private void instrumentInvokeTouchSwitch()
	{
		mv.visitMethodInsn(INVOKESTATIC, TOUCH_COLLECTOR_CLASS, "touchSwitch", "(Ljava/lang/String;III)V");
	}

	private void instrumentPutLineAndBranchNumbers()
	{
		mv.visitVarInsn(ILOAD, myVariableIndex);
		mv.visitVarInsn(ILOAD, myVariableIndex + 1);
	}

	private Label instrumentIsLastJump() {
		mv.visitVarInsn(ILOAD, myVariableIndex);
		mv.visitIntInsn(SIPUSH, lastJump.getLineNumber());
		Label newLabelX = new Label();
		mv.visitJumpInsn(IF_ICMPNE, newLabelX);
		mv.visitVarInsn(ILOAD, myVariableIndex + 1);
		mv.visitIntInsn(SIPUSH, lastJump.getJumpNumber());
		mv.visitJumpInsn(IF_ICMPNE, newLabelX);
		return newLabelX;
	}

	public void visitMaxs(int maxStack, int maxLocals)
	{
		mv.visitLocalVariable("__cobertura__line__number__", "I", null, startLabel, endLabel, myVariableIndex);
		mv.visitLocalVariable("__cobertura__branch__number__", "I", null, startLabel, endLabel, myVariableIndex + 1);
		super.visitMaxs(maxStack, maxLocals);
	}

}
