/*******************************************************************************
 * Copyright (c) 2007, 2018 Red Hat, Inc.
 *
 * 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:
 *    Red Hat - initial API and implementation
 *    Alphonse Van Assche
 *******************************************************************************/

package org.eclipse.linuxtools.rpm.ui.editor.parser;

import static org.eclipse.linuxtools.internal.rpm.ui.editor.RpmSections.BUILD_SECTION;
import static org.eclipse.linuxtools.internal.rpm.ui.editor.RpmSections.CHANGELOG_SECTION;
import static org.eclipse.linuxtools.internal.rpm.ui.editor.RpmSections.CLEAN_SECTION;
import static org.eclipse.linuxtools.internal.rpm.ui.editor.RpmSections.DESCRIPTION_SECTION;
import static org.eclipse.linuxtools.internal.rpm.ui.editor.RpmSections.FILES_SECTION;
import static org.eclipse.linuxtools.internal.rpm.ui.editor.RpmSections.INSTALL_SECTION;
import static org.eclipse.linuxtools.internal.rpm.ui.editor.RpmSections.PACKAGE_SECTION;
import static org.eclipse.linuxtools.internal.rpm.ui.editor.RpmSections.POSTTRANS_SECTION;
import static org.eclipse.linuxtools.internal.rpm.ui.editor.RpmSections.POSTUN_SECTION;
import static org.eclipse.linuxtools.internal.rpm.ui.editor.RpmSections.POST_SECTION;
import static org.eclipse.linuxtools.internal.rpm.ui.editor.RpmSections.PREP_SECTION;
import static org.eclipse.linuxtools.internal.rpm.ui.editor.RpmSections.PRETRANS_SECTION;
import static org.eclipse.linuxtools.internal.rpm.ui.editor.RpmSections.PREUN_SECTION;
import static org.eclipse.linuxtools.internal.rpm.ui.editor.RpmSections.PRE_SECTION;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.LineNumberReader;
import java.io.StringReader;
import java.util.Arrays;
import java.util.Iterator;
import java.util.List;

import org.eclipse.core.resources.IFile;
import org.eclipse.core.resources.IMarker;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.jface.text.Document;
import org.eclipse.jface.text.IDocument;
import org.eclipse.linuxtools.internal.rpm.ui.editor.Activator;
import org.eclipse.linuxtools.internal.rpm.ui.editor.ISpecfileSpecialSymbols;
import org.eclipse.linuxtools.internal.rpm.ui.editor.RpmTags;
import org.eclipse.linuxtools.internal.rpm.ui.editor.SpecfileLog;
import org.eclipse.linuxtools.internal.rpm.ui.editor.parser.Messages;
import org.eclipse.linuxtools.internal.rpm.ui.editor.parser.SpecfileParseException;
import org.eclipse.linuxtools.internal.rpm.ui.editor.parser.SpecfilePatchMacro;
import org.eclipse.linuxtools.internal.rpm.ui.editor.parser.SpecfileSource;
import org.eclipse.linuxtools.internal.rpm.ui.editor.parser.SpecfileSource.SourceType;
import org.eclipse.linuxtools.internal.rpm.ui.editor.parser.SpecfileTag;
import org.eclipse.linuxtools.internal.rpm.ui.editor.preferences.PreferenceConstants;
import org.eclipse.linuxtools.rpm.ui.editor.markers.SpecfileErrorHandler;
import org.eclipse.linuxtools.rpm.ui.editor.markers.SpecfileTaskHandler;

public class SpecfileParser {

	private static final String DEFINE_SEPARATOR = ":"; //$NON-NLS-1$

	private static final String SPACE_REGEX = "\\s+"; //$NON-NLS-1$

	/**
	 * These are SRPM-wide sections, and they also cannot have any flags like -n or
	 * -f. Hence they are called simple. This is probably a misleading name and it
	 * should be renamed to reflect that they are SRPM-wide sections.
	 */
	public static final String[] simpleSections = { PREP_SECTION, BUILD_SECTION, INSTALL_SECTION, CLEAN_SECTION,
			CHANGELOG_SECTION };

