/*******************************************************************************
 * Copyright (c) 2008 IBM Corporation and others.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v1.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/legal/epl-v10.html
 *
 * Contributors:
 *     IBM Corporation - initial API and implementation
 *******************************************************************************/
package org.eclipse.pde.api.tools.internal.tasks;

import java.io.BufferedWriter;
import java.io.File;
import java.io.FileFilter;
import java.io.FileWriter;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import javax.xml.parsers.ParserConfigurationException;
import javax.xml.parsers.SAXParser;
import javax.xml.parsers.SAXParserFactory;

import org.apache.tools.ant.BuildException;
import org.apache.tools.ant.Task;
import org.eclipse.pde.api.tools.internal.IApiXmlConstants;
import org.eclipse.pde.api.tools.internal.util.Util;
import org.xml.sax.Attributes;
import org.xml.sax.SAXException;
import org.xml.sax.helpers.DefaultHandler;

import com.ibm.icu.text.MessageFormat;

/**
 * This task can be used to convert all reports generated by the apitooling.apitoolsverification task
 * into html reports.
 */
public class FullReportConversionTask extends Task {
	private static final String[] NO_PROBLEMS = new String[0];
	private static final String[] NO_NON_API_BUNDLES = NO_PROBLEMS;

	static private final class Summary {
		String componentID;
		int compatibilityNumber;
		int apiUsageNumber;
		int bundleVersionNumber;
		String link;
		
		public Summary(Report report) {
			super();
			this.apiUsageNumber = report.getProblemSize(APIToolsVerificationTask.USAGE);
			this.bundleVersionNumber = report.getProblemSize(APIToolsVerificationTask.BUNDLE_VERSION);
			this.compatibilityNumber = report.getProblemSize(APIToolsVerificationTask.COMPATIBILITY);
			this.componentID = report.componentID;
			this.link = report.link;
		}
		
		public String toString() {
			return MessageFormat.format("{0} : compatibility {1}, api usage {2}, bundler version {3}, link {4}", //$NON-NLS-1$
					new String[] {
						this.componentID,
						Integer.toString(this.compatibilityNumber),
						Integer.toString(this.apiUsageNumber),
						Integer.toString(this.bundleVersionNumber),
						this.link
					});
		}
	}
	static private final class Report {
		String componentID;
		Map problemsPerCategories;
		List nonApiBundles;
		String link;

		Report(String componentID) {
			this.componentID = componentID;
		}

		public void addProblem(String category, String problemMessage) {
			if (this.problemsPerCategories == null) {
				this.problemsPerCategories = new HashMap();
			}
			List problemsList = (List) this.problemsPerCategories.get(category);
			if (problemsList == null) {
				problemsList = new ArrayList();
				this.problemsPerCategories.put(category, problemsList);
			}
			problemsList.add(problemMessage);
		}
		
		public void addNonApiBundles(String bundleName) {
			if (this.nonApiBundles == null) {
				this.nonApiBundles = new ArrayList();
			}
			this.nonApiBundles.add(bundleName);
		}

		public String[] getNonApiBundles() {
			if (this.nonApiBundles == null || this.nonApiBundles.size() == 0) {
				return NO_NON_API_BUNDLES;
			}
			String[] nonApiBundlesNames = new String[this.nonApiBundles.size()];
			this.nonApiBundles.toArray(nonApiBundlesNames);
			return nonApiBundlesNames;
		}
		
		public String[] getProblems(String category) {
			if (this.problemsPerCategories == null) return NO_PROBLEMS;
			List problemsList = (List) this.problemsPerCategories.get(category);
			int size = problemsList == null ? 0 : problemsList.size();
			if (size == 0) {
				return NO_PROBLEMS;
			}
			String[] problemsMessages = new String[size];
			problemsList.toArray(problemsMessages);
			return problemsMessages;
		}
		public int getProblemSize(String category) {
			if (this.problemsPerCategories == null) return 0;
			List problemsList = (List) this.problemsPerCategories.get(category);
			return problemsList == null ? 0 : problemsList.size();
		}
		
