/*******************************************************************************
 * 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.modeldata;

import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.HashMap;
import java.util.TreeSet;

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

import org.eclipse.linuxtools.internal.oprofile.core.opxml.AbstractDataAdapter;
import org.eclipse.linuxtools.internal.oprofile.core.opxml.info.InfoAdapter;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NodeList;
import org.xml.sax.SAXException;

/**
 * This class takes the XML that is output from 'opreport -X --details' for the
 * current session, and uses that data to modify it into the format expected by
 * the SAX parser.
 */
public class ModelDataAdapter extends AbstractDataAdapter {

	public final static String ID = "id"; //$NON-NLS-1$
	public final static String IDREF = "idref"; //$NON-NLS-1$
	public final static String NAME = "name"; //$NON-NLS-1$
	public final static String COUNT = "count"; //$NON-NLS-1$
	public final static String SAMPLE = "sample"; //$NON-NLS-1$
	public final static String LINE = "line"; //$NON-NLS-1$

	public final static String SYMBOL_DATA = "symboldata"; //$NON-NLS-1$
	public final static String SYMBOL_DETAILS = "symboldetails"; //$NON-NLS-1$
	public final static String SYMBOL = "symbol"; //$NON-NLS-1$

	public final static String FILE = "file"; //$NON-NLS-1$

	public final static String SETUP = "setup"; //$NON-NLS-1$
	public final static String EVENT_SETUP = "eventsetup"; //$NON-NLS-1$
	public final static String TIMER_SETUP = "timersetup"; //$NON-NLS-1$
	public final static String SETUP_COUNT = "setupcount"; //$NON-NLS-1$
	public final static String EVENT_NAME = "eventname"; //$NON-NLS-1$
	public final static String RTC_INTERRUPTS = "rtcinterrupts"; //$NON-NLS-1$

	public final static String PROFILE = "profile"; //$NON-NLS-1$
	public final static String MODEL_DATA = "model-data"; //$NON-NLS-1$

	public final static String MODULE = "module"; //$NON-NLS-1$
	public final static String DEPENDENT = "dependent"; //$NON-NLS-1$

	public final static String BINARY = "binary"; //$NON-NLS-1$
	public final static String IMAGE = "image"; //$NON-NLS-1$

	public final static String SYMBOLS = "symbols"; //$NON-NLS-1$
	public final static String SYMBOL_TABLE = "symboltable"; //$NON-NLS-1$
	public final static String DETAIL_TABLE = "detailtable"; //$NON-NLS-1$

	public final static String DETAIL_DATA = "detaildata"; //$NON-NLS-1$

	private boolean isParseable;
	private Document newDoc; // the document we intend to build
	private Element oldRoot; // the root of the document with data from opreport
	private Element newRoot; // the root of the document we intent to build

	/**
	 * Constructor to the ModelAdapter class
	 * 
	 * @param is The input stream to be parsed
	 */
	public ModelDataAdapter(InputStream is) {
		isParseable = true;
		DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
		DocumentBuilder builder;
		try {
			builder = factory.newDocumentBuilder();
			try {
				Document oldDoc = builder.parse(is);
				Element elem = (Element) oldDoc.getElementsByTagName(PROFILE).item(0);
				oldRoot = elem;

				newDoc = builder.newDocument();
				newRoot = newDoc.createElement(MODEL_DATA);
				newDoc.appendChild(newRoot);
			} catch (IOException e) {
				isParseable = false;
			} catch (SAXException e) {
				isParseable = false;
			}
		} catch (ParserConfigurationException e1) {
			e1.printStackTrace();
		}
	}

	@Override
	public void process() {
		createXML();
	}