	/**
	 * These are sections that apply to a particular sub-package (i.e. binary RPM),
	 * including the main package. These can also have flags like -f or -n appended
	 * to them, hence they are called complex. This should probably be renamed to
	 * reflect that they are in fact per-RPM sections.
	 */
	private static String[] complexSections = { PRETRANS_SECTION, PRE_SECTION, PREUN_SECTION, POST_SECTION,
			POSTUN_SECTION, POSTTRANS_SECTION, FILES_SECTION, PACKAGE_SECTION, DESCRIPTION_SECTION };

	private static String[] simpleDefinitions = { RpmTags.EPOCH, RpmTags.NAME, RpmTags.VERSION, RpmTags.RELEASE,
			RpmTags.URL, RpmTags.BUILD_ARCH };

	private static String[] directValuesDefinitions = { RpmTags.LICENSE, RpmTags.BUILD_ROOT };
	// Note that the ordering here should match that in
	// SpecfileSource#SOURCETYPE
	private static String[] complexDefinitions = { "Source", "Patch" }; //$NON-NLS-1$ //$NON-NLS-2$

	private static String[] packageLevelDefinitions = { RpmTags.SUMMARY, RpmTags.GROUP, RpmTags.OBSOLETES,
			RpmTags.PROVIDES, RpmTags.REQUIRES, RpmTags.REQUIRES_PRE, RpmTags.REQUIRES_POST, RpmTags.REQUIRES_POSTUN };

	private SpecfileErrorHandler errorHandler;
	private SpecfileTaskHandler taskHandler;
	private SpecfileSection lastSection;
	private SpecfilePackage activePackage;

	public Specfile parse(IDocument specfileDocument) {

		// remove all existing markers, if a SpecfileErrorHandler is
		// instantiated.
		if (errorHandler != null) {
			errorHandler.removeExistingMarkers();
		}
		if (taskHandler != null) {
			taskHandler.removeExistingMarkers();
		}
		LineNumberReader reader = new LineNumberReader(new StringReader(specfileDocument.get()));
		String line = ""; //$NON-NLS-1$
		int lineStartPosition = 0;
		Specfile specfile = new Specfile();
		specfile.setDocument(specfileDocument);
		try {
			while ((line = reader.readLine()) != null) {
				if (taskHandler != null) {
					generateTaskMarker(reader.getLineNumber() - 1, line);
				}
				// IDocument.getLine(#) is 0-indexed whereas
				// reader.getLineNumber appears to be 1-indexed
				SpecfileElement element = parseLine(line, specfile, reader.getLineNumber() - 1);
				if (element != null) {
					element.setLineNumber(reader.getLineNumber() - 1);
					element.setLineStartPosition(lineStartPosition);
					element.setLineEndPosition(lineStartPosition + line.length());
					if (element.getClass() == SpecfileTag.class) {
						SpecfileTag tag = (SpecfileTag) element;
						specfile.addDefine(tag);
					} else if ((element.getClass() == SpecfilePatchMacro.class)) {
						SpecfilePatchMacro thisPatchMacro = (SpecfilePatchMacro) element;
						if (thisPatchMacro != null) {
							thisPatchMacro.setSpecfile(specfile);
						}
						SpecfileSource thisPatch = specfile.getPatch(thisPatchMacro.getPatchNumber());
						if (thisPatch != null) {
							thisPatch.addLineUsed(reader.getLineNumber() - 1);
							thisPatch.setSpecfile(specfile);
						}
					} else if ((element.getClass() == SpecfileDefine.class)) {
						specfile.addDefine((SpecfileDefine) element);
					} else if ((element.getClass() == SpecfileSource.class)) {
						SpecfileSource source = (SpecfileSource) element;

						source.setLineNumber(reader.getLineNumber() - 1);
						if (source.getSourceType() == SpecfileSource.SourceType.SOURCE) {
							specfile.addSource(source);
						} else {
							specfile.addPatch(source);
						}
					}
				}

				// sets the last SpecfileSection's end line to that of the end
				// of
				// the end of the specfileDocument.
				// SpecfileParser#parseMacro will handle correcting the end line
				// if the last SpecfileSection was not truly the last 1
				// This is for the purpose of making DocumentRangeNode work
				if (lastSection != null) {
					lastSection.setSectionEndLine(specfileDocument.getNumberOfLines() - 1);
				}

				// The +1 is for the line delimiter. FIXME: will we end up off
				// by one on the last line?
				lineStartPosition += line.length() + 1;
			}
		} catch (IOException e) {
			// FIXME
			SpecfileLog.logError(e);
		}
		return specfile;
	}