		public void setLink(String link) {
			this.link = link;
		}
		public boolean hasNonApiBundles() {
			return this.nonApiBundles != null && this.nonApiBundles.size() != 0;
		}
	}
	static final class ConverterDefaultHandler extends DefaultHandler {
		boolean debug;
		Report report;
		String category;

		public ConverterDefaultHandler(boolean debug) {
			this.debug = debug;
		}
		public void startElement(String uri, String localName,
				String name, Attributes attributes) throws SAXException {
			if (IApiXmlConstants.ELEMENT_API_TOOL_REPORT.equals(name)) {
				String componentID = attributes.getValue(IApiXmlConstants.ATTR_COMPONENT_ID);
				if (debug) {
					System.out.println("component id : " + componentID); //$NON-NLS-1$
				}
				this.report = new Report(componentID);
			} else if (IApiXmlConstants.ELEMENT_API_PROBLEM_CATEGORY.equals(name)) {
				this.category = attributes.getValue(IApiXmlConstants.ATTR_VALUE);
				if (debug) {
					System.out.println("category : " + this.category); //$NON-NLS-1$
				}
			} else if (IApiXmlConstants.ELEMENT_API_PROBLEM.equals(name)) {
				String message = attributes.getValue(IApiXmlConstants.ATTR_MESSAGE);
				if (debug) {
					System.out.println("problem message : " + message); //$NON-NLS-1$
				}
				this.report.addProblem(this.category, message);
			} else if (IApiXmlConstants.ELEMENT_BUNDLE.equals(name)) {
				String bundleName = attributes.getValue(IApiXmlConstants.ATTR_NAME);
				if (debug) {
					System.out.println("bundle name : " + bundleName); //$NON-NLS-1$
				}
				this.report.addNonApiBundles(bundleName);
			}
		}
		public Report getReport() {
			return this.report;
		}
	}
	boolean debug;

	private String xmlReportsLocation;
	private String htmlReportsLocation;
	private File reportsRoot;
	private File htmlRoot;

