/*******************************************************************************
 * Copyright (c) 2007, 2018 Alphonse Van Assche and others.
 *
 * This program and the accompanying materials are made
 * available under the terms of the Eclipse Public License 2.0
 * which is available at https://www.eclipse.org/legal/epl-2.0/
 *
 * SPDX-License-Identifier: EPL-2.0
 *
 * Contributors:
 *    Alphonse Van Assche - initial API and implementation
 *******************************************************************************/
package org.eclipse.linuxtools.internal.rpm.rpmlint.parser;

import java.io.BufferedInputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.LineNumberReader;
import java.io.StringReader;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.List;

import org.eclipse.core.resources.IFile;
import org.eclipse.core.resources.IMarker;
import org.eclipse.core.resources.IResource;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.preferences.InstanceScope;
import org.eclipse.linuxtools.internal.rpm.rpmlint.Activator;
import org.eclipse.linuxtools.internal.rpm.rpmlint.RpmlintLog;
import org.eclipse.linuxtools.internal.rpm.rpmlint.builder.RpmlintBuilder;
import org.eclipse.linuxtools.internal.rpm.rpmlint.preferences.PreferenceConstants;
import org.eclipse.linuxtools.internal.rpm.rpmlint.resolutions.RpmlintMarkerResolutionGenerator;
import org.eclipse.linuxtools.rpm.core.utils.Utils;
import org.eclipse.ui.preferences.ScopedPreferenceStore;

/**
 * Parser for rpmlint output.
 *
 */
public class RpmlintParser {

	private static final String COLON = ":"; //$NON-NLS-1$
	private static final String SPACE = " "; //$NON-NLS-1$
	private static final String EMPTY_STRING = ""; //$NON-NLS-1$

	/**
	 * Parse visited resources.
	 *
	 * @param visitedResources The list of resources to parse.
	 *
	 * @return a <code>RpmlintItem</code> ArrayList.
	 */
	public static List<RpmlintItem> parseVisisted(List<String> visitedResources) {
		String rpmlintPath = new ScopedPreferenceStore(InstanceScope.INSTANCE, Activator.PLUGIN_ID)
				.getString(PreferenceConstants.P_RPMLINT_PATH);
		/*
		 * It's fine to fail silently if rpmlint is not installed as the actual user
		 * messages and etc. are displayed by the ui code and this is just a guard if we
		 * have configuration changing or someone playing with the project files.
		 */
		if (visitedResources.isEmpty() || !Files.exists(Paths.get(rpmlintPath))) {
			return new ArrayList<>();
		}
		return parseRpmlintOutput(runRpmlintCommand(visitedResources));
	}

	/**
	 * Adds a rpmlint marker.
	 *
	 * @param file                   The file to create the marker for.
	 * @param message                The marker message.
	 * @param lineNumber             The line at which the marker appears.
	 * @param charStart              The index of the starting char for the marker.
	 * @param charEnd                The index of the ending char for the marker.
	 * @param severity               The marker seveirty.
	 * @param rpmlintID              The id of the rpmlint warning/error.
	 * @param rpmlintrefferedContent Additional content reffered by the marker.
	 */
	public static void addMarker(IFile file, String message, int lineNumber, int charStart, int charEnd, int severity,
			String rpmlintID, String rpmlintrefferedContent) {
		try {
			IMarker marker = file.createMarker(RpmlintBuilder.MARKER_ID);
			marker.setAttribute(IMarker.LOCATION, file.getFullPath().toString());
			marker.setAttribute(IMarker.MESSAGE, message);
			marker.setAttribute(IMarker.SEVERITY, severity);
			marker.setAttribute(IMarker.LINE_NUMBER, lineNumber);
			marker.setAttribute(IMarker.CHAR_START, charStart);
			marker.setAttribute(IMarker.CHAR_END, charEnd);
			marker.setAttribute(RpmlintMarkerResolutionGenerator.RPMLINT_ERROR_ID, rpmlintID);
			marker.setAttribute(RpmlintMarkerResolutionGenerator.RPMLINT_REFFERED_CONTENT, rpmlintrefferedContent);

		} catch (CoreException e) {
			RpmlintLog.logError(e);
		}
	}