	/**
	 * Parse a File into a specfile
	 *
	 * @param file The File to be parsed
	 * @return A Specfile object
	 */
	public Specfile parse(IFile file) {
		SpecfileParser parser = new SpecfileParser();
		StringBuilder sb = new StringBuilder();
		String line = null;
		try {
			BufferedReader reader = new BufferedReader(new InputStreamReader(file.getContents()));
			while ((line = reader.readLine()) != null) {
				sb.append(line).append('\n');
			}
		} catch (IOException e) {
			SpecfileLog.logError(Messages.getString("SpecfileParseFile.1"), e); //$NON-NLS-1$
		} catch (CoreException e) {
			SpecfileLog.logError(Messages.getString("SpecfileParseFile.2"), e); //$NON-NLS-1$
		}
		return parser.parse(sb.toString());
	}

	private void generateTaskMarker(int lineNumber, String line) {
		String[] taskTags = Activator.getDefault().getPreferenceStore().getString(PreferenceConstants.P_TASK_TAGS)
				.split(";"); //$NON-NLS-1$
		int commentCharIndex = line.indexOf(ISpecfileSpecialSymbols.COMMENT_START);
		if (commentCharIndex > -1) {
			for (String item : taskTags) {
				int taskIndex = line.indexOf(item);
				if (taskIndex > commentCharIndex) {
					taskHandler.handleTask(lineNumber, line, item);
				}
			}
		}
	}

	public Specfile parse(String specfileContent) {
		return parse(new Document(specfileContent));
	}

	public SpecfileElement parseLine(String lineText, Specfile specfile, int lineNumber) {

		if (lineText.startsWith("%")) {//$NON-NLS-1$
			return parseMacro(lineText, specfile, lineNumber);
		}

		for (String simpleDefinition : simpleDefinitions) {
			if (lineText.startsWith(simpleDefinition + DEFINE_SEPARATOR)) {
				return parseSimpleDefinition(lineText, specfile, lineNumber, false);
			}
		}
		for (String directValuesDefinition : directValuesDefinitions) {
			if (lineText.startsWith(directValuesDefinition + DEFINE_SEPARATOR)) {
				return parseDirectDefinition(lineText, specfile, lineNumber);
			}
		}
		for (String directValuesDefinition : packageLevelDefinitions) {
			if (lineText.startsWith(directValuesDefinition + DEFINE_SEPARATOR)) {
				SpecfileElement definition = parseDirectDefinition(lineText, specfile, lineNumber);
				if (directValuesDefinition.equals(RpmTags.REQUIRES)) {
					if (activePackage != null) {
						activePackage.addRequire((SpecfileTag) definition);
					} else {
						specfile.addRequire((SpecfileTag) definition);
					}
				}
				return definition;
			}
		}

		if (lineText.startsWith(complexDefinitions[0]) && lineText.contains(DEFINE_SEPARATOR)) {
			return parseComplexDefinition(lineText, lineNumber, SourceType.SOURCE);
		} else if (lineText.startsWith(complexDefinitions[1]) && lineText.contains(DEFINE_SEPARATOR)) {
			return parseComplexDefinition(lineText, lineNumber, SourceType.PATCH);
		} else if (lineText.startsWith("BuildRequires")) { //$NON-NLS-1$
			return parseBuildRequire(lineText, lineNumber, specfile);
		}

		return null;
	}

	private SpecfileElement parseBuildRequire(String lineText, int lineNumber, Specfile specfile) {
		String value = lineText.substring(lineText.indexOf(':') + 1, lineText.length()).trim();
		SpecfileDefine buildRequire = new SpecfileDefine("BuildRequires", value, specfile, null); //$NON-NLS-1$
		buildRequire.setLineNumber(lineNumber);
		specfile.addBuildRequire(buildRequire);
		return buildRequire;
	}