	public void setXmlReportDirectory(String xmlFileLocation) {
		this.xmlReportsLocation = xmlFileLocation;
	}
	public void setHtmlReportDirectory(String htmlFileLocation) {
		this.htmlReportsLocation = htmlFileLocation;
	}
	public void setDebug(String debugValue) {
		this.debug = Boolean.toString(true).equals(debugValue); 
	}
	public void execute() throws BuildException {
		if (this.debug) {
			System.out.println("xmlReportsLocation : " + this.xmlReportsLocation); //$NON-NLS-1$
			System.out.println("htmlReportsLocation : " + this.htmlReportsLocation); //$NON-NLS-1$
		}
		if (this.xmlReportsLocation == null) {
			throw new BuildException("The directory that contains xml reports must be specified"); //$NON-NLS-1$
		}
		this.reportsRoot = new File(this.xmlReportsLocation);
		if (!this.reportsRoot.exists() || !this.reportsRoot.isDirectory()) {
			throw new BuildException("This is not a valid directory name : " + this.xmlReportsLocation); //$NON-NLS-1$
		}
		SAXParserFactory factory = SAXParserFactory.newInstance();
		SAXParser parser = null;
		try {
			parser = factory.newSAXParser();
		} catch (ParserConfigurationException e) {
			e.printStackTrace();
		} catch (SAXException e) {
			e.printStackTrace();
		}
		if (parser == null) {
			throw new BuildException("Could not create a sax parser"); //$NON-NLS-1$
		}

		if (this.htmlReportsLocation == null) {
			this.htmlReportsLocation = this.xmlReportsLocation;
		}
		this.htmlRoot = new File(this.htmlReportsLocation);
		if (!this.htmlRoot.exists()) {
			if (!this.htmlRoot.mkdirs()) {
				throw new BuildException("Could not create file " + this.htmlReportsLocation); //$NON-NLS-1$
			}
		}
		if (this.debug) {
			System.out.println("output name :" + this.htmlReportsLocation); //$NON-NLS-1$
		}
		try {
			// retrieve all xml reports
			File[] allFiles = Util.getAllFiles(reportsRoot, new FileFilter() {
				public boolean accept(File pathname) {
					return pathname.isDirectory() || pathname.getName().endsWith(".xml"); //$NON-NLS-1$
				}
			});
			if (allFiles != null) {
				int length = allFiles.length;
				List summariesList = new ArrayList(length);
				Summary nonApiBundleSummary = null;
				for (int i = 0; i < length; i++) {
					File file = allFiles[i];
					ConverterDefaultHandler defaultHandler = new ConverterDefaultHandler(this.debug);
					parser.parse(file, defaultHandler);
					Report report = defaultHandler.getReport();
					dumpReport(file, report);
					if (!report.hasNonApiBundles()) {
						summariesList.add(new Summary(report));
					} else {
						nonApiBundleSummary = new Summary(report);
					}
				}
				// dump index file
				Summary[] summaries = new Summary[summariesList.size()];
				summariesList.toArray(summaries);
				dumpIndexFile(reportsRoot, summaries, nonApiBundleSummary);
			}
		} catch (SAXException e) {
			// ignore
		} catch (IOException e) {
			// ignore
		}
	}
	private void dumpIndexFile(File reportsRoot, Summary[] summaries, Summary allNonApiBundleSummary) {
		File htmlFile = new File(this.htmlReportsLocation, "index.html"); //$NON-NLS-1$
		PrintWriter writer = null;
		try {
			FileWriter fileWriter = new FileWriter(htmlFile);
			writer = new PrintWriter(new BufferedWriter(fileWriter));
			if (allNonApiBundleSummary != null) {
				writer.println(
					MessageFormat.format(
						Messages.fullReportTask_indexheader,
						new String[] {
								"<p>List of <a href=\"" + allNonApiBundleSummary.link + "\">bundles not configured for API analysis</a>.</p>" //$NON-NLS-1$ //$NON-NLS-2$
						}));
			} else {
				writer.println(
						MessageFormat.format(
							Messages.fullReportTask_indexheader,
							new String[] {
									"<p>All bundles have been converted to API tools.</p>"//$NON-NLS-1$
							}));
			}
			Arrays.sort(summaries, new Comparator() {
				public int compare(Object o1, Object o2) {
					Summary summary1 = (Summary) o1; 
					Summary summary2 = (Summary) o2;
					return summary1.componentID.compareTo(summary2.componentID);
				}
			});
			for (int i = 0, max = summaries.length; i < max; i++) {
				dumpIndexEntry(i, writer, summaries[i]);
			}
			writer.println(Messages.fullReportTask_indexfooter);
			writer.flush();
		} catch (IOException e) {
			throw new BuildException("An IOException occured while dumping : " + htmlFile.getAbsolutePath()); //$NON-NLS-1$
		} finally {
			if (writer != null) {
				writer.close();
			}
		}
	}
	private void dumpIndexEntry(int i, PrintWriter writer, Summary summary) {
		if (debug) {
			System.out.println(summary);
		}
		if ((i % 2) == 0) {
			writer.println(
					MessageFormat.format(
						Messages.fullReportTask_indexsummary_even,
						new String[] {
							summary.componentID,
							Integer.toString(summary.compatibilityNumber),
							Integer.toString(summary.apiUsageNumber),
							Integer.toString(summary.bundleVersionNumber),
							summary.link,
						}));
		} else {
			writer.println(
				MessageFormat.format(
					Messages.fullReportTask_indexsummary_odd,
					new String[] {
						summary.componentID,
						Integer.toString(summary.compatibilityNumber),
						Integer.toString(summary.apiUsageNumber),
						Integer.toString(summary.bundleVersionNumber),
						summary.link,
					}));
		}
	}

