/*******************************************************************************
 * Copyright (c) 2013 Red Hat, Inc.
 * 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:
 *     Neil Guzman - ruby implementation (B#350066)
 *******************************************************************************/
package org.eclipse.linuxtools.internal.rpmstubby.parser;

import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.eclipse.core.resources.IFile;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.linuxtools.internal.rpmstubby.StubbyLog;

/**
 * Class to parse a Ruby gemspec to grab specfile properties
 *
 */
public class RubyGemParser {

	private IFile file;
	private String gemVariable;
	private Map<String, List<String>> mSetupDefinitions;
	private Map<String, ArrayList<String>> mSetupDependencies;

	private static final String SETUP_START = "^gem[:\\.]{1,2}specification[:\\.]{1,2}new(\\s+)?do(\\s+)?\\|(\\s+)?(\\w+)(\\s+)?\\|";
	private static final String STRING = "(?:\\\"([^\\\"]+)\\\"|'([^']+)'|(?:%q|%Q)(?:([^\\w])([^/3].+)[^\\w]))";		// (%q|%Q) "value" | 'value' | {value}
	private static final String LIST =  	"(?!%q|%Q)(?:%w|%W)?(?:([\\W])(.+)[\\W])"; 									// (%w|%W) [value, value2] | {"value", "value2"}
	private static final String STRING_LIST = "(?:\\\"([^\\\"]+)\\\"|'([^']+)')(?:[, ])??";								// "test", "test2" | 'test' 'test2'
	private static final String GENERIC_LIST = "(?:\\S+)(?:\\s+)??"; 													// test, test2 | test test2

	private static final String REPLACE_ME = "(%REPLACE_ME)";
	private String simpleDefineRx = "(\\s+)?(?!#)(?:\\b(%REPLACE_ME)\\b\\.(\\w+))(\\s+)?=(?!=)(\\s+)?(.*)"; 		// gem.variable = ...
	private String genericDefineRx = "(\\s+)?(?!#)(?:\\b(%REPLACE_ME)\\b\\.(\\w+))(\\s+)?(.*)"; 					// gem.variable...
	private String simpleFunctionRx = "(\\s+)?(?!#)(?:\\b(%REPLACE_ME)\\b\\.(\\w+))(\\s+)?(?:\\((.*)\\))(.*)?"; 	// gem.variable(...)

	/**
	 * Initialize
	 *
	 * @param file
	 *            The gemspec file
	 * @throws IOException Could not read from file
	 * @throws CoreException File is not valid
	 */
	public RubyGemParser(IFile file) throws IOException, CoreException {
		mSetupDefinitions = new HashMap<String, List<String>>();
		mSetupDependencies = new HashMap<String, ArrayList<String>>();
		if (file.getContents().available() <= 0) {
			return;
		}
		this.file = file;
		gemVariable = "";
		parse();
	}

	/**
	 * Parse the Ruby gemspec file
	 *
	 */
	public void parse() {
		List<String> rawSetupDefinitions = new ArrayList<String>();
		List<String> lSetupDefinitions = new ArrayList<String>();
		String line = "";
		long startPos;
		long endPos;
		try {
			RandomAccessFile raf = new RandomAccessFile(file.getRawLocation()
					.makeAbsolute().toFile(), "r");
			startPos = findStart(raf);
			endPos = findEnd(raf, startPos);

			raf.seek(startPos);
			while ((line = raf.readLine()) != null
					&& raf.getFilePointer() < endPos) {
				rawSetupDefinitions.add(line);
			}

			lSetupDefinitions = prepareOptions(rawSetupDefinitions);

			for (String str : lSetupDefinitions) {
				parseLine(str);
			}
			raf.close();
		} catch (FileNotFoundException e) {
			StubbyLog.logError(e);
		} catch (IOException e) {
			StubbyLog.logError(e);
		}
	}

	/**
	 * Get the values taken from parsing the file as a list object
	 *
	 * @param key
	 *            The gemspec option to get the values for
	 * @return The values of the gemspec option
	 */
	public List<String> getValueList(String key) {
		List<String> rc = new ArrayList<String>();

		if (mSetupDependencies.containsKey(key)) {
			if (!mSetupDependencies.get(key).isEmpty()) {
				for (String element : mSetupDependencies.get(key)) {
					rc.add(element);
				}
			}
		} else if (mSetupDefinitions.containsKey(key)) {
			if (!mSetupDefinitions.get(key).isEmpty()) {
				for (String element : mSetupDefinitions.get(key)) {
					rc.add(element);
				}
			}
		}

		return rc;
	}