	private SpecfileSection parseSection(String lineText, Specfile specfile, int lineNumber) {
		List<String> tokens = Arrays.asList(lineText.split(SPACE_REGEX));
		SpecfileSection toReturn = null;
		boolean isSimpleSection = false;
		for (Iterator<String> iter = tokens.iterator(); iter.hasNext();) {
			String token = iter.next();

			// Sections
			// Simple Section Headers
			for (String simpleSection : simpleSections) {
				if (token.equals(simpleSection)) {
					toReturn = new SpecfileSection(token.substring(1), specfile);
					specfile.addSection(toReturn);
					isSimpleSection = true;
				}

			}

			// Complex Section Headers
			for (String complexSection : complexSections) {
				if (token.equals(complexSection)) {
					String name = token.substring(1);
					if (!name.equals("package")) { //$NON-NLS-1$
						toReturn = new SpecfileSection(name, specfile);
						specfile.addComplexSection(toReturn);
					}
					while (iter.hasNext()) {
						String nextToken = iter.next();
						if (nextToken.equals("-n")) { //$NON-NLS-1$
							if (!iter.hasNext()) {
								errorHandler.handleError(new SpecfileParseException(
										Messages.getString("SpecfileParser.1") //$NON-NLS-1$
												+ name + Messages.getString("SpecfileParser.2"), //$NON-NLS-1$
										lineNumber, 0, lineText.length(), IMarker.SEVERITY_ERROR));
								continue;
							}

							nextToken = iter.next();
							if (nextToken.startsWith("-")) { //$NON-NLS-1$
								errorHandler.handleError(new SpecfileParseException(
										Messages.getString("SpecfileParser.3") //$NON-NLS-1$
												+ nextToken + Messages.getString("SpecfileParser.4"), //$NON-NLS-1$
										lineNumber, 0, lineText.length(), IMarker.SEVERITY_ERROR));
							}

						} else if (nextToken.equals("-p")) { //$NON-NLS-1$
							// FIXME: rest of line is the actual section
							break;
						} else if (nextToken.equals("-f")) { //$NON-NLS-1$
							break;
						}

						// this is a package
						if (toReturn == null) {
							SpecfilePackage tmpPackage = specfile.getPackage(nextToken);

							if (tmpPackage == null) {
								tmpPackage = new SpecfilePackage(nextToken, specfile);
								specfile.addPackage(tmpPackage);
							}
							activePackage = tmpPackage;
							return tmpPackage;
						}

						// this is another section
						SpecfilePackage enclosingPackage = specfile.getPackage(nextToken);
						if (enclosingPackage == null) {
							enclosingPackage = new SpecfilePackage(nextToken, specfile);
							specfile.addPackage(enclosingPackage);
						}
						toReturn.setPackage(enclosingPackage);
						enclosingPackage.addSection(toReturn);
					}
				}
			}
		}

		// if this package is part of the top level package, add it to
		// it
		if (toReturn != null && toReturn.getPackage() == null) {
			SpecfilePackage topPackage = specfile.getPackage(specfile.getName());
			if (topPackage == null) {
				topPackage = new SpecfilePackage(specfile.getName(), specfile);
				specfile.addPackage(topPackage);
			}
			if (!isSimpleSection) {
				topPackage.addSection(toReturn);
			}
		}
		if (lastSection != null) {
			lastSection.setSectionEndLine(lineNumber);
		}
		return toReturn;
	}

	private SpecfileElement parseMacro(String lineText, Specfile specfile, int lineNumber) {
		// FIXME: handle other macros

		if (lineText.startsWith("%define") || lineText.startsWith("%global")) { //$NON-NLS-1$ //$NON-NLS-2$
			return parseDefine(lineText, specfile, lineNumber);
		} else if (lineText.startsWith("%patch")) { //$NON-NLS-1$
			return parsePatch(lineText, lineNumber);
		}

		String[] sections = new String[simpleSections.length + complexSections.length];
		System.arraycopy(simpleSections, 0, sections, 0, simpleSections.length);
		System.arraycopy(complexSections, 0, sections, simpleSections.length, complexSections.length);
		for (String section : sections) {
			if (lineText.startsWith(section)) {
				lastSection = parseSection(lineText, specfile, lineNumber);
				if (lastSection != null) {
					lastSection.setSectionEndLine(lineNumber + 1);
				}
				return lastSection;
			}
		}
		// FIXME: add handling of lines containing %{SOURCENNN}
		return null;
	}