	private void dumpReport(File xmlFile, Report report) {
		// create file writer
		// generate the html file name from the xml file name
		String htmlName = extractNameFromXMLName(xmlFile);
		report.setLink(extractLinkFrom(htmlName));
		File htmlFile = new File(htmlName);
		File parent = htmlFile.getParentFile();
		if (!parent.exists()) {
			if (!parent.mkdirs()) {
				throw new BuildException("Could not create file " + htmlName); //$NON-NLS-1$
			}
		}
		PrintWriter writer = null;
		try {
			FileWriter fileWriter = new FileWriter(htmlFile);
			writer = new PrintWriter(new BufferedWriter(fileWriter));
			if (report.hasNonApiBundles()) {
				dumpNonApiBundles(writer, report);
			} else {
				dumpHeader(writer, report);
				// generate compatibility category
				dumpProblems(writer, "Compatibility", report.getProblems(APIToolsVerificationTask.COMPATIBILITY)); //$NON-NLS-1$
				writer.println(Messages.fullReportTask_categoryseparator);
				dumpProblems(writer, "API Usage", report.getProblems(APIToolsVerificationTask.USAGE)); //$NON-NLS-1$
				writer.println(Messages.fullReportTask_categoryseparator);
				dumpProblems(writer, "Bundle Version", report.getProblems(APIToolsVerificationTask.BUNDLE_VERSION)); //$NON-NLS-1$
				dumpFooter(writer);
			}
			writer.flush();
		} catch (IOException e) {
			throw new BuildException("An IOException occured while dumping : " + htmlName); //$NON-NLS-1$
		} finally {
			if (writer != null) {
				writer.close();
			}
		}
	}
	private String extractLinkFrom(String fileName) {
		StringBuffer buffer = new StringBuffer();
		buffer.append('.').append(fileName.substring(this.htmlRoot.getAbsolutePath().length()).replace('\\', '/'));
		return String.valueOf(buffer);
	}
	private String extractNameFromXMLName(File xmlFile) {
		String fileName = xmlFile.getAbsolutePath();
		int index = fileName.lastIndexOf('.');
		StringBuffer buffer = new StringBuffer();
		buffer.append(fileName.substring(this.reportsRoot.getAbsolutePath().length(), index)).append(".html"); //$NON-NLS-1$
		File htmlFile = new File(this.htmlReportsLocation, String.valueOf(buffer));
		return htmlFile.getAbsolutePath();
	}
	private void dumpFooter(PrintWriter writer) {
		writer.println(Messages.fullReportTask_apiproblemfooter);
	}
	private void dumpHeader(PrintWriter writer, Report report) {
		writer.println(
			MessageFormat.format(
				Messages.fullReportTask_apiproblemheader,
				new String[] {
						report.componentID
				}));
		// dump the summary
		writer.println(
			MessageFormat.format(
				Messages.fullReportTask_apiproblemsummary,
				new String[] {
					Integer.toString(report.getProblemSize(APIToolsVerificationTask.COMPATIBILITY)),
					Integer.toString(report.getProblemSize(APIToolsVerificationTask.USAGE)),
					Integer.toString(report.getProblemSize(APIToolsVerificationTask.BUNDLE_VERSION)),
				}));
	}
	private void dumpProblems(PrintWriter writer, String categoryName, String[] problemMessages) {
		if (problemMessages != null && problemMessages.length != 0) {
			writer.println(
					MessageFormat.format(
						Messages.fullReportTask_categoryheader,
						new String[] {categoryName}));
			for (int i = 0, max = problemMessages.length; i < max; i++) {
				String problemMessage = problemMessages[i];
				if ((i % 2) == 0) {
					writer.println(MessageFormat.format(Messages.fullReportTask_problementry_even, new String[] { problemMessage }));
				} else { 
					writer.println(MessageFormat.format(Messages.fullReportTask_problementry_odd, new String[] { problemMessage }));
				}
			}
			writer.println(Messages.fullReportTask_categoryfooter);
		} else {
			writer.println(
					MessageFormat.format(
						Messages.fullReportTask_category_no_elements,
						new String[] {categoryName}));
		}
	}
	private void dumpNonApiBundles(PrintWriter writer, Report report) {
		writer.println(Messages.fullReportTask_bundlesheader);
		String[] nonApiBundleNames = report.getNonApiBundles();
		for (int i = 0; i < nonApiBundleNames.length; i++) {
			String bundleName = nonApiBundleNames[i];
			if ((i % 2) == 0) {
				writer.println(MessageFormat.format(Messages.fullReportTask_bundlesentry_even, new String[] { bundleName }));
			} else { 
				writer.println(MessageFormat.format(Messages.fullReportTask_bundlesentry_odd, new String[] { bundleName }));
			}
		}
		writer.println(Messages.fullReportTask_bundlesfooter);
	}
}