	/**
	 * Parses a line to figure out what type of line it is
	 *
	 * @param str
	 *            The line to parse
	 */
	private void parseLine(String str) {
		if (str.matches(simpleDefineRx)) {
			parseSimpleDefine(str);
		} else if (str.matches(simpleFunctionRx)) {
			parseSimpleFunction(str);
		} else if (str.matches(genericDefineRx)) {
			parseGenericOption(str);
		}
	}

	/**
	 * Parses a string to figure its value
	 *
	 * @param str
	 *            The string parse
	 * @return A list of objects that was found
	 */
	private static List<String> parseValue(String str) {
		List<String> rc = new ArrayList<String>();
		String temp = str.trim();
		Pattern pattern = null;
		Matcher variableMatcher = null;

		if (temp.matches(STRING)) {
			pattern = Pattern.compile(STRING,
					Pattern.CASE_INSENSITIVE);
			variableMatcher = pattern.matcher(temp);
			// "" matches group 1
			if (temp.startsWith("\"") && variableMatcher.matches()) {
				rc.add(variableMatcher.group(1));
			} // '' matches group 2
			else if (temp.startsWith("'") && variableMatcher.matches()) {
				rc.add(variableMatcher.group(2));
			} // %q|%Q match
			else if ((temp.startsWith("%q") || temp.startsWith("%Q"))
					&& variableMatcher.matches()) {
				rc.add(variableMatcher.group(4));
			}
		} else if (temp.matches(LIST)) {
			pattern = Pattern.compile(LIST,
					Pattern.CASE_INSENSITIVE);
			variableMatcher = pattern.matcher(temp);
			if (variableMatcher.matches()) {
				rc.addAll(parseList(variableMatcher.group(2)));
			}
		}

		return rc;
	}

	/**
	 * Parse the long string into a list
	 *
	 * @param str
	 *            The string to parse into a list
	 * @return A list containing the found values
	 */
	private static List<String> parseList(String str) {
		List<String> rc = new ArrayList<String>();
		String temp = str.trim();
		Pattern pattern = isPatternFoundList(str);

		if (pattern == null) {
			return rc;
		}

		Matcher variableMatcher = pattern.matcher(temp);

		if (variableMatcher != null) {
			while (variableMatcher.find()) {
				rc.add(variableMatcher.group());
			}
		}
		return rc;
	}

	/**
	 * Check if the string is a list of string values e.g. ["test", "test2"] OR
	 * ['test' 'test2']
	 *
	 * @param str
	 *            The string containing the list
	 * @return The pattern of the string
	 */
	private static Pattern isPatternFoundList(String str) {
		Pattern rc = Pattern.compile("");
		Pattern pattern = Pattern.compile(STRING_LIST,
				Pattern.CASE_INSENSITIVE);
		Matcher variableMatcher = pattern.matcher(str);

		if (variableMatcher.find()) {
			rc = pattern;
		} else {
			rc = isGenericFoundList(str);
		}

		return rc;
	}

	/**
	 * Check if the string is a list of generic values (non-strings) e.g. [test,
	 * test2] OR [test test2]
	 *
	 * @param str
	 *            The string containing the list
	 * @return The pattern of the string
	 */
	private static Pattern isGenericFoundList(String str) {
		Pattern rc = Pattern.compile("");
		Pattern pattern = Pattern.compile(GENERIC_LIST,
				Pattern.CASE_INSENSITIVE);
		Matcher variableMatcher = pattern.matcher(str);

		if (variableMatcher.find()) {
			rc = pattern;
		}

		return rc;
	}

	/**
	 * Parse and grab the value of a simple define. The value taken is anything
	 * after "gem.variable = ..."
	 *
	 * @param str
	 *            The simple define to parse
	 */
	private void parseSimpleDefine(String str) {
		String temp = str.trim();
		Pattern pattern = Pattern.compile(simpleDefineRx,
				Pattern.CASE_INSENSITIVE);
		Matcher variableMatcher = pattern.matcher(temp);

		if (variableMatcher.find()) {
			String optionName = variableMatcher.group(2);
			List<String> value = parseValue(variableMatcher
					.group(5));
			if (!value.isEmpty()) {
				mSetupDefinitions.put(optionName, value);
			}
		}
	}

	/**
	 * Parse and grab the value of a simple function. The value taken is
	 * anything in "gem.variable(...)"
	 *
	 * @param str
	 *            The function to parse
	 */
	private void parseSimpleFunction(String str) {
		String temp = str.trim();
		Pattern pattern = Pattern.compile(simpleFunctionRx,
				Pattern.CASE_INSENSITIVE);
		Matcher variableMatcher = pattern.matcher(temp);

		if (variableMatcher.find()) {
			String functionName = variableMatcher.group(2);
			ArrayList<String> dependencies = new ArrayList<String>();
			dependencies.add(variableMatcher.group(4));
			if (!mSetupDependencies.containsKey(functionName)) {
				mSetupDependencies.put(functionName, dependencies);
			} else {
				if (!mSetupDependencies.get(functionName).containsAll(
						dependencies)) {
					mSetupDependencies.get(functionName).addAll(dependencies);
				}
			}
		}
	}

