/*******************************************************************************
 * Copyright (c) 2010, 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
 *******************************************************************************/
package org.eclipse.linuxtools.internal.oprofile.core.opxml;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.HashMap;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;

import org.eclipse.core.filesystem.EFS;
import org.eclipse.core.filesystem.IFileStore;
import org.eclipse.core.resources.IProject;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.NullProgressMonitor;
import org.eclipse.linuxtools.internal.oprofile.core.Oprofile;
import org.eclipse.linuxtools.internal.oprofile.core.opxml.info.InfoAdapter;
import org.eclipse.linuxtools.profiling.launch.IRemoteFileProxy;
import org.eclipse.linuxtools.profiling.launch.RemoteProxyManager;
import org.eclipse.linuxtools.tools.launch.core.factory.RuntimeProcessFactory;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NodeList;
import org.xml.sax.SAXException;

/**
 * Caches the event data used by the CheckEventAdapter. The performance
 * improvement is targeted at the first time a call with the given arguments is
 * made. The first given call to check-event will take roughly O(n), and all
 * other calls whether they be new or recurring take O(1). Note that recurring
 * calls are handled by an entirely different cache. This particular class
 * simply parses the XML from ophelp -X and stores it.
 */
public class EventIdCache {

	private static final String HELP_EVENTS = "help_events"; //$NON-NLS-1$
	private static final String HEADER = "header"; //$NON-NLS-1$
	private static final String SCHEMA = "schemaversion"; //$NON-NLS-1$
	private static final String CATEGORY = "category"; //$NON-NLS-1$
	private static final String OPHELP = "ophelp"; //$NON-NLS-1$
	private static final String EVENT = "event"; //$NON-NLS-1$
	private static final String EVENT_NAME = "event_name"; //$NON-NLS-1$

	private static final String LOCAL = "local"; //$NON-NLS-1$

	private Document eventDoc; // the document to hold the xml from ophelp
	private Element eventRoot; // the root corresponding to the xml from ophelp
	// name - the name of the event
	// Element - the DOM node
	private HashMap<String, Element> nameMap;

	// Map containing the caches for remote machines
	private static HashMap<String, EventIdCache> cacheMap;

	public static EventIdCache getInstance() {

		if (cacheMap == null) {
			cacheMap = new HashMap<>();
		}

		IProject project = Oprofile.OprofileProject.getProject();
		EventIdCache newCache = new EventIdCache();
		if (project != null) {
			EventIdCache eventIdCache = cacheMap.get(project.getLocationURI().getHost());
			if (eventIdCache == null) {
				cacheMap.put(project.getLocationURI().getHost(), newCache);
			} else {
				return eventIdCache;
			}
		} else {
			// If no project associated we should launch locally
			EventIdCache eventIdCache = cacheMap.get(LOCAL);
			if (eventIdCache == null) {
				cacheMap.put(LOCAL, newCache);
			} else {
				return eventIdCache;
			}
		}

		return newCache;
	}

	/**
	 * @param id the id corresponding to an event
	 * @return the DOM Element corresponding to the event tag
	 */
	public Element getElementWithName(String name) {
		IProject project = Oprofile.OprofileProject.getProject();
		EventIdCache eventIdCache;
		if (project != null) {
			eventIdCache = cacheMap.get(project.getLocationURI().getHost());
		} else {
			eventIdCache = cacheMap.get(LOCAL);
		}

		if (eventIdCache.nameMap == null) {
			readXML(eventIdCache);
			buildCache(eventIdCache);
		}
		return eventIdCache.nameMap.get(name) != null ? (Element) eventIdCache.nameMap.get(name) : null;

	}

	/**
	 * Build the cache
	 */
	private void buildCache(EventIdCache eventId) {
		eventId.nameMap = new HashMap<>();
		NodeList eventList = eventId.eventRoot.getElementsByTagName(EVENT);
		for (int i = 0; i < eventList.getLength(); i++) {
			Element elem = (Element) eventList.item(i);
			String eventName = elem.getAttribute(EVENT_NAME);
			eventId.nameMap.put(eventName, elem);
		}
	}

