/*
 * Cobertura - http://cobertura.sourceforge.net/
 *
 * Copyright (C) 2003 jcoverage ltd.
 * Copyright (C) 2005 Mark Doliner
 * Copyright (C) 2005 Mark Sinke
 * Copyright (C) 2006 Jiri Mares
 *
 * 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.coveragedata;

import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

import net.sourceforge.cobertura.util.StringUtil;

/**
 * <p>
 * This class implements HasBeenInstrumented so that when cobertura
 * instruments itself, it will omit this class.  It does this to
 * avoid an infinite recursion problem because instrumented classes
 * make use of this class.
 * </p>
 */
public class LineData
		implements Comparable, CoverageData, HasBeenInstrumented, Serializable
{
	private static final long serialVersionUID = 4;

	private transient Lock lock;

	private long hits;
	private List jumps;
	private List switches;
	private final int lineNumber;
	private String methodDescriptor;
	private String methodName;

	LineData(int lineNumber)
	{
		this(lineNumber, null, null);
	}

	LineData(int lineNumber, String methodName, String methodDescriptor)
	{
		this.hits = 0;
		this.jumps = null;
		this.lineNumber = lineNumber;
		this.methodName = methodName;
		this.methodDescriptor = methodDescriptor;
		initLock();
	}
	
	private void initLock()
	{
		 lock = new ReentrantLock();
	}

	/**
	 * This is required because we implement Comparable.
	 */
	public int compareTo(Object o)
	{
		if (!o.getClass().equals(LineData.class))
			return Integer.MAX_VALUE;
		return this.lineNumber - ((LineData)o).lineNumber;
	}

	public boolean equals(Object obj)
	{
		if (this == obj)
			return true;
		if ((obj == null) || !(obj.getClass().equals(this.getClass())))
			return false;

		LineData lineData = (LineData)obj;
		getBothLocks(lineData);
		try
		{
			return (this.hits == lineData.hits)
					&& ((this.jumps == lineData.jumps) || ((this.jumps != null) && (this.jumps.equals(lineData.jumps))))
					&& ((this.switches == lineData.switches) || ((this.switches != null) && (this.switches.equals(lineData.switches))))
					&& (this.lineNumber == lineData.lineNumber)
					&& (this.methodDescriptor.equals(lineData.methodDescriptor))
					&& (this.methodName.equals(lineData.methodName));
		}
		finally
		{
			lock.unlock();
			lineData.lock.unlock();
		}
	}

	public double getBranchCoverageRate()
	{
		if (getNumberOfValidBranches() == 0)
			return 1d;
		lock.lock();
		try
		{
			return ((double) getNumberOfCoveredBranches()) / getNumberOfValidBranches();
		}
		finally
		{
			lock.unlock();
		}
	}

	public String getConditionCoverage()
	{
		StringBuffer ret = new StringBuffer();
		if (getNumberOfValidBranches() == 0)
		{
			ret.append(StringUtil.getPercentValue(1.0));
		}
		else
		{
			lock.lock();
			try
			{
				ret.append(StringUtil.getPercentValue(getBranchCoverageRate()));
				ret.append(" (").append(getNumberOfCoveredBranches()).append("/").append(getNumberOfValidBranches()).append(")");
			}
			finally
			{
				lock.unlock();
			}
		}
		return ret.toString();
	}
	
	public long getHits()
	{
		lock.lock();
		try
		{
			return hits;
		}
		finally
		{
			lock.unlock();
		}
	}

	public boolean isCovered()
	{
		lock.lock();
		try
		{
			return (getHits() > 0) && ((getNumberOfValidBranches() == 0) || ((1.0 - getBranchCoverageRate()) < 0.0001));
		}
		finally
		{
			lock.unlock();
		}
	}
	
	public double getLineCoverageRate()
	{
		return (getHits() > 0) ? 1 : 0;
	}

	public int getLineNumber()
	{
		return lineNumber;
	}

	public String getMethodDescriptor()
	{
		lock.lock();
		try
		{
			return methodDescriptor;
		}
		finally
		{
			lock.unlock();
		}
	}

	public String getMethodName()
	{
		lock.lock();
		try
		{
			return methodName;
		}
		finally
		{
			lock.unlock();
		}
	}

	/**
	 * @see net.sourceforge.cobertura.coveragedata.CoverageData#getNumberOfCoveredBranches()
	 */
	/*public int getNumberOfCoveredBranches()
	{
		if (this.branches == null) 
			return 0;
		int covered = 0;
		for (Iterator i = this.branches.iterator(); i.hasNext(); covered += ((BranchData) i.next()).getNumberOfCoveredBranches());
		return covered;
	}*/

	public int getNumberOfCoveredLines()
	{
		return (getHits() > 0) ? 1 : 0;
	}

	public int getNumberOfValidBranches()
	{
		int ret = 0;
		lock.lock();
		try
		{
			if (jumps != null)
				for (int i = jumps.size() - 1; i >= 0; i--)
					ret += ((JumpData) jumps.get(i)).getNumberOfValidBranches();
			if (switches != null)
				for (int i = switches.size() - 1; i >= 0; i--)
					ret += ((SwitchData) switches.get(i)).getNumberOfValidBranches();
			return ret;
		}
		finally
		{
			lock.unlock();
		}
	}
	
	public int getNumberOfCoveredBranches()
	{
		int ret = 0;
		lock.lock();
		try
		{
			if (jumps != null)
				for (int i = jumps.size() - 1; i >= 0; i--)
					ret += ((JumpData) jumps.get(i)).getNumberOfCoveredBranches();
			if (switches != null)
				for (int i = switches.size() - 1; i >= 0; i--)
					ret += ((SwitchData) switches.get(i)).getNumberOfCoveredBranches();
			return ret;
		}
		finally
		{
			lock.unlock();
		}
	}

	public int getNumberOfValidLines()
	{
		return 1;
	}

	public int hashCode()
	{
		return this.lineNumber;
	}

	public boolean hasBranch()
	{
		lock.lock();
		try
		{
			return (jumps != null) || (switches != null);
		}
		finally
		{
			lock.unlock();
		}
	}

	public void merge(CoverageData coverageData)
	{
		LineData lineData = (LineData)coverageData;
		getBothLocks(lineData);
		try
		{
			this.hits += lineData.hits;
			if (lineData.jumps != null)
				if (this.jumps == null) 
					this.jumps = lineData.jumps;
				else
				{
					for (int i = Math.min(this.jumps.size(), lineData.jumps.size()) - 1; i >= 0; i--)
						((JumpData) this.jumps.get(i)).merge((JumpData) lineData.jumps.get(i));
					for (int i = Math.min(this.jumps.size(), lineData.jumps.size()); i < lineData.jumps.size(); i++) 
						this.jumps.add(lineData.jumps.get(i));
				}
			if (lineData.switches != null)
				if (this.switches == null) 
					this.switches = lineData.switches;
				else
				{
					for (int i = Math.min(this.switches.size(), lineData.switches.size()) - 1; i >= 0; i--)
						((SwitchData) this.switches.get(i)).merge((SwitchData) lineData.switches.get(i));
					for (int i = Math.min(this.switches.size(), lineData.switches.size()); i < lineData.switches.size(); i++) 
						this.switches.add(lineData.switches.get(i));
				}
			if (lineData.methodName != null)
				this.methodName = lineData.methodName;
			if (lineData.methodDescriptor != null)
				this.methodDescriptor = lineData.methodDescriptor;
		}
		finally
		{
			lock.unlock();
			lineData.lock.unlock();
		}
	}

	void addJump(int jumpNumber)
	{
		getJumpData(jumpNumber);
	}

	void addSwitch(int switchNumber, int[] keys)
	{
		getSwitchData(switchNumber, new SwitchData(switchNumber, keys));
	}

	void addSwitch(int switchNumber, int min, int max)
	{
		getSwitchData(switchNumber, new SwitchData(switchNumber, min, max));
	}

	void setMethodNameAndDescriptor(String name, String descriptor)
	{
		lock.lock();
		try
		{
			this.methodName = name;
			this.methodDescriptor = descriptor;
		}
		finally
		{
			lock.unlock();
		}
	}

	void touch(int new_hits)
	{
		lock.lock();
		try
		{
			this.hits+=new_hits;
		}
		finally
		{
			lock.unlock();
		}
	}
	
	void touchJump(int jumpNumber, boolean branch,int hits) 
	{
		getJumpData(jumpNumber).touchBranch(branch,hits);
	}
	
	void touchSwitch(int switchNumber, int branch,int hits) 
	{
		getSwitchData(switchNumber, null).touchBranch(branch,hits);
	}
	
	public int getConditionSize() {
		lock.lock();
		try
		{
			return ((jumps == null) ? 0 : jumps.size()) + ((switches == null) ? 0 :switches.size());
		}
		finally
		{
			lock.unlock();
		}
	}
	
	public Object getConditionData(int index)
	{
		Object branchData = null;
		lock.lock();
		try
		{
			int jumpsSize = (jumps == null) ? 0 : jumps.size();
			int switchesSize = (switches == null) ? 0 :switches.size();
			if (index < jumpsSize) 
			{
				branchData = jumps.get(index);
			}
			else if (index < jumpsSize + switchesSize)
			{
				branchData = switches.get(index - jumpsSize);
			}
			return branchData;
		}
		finally
		{
			lock.unlock();
		}
	}
	
	public String getConditionCoverage(int index) {
		Object branchData = getConditionData(index);
		if (branchData == null)
		{
			return StringUtil.getPercentValue(1.0);
		} 
		else if (branchData instanceof JumpData) 
		{
			JumpData jumpData = (JumpData) branchData;
			return StringUtil.getPercentValue(jumpData.getBranchCoverageRate());
		}
		else
		{
			SwitchData switchData = (SwitchData) branchData;
			return StringUtil.getPercentValue(switchData.getBranchCoverageRate());

		}
	}
	
	JumpData getJumpData(int jumpNumber) 
	{
		lock.lock();
		try
		{
			if (jumps == null) 
			{
				jumps = new ArrayList();
			}
			if (jumps.size() <= jumpNumber) 
			{
				for (int i = jumps.size(); i <= jumpNumber; jumps.add(new JumpData(i++)));
			}
			return (JumpData) jumps.get(jumpNumber);
		}
		finally
		{
			lock.unlock();
		}
	}
	
	SwitchData getSwitchData(int switchNumber, SwitchData data) 
	{
		lock.lock();
		try
		{
			if (switches == null) 
			{
				switches = new ArrayList();
			}
			if (switches.size() < switchNumber) 
			{
				for (int i = switches.size(); i < switchNumber; switches.add(new SwitchData(i++)));
			}
			if (switches.size() == switchNumber) 
				if (data != null)
					switches.add(data);
				else
					switches.add(new SwitchData(switchNumber));
			return (SwitchData) switches.get(switchNumber);
		}
		finally
		{
			lock.unlock();
		}
	}

	private void getBothLocks(LineData other) {
		/*
		 * To prevent deadlock, we need to get both locks or none at all.
		 * 
		 * When this method returns, the thread will have both locks.
		 * Make sure you unlock them!
		 */
		boolean myLock = false;
		boolean otherLock = false;
		while ((!myLock) || (!otherLock))
		{
			try
			{
				myLock = lock.tryLock();
				otherLock = other.lock.tryLock();
			}
			finally
			{
				if ((!myLock) || (!otherLock))
				{
					//could not obtain both locks - so unlock the one we got.
					if (myLock)
					{
						lock.unlock();
					}
					if (otherLock)
					{
						other.lock.unlock();
					}
					//do a yield so the other threads will get to work.
					Thread.yield();
				}
			}
		}
	}

	private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException
	{
		in.defaultReadObject();
		initLock();
	}
}