	/**
	 * Adds a rpmlint marker.
	 *
	 * @param file                   The file to create the marker for.
	 * @param message                The marker message.
	 * @param severity               The marker severity.
	 * @param rpmlintID              The id of the rpmlint warning/error.
	 * @param rpmlintrefferedContent Additional content referred by the marker.
	 */
	public static void addMarker(IFile file, String message, int severity, String rpmlintID,
			String rpmlintrefferedContent) {
		try {
			IMarker marker = file.createMarker(RpmlintBuilder.MARKER_ID);
			marker.setAttribute(IMarker.LOCATION, file.getFullPath().toString());
			marker.setAttribute(IMarker.MESSAGE, message);
			marker.setAttribute(IMarker.SEVERITY, severity);
			marker.setAttribute(RpmlintMarkerResolutionGenerator.RPMLINT_ERROR_ID, rpmlintID);
			marker.setAttribute(RpmlintMarkerResolutionGenerator.RPMLINT_REFFERED_CONTENT, rpmlintrefferedContent);

		} catch (CoreException e) {
			RpmlintLog.logError(e);
		}
	}

	/**
	 * Clear the rpmlint specific markers.
	 *
	 * @param resource The resource for which to clean the marker.
	 */
	public static void deleteMarkers(IResource resource) {
		try {
			resource.deleteMarkers(RpmlintBuilder.MARKER_ID, false, IResource.DEPTH_ZERO);
		} catch (CoreException e) {
			RpmlintLog.logError(e);
		}
	}

	/**
	 * Parse a given rpmlint <code>InputStream</code>
	 *
	 * @param rpmlint <code>InputStream</code> to parse.
	 * @return a <code>RpmlintItem</code> ArrayList.
	 */
	private static List<RpmlintItem> parseRpmlintOutput(BufferedInputStream in) {
		RpmlintItem item = new RpmlintItem();
		ArrayList<RpmlintItem> rpmlintItems = new ArrayList<>();
		LineNumberReader reader = new LineNumberReader(new InputStreamReader(in));
		String line;
		boolean isFirtItemLine = true;
		String[] lineItems;
		String description = EMPTY_STRING;
		try {
			while ((line = reader.readLine()) != null) {
				if (isFirtItemLine) {
					isFirtItemLine = false;
					lineItems = line.split(COLON, 4);
					item.setFileName(lineItems[0]);
					int lineNbr;

					// FIXME: last rpmlint version (0.83) contain a summary
					// line at the bottom of it output, so if we
					// detected this line we can safely return rpmlintItems,
					// maybe we can find a better way to detect this line.
					try {
						Integer.parseInt(line.split(SPACE)[0]);
						return rpmlintItems;
					} catch (NumberFormatException e) {
						// this line is not the summary
					}

					// TODO: ask rpmlint upstream to display always the same
					// output.
					// at the moment the line number is not always displayed.
					// If the same output is always used, all the workarounds
					// for the line number can be
					// removed.
					try {
						lineNbr = Integer.parseInt(lineItems[1]);
						item.setSeverity(lineItems[2]);
						lineItems = lineItems[3].trim().split(SPACE, 2);
					} catch (NumberFormatException e) {
						// No line number showed for this rpmlint warning.
						lineItems = line.split(COLON, 3);
						lineNbr = -1;
						item.setSeverity(lineItems[1]);
						lineItems = lineItems[2].trim().split(SPACE, 2);
					}
					item.setLineNbr(lineNbr);
					item.setId(lineItems[0]);
					if (lineItems.length > 1) {
						// Maybe this error occur when rpmlint execute 'rpm -q
						// --qf=
						// --specfile file.spec' command
						RpmlintItem tmpItem = parseRpmOutput(item, lineItems[1]);
						if (tmpItem == null) {
							item.setRefferedContent(lineItems[1]);
						} else {
							item = tmpItem;
						}
					} else {
						item.setRefferedContent(EMPTY_STRING);
					}
				} else {
					description += line + '\n';
				}

				if (line.equals(EMPTY_STRING)) {
					if (item.getMessage() == null) {
						item.setMessage(description.substring(0, description.length() - 2));
					}
					int useOfTabsAndSpaces = getMixedUseOfTabsAndSpaces(item.getRefferedContent());
					if (useOfTabsAndSpaces != -1) {
						item.setLineNbr(useOfTabsAndSpaces);
					}
					rpmlintItems.add(item);
					item = new RpmlintItem();

					// Reinitialize parser for the next item
					isFirtItemLine = true;
					description = EMPTY_STRING;
				}

			}
			// Close the input stream
			in.close();
		} catch (IOException e) {
			RpmlintLog.logError(e);
		}
		return rpmlintItems;
	}

