/*******************************************************************************
 * Copyright (c) 2004, 2015 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.core.tests.session;

import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.StringTokenizer;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.FactoryConfigurationError;
import javax.xml.parsers.ParserConfigurationException;
import org.eclipse.core.runtime.Platform;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NamedNodeMap;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.SAXException;

public class SetupManager {
	public static class SetupException extends Exception {
		/**
		 * All serializable objects should have a stable serialVersionUID
		 */
		private static final long serialVersionUID = 1L;

		public SetupException(String message, Throwable cause) {
			super(message, cause);
		}

		public SetupException(String message) {
			super(message);
		}
	}

	private static SetupManager instance;
	private static final String SETUP_DEBUG = "setup.debug";
	private static final String SETUP_FILES = "setup.files";
	private static final String SETUP_OPTIONS = "setup.options";
	private static final String SETUP_OVERRIDE_ECLIPSEARGS = "setup.override.eclipseArgs";
	private static final String SETUP_OVERRIDE_SYSTEMPROPERTIES = "setup.override.systemProperties";
	private static final String SETUP_OVERRIDE_VMARGS = "setup.override.vmArgs";
	private String defaultOptionSetIds = "";
	private Map<String, Setup> setupById;
	private Collection<Setup> setups;

	private static boolean contains(Object[] set, Object element) {
		for (Object setElement : set) {
			if (element.equals(setElement)) {
				return true;
			}
		}
		return false;
	}

	public synchronized static SetupManager getInstance() throws SetupException {
		if (instance != null) {
			return instance;
		}
		instance = new SetupManager();
		return instance;
	}

	public static boolean inDebugMode() {
		return Boolean.getBoolean(SETUP_DEBUG);
	}

	public static void main(String[] args) throws Exception {
		SetupManager manager = SetupManager.getInstance();
		System.out.println(manager.getDefaultSetup());
	}

	static String[] parseItems(String string) {
		if (string == null) {
			return new String[0];
		}
		StringTokenizer tokenizer = new StringTokenizer(string, ","); //$NON-NLS-1$
		if (!tokenizer.hasMoreTokens()) {
			return new String[0];
		}
		String first = tokenizer.nextToken().trim();
		if (!tokenizer.hasMoreTokens()) {
			return new String[] {first};
		}
		ArrayList<String> items = new ArrayList<>();
		items.add(first);
		do {
			items.add(tokenizer.nextToken().trim());
		} while (tokenizer.hasMoreTokens());
		return items.toArray(new String[items.size()]);
	}

	protected SetupManager() throws SetupException {
		setups = new ArrayList<>();
		setupById = new HashMap<>();
		try {
			loadSetups();
		} catch (SetupException e) {
			throw e;
		} catch (Exception e) {
			throw new SetupException("Problems initializing SetupManager", e);
		}
	}

	public Setup buildSetup(String[] optionSets) {
		Setup defaultSetup = Setup.getDefaultSetup(this);
		for (Setup customSetup : setups) {
			if ((customSetup.getId() == null || contains(optionSets, customSetup.getId())) && customSetup.isSatisfied(optionSets)) {
				defaultSetup.merge(customSetup);
			}
		}
		defaultSetup.setEclipseArguments(parseOptions(System.getProperty(SETUP_OVERRIDE_ECLIPSEARGS)));
		defaultSetup.setVMArguments(parseOptions(System.getProperty(SETUP_OVERRIDE_VMARGS)));
		defaultSetup.setSystemProperties(parseOptions(System.getProperty(SETUP_OVERRIDE_SYSTEMPROPERTIES)));
		return defaultSetup;
	}

	private String getAttribute(NamedNodeMap attributes, String name) {
		Node selected = attributes.getNamedItem(name);
		return selected == null ? null : selected.getNodeValue();
	}

	private String[] getDefaultOptionSets() {
		return parseItems(System.getProperty(SETUP_OPTIONS, defaultOptionSetIds));
	}

	/**
	 * Returns a brand new setup object configured according to the current
	 * default setup settings.
	 *
	 * @return a new setup object
	 */
	public Setup getDefaultSetup() {
		return buildSetup(getDefaultOptionSets());
	}

	public Setup getSetup(String id) {
		return setupById.get(id);
	}

	private void loadEclipseArgument(Setup newSetup, Element toParse) {
		newSetup.setEclipseArgument(toParse.getAttribute("option"), toParse.getAttribute("value"));
	}

	private void loadProperty(Setup newSetup, Element toParse) {
		newSetup.setSystemProperty(toParse.getAttribute("key"), toParse.getAttribute("value"));
	}

	private void loadSetup(Element markup) {
		NamedNodeMap attributes = markup.getAttributes();
		if (attributes == null) {
			return;
		}
		Setup newSetup = new Setup(this);
		newSetup.setId(getAttribute(attributes, "id"));
		newSetup.setName(getAttribute(attributes, "name"));
		String timeout = getAttribute(attributes, "timeout");
		newSetup.setBaseSetups(parseItems(getAttribute(attributes, "base")));
		newSetup.setRequiredSets(parseItems(getAttribute(attributes, "with")));

		if (timeout != null) {
			newSetup.setTimeout(Integer.parseInt(timeout));
		}
		NodeList children = markup.getChildNodes();
		for (int i = 0; i < children.getLength(); i++) {
			Node next = children.item(i);
			if (!(next instanceof Element)) {
				continue;
			}
			Element toParse = (Element) next;
			switch (toParse.getTagName()) {
			case "eclipseArg":
				loadEclipseArgument(newSetup, toParse);
				break;
			case "vmArg":
				loadVMArgument(newSetup, toParse);
				break;
			case "systemProperty":
				loadProperty(newSetup, toParse);
				break;
			default:
				break;
			}
		}
		setups.add(newSetup);
		if (newSetup.getId() != null) {
			setupById.put(newSetup.getId(), newSetup);
		}
	}

	private void loadSetups() throws ParserConfigurationException, FactoryConfigurationError, SAXException, IOException, SetupException {
		String setupFilesProperty = System.getProperty(SETUP_FILES);
		boolean defaultLocation = false;
		if (setupFilesProperty == null) {
			setupFilesProperty = "default-setup.xml";
			defaultLocation = true;
		}
		String[] setupFileNames = parseItems(setupFilesProperty);
		File[] setupFiles = new File[setupFileNames.length];
		int found = 0;
		for (int i = 0; i < setupFiles.length; i++) {
			setupFiles[found] = new File(setupFileNames[i]);
			if (!setupFiles[found].isFile()) {
				if (!defaultLocation) {
					// warn if user-provided location does not exist
					System.out.println("No setup files found at '" + setupFiles[i].getAbsolutePath() + "'. ");
				}
				continue;
			}
			found++;
		}
		if (found == 0) {
			if (Platform.isRunning()) {
				// No setup descriptions found, only the default setup will be available
				return;
			}
			// no setup files found, and we are not running in Eclipse...
			throw new SetupException(
					"No setup descriptions found. Run as Plug-in Test or ensure you are specifying the path for an existing setup file (e.g. -Dsetup.files=<setup-file-location1>[...,<setup-file-locationN>])");
		}
		@SuppressWarnings("restriction")
		DocumentBuilder docBuilder = org.eclipse.core.internal.runtime.XmlProcessorFactory
				.createDocumentBuilderWithErrorOnDOCTYPE();
		for (int fileIndex = 0; fileIndex < found; fileIndex++) {
			Document doc = docBuilder.parse(setupFiles[fileIndex]);
			Element root = doc.getDocumentElement();
			String setupDefaultOptionSets = root.getAttribute("default");
			if (setupDefaultOptionSets != null) {
				defaultOptionSetIds = defaultOptionSetIds == null ? setupDefaultOptionSets : (defaultOptionSetIds + ',' + setupDefaultOptionSets);
			}
			NodeList optionSets = root.getChildNodes();
			for (int i = 0; i < optionSets.getLength(); i++) {
				Node next = optionSets.item(i);
				if (!(next instanceof Element)) {
					continue;
				}
				Element toParse = (Element) next;
				if (!toParse.getTagName().equals("optionSet")) {
					continue;
				}
				loadSetup(toParse);
			}
		}
	}

	private void loadVMArgument(Setup newSetup, Element toParse) {
		newSetup.setVMArgument(toParse.getAttribute("option"), toParse.getAttribute("value"));
	}

	/*
	 * Use a double equal sign to escape and equal to treat it from becoming
	 * a key/value pair
	 */
	private static Map<String, String> parseOptions(String options) {
		if (options == null) {
			return Collections.EMPTY_MAP;
		}
		Map<String, String> result = new HashMap<>();
		StringTokenizer tokenizer = new StringTokenizer(options.trim(), ";");
		while (tokenizer.hasMoreTokens()) {
			String option = tokenizer.nextToken();
			int separatorIndex = option.indexOf('=');
			if (separatorIndex == -1 || separatorIndex == option.length() - 1) { // property with no value defined
				result.put(option, "");
				continue;
			}
			// the 90% case is that we won't have an escaped equals so check to see if we can short-circuit
			if (!option.contains("==")) {
				String key = option.substring(0, separatorIndex);
				String value = option.substring(separatorIndex + 1);
				result.put(key, value);
				continue;
			}
			// otherwise we have an escaped equals somewhere in this option
			int valueStart = -1;
			// strip out the key (first non-escaped equal)
			StringBuilder key = new StringBuilder();
			for (int i = 0; i < option.length(); i++) {
				char c = option.charAt(i);
				// if we don't have an equal sign, then just add it to the key
				if (c != '=') {
					key.append(c);
					continue;
				}
				i++;
				if (i >= option.length()) {
					break;
				}
				char next = option.charAt(i);
				if (next == '=') {
					key.append('=');
					continue;
				}
				// we had a single equal
				valueStart = i;
				break;
			}
			String value = "";
			// now get the value. replace == by =
			if (valueStart > -1) {
				value = option.substring(valueStart).replace("==", "=");
			}
			result.put(key.toString(), value);
		}
		return result;
	}
}
