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

import java.util.*;

/**
 * <p>
 * ProjectData information is typically serialized to a file. An
 * instance of this class records coverage information for a single
 * class that has been instrumented.
 * </p>
 */

@CoverageIgnore
public class ClassData extends CoverageDataContainer
		implements
			Comparable<ClassData> {
	private static final long serialVersionUID = 5;

	/**
	 * Each key is a line number in this class, stored as an Integer object.
	 * Each value is information about the line, stored as a LineData object.
	 */
	private Map<Integer, LineData> branches = new HashMap<Integer, LineData>();

	private boolean containsInstrumentationInfo = false;

	private Set<String> methodNamesAndDescriptors = new HashSet<String>();

	private String name = null;

	private String sourceFileName = null;

	public ClassData() {
	}

	/**
	 * @param name In the format "net.sourceforge.cobertura.coveragedata.ClassData"
	 */
	public ClassData(String name) {
		if (name == null)
			throw new IllegalArgumentException("Class name must be specified.");
		this.name = name;
	}

	public LineData addLine(int lineNumber, String methodName,
			String methodDescriptor) {
		lock.lock();
		try {
			LineData lineData = getLineData(lineNumber);
			if (lineData == null) {
				lineData = new LineData(lineNumber);
				// Each key is a line number in this class, stored as an Integer object.
				// Each value is information about the line, stored as a LineData object.
				children.put(new Integer(lineNumber), lineData);
			}
			lineData.setMethodNameAndDescriptor(methodName, methodDescriptor);

			// methodName and methodDescriptor can be null when cobertura.ser with
			// no line information was loaded (or was not loaded at all).
			if (methodName != null && methodDescriptor != null)
				methodNamesAndDescriptors.add(methodName + methodDescriptor);
			return lineData;
		} finally {
			lock.unlock();
		}
	}

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

	public boolean containsInstrumentationInfo() {
		lock.lock();
		try {
			return this.containsInstrumentationInfo;
		} finally {
			lock.unlock();
		}
	}

	/**
	 * Returns true if the given object is an instance of the
	 * ClassData class, and it contains the same data as this
	 * class.
	 */
	public boolean equals(Object obj) {
		if (this == obj)
			return true;
		if ((obj == null) || !(obj.getClass().equals(this.getClass())))
			return false;

		ClassData classData = (ClassData) obj;
		getBothLocks(classData);
		try {
			return super.equals(obj)
					&& this.branches.equals(classData.branches)
					&& this.methodNamesAndDescriptors
							.equals(classData.methodNamesAndDescriptors)
					&& this.name.equals(classData.name)
					&& this.sourceFileName.equals(classData.sourceFileName);
		} finally {
			lock.unlock();
			classData.lock.unlock();
		}
	}

	public String getBaseName() {
		int lastDot = this.name.lastIndexOf('.');
		if (lastDot == -1) {
			return this.name;
		}
		return this.name.substring(lastDot + 1);
	}

	/**
	 * @return The branch coverage rate for a particular method.
	 */
	public double getBranchCoverageRate(String methodNameAndDescriptor) {
		int total = 0;
		int covered = 0;

		lock.lock();
		try {
			for (Iterator<LineData> iter = branches.values().iterator(); iter
					.hasNext();) {
				LineData next = (LineData) iter.next();
				if (methodNameAndDescriptor.equals(next.getMethodName()
						+ next.getMethodDescriptor())) {
					total += next.getNumberOfValidBranches();
					covered += next.getNumberOfCoveredBranches();
				}
			}
			if (total == 0)
				return 1.0;
			return (double) covered / total;
		} finally {
			lock.unlock();
		}
	}

	public Collection<Integer> getBranches() {
		lock.lock();
		try {
			return Collections.unmodifiableCollection(branches.keySet());
		} finally {
			lock.unlock();
		}
	}

	/**
	 * @param lineNumber The source code line number.
	 *
	 * @return The coverage of the line
	 */
	public LineData getLineCoverage(int lineNumber) {
		Integer lineObject = new Integer(lineNumber);
		lock.lock();
		try {
			if (!children.containsKey(lineObject)) {
				return null;
			}

			return (LineData) children.get(lineObject);
		} finally {
			lock.unlock();
		}
	}

	/**
	 * @return The line coverage rate for particular method
	 */
	public double getLineCoverageRate(String methodNameAndDescriptor) {
		int total = 0;
		int hits = 0;

		lock.lock();
		try {
			Iterator<CoverageData> iter = children.values().iterator();
			while (iter.hasNext()) {
				LineData next = (LineData) iter.next();
				if (methodNameAndDescriptor.equals(next.getMethodName()
						+ next.getMethodDescriptor())) {
					total++;
					if (next.getHits() > 0) {
						hits++;
					}
				}
			}
			if (total == 0)
				return 1d;
			return (double) hits / total;
		} finally {
			lock.unlock();
		}
	}

	public LineData getLineData(int lineNumber) {
		lock.lock();
		try {
			return (LineData) children.get(Integer.valueOf(lineNumber));
		} finally {
			lock.unlock();
		}
	}

	public SortedSet<CoverageData> getLines() {
		lock.lock();
		try {
			return new TreeSet<CoverageData>(this.children.values());
		} finally {
			lock.unlock();
		}
	}

	public Collection<CoverageData> getLines(String methodNameAndDescriptor) {
		Collection<CoverageData> lines = new HashSet<CoverageData>();
		lock.lock();
		try {
			Iterator<CoverageData> iter = children.values().iterator();
			while (iter.hasNext()) {
				LineData next = (LineData) iter.next();
				if (methodNameAndDescriptor.equals(next.getMethodName()
						+ next.getMethodDescriptor())) {
					lines.add(next);
				}
			}
			return lines;
		} finally {
			lock.unlock();
		}
	}

	/**
	 * @return The method name and descriptor of each method found in the
	 *         class represented by this instrumentation.
	 */
	public Set<String> getMethodNamesAndDescriptors() {
		lock.lock();
		try {
			return methodNamesAndDescriptors;
		} finally {
			lock.unlock();
		}
	}

	public String getName() {
		return name;
	}

	/**
	 * @return The number of branches in this class.
	 */
	public int getNumberOfValidBranches() {
		int number = 0;
		lock.lock();
		try {
			for (Iterator<LineData> i = branches.values().iterator(); i
					.hasNext(); number += ((LineData) i.next())
					.getNumberOfValidBranches());
			return number;
		} finally {
			lock.unlock();
		}
	}

	/**
	 * @see net.sourceforge.cobertura.coveragedata.CoverageData#getNumberOfCoveredBranches()
	 */
	public int getNumberOfCoveredBranches() {
		int number = 0;
		lock.lock();
		try {
			for (Iterator<LineData> i = branches.values().iterator(); i
					.hasNext(); number += ((LineData) i.next())
					.getNumberOfCoveredBranches());
			return number;
		} finally {
			lock.unlock();
		}
	}

	public String getPackageName() {
		int lastDot = this.name.lastIndexOf('.');
		if (lastDot == -1) {
			return "";
		}
		return this.name.substring(0, lastDot);
	}

	/**
	 * Return the name of the file containing this class.  If this
	 * class' sourceFileName has not been set (for whatever reason)
	 * then this method will attempt to infer the name of the source
	 * file using the class name.
	 *
	 * @return The name of the source file, for example
	 *         net/sourceforge/cobertura/coveragedata/ClassData.java
	 */
	public String getSourceFileName() {
		String baseName;
		lock.lock();
		try {
			if (sourceFileName != null)
				baseName = sourceFileName;
			else {
				baseName = getBaseName();
				int firstDollarSign = baseName.indexOf('$');
				if (firstDollarSign == -1 || firstDollarSign == 0)
					baseName += ".java";
				else
					baseName = baseName.substring(0, firstDollarSign) + ".java";
			}

			String packageName = getPackageName();
			if (packageName.equals(""))
				return baseName;
			return packageName.replace('.', '/') + '/' + baseName;
		} finally {
			lock.unlock();
		}
	}

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

	/**
	 * @return True if the line contains at least one condition jump (branch)
	 */
	public boolean hasBranch(int lineNumber) {
		lock.lock();
		try {
			return branches.containsKey(Integer.valueOf(lineNumber));
		} finally {
			lock.unlock();
		}
	}

	/**
	 * Determine if a given line number is a valid line of code.
	 *
	 * @return True if the line contains executable code.  False
	 *         if the line is empty, or a comment, etc.
	 */
	public boolean isValidSourceLineNumber(int lineNumber) {
		lock.lock();
		try {
			return children.containsKey(Integer.valueOf(lineNumber));
		} finally {
			lock.unlock();
		}
	}

	public void addLineJump(int lineNumber, int branchNumber) {
		lock.lock();
		try {
			LineData lineData = getLineData(lineNumber);
			if (lineData != null) {
				lineData.addJump(branchNumber);
				this.branches.put(Integer.valueOf(lineNumber), lineData);
			}
		} finally {
			lock.unlock();
		}
	}

	public void addLineSwitch(int lineNumber, int switchNumber, int min,
			int max, int maxBranches) {
		lock.lock();
		try {
			LineData lineData = getLineData(lineNumber);
			if (lineData != null) {
				lineData.addSwitch(switchNumber, min, max, maxBranches);
				this.branches.put(Integer.valueOf(lineNumber), lineData);
			}
		} finally {
			lock.unlock();
		}
	}

	/**
	 * Merge some existing instrumentation with this instrumentation.
	 *
	 * @param coverageData Some existing coverage data.
	 */
	public void merge(CoverageData coverageData) {
		ClassData classData = (ClassData) coverageData;

		// If objects contain data for different classes then don't merge
		if (!this.getName().equals(classData.getName()))
			return;

		getBothLocks(classData);
		try {
			super.merge(coverageData);

			// We can't just call this.branches.putAll(classData.branches);
			// Why not?  If we did a putAll, then the LineData objects from
			// the coverageData class would overwrite the LineData objects
			// that are already in "this.branches"  And we don't need to
			// update the LineData objects that are already in this.branches
			// because they are shared between this.branches and this.children,
			// so the object hit counts will be moved when we called
			// super.merge() above.
			for (Iterator<Integer> iter = classData.branches.keySet()
					.iterator(); iter.hasNext();) {
				Integer key = iter.next();
				if (!this.branches.containsKey(key)) {
					this.branches.put(key, classData.branches.get(key));
				}
			}

			this.containsInstrumentationInfo |= classData.containsInstrumentationInfo;
			this.methodNamesAndDescriptors.addAll(classData
					.getMethodNamesAndDescriptors());
			if (classData.sourceFileName != null)
				this.sourceFileName = classData.sourceFileName;
		} finally {
			lock.unlock();
			classData.lock.unlock();
		}
	}

	public void removeLine(int lineNumber) {
		Integer lineObject = Integer.valueOf(lineNumber);
		lock.lock();
		try {
			children.remove(lineObject);
			branches.remove(lineObject);
		} finally {
			lock.unlock();
		}
	}

	public void setContainsInstrumentationInfo() {
		lock.lock();
		try {
			this.containsInstrumentationInfo = true;
		} finally {
			lock.unlock();
		}
	}

	public void setSourceFileName(String sourceFileName) {
		lock.lock();
		try {
			this.sourceFileName = sourceFileName;
		} finally {
			lock.unlock();
		}
	}

	/**
	 * Increment the number of hits for a particular line of code.
	 *
	 * @param lineNumber the line of code to increment the number of hits.
	 * @param hits       how many times the piece was called
	 */
	public void touch(int lineNumber, int hits) {
		lock.lock();
		try {
			LineData lineData = getLineData(lineNumber);
			if (lineData == null)
				lineData = addLine(lineNumber, null, null);
			lineData.touch(hits);
		} finally {
			lock.unlock();
		}
	}

	/**
	 * Increments the number of hits for particular hit counter of particular branch on particular line number.
	 *
	 * @param lineNumber   The line of code where the branch is
	 * @param branchNumber The branch on the line to change the hit counter
	 * @param branch       The hit counter (true or false)
	 * @param hits         how many times the piece was called
	 */
	public void touchJump(int lineNumber, int branchNumber, boolean branch,
			int hits) {
		lock.lock();
		try {
			LineData lineData = getLineData(lineNumber);
			if (lineData == null)
				lineData = addLine(lineNumber, null, null);
			lineData.touchJump(branchNumber, branch, hits);
		} finally {
			lock.unlock();
		}
	}

	/**
	 * Increments the number of hits for particular hit counter of particular switch branch on particular line number.
	 *
	 * @param lineNumber   The line of code where the branch is
	 * @param switchNumber The switch on the line to change the hit counter
	 * @param branch       The hit counter
	 * @param hits         how many times the piece was called
	 */
	public void touchSwitch(int lineNumber, int switchNumber, int branch,
			int hits) {
		lock.lock();
		try {
			LineData lineData = getLineData(lineNumber);
			if (lineData == null)
				lineData = addLine(lineNumber, null, null);
			lineData.touchSwitch(switchNumber, branch, hits);
		} finally {
			lock.unlock();
		}
	}

}