	private SpecfileElement parsePatch(String lineText, int lineNumber) {

		SpecfilePatchMacro toReturn = null;

		List<String> tokens = Arrays.asList(lineText.split(SPACE_REGEX));

		for (String token : tokens) {
			// %patchN+
			try {
				if (token.startsWith("%patch")) { //$NON-NLS-1$
					int patchNumber = 0;
					if (token.length() > 6) {
						patchNumber = Integer.parseInt(token.substring(6));
					}
					toReturn = new SpecfilePatchMacro(patchNumber);
				}
			} catch (NumberFormatException e) {
				errorHandler.handleError(new SpecfileParseException(Messages.getString("SpecfileParser.5"), //$NON-NLS-1$
						lineNumber, 0, lineText.length(), IMarker.SEVERITY_ERROR));
				return null;
			}
		}

		return toReturn;
	}

	private SpecfileDefine parseDefine(String lineText, Specfile specfile, int lineNumber) {
		List<String> tokens = Arrays.asList(lineText.split(SPACE_REGEX));
		SpecfileDefine toReturn = null;
		for (Iterator<String> iter = tokens.iterator(); iter.hasNext();) {
			// Eat the actual "%define" or "%global" token
			iter.next();
			while (iter.hasNext()) {
				String defineName = iter.next();
				// FIXME: is this true? investigate in rpmbuild source
				// Definitions must being with a letter
				if (!Character.isLetter(defineName.charAt(0)) && (defineName.charAt(0) != '_')) {
					errorHandler.handleError(new SpecfileParseException(Messages.getString("SpecfileParser.6"), //$NON-NLS-1$
							lineNumber, 0, lineText.length(), IMarker.SEVERITY_ERROR));
					return null;
				} else {
					if (iter.hasNext()) {
						String defineStringValue = iter.next();
						// Defines that are more than one token
						if (iter.hasNext()) {
							defineStringValue = lineText.substring(lineText.indexOf(defineStringValue));
							// Eat up the rest of the tokens
							while (iter.hasNext()) {
								iter.next();
							}
						}
						int defineIntValue = -1;
						try {
							defineIntValue = Integer.parseInt(defineStringValue);
						} catch (NumberFormatException e) {
							toReturn = new SpecfileDefine(defineName, defineStringValue, specfile, null);
						}
						if (toReturn == null) {
							toReturn = new SpecfileDefine(defineName, defineIntValue, specfile, null);
						}
					} else {
						errorHandler.handleError(
								new SpecfileParseException(defineName + Messages.getString("SpecfileParser.14"), //$NON-NLS-1$
										lineNumber, 0, lineText.length(), IMarker.SEVERITY_ERROR));
					}
				}
			}
		}
		return toReturn;
	}

	private SpecfileElement parseComplexDefinition(String lineText, int lineNumber, SourceType sourceType) {
		SpecfileSource toReturn = null;
		List<String> tokens = Arrays.asList(lineText.split(SPACE_REGEX));
		int number = -1;
		boolean firstToken = true;

		for (Iterator<String> iter = tokens.iterator(); iter.hasNext();) {
			String token = iter.next();
			if (token != null && token.length() > 0) {
				if (firstToken) {
					if (token.endsWith(DEFINE_SEPARATOR)) {
						token = token.substring(0, token.length() - 1);
					} else {
						// FIXME: come up with a better error message here
						// FIXME: what about descriptions that begin a line with
						// the word "Source" or "Patch"?
						errorHandler.handleError(new SpecfileParseException(Messages.getString("SpecfileParser.8"), //$NON-NLS-1$
								lineNumber, 0, lineText.length(), IMarker.SEVERITY_WARNING));
						return null;
					}
					if (sourceType == SourceType.PATCH) {
						if (token.length() > 5) {
							number = Integer.parseInt(token.substring(5));
							if (!("patch" + number).equalsIgnoreCase(token)) { //$NON-NLS-1$
								errorHandler
										.handleError(new SpecfileParseException(Messages.getString("SpecfileParser.10"), //$NON-NLS-1$
												lineNumber, 0, lineText.length(), IMarker.SEVERITY_ERROR));
								return null;
							}
						} else {
							number = 0;
						}
					} else {
						if (token.length() > 6) {
							number = Integer.parseInt(token.substring(6));
							if (!("source" + number).equalsIgnoreCase(token)) { //$NON-NLS-1$
								errorHandler
										.handleError(new SpecfileParseException(Messages.getString("SpecfileParser.11"), //$NON-NLS-1$
												lineNumber, 0, lineText.length(), IMarker.SEVERITY_ERROR));
								return null;
							}
						} else {
							number = 0;
						}
					}
					toReturn = new SpecfileSource(number, ""); //$NON-NLS-1$
					toReturn.setSourceType(sourceType);
					firstToken = false;
				} else {
					// toReturn should never be null but check just in case
					if (toReturn != null) {
						toReturn.setFileName(token);
					}
					if (iter.hasNext()) {
						errorHandler.handleError(new SpecfileParseException(Messages.getString("SpecfileParser.12"), //$NON-NLS-1$
								lineNumber, 0, lineText.length(), IMarker.SEVERITY_ERROR));
					}
				}
			}
		}

		return toReturn;
	}

