/*******************************************************************************
 * Copyright (c) 2008, 2024 IBM Corporation and others.
 *
 * This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License 2.0
 * which accompanies this distribution, and is available at
 * https://www.eclipse.org/legal/epl-2.0/
 *
 * SPDX-License-Identifier: EPL-2.0
 *
 * 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.FileWriter;
import java.io.IOException;
import java.io.PrintWriter;
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

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

import org.apache.tools.ant.BuildException;
import org.apache.tools.ant.Task;
import org.eclipse.osgi.util.NLS;
import org.eclipse.pde.api.tools.internal.IApiXmlConstants;
import org.eclipse.pde.api.tools.internal.provisional.ApiPlugin;
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;

/**
 * This task can be used to convert all reports generated by the
 * apitooling.analysis task into html reports.
 */
public class AnalysisReportConversionTask extends Task {
	static final class Problem {
		String message;
		int severity;

		public Problem(String message, int severity) {
			this.message = message;
			this.severity = severity;
		}

		public String getHtmlMessage() {
			StringBuilder buffer = new StringBuilder();
			char[] chars = this.message.toCharArray();
			for (char character : chars) {
				switch (character) {
					case '<':
						buffer.append("&lt;"); //$NON-NLS-1$
						break;
					case '>':
						buffer.append("&gt;"); //$NON-NLS-1$
						break;
					case '&':
						buffer.append("&amp;"); //$NON-NLS-1$
						break;
					case '"':
						buffer.append("&quot;"); //$NON-NLS-1$
						break;
					default:
						buffer.append(character);
				}
			}
			return String.valueOf(buffer);
		}

		@Override
		public String toString() {
			StringBuilder buffer = new StringBuilder();
			buffer.append("Problem : ").append(this.message).append(' ').append(this.severity); //$NON-NLS-1$
			return String.valueOf(buffer);
		}
	}

	static final class ConverterDefaultHandler extends DefaultHandler {
		String category;
		boolean debug;
		Report report;

		public ConverterDefaultHandler(boolean debug) {
			this.debug = debug;
		}

		public Report getReport() {
			return this.report;
		}