	private void createXML() {

		// get the binary name and the image count
		Element oldImage = (Element) oldRoot.getElementsByTagName(BINARY).item(0);
		Element newImage = newDoc.createElement(IMAGE);

		String binName = oldImage.getAttribute(NAME);
		newImage.setAttribute(NAME, binName);

		Element countTag = (Element) oldImage.getElementsByTagName(COUNT).item(0);
		String imageCount = countTag.getTextContent().trim();
		newImage.setAttribute(COUNT, imageCount);

		// There is no setup count in timer mode
		if (!InfoAdapter.hasTimerSupport()) {
			// get the count that was used to profile
			Element setupTag = (Element) oldRoot.getElementsByTagName(SETUP).item(0);
			Element eventSetupTag = (Element) setupTag.getElementsByTagName(EVENT_SETUP).item(0);
			String setupcount = eventSetupTag.getAttribute(SETUP_COUNT);
			newImage.setAttribute(SETUP_COUNT, setupcount);
		}

		// these elements contain the data needed to populate the new symbol table
		Element oldSymbolTableTag = (Element) oldRoot.getElementsByTagName(SYMBOL_TABLE).item(0);
		NodeList oldSymbolDataList = oldSymbolTableTag.getElementsByTagName(SYMBOL_DATA);

		Element oldDetailTableTag = (Element) oldRoot.getElementsByTagName(DETAIL_TABLE).item(0);
		NodeList oldDetailTableList = oldDetailTableTag.getElementsByTagName(SYMBOL_DETAILS);

		// parse the data into HashMaps for O(1) lookup time, as opposed to O(n).
		HashMap<String, HashMap<String, String>> oldSymbolDataListMap = parseDataList(oldSymbolDataList);
		HashMap<String, NodeList> oldDetailTableListMap = parseDetailTable(oldDetailTableList);

		// An ArrayList to hold the binary and other modules
		ArrayList<Element> oldImageList = new ArrayList<>();
		// The first element is the original binary!
		oldImageList.add(oldImage);

		NodeList oldModuleList = oldImage.getElementsByTagName(MODULE);
		// Set up the dependent tag for any modules run by this binary
		Element dependentTag = newDoc.createElement(DEPENDENT);
		if (oldModuleList.getLength() > 0) {
			dependentTag.setAttribute(COUNT, "0"); //$NON-NLS-1$

			for (int t = 0; t < oldModuleList.getLength(); t++) {
				oldImageList.add((Element) oldModuleList.item(t));
			}
		}

		// iterate through all (binary/modules)
		for (Element oldImg : oldImageList) {
			Element newImg;
			if (oldImg.getTagName().equals(BINARY)) {
				newImg = newImage;
			} else {
				newImg = newDoc.createElement(IMAGE);

				String imgName = oldImg.getAttribute(NAME);
				newImg.setAttribute(NAME, imgName);

				Element modCountTag = (Element) oldImg.getElementsByTagName(COUNT).item(0);
				String imgCount = modCountTag.getTextContent().trim();
				newImg.setAttribute(COUNT, imgCount);
			}

			Element newSymbolsTag = newDoc.createElement(SYMBOLS);

			// these elements contain the data needed to populate the new symbol table
			NodeList oldSymbolList = oldImg.getElementsByTagName(SYMBOL);

			// iterate through all symbols
			for (int i = 0; i < oldSymbolList.getLength(); i++) {
				Element oldSymbol = (Element) oldSymbolList.item(i);

				/**
				 * The original binary is a parent for all symbols We only want library function
				 * calls under their respective modules, and not under the original binary as
				 * well.
				 */
				if (!oldSymbol.getParentNode().isSameNode(oldImg)) {
					continue;
				}

				Element newSymbol = newDoc.createElement(SYMBOL);
				String idref = oldSymbol.getAttribute(IDREF);
				String symbolCount = ((Element) oldSymbol.getElementsByTagName(COUNT).item(0)).getTextContent().trim();
				newSymbol.setAttribute(COUNT, symbolCount);

				// get the symboltable entry corresponding to the id of this symbol
				HashMap<String, String> symbolData = oldSymbolDataListMap.get(idref);
				newSymbol.setAttribute(NAME, symbolData.get(NAME));
				newSymbol.setAttribute(FILE, symbolData.get(FILE));
				newSymbol.setAttribute(LINE, symbolData.get(LINE));

				// get the symboldetails entry corresponding to the id of this symbol
				NodeList detailDataList = oldDetailTableListMap.get(idref);

				// go through the detail data of each symbol's details
				HashMap<String, Element> tmp = new HashMap<>();
				// temporary place to store the elements for sorting
				TreeSet<Element> sorted = new TreeSet<>(SAMPLE_COUNT_ORDER);
				for (int l = 0; l < detailDataList.getLength(); l++) {

					Element detailData = (Element) detailDataList.item(l);
					String sampleFile = detailData.getAttribute(FILE);
					String sampleLine = detailData.getAttribute(LINE);

					// The sample has a line number but no file
					// This means that the file is the same as the symbol (parent)
					if (sampleFile.equals("") && !sampleLine.isEmpty()) { //$NON-NLS-1$ $NON-NLS-2$
						sampleFile = symbolData.get(FILE);
					} else {
						if (sampleFile.equals("")) { //$NON-NLS-1$
							sampleFile = "??"; //$NON-NLS-1$
						}
						if (sampleLine.equals("")) { //$NON-NLS-1$
							sampleLine = "0"; //$NON-NLS-1$
						}
					}
					Element detailDataCount = (Element) detailData.getElementsByTagName(COUNT).item(0);
					String count = detailDataCount.getTextContent().trim();

					// if a sample at this line already exists then increase count for that line.
					if (tmp.containsKey(sampleLine)) {
						Element elem = (Element) tmp.get(sampleLine).getElementsByTagName(COUNT).item(0);
						int val = Integer.parseInt(elem.getTextContent().trim()) + Integer.parseInt(count);
						elem.setTextContent(String.valueOf(val));
					} else {
						Element sampleTag = newDoc.createElement(SAMPLE);

						Element fileTag = newDoc.createElement(FILE);
						fileTag.setTextContent(sampleFile);

						Element lineTag = newDoc.createElement(LINE);
						lineTag.setTextContent(sampleLine);

						Element sampleCountTag = newDoc.createElement(COUNT);
						sampleCountTag.setTextContent(count);

						sampleTag.appendChild(fileTag);
						sampleTag.appendChild(lineTag);
						sampleTag.appendChild(sampleCountTag);

						tmp.put(sampleLine, sampleTag);
					}
				}

				// add the elements to the sorter
				for (Element elem : tmp.values()) {
					sorted.add(elem);
				}

				// append the elements in sorted order
				for (Element e : sorted) {
					newSymbol.appendChild(e);
				}

				newSymbolsTag.appendChild(newSymbol);
			}

			newImg.appendChild(newSymbolsTag);
			// If this is a module, attach it to the dependent tag
			if (oldImg.getTagName().equals(MODULE)) {
				dependentTag.appendChild(newImg);
				int currVal = Integer.parseInt(dependentTag.getAttribute(COUNT));
				int val = Integer.parseInt(newImg.getAttribute(COUNT));
				dependentTag.setAttribute(COUNT, String.valueOf(currVal + val));
			} else {
				newRoot.appendChild(newImg);
			}
		}

		if (oldModuleList.getLength() > 0) {
			newImage.appendChild(dependentTag);
		}

	}