	private static RpmlintItem parseRpmOutput(RpmlintItem item, String line) {
		String[] rpmErrorItems = line.split(COLON, 4);
		if (item.getId().equalsIgnoreCase("specfile-error")) { //$NON-NLS-1$
			// set severity
			item.setSeverity("E"); //$NON-NLS-1$
		} else {
			return null;
		}
		// set line number
		try {
			if (rpmErrorItems[1].matches(" line [0-9]+$")) { //$NON-NLS-1$
				item.setLineNbr(Integer.parseInt(rpmErrorItems[1].replace(" line ", ""))); //$NON-NLS-1$ //$NON-NLS-2$
				item.setMessage(rpmErrorItems[2]);
				item.setRefferedContent(rpmErrorItems[3]);
			} else {
				item.setLineNbr(-1);
				item.setMessage(rpmErrorItems[1]);
				item.setRefferedContent(""); //$NON-NLS-1$
			}
		} catch (NumberFormatException e) {
			return null;
		}

		return item;
	}

	/**
	 * Run rpmlint command on given visitedResources.
	 *
	 * @param specContent The specfile content.
	 * @return The rpmlint command <code>InputStream</code>.
	 */
	private static BufferedInputStream runRpmlintCommand(List<String> visitedResources) {
		BufferedInputStream in = null;
		int i = 2;
		String[] cmd = new String[visitedResources.size() + i];
		cmd[0] = new ScopedPreferenceStore(InstanceScope.INSTANCE, Activator.PLUGIN_ID)
				.getString(PreferenceConstants.P_RPMLINT_PATH);
		cmd[1] = "-i"; //$NON-NLS-1$
		for (String resource : visitedResources) {
			cmd[i] = resource;
			i++;
		}
		try {
			in = Utils.runCommandToInputStream(cmd);
		} catch (IOException e) {
			// FIXME: rpmlint is not installed in the default place -> ask user
			// to open the prefs page.
			RpmlintLog.logError(e);
		}
		return in;
	}

	/**
	 *
	 * Return the line number for given specContent and strToFind, it returns -1 if
	 * the string to find is not found.
	 *
	 * @param specContent The content of the spec file.
	 *
	 * @param strToFind   The string we are looking for.
	 * @return The line number.
	 */
	public static int getRealLineNbr(String specContent, String strToFind) {
		int ret = -1;
		if (strToFind.isEmpty()) {
			return ret;
		}
		String line;
		LineNumberReader reader = new LineNumberReader(new StringReader(specContent));
		try {
			while ((line = reader.readLine()) != null) {
				if (line.replaceAll("\t| ", EMPTY_STRING).indexOf( //$NON-NLS-1$
						strToFind.replaceAll("\t| ", EMPTY_STRING)) > -1) { //$NON-NLS-1$
					ret = reader.getLineNumber();
				}
			}
		} catch (IOException e) {
			// return -1 if an I/O Exception occure.
		}
		return ret;
	}

	private static int getMixedUseOfTabsAndSpaces(String refferedContent) {
		int lineNbr = -1;
		if (refferedContent.indexOf("(spaces: line") > -1) { //$NON-NLS-1$
			String tabsAndSpacesPref = new ScopedPreferenceStore(InstanceScope.INSTANCE, Activator.PLUGIN_ID)
					.getString(PreferenceConstants.P_RPMLINT_TABS_AND_SPACES);
			String[] spacesAndTabs = refferedContent.split("line"); //$NON-NLS-1$
			if (tabsAndSpacesPref == PreferenceConstants.P_RPMLINT_SPACES) {
				lineNbr = Integer.parseInt(spacesAndTabs[1].split(",")[0].trim()); //$NON-NLS-1$
			} else {
				lineNbr = Integer.parseInt(spacesAndTabs[2].replaceFirst("\\)", EMPTY_STRING).trim()); //$NON-NLS-1$
			}
		}
		return lineNbr;
	}

}