	/**
	 * Read the XML from ophelp
	 */
	private void readXML(EventIdCache eventId) {
		if (eventId.eventRoot != null) {
			return;
		}

		try {
			Process p = RuntimeProcessFactory.getFactory().exec(OPHELP + ' ' + "-X", //$NON-NLS-1$
					Oprofile.OprofileProject.getProject());

			DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
			DocumentBuilder builder;
			try {
				builder = factory.newDocumentBuilder();
				try {
					eventId.eventDoc = builder.parse(p.getInputStream());
					Element elem = (Element) eventId.eventDoc.getElementsByTagName(HELP_EVENTS).item(0);
					eventId.eventRoot = elem;
				} catch (IOException | SAXException e) {
				}
			} catch (ParserConfigurationException e1) {
				e1.printStackTrace();
			}

		} catch (IOException e) {
			e.printStackTrace();
		}
	}

	/**
	 * Get the unit mask type. Schema Version 1.1 and newer of ophelp XML will list
	 * the unit mask type as an attribute. Older version will not so we default to
	 * file lookups.
	 *
	 * @param name the name of the event
	 * @return the type of unit mask. This can be either mandatory, exclusive,
	 *         bitmask, or null if none could be found.
	 */
	public String getUnitMaskType(String name) {
		IProject project = Oprofile.OprofileProject.getProject();
		EventIdCache eventIdCache;
		if (project != null) {
			eventIdCache = cacheMap.get(project.getLocationURI().getHost());
		} else {
			eventIdCache = cacheMap.get(LOCAL);
		}

		if (eventIdCache.eventRoot == null) {
			readXML(eventIdCache);
			buildCache(eventIdCache);
		}

		Element header = (Element) eventIdCache.eventRoot.getElementsByTagName(HEADER).item(0);

		double schemaVersion = 0;

		if (!eventIdCache.eventRoot.getAttribute(SCHEMA).isEmpty()) {
			schemaVersion = Double.parseDouble(eventIdCache.eventRoot.getAttribute(SCHEMA));
		} else {
			schemaVersion = Double.parseDouble(header.getAttribute(SCHEMA));
		}

		String unitMaskType = null;
		IRemoteFileProxy proxy = null;
		try {
			proxy = RemoteProxyManager.getInstance().getFileProxy(Oprofile.OprofileProject.getProject());
		} catch (CoreException e) {
			e.printStackTrace();
		}

		// Schema Version > 1.0 has the unit mask type within the XML
		if (schemaVersion > 1.0) {
			Element event = getElementWithName(name);
			Element unitMaskTag = (Element) event.getElementsByTagName(InfoAdapter.UNIT_MASKS).item(0);
			return unitMaskTag.getAttribute(CATEGORY);
		} else {
			IFileStore fileStore = proxy.getResource(InfoAdapter.CPUTYPE);
			try (InputStream fileInputStream = fileStore.openInputStream(EFS.NONE, new NullProgressMonitor());
					BufferedReader bi = new BufferedReader(new InputStreamReader(fileInputStream))) {

				String cpuType = bi.readLine();
				IFileStore opArchEvents = proxy.getResource(InfoAdapter.OP_SHARE + cpuType + "/" + InfoAdapter.EVENTS); //$NON-NLS-1$
				IFileStore opArchUnitMasks = proxy
						.getResource(InfoAdapter.OP_SHARE + cpuType + "/" + InfoAdapter.UNIT_MASKS); //$NON-NLS-1$

				try (InputStream inputStreamEvents = opArchEvents.openInputStream(EFS.NONE, new NullProgressMonitor());
						BufferedReader eventReader = new BufferedReader(new InputStreamReader(inputStreamEvents))) {
					String line;
					while ((line = eventReader.readLine()) != null) {
						// find the line with the event name
						if (line.contains("name:" + name + ' ')) { //$NON-NLS-1$
							int start = line.indexOf("um:") + 3; //$NON-NLS-1$
							int end = line.indexOf(' ', start);
							// grab the string that references the unit mask type
							String um = line.substring(start, end);
							try (InputStream inputStreamMasks = opArchUnitMasks.openInputStream(EFS.NONE,
									new NullProgressMonitor());
									BufferedReader unitMaskReader = new BufferedReader(
											new InputStreamReader(inputStreamMasks))) {
								while ((line = unitMaskReader.readLine()) != null) {
									if (line.contains("name:" + um + ' ')) { //$NON-NLS-1$
										start = line.indexOf("type:") + 5; //$NON-NLS-1$
										end = line.indexOf(' ', start);
										unitMaskType = line.substring(start, end);
										return unitMaskType;
									}
								}
							}
						}
					}
				}
			} catch (IOException | CoreException e) {
			}
		}
		return unitMaskType;
	}

	/**
	 * @since 3.0
	 */
	public void setCacheDoc(Element oldRoot) {
		eventRoot = oldRoot;
	}
}