	/**
	 * Parse and grab the value of a generic option. The value taken is anything
	 * after "gem.variable..."
	 *
	 * @param str
	 *            The option to parse
	 */
	private void parseGenericOption(String str) {
		String temp = str.trim();
		Pattern pattern = Pattern.compile(genericDefineRx,
				Pattern.CASE_INSENSITIVE);
		Matcher variableMatcher = pattern.matcher(temp);

		if (variableMatcher.find()) {
			String functionName = variableMatcher.group(2);
			ArrayList<String> dependencies = new ArrayList<String>();
			dependencies.add(variableMatcher.group(4));
			if (!mSetupDependencies.containsKey(functionName)) {
				mSetupDependencies.put(functionName, dependencies);
			} else {
				if (!mSetupDependencies.get(functionName).containsAll(
						dependencies)) {
					mSetupDependencies.get(functionName).addAll(dependencies);
				}
			}
		}
	}

	/**
	 * Prepare the options within the specification by taking multiple lines and
	 * concatenating them to the option it belongs to
	 *
	 * @param list
	 *            The list of options in their raw form
	 * @return A refined list of options with a single line for each option
	 */
	private List<String> prepareOptions(List<String> list) {
		List<String> rc = new ArrayList<String>();
		String temp = "";

		for (String str : list) {
			temp = str.trim();
			if (isLineValidOption(temp)) {
				rc.add(str);
			} else if (!temp.startsWith("#") && !rc.isEmpty()) {
				rc.set(rc.size() - 1, rc.get(rc.size() - 1).concat(str));
			}
		}

		return rc;
	}

	/**
	 * Check to see if the line being read is a valid option within the
	 * specification
	 *
	 * @param line
	 *            The line to check
	 * @return True if the option within the specification is valid
	 */
	private boolean isLineValidOption(String line) {
		boolean rc = false;
		Pattern pattern = Pattern.compile(genericDefineRx,
				Pattern.CASE_INSENSITIVE);
		Matcher variableMatcher = pattern.matcher(line);

		if (variableMatcher.matches()) {
			rc = true;
		}

		return rc;
	}

	/**
	 * Find the offset of when the specification starts and also store the
	 * gemVariable used in the specification
	 *
	 * @param reader
	 *            The file reader
	 * @return The position of the start of the specification
	 * @throws IOException
	 */
	private long findStart(RandomAccessFile raf) throws IOException {
		long rc = -1;
		Pattern pattern = Pattern
				.compile(
						SETUP_START,
						Pattern.CASE_INSENSITIVE);
		Matcher variableMatcher = null;
		String line = "";

		raf.seek(0);
		while ((line = raf.readLine()) != null && rc == -1) {
			variableMatcher = pattern.matcher(line.trim());
			if (variableMatcher.matches()) {
				setGemVariable(variableMatcher.group(4));
				rc = raf.getFilePointer();
			}
		}

		return rc;
	}

	/**
	 * Find the offset of when specification ends based on the position before
	 * the end statement
	 *
	 * @param reader
	 *            The file reader
	 * @param startPosition
	 *            The position of the start of the specification
	 * @return The position of the end of the specification
	 * @throws IOException
	 */
	private static long findEnd(RandomAccessFile raf, long startPos)
			throws IOException {
		long rc = -1;
		Pattern pattern = Pattern.compile("^end", Pattern.CASE_INSENSITIVE);
		Matcher variableMatcher = null;
		String line = "";

		raf.seek(startPos);
		while ((line = raf.readLine()) != null && rc == -1) {
			variableMatcher = pattern.matcher(line.trim());
			if (variableMatcher.matches()) {
				rc = raf.getFilePointer();
			}
		}

		return rc;
	}

	/**
	 * Sets the gemVariable taken from Gem::Specification.new do |gemVariable|
	 *
	 * @param str
	 *            The variable to set the gemVariable as
	 */
	private void setGemVariable(String str) {
		if (gemVariable.isEmpty()) {
			gemVariable = str.trim();
			simpleDefineRx = simpleDefineRx.replace(REPLACE_ME, gemVariable);
			simpleFunctionRx = simpleFunctionRx.replace(REPLACE_ME, gemVariable);
			genericDefineRx = genericDefineRx.replace(REPLACE_ME, gemVariable);
		}
	}
}