		@Override
		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 (this.debug) {
					System.out.println("component id : " + String.valueOf(componentID)); //$NON-NLS-1$
				}
				this.report = new Report(componentID);
			} else if (IApiXmlConstants.ATTR_CATEGORY.equals(name)) {
				this.category = attributes.getValue(IApiXmlConstants.ATTR_VALUE);
				if (this.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 (this.debug) {
					System.out.println("problem message : " + message); //$NON-NLS-1$
				}
				int severity = Integer.parseInt(attributes.getValue(IApiXmlConstants.ATTR_SEVERITY));
				if (this.debug) {
					System.out.println("problem severity : " + severity); //$NON-NLS-1$
				}
				this.report.addProblem(this.category, new Problem(message, severity));
			} else if (IApiXmlConstants.ELEMENT_RESOLVER_ERROR.equals(name)) {
				String message = attributes.getValue(IApiXmlConstants.ATTR_MESSAGE);
				if (this.debug) {
					System.out.println("Resolver error : " + message); //$NON-NLS-1$
				}
				this.report.addProblem(this.category, new Problem(message, ApiPlugin.SEVERITY_WARNING));
			} else if (IApiXmlConstants.ELEMENT_BUNDLE.equals(name)) {
				String bundleName = attributes.getValue(IApiXmlConstants.ATTR_NAME);
				if (this.debug) {
					System.out.println("bundle name : " + bundleName); //$NON-NLS-1$
				}
				this.report.addNonApiBundles(bundleName);
			} else if (this.debug) {
				if (!IApiXmlConstants.ELEMENT_PROBLEM_MESSAGE_ARGUMENTS.equals(name) && !IApiXmlConstants.ELEMENT_PROBLEM_MESSAGE_ARGUMENT.equals(name) && !IApiXmlConstants.ELEMENT_API_PROBLEMS.equals(name) && !IApiXmlConstants.ELEMENT_PROBLEM_EXTRA_ARGUMENT.equals(name) && !IApiXmlConstants.ELEMENT_PROBLEM_EXTRA_ARGUMENTS.equals(name)) {
					System.out.println("unknown element : " + String.valueOf(name)); //$NON-NLS-1$
				}
			}
		}
	}

	static private final class Report {
		String componentID;
		String link;
		List<String> nonApiBundles;
		Map<String, List<Problem>> problemsPerCategories;

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

		public void addNonApiBundles(String bundleName) {
			if (this.nonApiBundles == null) {
				this.nonApiBundles = new ArrayList<>();
			}
			this.nonApiBundles.add(bundleName);
		}

		public void addProblem(String category, Problem problem) {
			if (this.problemsPerCategories == null) {
				this.problemsPerCategories = new HashMap<>();
			}
			List<Problem> problemsList = this.problemsPerCategories.get(category);
			if (problemsList == null) {
				problemsList = new ArrayList<>();
				this.problemsPerCategories.put(category, problemsList);
			}
			problemsList.add(problem);
		}

		public String[] getNonApiBundles() {
			if (this.nonApiBundles == null || this.nonApiBundles.isEmpty()) {
				return NO_NON_API_BUNDLES;
			}
			String[] nonApiBundlesNames = new String[this.nonApiBundles.size()];
			this.nonApiBundles.toArray(nonApiBundlesNames);
			return nonApiBundlesNames;
		}

		public Problem[] getProblems(String category) {
			if (this.problemsPerCategories == null) {
				return NO_PROBLEMS;
			}
			List<Problem> problemsList = this.problemsPerCategories.get(category);
			int size = problemsList == null ? 0 : problemsList.size();
			if (size == 0) {
				return NO_PROBLEMS;
			}
			Problem[] problems = new Problem[size];
			problemsList.toArray(problems);
			return problems;
		}

		public int getProblemSize(String category) {
			if (this.problemsPerCategories == null) {
				return 0;
			}
			List<Problem> problemsList = this.problemsPerCategories.get(category);
			return problemsList == null ? 0 : problemsList.size();
		}

		public boolean isNonApiBundlesReport() {
			return this.componentID == null;
		}

		public void setLink(String link) {
			this.link = link;
		}
	}

	static private final class Summary {
		int apiUsageNumber;
		int bundleVersionNumber;
		int compatibilityNumber;
		int componentResolutionNumber;
		String componentID;
		String link;

		public Summary(Report report) {
			super();
			this.apiUsageNumber = report.getProblemSize(APIToolsAnalysisTask.USAGE);
			this.bundleVersionNumber = report.getProblemSize(APIToolsAnalysisTask.BUNDLE_VERSION);
			this.compatibilityNumber = report.getProblemSize(APIToolsAnalysisTask.COMPATIBILITY);
			this.componentResolutionNumber = report.getProblemSize(APIToolsAnalysisTask.COMPONENT_RESOLUTION);
			this.componentID = report.componentID;
			this.link = report.link;
		}

		@Override
		public String toString() {
			return MessageFormat.format("{0} : compatibility {1}, api usage {2}, bundle version {3}, resolution {4}, link {5}", //$NON-NLS-1$
							this.componentID,
							Integer.toString(this.compatibilityNumber),
							Integer.toString(this.apiUsageNumber),
							Integer.toString(this.bundleVersionNumber),
							Integer.toString(this.componentResolutionNumber),
							this.link );
		}
	}

	static final Problem[] NO_PROBLEMS = new Problem[0];
	static final String[] NO_NON_API_BUNDLES = new String[0];
	boolean debug;

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

	private void dumpFooter(PrintWriter writer) {
		writer.println(Messages.fullReportTask_apiproblemfooter);
	}

	private void dumpHeader(PrintWriter writer, Report report) {
		writer.println(MessageFormat.format(Messages.fullReportTask_apiproblemheader, report.componentID));

		// dump the summary
		writer.println(MessageFormat.format(Messages.fullReportTask_apiproblemsummary,
				Integer.toString(report.getProblemSize(APIToolsAnalysisTask.COMPATIBILITY)),
				Integer.toString(report.getProblemSize(APIToolsAnalysisTask.USAGE)),
				Integer.toString(report.getProblemSize(APIToolsAnalysisTask.BUNDLE_VERSION))));

		if (report.getProblemSize(APIToolsAnalysisTask.COMPONENT_RESOLUTION) == 1) {
			writer.println(MessageFormat.format(Messages.fullReportTask_resolutiondetailsSingle,
					report.componentID,
					Integer.toString(report.getProblemSize(APIToolsAnalysisTask.COMPONENT_RESOLUTION))));
		} else if (report.getProblemSize(APIToolsAnalysisTask.COMPONENT_RESOLUTION) > 1) {
			writer.println(MessageFormat.format(Messages.fullReportTask_resolutiondetails,
					report.componentID,
					Integer.toString(report.getProblemSize(APIToolsAnalysisTask.COMPONENT_RESOLUTION))));
		}
	}

	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,
					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,
					summary.componentID,
					Integer.toString(summary.compatibilityNumber),
					Integer.toString(summary.apiUsageNumber),
					Integer.toString(summary.bundleVersionNumber),
					summary.link));
		}
		if (summary.componentResolutionNumber > 0) {
			if ((i % 2) == 0) {
				writer.println(MessageFormat.format(Messages.fullReportTask_resolutionsummary_even,
						summary.componentID,
						Integer.toString(summary.componentResolutionNumber) ));
			} else {
				writer.println(MessageFormat.format(Messages.fullReportTask_resolutionsummary_odd,
						summary.componentID,
						Integer.toString(summary.componentResolutionNumber) ));
			}
		}
	}

	/**
	 * Write out the index file
	 */
	private void dumpIndexFile(Summary[] summaries, Summary allNonApiBundleSummary) {
		File htmlFile = new File(this.htmlReportsLocation, "index.html"); //$NON-NLS-1$
		try (PrintWriter writer = new PrintWriter(new BufferedWriter(new FileWriter(htmlFile)))) {
			if (allNonApiBundleSummary != null) {
				writer.println(MessageFormat.format(Messages.fullReportTask_indexheader, NLS.bind(Messages.fullReportTask_nonApiBundleSummary, allNonApiBundleSummary.link)));
			} else {
				writer.println(MessageFormat.format(Messages.fullReportTask_indexheader, Messages.fullReportTask_apiBundleSummary));
			}
			Arrays.sort(summaries, (o1, o2) -> {
				Summary summary1 = o1;
				Summary summary2 = 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(NLS.bind(Messages.ioexception_writing_html_file, htmlFile.getAbsolutePath()));
		}
	}

	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, bundleName));
			} else {
				writer.println(MessageFormat.format(Messages.fullReportTask_bundlesentry_odd, bundleName));
			}
		}
		writer.println(Messages.fullReportTask_bundlesfooter);
	}

	private void dumpProblems(PrintWriter writer, String categoryName, Problem[] problems, boolean printEmptyCategory) {
		if (problems != null && problems.length != 0) {
			writer.println(MessageFormat.format(Messages.fullReportTask_categoryheader, categoryName));
			for (int i = 0, max = problems.length; i < max; i++) {
				Problem problem = problems[i];
				if ((i % 2) == 0) {
					switch (problem.severity) {
						case ApiPlugin.SEVERITY_ERROR:
							writer.println(MessageFormat.format(Messages.fullReportTask_problementry_even_error, problem.getHtmlMessage()));
							break;
						case ApiPlugin.SEVERITY_WARNING:
							writer.println(MessageFormat.format(Messages.fullReportTask_problementry_even_warning, problem.getHtmlMessage()));
							break;
						default:
							break;
					}
				} else {
					switch (problem.severity) {
						case ApiPlugin.SEVERITY_ERROR:
							writer.println(MessageFormat.format(Messages.fullReportTask_problementry_odd_error, problem.getHtmlMessage()));
							break;
						case ApiPlugin.SEVERITY_WARNING:
							writer.println(MessageFormat.format(Messages.fullReportTask_problementry_odd_warning, problem.getHtmlMessage()));
							break;
						default:
							break;
					}
				}
			}
			writer.println(Messages.fullReportTask_categoryfooter);
		} else if (printEmptyCategory) {
			writer.println(MessageFormat.format(Messages.fullReportTask_category_no_elements, categoryName));
		}
	}

	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(NLS.bind(Messages.could_not_create_file, htmlName));
			}
		}
		try (PrintWriter writer = new PrintWriter(new BufferedWriter(new FileWriter(htmlFile)))) {
			if (report.isNonApiBundlesReport()) {
				dumpNonApiBundles(writer, report);
			} else {
				dumpHeader(writer, report);
				dumpProblems(writer, Messages.AnalysisReportConversionTask_component_resolution_header, report.getProblems(APIToolsAnalysisTask.COMPONENT_RESOLUTION), false);
				dumpProblems(writer, Messages.fullReportTask_compatibility_header, report.getProblems(APIToolsAnalysisTask.COMPATIBILITY), true);
				dumpProblems(writer, Messages.fullReportTask_api_usage_header, report.getProblems(APIToolsAnalysisTask.USAGE), true);
				dumpProblems(writer, Messages.fullReportTask_bundle_version_header, report.getProblems(APIToolsAnalysisTask.BUNDLE_VERSION), true);
				dumpFooter(writer);
			}
			writer.flush();
		} catch (IOException e) {
			throw new BuildException(NLS.bind(Messages.ioexception_writing_html_file, htmlName));
		}
	}

	/**
	 * Run the ant task
	 */
	@Override
	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(Messages.missing_xml_files_location);
		}
		this.reportsRoot = new File(this.xmlReportsLocation);
		if (!this.reportsRoot.exists() || !this.reportsRoot.isDirectory()) {
			throw new BuildException(NLS.bind(Messages.invalid_directory_name, this.xmlReportsLocation));
		}
		SAXParser parser = createSAXParser();

		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(NLS.bind(Messages.could_not_create_file, this.htmlReportsLocation));
			}
		}
		if (this.debug) {
			System.out.println("output name :" + this.htmlReportsLocation); //$NON-NLS-1$
		}
		try {
			// retrieve all xml reports
			File[] allFiles = Util.getAllFiles(reportsRoot, pathname -> pathname.isDirectory() || pathname.getName().endsWith(".xml")); //$NON-NLS-1$
			if (allFiles != null) {
				int length = allFiles.length;
				List<Summary> 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();
					if (report == null) {
						// ignore that file. It could be the counts.xml file.
						if (this.debug) {
							System.out.println("Skipped file : " + file.getAbsolutePath()); //$NON-NLS-1$
						}
						continue;
					}
					dumpReport(file, report);
					if (report.isNonApiBundlesReport()) {
						nonApiBundleSummary = new Summary(report);
					} else {
						summariesList.add(new Summary(report));
					}
				}
				// dump index file if there is at least one summary or non api
				// tools bundles (ignore count.xml)
				if (!summariesList.isEmpty() || nonApiBundleSummary != null) {
					Summary[] summaries = new Summary[summariesList.size()];
					summariesList.toArray(summaries);
					dumpIndexFile(summaries, nonApiBundleSummary);
				}
			}
		} catch (SAXException | IOException e) {
			// ignore
		}
	}

	@SuppressWarnings("restriction")
	static SAXParser createSAXParser() {
		try {
			return org.eclipse.core.internal.runtime.XmlProcessorFactory.createSAXParserWithErrorOnDOCTYPE();
		} catch (ParserConfigurationException | SAXException e) {
			throw new BuildException(Messages.could_not_create_sax_parser, e);
		}
	}

	private String extractLinkFrom(String fileName) {
		StringBuilder buffer = new StringBuilder();
		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('.');
		StringBuilder buffer = new StringBuilder();
		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();
	}

	/**
	 * Set the debug value.
	 * <p>
	 * The possible values are: <code>true</code>, <code>false</code>
	 * </p>
	 * <p>
	 * Default is <code>false</code>.
	 * </p>
	 *
	 * @param debugValue the given debug value
	 */
	public void setDebug(String debugValue) {
		this.debug = Boolean.toString(true).equals(debugValue);
	}

	/**
	 * Set the location where the html reports are generated.
	 *
	 * <p>
	 * This is optional. If not set, the html files are created in the same
	 * folder as the xml files.
	 * </p>
	 * <p>
	 * The location is set using an absolute path.
	 * </p>
	 *
	 * @param htmlFilesLocation the given the location where the html reports
	 *            are generated
	 */
	public void setHtmlFiles(String htmlFilesLocation) {
		this.htmlReportsLocation = htmlFilesLocation;
	}

	/**
	 * Set the location where the xml reports are retrieved.
	 *
	 * <p>
	 * The location is set using an absolute path.
	 * </p>
	 *
	 * @param xmlFilesLocation the given location to retrieve the xml reports
	 */
	public void setXmlFiles(String xmlFilesLocation) {
		this.xmlReportsLocation = xmlFilesLocation;
	}
}