	/**
	 *
	 * @param oldDetailTableList the list of 'symboldetails' tags within detailtable
	 * @return a HashMap where the key is a function id and the value is a NodeList
	 *         containing a list of the 'detaildata' tags that contain sample
	 *         information.
	 */
	private HashMap<String, NodeList> parseDetailTable(NodeList oldDetailTableList) {
		HashMap<String, NodeList> ret = new HashMap<>();
		for (int i = 0; i < oldDetailTableList.getLength(); i++) {
			Element symbolDetails = (Element) oldDetailTableList.item(i);
			String id = symbolDetails.getAttribute(ID);
			NodeList detailDataList = symbolDetails.getElementsByTagName(DETAIL_DATA);
			ret.put(id, detailDataList);
		}
		return ret;
	}

	/**
	 *
	 * @param oldSymbolDataList the list of 'symboldata' tags within symboltable
	 * @return a Hashmap where the key is a function id and the value is a HashMap
	 *         with various parameters of data
	 */
	private HashMap<String, HashMap<String, String>> parseDataList(NodeList oldSymbolDataList) {
		HashMap<String, HashMap<String, String>> ret = new HashMap<>();
		for (int j = 0; j < oldSymbolDataList.getLength(); j++) {
			HashMap<String, String> tmp = new HashMap<>();
			Element symbolData = (Element) oldSymbolDataList.item(j);
			String id = symbolData.getAttribute(ID);
			String name = symbolData.getAttribute(NAME);
			String file = symbolData.getAttribute(FILE);
			if (file.equals("")) { //$NON-NLS-1$
				file = "??"; //$NON-NLS-1$
			}
			String line = symbolData.getAttribute(LINE);
			if (line.equals("")) { //$NON-NLS-1$
				line = "0"; //$NON-NLS-1$
			}
			tmp.put(NAME, name);
			tmp.put(FILE, file);
			tmp.put(LINE, line);
			ret.put(id, tmp);
		}
		return ret;
	}

	@Override
	public Document getDocument() {
		return newDoc;
	}

	/**
	 * Helper class to sort the samples of a given symbol in descending order from
	 * largest to smallest
	 */
	private static final Comparator<Element> SAMPLE_COUNT_ORDER = new Comparator<Element>() {
		@Override
		public int compare(Element a, Element b) {
			// sort from largest to smallest count in descending order
			// items with the same count are sorted by line number from smallest
			// to largest in descending order
			Element a_countTag = (Element) a.getElementsByTagName(COUNT).item(0);
			Element b_countTag = (Element) b.getElementsByTagName(COUNT).item(0);
			Element a_LineTag = (Element) a.getElementsByTagName(LINE).item(0);
			Element b_LineTag = (Element) b.getElementsByTagName(LINE).item(0);

			Integer a_count = Integer.parseInt(a_countTag.getTextContent().trim());
			Integer b_count = Integer.parseInt(b_countTag.getTextContent().trim());
			Integer a_line = Integer.parseInt(a_LineTag.getTextContent().trim());
			Integer b_line = Integer.parseInt(b_LineTag.getTextContent().trim());

			if (a_count.compareTo(b_count) == 0) {
				return a_line.compareTo(b_line);
			}
			return -a_count.compareTo(b_count);
		}
	};

	/**
	 * Returns if parseable
	 * 
	 * @return isParseable boolean variable
	 */
	public boolean isParseable() {
		return isParseable;
	}

}