	private SpecfileElement parseSimpleDefinition(String lineText, Specfile specfile, int lineNumber,
			boolean warnMultipleValues) {
		List<String> tokens = Arrays.asList(lineText.split(SPACE_REGEX));
		SpecfileTag toReturn = null;

		for (Iterator<String> iter = tokens.iterator(); iter.hasNext();) {
			String token = iter.next();

			if (token.length() <= 0) {
				break;
			}

			if (iter.hasNext()) {
				String possValue = iter.next();
				if (possValue.startsWith("%") && iter.hasNext()) { //$NON-NLS-1$
					possValue += ' ' + iter.next();
				}
				toReturn = new SpecfileTag(token.substring(0, token.length() - 1).toLowerCase(), possValue, specfile,
						null);
				if (iter.hasNext() && !warnMultipleValues) {
					errorHandler.handleError(new SpecfileParseException(
							token.substring(0, token.length() - 1) + Messages.getString("SpecfileParser.13"), //$NON-NLS-1$
							lineNumber, 0, lineText.length(), IMarker.SEVERITY_ERROR));
					return null;
				}
			} else {
				errorHandler.handleError(new SpecfileParseException(
						token.substring(0, token.length() - 1) + Messages.getString("SpecfileParser.14"), lineNumber, //$NON-NLS-1$
						0, lineText.length(), IMarker.SEVERITY_ERROR));
				toReturn = null;
			}
		}
		if ((toReturn != null)) {
			String returnStringValue = toReturn.getStringValue();
			if (returnStringValue != null) {
				try {
					int intValue = Integer.parseInt(returnStringValue);
					toReturn.setValue(intValue);
				} catch (NumberFormatException e) {
					if (toReturn.getName().equalsIgnoreCase(RpmTags.EPOCH)) {
						errorHandler.handleError(
								new SpecfileParseException(Messages.getString("SpecfileParser.16"), lineNumber, //$NON-NLS-1$
										0, lineText.length(), IMarker.SEVERITY_ERROR));
						toReturn = null;
					}
				}
			}
		}
		return toReturn;
	}

	private SpecfileElement parseDirectDefinition(String lineText, Specfile specfile, int lineNumber) {
		String[] parts = lineText.split(DEFINE_SEPARATOR, 2);
		SpecfileTag directDefinition;
		if (parts.length == 2) {
			directDefinition = new SpecfileTag(parts[0], parts[1].trim(), specfile, activePackage);
			directDefinition.setLineNumber(lineNumber);
		} else {
			errorHandler.handleError(
					new SpecfileParseException(parts[0] + Messages.getString("SpecfileParser.14"), lineNumber, //$NON-NLS-1$
							0, lineText.length(), IMarker.SEVERITY_ERROR));
			directDefinition = null;
		}
		return directDefinition;
	}

	public void setErrorHandler(SpecfileErrorHandler specfileErrorHandler) {
		errorHandler = specfileErrorHandler;
	}

	public void setTaskHandler(SpecfileTaskHandler specfileTaskHandler) {
		taskHandler = specfileTaskHandler;
	}
}
