/*
 * Cobertura - http://cobertura.sourceforge.net/
 *
 * Copyright (C) 2005 Mark Doliner
 * Copyright (C) 2005 Grzegorz Lukasik
 * Copyright (C) 2005 Jeremy Thomerson
 * Copyright (C) 2006 Naoki Iwami
 * Copyright (C) 2009 Charlie Squires
 * Copyright (C) 2009 John Lewis
 *
 * 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.reporting.html;

import java.io.BufferedReader;
import java.io.File;
import java.io.InputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.io.UnsupportedEncodingException;
import java.text.DateFormat;
import java.text.DecimalFormat;
import java.text.NumberFormat;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
import java.util.Iterator;
import java.util.SortedSet;
import java.util.TreeSet;
import java.util.Vector;

import net.sourceforge.cobertura.coveragedata.ClassData;
import net.sourceforge.cobertura.coveragedata.CoverageData;
import net.sourceforge.cobertura.coveragedata.LineData;
import net.sourceforge.cobertura.coveragedata.PackageData;
import net.sourceforge.cobertura.coveragedata.ProjectData;
import net.sourceforge.cobertura.coveragedata.SourceFileData;
import net.sourceforge.cobertura.reporting.ComplexityCalculator;
import net.sourceforge.cobertura.reporting.html.files.CopyFiles;
import net.sourceforge.cobertura.util.FileFinder;
import net.sourceforge.cobertura.util.Header;
import net.sourceforge.cobertura.util.IOUtil;
import net.sourceforge.cobertura.util.Source;
import net.sourceforge.cobertura.util.StringUtil;

import org.apache.log4j.Logger;

public class HTMLReport
{

	private static final Logger LOGGER = Logger.getLogger(HTMLReport.class);

	private File destinationDir;

	private FileFinder finder;

	private ComplexityCalculator complexity;

	private ProjectData projectData;

	private String encoding;

	/**
	 * Create a coverage report
	 * @param encoding 
	 */
	public HTMLReport(ProjectData projectData, File outputDir,
			FileFinder finder, ComplexityCalculator complexity, String encoding)
			throws Exception
	{
		this.destinationDir = outputDir;
		this.finder = finder;
		this.complexity = complexity;
		this.projectData = projectData;
		this.encoding = encoding;

		CopyFiles.copy(outputDir);
		generatePackageList();
		generateSourceFileLists();
		generateOverviews();
		generateSourceFiles();
	}

	private String generatePackageName(PackageData packageData)
	{
		if (packageData.getName().equals(""))
			return "(default)";
		return packageData.getName();
	}

	private void generatePackageList() throws IOException
	{
		File file = new File(destinationDir, "frame-packages.html");
		PrintWriter out = null;

		try
		{
			out = IOUtil.getPrintWriter(file);

			out
					.println("<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.0 Transitional//EN\"");
			out
					.println("           \"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd\">");

			out
					.println("<html xmlns=\"http://www.w3.org/1999/xhtml\" xml:lang=\"en\" lang=\"en\">");
			out.println("<head>");
			out
					.println("<meta http-equiv=\"Content-Type\" content=\"text/html; charset=UTF-8\" />");
			out.println("<title>\u30ab\u30d0\u30ec\u30c3\u30b8\u30ec\u30dd\u30fc\u30c8</title>");
			out
					.println("<link title=\"Style\" type=\"text/css\" rel=\"stylesheet\" href=\"css/main.css\" />");
			out.println("</head>");
			out.println("<body>");
			out.println("<h5>\u30d1\u30c3\u30b1\u30fc\u30b8</h5>");
			out.println("<table width=\"100%\">");
			out.println("<tr>");
			out
					.println("<td nowrap=\"nowrap\"><a href=\"frame-summary.html\" onclick='parent.sourceFileList.location.href=\"frame-sourcefiles.html\"' target=\"summary\">All</a></td>");
			out.println("</tr>");

			Iterator iter = projectData.getPackages().iterator();
			while (iter.hasNext())
			{
				PackageData packageData = (PackageData)iter.next();
				String url1 = "frame-summary-" + packageData.getName()
						+ ".html";
				String url2 = "frame-sourcefiles-" + packageData.getName()
						+ ".html";
				out.println("<tr>");
				out.println("<td nowrap=\"nowrap\"><a href=\"" + url1
						+ "\" onclick='parent.sourceFileList.location.href=\""
						+ url2 + "\"' target=\"summary\">"
						+ generatePackageName(packageData) + "</a></td>");
				out.println("</tr>");
			}
			out.println("</table>");
			out.println("</body>");
			out.println("</html>");
		}
		finally
		{
			if (out != null)
			{
				out.close();
			}
		}
	}

	private void generateSourceFileLists() throws IOException
	{
		generateSourceFileList(null);
		Iterator iter = projectData.getPackages().iterator();
		while (iter.hasNext())
		{
			PackageData packageData = (PackageData)iter.next();
			generateSourceFileList(packageData);
		}
	}

	private void generateSourceFileList(PackageData packageData)
			throws IOException
	{
		String filename;
		Collection sourceFiles;
		if (packageData == null)
		{
			filename = "frame-sourcefiles.html";
			sourceFiles = projectData.getSourceFiles();
		}
		else
		{
			filename = "frame-sourcefiles-" + packageData.getName() + ".html";
			sourceFiles = packageData.getSourceFiles();
		}

		// sourceFiles may be sorted, but if so it's sorted by
		// the full path to the file, and we only want to sort
		// based on the file's basename.
		Vector sortedSourceFiles = new Vector();
		sortedSourceFiles.addAll(sourceFiles);
		Collections.sort(sortedSourceFiles,
				new SourceFileDataBaseNameComparator());

		File file = new File(destinationDir, filename);
		PrintWriter out = null;
		try
		{
			out = IOUtil.getPrintWriter(file);

			out
					.println("<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.0 Transitional//EN\"");
			out
					.println("           \"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd\">");

			out.println("<html>");
			out.println("<head>");
			out
					.println("<meta http-equiv=\"Content-Type\" content=\"text/html; charset=UTF-8\"/>");
			out.println("<title>\u30ab\u30d0\u30ec\u30c3\u30b8\u30ec\u30dd\u30fc\u30c8\u30af\u30e9\u30b9</title>");
			out
					.println("<link title=\"Style\" type=\"text/css\" rel=\"stylesheet\" href=\"css/main.css\"/>");
			out.println("</head>");
			out.println("<body>");
			out.println("<h5>");
			out.println(packageData == null ? "\u3059\u3079\u3066\u306e\u30d1\u30c3\u30b1\u30fc\u30b8"
					: generatePackageName(packageData));
			out.println("</h5>");
			out.println("<div class=\"separator\">&nbsp;</div>");
			out.println("<h5>\u30af\u30e9\u30b9</h5>");
			if (!sortedSourceFiles.isEmpty())
			{
				out.println("<table width=\"100%\">");
				out.println("<tbody>");

				for (Iterator iter = sortedSourceFiles.iterator(); iter
						.hasNext();)
				{
					SourceFileData sourceFileData = (SourceFileData)iter.next();
					out.println("<tr>");
					String percentCovered;
					if (sourceFileData.getNumberOfValidLines() > 0)
						percentCovered = getPercentValue(sourceFileData
								.getLineCoverageRate());
					else
						percentCovered = "\u8a72\u5f53\u306a\u3057";
					out
							.println("<td nowrap=\"nowrap\"><a target=\"summary\" href=\""
									+ sourceFileData.getNormalizedName()
									+ ".html\">"
									+ sourceFileData.getBaseName()
									+ "</a> <i>("
									+ percentCovered
									+ ")</i></td>");
					out.println("</tr>");
				}
				out.println("</tbody>");
				out.println("</table>");
			}

			out.println("</body>");
			out.println("</html>");
		}
		finally
		{
			if (out != null)
			{
				out.close();
			}
		}
	}

	private void generateOverviews() throws IOException
	{
		generateOverview(null);
		Iterator iter = projectData.getPackages().iterator();
		while (iter.hasNext())
		{
			PackageData packageData = (PackageData)iter.next();
			generateOverview(packageData);
		}
	}

	private void generateOverview(PackageData packageData) throws IOException
	{
		Iterator iter;

		String filename;
		if (packageData == null)
		{
			filename = "frame-summary.html";
		}
		else
		{
			filename = "frame-summary-" + packageData.getName() + ".html";
		}
		File file = new File(destinationDir, filename);
		PrintWriter out = null;

		try
		{
			out = IOUtil.getPrintWriter(file);;

			out
					.println("<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.0 Transitional//EN\"");
			out
					.println("           \"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd\">");

			out.println("<html>");
			out.println("<head>");
			out
					.println("<meta http-equiv=\"Content-Type\" content=\"text/html; charset=UTF-8\"/>");
			out.println("<title>\u30ab\u30d0\u30ec\u30c3\u30b8\u30ec\u30dd\u30fc\u30c8</title>");
			out
					.println("<link title=\"Style\" type=\"text/css\" rel=\"stylesheet\" href=\"css/main.css\"/>");
			out
					.println("<link title=\"Style\" type=\"text/css\" rel=\"stylesheet\" href=\"css/sortabletable.css\"/>");
			out
					.println("<script type=\"text/javascript\" src=\"js/popup.js\"></script>");
			out
					.println("<script type=\"text/javascript\" src=\"js/sortabletable.js\"></script>");
			out
					.println("<script type=\"text/javascript\" src=\"js/customsorttypes.js\"></script>");
			out.println("</head>");
			out.println("<body>");

			out.print("<h5>\u30ab\u30d0\u30ec\u30c3\u30b8\u30ec\u30dd\u30fc\u30c8 - ");
			out.print(packageData == null ? "\u3059\u3079\u3066\u306e\u30d1\u30c3\u30b1\u30fc\u30b8"
					: generatePackageName(packageData));
			out.println("</h5>");
			out.println("<div class=\"separator\">&nbsp;</div>");
			out.println("<table class=\"report\" id=\"packageResults\">");
			out.println(generateTableHeader("\u30d1\u30c3\u30b1\u30fc\u30b8", true));
			out.println("<tbody>");

			SortedSet packages;
			if (packageData == null)
			{
				// Output a summary line for all packages
				out.println(generateTableRowForTotal());

				// Get packages
				packages = projectData.getPackages();
			}
			else
			{
				// Get subpackages
				packages = projectData.getSubPackages(packageData.getName());
			}

			// Output a line for each package or subpackage
			iter = packages.iterator();
			while (iter.hasNext())
			{
				PackageData subPackageData = (PackageData)iter.next();
				out.println(generateTableRowForPackage(subPackageData));
			}

			out.println("</tbody>");
			out.println("</table>");
			out.println("<script type=\"text/javascript\">");
			out
					.println("var packageTable = new SortableTable(document.getElementById(\"packageResults\"),");
			out
					.println("    [\"\u6587\u5b57\u5217\", \"\u6570\u5b57\", \"\u30d1\u30fc\u30bb\u30f3\u30c8\", \"\u30d1\u30fc\u30bb\u30f3\u30c8\", \"\u30d5\u30a9\u30fc\u30de\u30c3\u30c8\u3055\u308c\u305f\u6570\u5b57\"]);");
			out.println("packageTable.sort(0);");
			out.println("</script>");

			// Get the list of source files in this package
			Collection sourceFiles;
			if (packageData == null)
			{
				PackageData defaultPackage = (PackageData)projectData
						.getChild("");
				if (defaultPackage != null)
				{
					sourceFiles = defaultPackage.getSourceFiles();
				}
				else
				{
					sourceFiles = new TreeSet();
				}
			}
			else
			{
				sourceFiles = packageData.getSourceFiles();
			}

			// Output a line for each source file
			if (sourceFiles.size() > 0)
			{
				out.println("<div class=\"separator\">&nbsp;</div>");
				out.println("<table class=\"report\" id=\"classResults\">");
				out.println(generateTableHeader("\u30d1\u30c3\u30b1\u30fc\u30b8\u5185\u306e\u30af\u30e9\u30b9",
						false));
				out.println("<tbody>");

				iter = sourceFiles.iterator();
				while (iter.hasNext())
				{
					SourceFileData sourceFileData = (SourceFileData)iter.next();
					out.println(generateTableRowsForSourceFile(sourceFileData));
				}

				out.println("</tbody>");
				out.println("</table>");
				out.println("<script type=\"text/javascript\">");
				out
						.println("var classTable = new SortableTable(document.getElementById(\"classResults\"),");
				out
						.println("    [\"\u6587\u5b57\u5217\", \"\u30d1\u30fc\u30bb\u30f3\u30c8\", \"\u30d1\u30fc\u30bb\u30f3\u30c8\", \"\u30d5\u30a9\u30fc\u30de\u30c3\u30c8\u3055\u308c\u305f\u6570\u5b57\"]);");
				out.println("classTable.sort(0);");
				out.println("</script>");
			}

			out.println(generateFooter());

			out.println("</body>");
			out.println("</html>");
		}
		finally
		{
			if (out != null)
			{
				out.close();
			}
		}
	}

	private void generateSourceFiles()
	{
		Iterator iter = projectData.getSourceFiles().iterator();
		while (iter.hasNext())
		{
			SourceFileData sourceFileData = (SourceFileData)iter.next();
			try
			{
				generateSourceFile(sourceFileData);
			}
			catch (IOException e)
			{
				LOGGER.info("\u3053\u306e\u30bd\u30fc\u30b9\u30d5\u30a1\u30a4\u30eb\u304b\u3089HTML\u3092\u751f\u6210\u3059\u308b\u3053\u3068\u306f\u3067\u304d\u307e\u305b\u3093\u3002 "
						+ sourceFileData.getName() + ": "
						+ e.getLocalizedMessage());
			}
		}
	}

	private void generateSourceFile(SourceFileData sourceFileData)
			throws IOException
	{
		if (!sourceFileData.containsInstrumentationInfo())
		{
			LOGGER.info("\u30c7\u30fc\u30bf\u30d5\u30a1\u30a4\u30eb" + sourceFileData.getName()	+ "\u306binstrumentation\u60c5\u5831"
					+ "\u304c\u542b\u307e\u308c\u3066\u3044\u307e\u305b\u3093\u3002\u30af\u30e9\u30b9\u30d5\u30a1\u30a4\u30eb\u306binstrumention\u60c5\u5831"
					+ "\u3092\u542b\u3080\u3088\u3046\u306b\u3057\u3066\u304f\u3060\u3055\u3044\u3002");
		}

		String filename = sourceFileData.getNormalizedName() + ".html";
		File file = new File(destinationDir, filename);
		PrintWriter out = null;

		try
		{
			out = IOUtil.getPrintWriter(file);

			out
					.println("<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.0 Transitional//EN\"");
			out
					.println("           \"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd\">");

			out.println("<html>");
			out.println("<head>");
			out
					.println("<meta http-equiv=\"Content-Type\" content=\"text/html; charset=UTF-8\"/>");
			out.println("<title>\u30ab\u30d0\u30ec\u30c3\u30b8\u30ec\u30dd\u30fc\u30c8</title>");
			out
					.println("<link title=\"Style\" type=\"text/css\" rel=\"stylesheet\" href=\"css/main.css\"/>");
			out
					.println("<script type=\"text/javascript\" src=\"js/popup.js\"></script>");
			out.println("</head>");
			out.println("<body>");
			out.print("<h5>\u30ab\u30d0\u30ec\u30c3\u30b8\u30ec\u30dd\u30fc\u30c8 - ");
			String classPackageName = sourceFileData.getPackageName();
			if ((classPackageName != null) && classPackageName.length() > 0)
			{
				out.print(classPackageName + ".");
			}
			out.print(sourceFileData.getBaseName());
			out.println("</h5>");

			// Output the coverage summary for this class
			out.println("<div class=\"separator\">&nbsp;</div>");
			out.println("<table class=\"report\">");
			out.println(generateTableHeader("\u30d5\u30a1\u30a4\u30eb\u5185\u306e\u30af\u30e9\u30b9", false));
			out.println(generateTableRowsForSourceFile(sourceFileData));
			out.println("</table>");

			// Output the coverage summary for methods in this class
			// TODO

			// Output this class's source code with syntax and coverage highlighting
			out.println("<div class=\"separator\">&nbsp;</div>");
			out.println(generateHtmlizedJavaSource(sourceFileData));

			out.println(generateFooter());

			out.println("</body>");
			out.println("</html>");
		}
		finally
		{
			if (out != null)
			{
				out.close();
			}
		}
	}
   
	private String generateBranchInfo(LineData lineData, String content) {
		boolean hasBranch = (lineData != null) ? lineData.hasBranch() : false;
		if (hasBranch) 
		{
			StringBuffer ret = new StringBuffer();
			ret.append("<a title=\"\u884c ").append(lineData.getLineNumber()).append(": \u5206\u5c90\u30ab\u30d0\u30ec\u30c3\u30b8 ")
			   .append(lineData.getConditionCoverage());
			if (lineData.getConditionSize() > 1)
			{
				ret.append(" [\u5206\u5c90\uff1a ");
				for (int i = 0; i < lineData.getConditionSize(); i++)
				{
					if (i > 0)
						ret.append(", ");
					ret.append(lineData.getConditionCoverage(i));
				}
				ret.append("]");
			}
			ret.append(".\">").append(content).append("</a>");
			return ret.toString();
		}
		else
		{
			return content;
		}
	}

	private String generateHtmlizedJavaSource(SourceFileData sourceFileData)
	{
		Source source = finder.getSource(sourceFileData.getName());
		
		if (source == null)
		{
			return "<p>" + sourceFileData.getName()
					+ "\u304c\u898b\u3064\u304b\u308a\u307e\u305b\u3093\u3002\u30bd\u30fc\u30b9\u30c7\u30a3\u30ec\u30af\u30c8\u30ea\u3092\u6307\u5b9a\u3057\u307e\u3057\u305f\u304b\uff1f</p>";
		}

		BufferedReader br = null;
		try
		{
			br = new BufferedReader(new InputStreamReader(source.getInputStream(), encoding));
		}
		catch (UnsupportedEncodingException e)
		{
			return "<p>" + source.getOriginDesc()
					+ "\u3092\u958b\u304f\u3053\u3068\u304c\u3067\u304d\u307e\u305b\u3093\u3002JVM\u3067 '" + encoding +"' \u30a8\u30f3\u30b3\u30fc\u30c7\u30a3\u30f3\u30b0\u306f\u672a\u30b5\u30dd\u30fc\u30c8\u3067\u3059\u3002</p>";
		}
		catch (Throwable t)
		{
			return "<p>" + source.getOriginDesc() + "\u3092\u958b\u304f\u3053\u3068\u306f\u3067\u304d\u307e\u305b\u3093\u3002 " + t.getLocalizedMessage() + "</p>";
		}

		StringBuffer ret = new StringBuffer();
		ret
				.append("<table cellspacing=\"0\" cellpadding=\"0\" class=\"src\">\n");
		try
		{
			String lineStr;
			JavaToHtml javaToHtml = new JavaToHtml();
			int lineNumber = 1;
			while ((lineStr = br.readLine()) != null)
			{
				ret.append("<tr>");
				if (sourceFileData.isValidSourceLineNumber(lineNumber))
				{
					LineData lineData = sourceFileData.getLineCoverage(lineNumber);
					ret.append("  <td class=\"numLineCover\">&nbsp;"
							+ lineNumber + "</td>");
					if ((lineData != null) && (lineData.isCovered()))
					{
						ret.append("  <td class=\"nbHitsCovered\">" 
								+ generateBranchInfo(lineData, "&nbsp;" + ((lineData != null) ? lineData.getHits() : 0)) 
								+ "</td>");
						ret
							.append("  <td class=\"src\"><pre class=\"src\">&nbsp;"
									+ generateBranchInfo(lineData, javaToHtml.process(lineStr))
									+ "</pre></td>");
					}
					else
					{
						ret.append("  <td class=\"nbHitsUncovered\">"
								+ generateBranchInfo(lineData, "&nbsp;" + ((lineData != null) ? lineData.getHits() : 0))
								+ "</td>");
						ret
							.append("  <td class=\"src\"><pre class=\"src\"><span class=\"srcUncovered\">&nbsp;"
									+ generateBranchInfo(lineData, javaToHtml.process(lineStr))
									+ "</span></pre></td>");
					}
				}
				else
				{
					ret.append("  <td class=\"numLine\">&nbsp;" + lineNumber
							+ "</td>");
					ret.append("  <td class=\"nbHits\">&nbsp;</td>\n");
					ret.append("  <td class=\"src\"><pre class=\"src\">&nbsp;"
							+ javaToHtml.process(lineStr) + "</pre></td>");
				}
				ret.append("</tr>\n");
				lineNumber++;
			}
		}
		catch (IOException e)
		{
			ret.append("<tr><td>"
					+ source.getOriginDesc() + "\u306e\u8aad\u8fbc\u307f\u30a8\u30e9\u30fc\u3002 "
					+ e.getLocalizedMessage() + "</td></tr>\n");
		}
		finally
		{
			try
			{
				br.close();
				source.close();
			}
			catch (IOException e)
			{
			}
		}

		ret.append("</table>\n");

		return ret.toString();
	}

	private static String generateFooter()
	{
		return "<div class=\"footer\">\u3053\u306e\u30ec\u30dd\u30fc\u30c8\u306f"
				+ "<a href=\"http://cobertura.sourceforge.net/\" target=\"_top\">Cobertura</a>"
				+ Header.version() + " \u3067\u751f\u6210\u3055\u308c\u307e\u3057\u305f\u3002 "
				+ DateFormat.getInstance().format(new Date()) + ".</div>";
	}

	private static String generateTableHeader(String title,
			boolean showColumnForNumberOfClasses)
	{
		StringBuffer ret = new StringBuffer();
		ret.append("<thead>");
		ret.append("<tr>");
		ret.append("  <td class=\"heading\">" + title + "</td>");
		if (showColumnForNumberOfClasses)
		{
			ret.append("  <td class=\"heading\"># \u30af\u30e9\u30b9</td>");
		}
		ret.append("  <td class=\"heading\">"
				+ generateHelpURL("\u884c\u30ab\u30d0\u30ec\u30c3\u30b8",
						"\u3053\u306e\u30c6\u30b9\u30c8\u3067\u5b9f\u884c\u3055\u308c\u305f\u884c\u306e\u6bd4\u7387\u3002")
				+ "</td>");
		ret.append("  <td class=\"heading\">"
				+ generateHelpURL("\u5206\u5c90\u30ab\u30d0\u30ec\u30c3\u30b8",
						"\u3053\u306e\u30c6\u30b9\u30c8\u3067\u5b9f\u884c\u3055\u308c\u305f\u5206\u5c90\u306e\u6bd4\u7387\u3002")
				+ "</td>");
		ret
				.append("  <td class=\"heading\">"
						+ generateHelpURL(
								"\u8907\u96d1\u5ea6",
								"\u3059\u3079\u3066\u306e\u30e1\u30bd\u30c3\u30c9\u306e\u62dd\u91d1McCabe\u306e\u5faa\u74b0\u7684\u8907\u96d1\u5ea6\u3002\u5927\u96d1\u628a\u306b\u8a00\u3046\u3068\u3001\uff11\u3064\u306e\u30e1\u30bd\u30c3\u30c9\u3078\u8fbf\u308a\u4ed8\u304f\u72ec\u7acb\u3057\u305f\u7d4c\u8def\u306e\u76f4\u63a5\u6570\uff08if\u6587\u3001while\u6587\u306a\u3069\u6bce\u306e\u7d4c\u8def\u6570\uff09\u3002")
						+ "</td>");
		ret.append("</tr>");
		ret.append("</thead>");
		return ret.toString();
	}

	private static String generateHelpURL(String text, String description)
	{
		StringBuffer ret = new StringBuffer();
		boolean popupTooltips = false;
		if (popupTooltips)
		{
			ret
					.append("<a class=\"hastooltip\" href=\"help.html\" onclick=\"popupwindow('help.html'); return false;\">");
			ret.append(text);
			ret.append("<span>" + description + "</span>");
			ret.append("</a>");
		}
		else
		{
			ret
					.append("<a class=\"dfn\" href=\"help.html\" onclick=\"popupwindow('help.html'); return false;\">");
			ret.append(text);
			ret.append("</a>");
		}
		return ret.toString();
	}

	private String generateTableRowForTotal()
	{
		StringBuffer ret = new StringBuffer();
		double ccn = complexity.getCCNForProject(projectData);

		ret.append("  <tr>");
		ret.append("<td><b>\u3059\u3079\u3066\u306e\u30d1\u30c3\u30b1\u30fc\u30b8</b></td>");
		ret.append("<td class=\"value\">"
				+ projectData.getNumberOfClasses() + "</td>");
		ret.append(generateTableColumnsFromData(projectData, ccn));
		ret.append("</tr>");
		return ret.toString();
	}

	private String generateTableRowForPackage(PackageData packageData)
	{
		StringBuffer ret = new StringBuffer();
		String url1 = "frame-summary-" + packageData.getName() + ".html";
		String url2 = "frame-sourcefiles-" + packageData.getName() + ".html";
		double ccn = complexity.getCCNForPackage(packageData);

		ret.append("  <tr>");
		ret.append("<td><a href=\"" + url1
				+ "\" onclick='parent.sourceFileList.location.href=\"" + url2
				+ "\"'>" + generatePackageName(packageData) + "</a></td>");
		ret.append("<td class=\"value\">" + packageData.getNumberOfChildren()
				+ "</td>");
		ret.append(generateTableColumnsFromData(packageData, ccn));
		ret.append("</tr>");
		return ret.toString();
	}

	private String generateTableRowsForSourceFile(SourceFileData sourceFileData)
	{
		StringBuffer ret = new StringBuffer();
		String sourceFileName = sourceFileData.getNormalizedName();
		// TODO: ccn should be calculated per-class, not per-file
		double ccn = complexity.getCCNForSourceFile(sourceFileData);

		Iterator iter = sourceFileData.getClasses().iterator();
		while (iter.hasNext())
		{
			ClassData classData = (ClassData)iter.next();
			ret
					.append(generateTableRowForClass(classData, sourceFileName,
							ccn));
		}

		return ret.toString();
	}

	private String generateTableRowForClass(ClassData classData,
			String sourceFileName, double ccn)
	{
		StringBuffer ret = new StringBuffer();

		ret.append("  <tr>");
		// TODO: URL should jump straight to the class (only for inner classes?)
		ret.append("<td><a href=\"" + sourceFileName
				+ ".html\">" + classData.getBaseName() + "</a></td>");
		ret.append(generateTableColumnsFromData(classData, ccn));
		ret.append("</tr>\n");
		return ret.toString();
	}

	/**
	 * Return a string containing three HTML table cells.  The first
	 * cell contains a graph showing the line coverage, the second
	 * cell contains a graph showing the branch coverage, and the
	 * third cell contains the code complexity.
	 *
	 * @param ccn The code complexity to display.  This should be greater
	 *        than 1.
	 * @return A string containing the HTML for three table cells.
	 */
	private static String generateTableColumnsFromData(CoverageData coverageData,
			double ccn)
	{
		int numLinesCovered = coverageData.getNumberOfCoveredLines();
		int numLinesValid = coverageData.getNumberOfValidLines();
		int numBranchesCovered = coverageData.getNumberOfCoveredBranches();
		int numBranchesValid = coverageData.getNumberOfValidBranches();

		// The "hidden" CSS class is used below to write the ccn without
		// any formatting so that the table column can be sorted correctly
		return "<td>" + generatePercentResult(numLinesCovered, numLinesValid)
				+"</td><td>"
				+ generatePercentResult(numBranchesCovered, numBranchesValid)
				+ "</td><td class=\"value\"><span class=\"hidden\">"
				+ ccn + ";</span>" + getDoubleValue(ccn) + "</td>";
	}

	/**
	 * This is crazy complicated, and took me a while to figure out,
	 * but it works.  It creates a dandy little percentage meter, from
	 * 0 to 100.
	 * @param dividend The number of covered lines or branches.
	 * @param divisor  The number of valid lines or branches.
	 * @return A percentage meter.
	 */
	private static String generatePercentResult(int dividend, int divisor)
	{
		StringBuffer sb = new StringBuffer();

		sb.append("<table cellpadding=\"0px\" cellspacing=\"0px\" class=\"percentgraph\"><tr class=\"percentgraph\"><td align=\"right\" class=\"percentgraph\" width=\"40\">");
		if (divisor > 0)
			sb.append(getPercentValue((double)dividend / divisor));
		else
			sb.append(generateHelpURL(
					"\u8a72\u5f53\u306a\u3057",
					".class\u30d5\u30a1\u30a4\u30eb\u304b\u3089\u884c\u756a\u53f7\u60c5\u5831\u3092\u53d6\u5f97\u3067\u304d\u306a\u3044\u5834\u5408\u306f\u884c\u30ab\u30d0\u30ec\u30c3\u30b8\u53ca\u3073\u5206\u5c90\u30ab\u30d0\u30ec\u30c3\u30b8\u306b\u300c\u8a72\u5f53\u306a\u3057\u300d\u3068\u8868\u793a\u3055\u308c\u307e\u3059\u3002\u30b9\u30bf\u30d6\u30af\u30e9\u30b9\u3001\u30b9\u30b1\u30eb\u30c8\u30f3\u30af\u30e9\u30b9\u3001\u30a4\u30f3\u30bf\u30fc\u30d5\u30a7\u30fc\u30b9\u3001\u53ca\u3073 \"debug=true\"\u304c\u8a2d\u5b9a\u306a\u3057\u3067\u30af\u30e9\u30b9\u304c\u30b3\u30f3\u30d1\u30a4\u30eb\u3057\u305f\u5834\u5408\u306b\u8868\u793a\u3055\u308c\u307e\u3059\u3002"));
		sb.append("</td><td class=\"percentgraph\"><div class=\"percentgraph\">");
		if (divisor > 0)
		{
			sb.append("<div class=\"greenbar\" style=\"width:"
					+ (dividend * 100 / divisor) + "px\">");
			sb.append("<span class=\"text\">");
			sb.append(dividend);
			sb.append("/");
			sb.append(divisor);
		}
		else
		{
			sb.append("<div class=\"na\" style=\"width:100px\">");
			sb.append("<span class=\"text\">");
			sb.append(generateHelpURL(
					"\u8a72\u5f53\u306a\u3057",
					".class\u30d5\u30a1\u30a4\u30eb\u304b\u3089\u884c\u756a\u53f7\u60c5\u5831\u3092\u53d6\u5f97\u3067\u304d\u306a\u3044\u5834\u5408\u306f\u884c\u30ab\u30d0\u30ec\u30c3\u30b8\u53ca\u3073\u5206\u5c90\u30ab\u30d0\u30ec\u30c3\u30b8\u306b\u300c\u8a72\u5f53\u306a\u3057\u300d\u3068\u8868\u793a\u3055\u308c\u307e\u3059\u3002\u30b9\u30bf\u30d6\u30af\u30e9\u30b9\u3001\u30b9\u30b1\u30eb\u30c8\u30f3\u30af\u30e9\u30b9\u3001\u30a4\u30f3\u30bf\u30fc\u30d5\u30a7\u30fc\u30b9\u3001\u53ca\u3073 \"debug=true\"\u304c\u8a2d\u5b9a\u306a\u3057\u3067\u30af\u30e9\u30b9\u304c\u30b3\u30f3\u30d1\u30a4\u30eb\u3057\u305f\u5834\u5408\u306b\u8868\u793a\u3055\u308c\u307e\u3059\u3002"));
		sb.append("</td><td class=\"percentgraph\"><div class=\"percentgraph\">");
		}
		sb.append("</span></div></div></td></tr></table>");

		return sb.toString();
	}

	private static String getDoubleValue(double value)
	{
		return new DecimalFormat().format(value);
	}

	private static String getPercentValue(double value)
	{
		return StringUtil.getPercentValue(value);
	}

}
