/*******************************************************************************
 * Copyright (c) 2011, 2013 Anton Gorenkov and others.
 * 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:
 *     Anton Gorenkov - initial API and implementation
 *     Marc-Andre Laperle (Ericsson)
 *******************************************************************************/
package org.eclipse.cdt.testsrunner.internal.boost;

import java.text.MessageFormat;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.Stack;

import org.eclipse.cdt.testsrunner.model.ITestModelUpdater;
import org.eclipse.cdt.testsrunner.model.ITestItem;
import org.eclipse.cdt.testsrunner.model.ITestMessage;
import org.eclipse.cdt.testsrunner.model.ITestItem.Status;
import org.xml.sax.Attributes;
import org.xml.sax.SAXException;
import org.xml.sax.SAXParseException;
import org.xml.sax.helpers.DefaultHandler;


/**
 * Parses the Boost.Test XML log and notifies the Tests Runner Core about how
 * the testing process is going.
 */
public class BoostXmlLogHandler extends DefaultHandler {
	
	// Boost.Test XML log tags
	private static final String XML_NODE_TEST_LOG = "TestLog"; //$NON-NLS-1$
	private static final String XML_NODE_TEST_SUITE = "TestSuite"; //$NON-NLS-1$
	private static final String XML_NODE_TEST_CASE = "TestCase"; //$NON-NLS-1$
	private static final String XML_NODE_TESTING_TIME = "TestingTime"; //$NON-NLS-1$
	private static final String XML_NODE_LAST_CHECKPOINT = "LastCheckpoint"; //$NON-NLS-1$
	
	// Boost.Test XML log message levels representation
	private static final String XML_NODE_INFO = "Info"; //$NON-NLS-1$
	private static final String XML_NODE_MESSAGE = "Message"; //$NON-NLS-1$
	private static final String XML_NODE_WARNING = "Warning"; //$NON-NLS-1$
	private static final String XML_NODE_ERROR = "Error"; //$NON-NLS-1$
	private static final String XML_NODE_FATAL_ERROR = "FatalError"; //$NON-NLS-1$
	private static final String XML_NODE_EXCEPTION = "Exception"; //$NON-NLS-1$
	
	// Boost.Test XML log attributes
	private static final String XML_ATTR_TEST_SUITE_NAME = "name"; //$NON-NLS-1$
	private static final String XML_ATTR_TEST_CASE_NAME = "name"; //$NON-NLS-1$
	private static final String XML_ATTR_MESSAGE_FILE = "file"; //$NON-NLS-1$
	private static final String XML_ATTR_MESSAGE_LINE = "line"; //$NON-NLS-1$

	/** Maps the string message level representation to the Tests Runner internal enum code. */
    private static final Map<String, ITestMessage.Level> STRING_TO_MESSAGE_LEVEL;
    static {
        Map<String, ITestMessage.Level> aMap = new HashMap<String, ITestMessage.Level>();
        aMap.put(XML_NODE_INFO, ITestMessage.Level.Info);
        aMap.put(XML_NODE_MESSAGE, ITestMessage.Level.Message);
        aMap.put(XML_NODE_WARNING, ITestMessage.Level.Warning);
        aMap.put(XML_NODE_ERROR, ITestMessage.Level.Error);
        aMap.put(XML_NODE_FATAL_ERROR, ITestMessage.Level.FatalError);
        // NOTE: Exception node is processed separately
        STRING_TO_MESSAGE_LEVEL = Collections.unmodifiableMap(aMap);
    }

    /** The default file name for test message location. */
	private static final String DEFAULT_LOCATION_FILE = null;

	/** The default line number for test message location. */
	private static final int DEFAULT_LOCATION_LINE = -1;
    
	/** The interface to notify the Tests Runner Core */
	private ITestModelUpdater modelUpdater;
	
	/** Stores the text between XML tags. */
	private Stack<StringBuilder> elementDataStack = new Stack<StringBuilder>();
	
	/** File name for current test message location. */
	private String fileName;

	/** Line number for current test message location. */
	private int lineNumber;

	/** Current test case status. */
	private ITestItem.Status testStatus;
	
	/**
	 * Keep track of the last test case name so that we can handle
	 * parameterized test cases which have the same name
	 */
	private String lastTestCaseName = ""; //$NON-NLS-1$
	private static final int SAME_TEST_CASE_NAME_COUNT_START = 2;
	private int sameTestCaseNameCount = SAME_TEST_CASE_NAME_COUNT_START;

	BoostXmlLogHandler(ITestModelUpdater modelUpdater) {
		this.modelUpdater = modelUpdater;
	}
	
	@Override
	public void startElement(String namespaceURI, String localName, String qName, Attributes attrs) throws SAXException {
		
		elementDataStack.push(new StringBuilder());
		if (qName == XML_NODE_TEST_SUITE) {
			String testSuiteName = attrs.getValue(XML_ATTR_TEST_SUITE_NAME);
			modelUpdater.enterTestSuite(testSuiteName);

		} else if (qName == XML_NODE_TEST_CASE) {
			String testCaseName = attrs.getValue(XML_ATTR_TEST_CASE_NAME);

			if (lastTestCaseName.equals(testCaseName)) {
				testCaseName += " (" + sameTestCaseNameCount + ")"; //$NON-NLS-1$ //$NON-NLS-2$
				++sameTestCaseNameCount;
			} else {
				lastTestCaseName = testCaseName;
				sameTestCaseNameCount = SAME_TEST_CASE_NAME_COUNT_START;
			}

			modelUpdater.enterTestCase(testCaseName);
			testStatus = Status.Passed;

		} else if (STRING_TO_MESSAGE_LEVEL.containsKey(qName)
				|| qName == XML_NODE_LAST_CHECKPOINT) {
			fileName = attrs.getValue(XML_ATTR_MESSAGE_FILE);
			String lineNumberStr = attrs.getValue(XML_ATTR_MESSAGE_LINE);
			lineNumber = lineNumberStr != null ? Integer.parseInt(lineNumberStr.trim()) : DEFAULT_LOCATION_LINE;
			
		} else if (qName == XML_NODE_EXCEPTION) {
			fileName = DEFAULT_LOCATION_FILE;
			lineNumber = DEFAULT_LOCATION_LINE;

		} else if (qName == XML_NODE_TESTING_TIME ) {
			
		} else if (qName == XML_NODE_TEST_LOG) {
			/* just skip, do nothing */
			
		} else {
			logAndThrowErrorForElement(qName);
		}
	}
	
	/**
	 * Common routing: notifies the Tests Runner core about new test message
	 * and resets the internal state.
	 * 
	 * @param level test message level
	 */
	private void addCurrentMessage(ITestMessage.Level level) {
		modelUpdater.addTestMessage(fileName, lineNumber, level, elementDataStack.peek().toString());
		fileName = DEFAULT_LOCATION_FILE;
		lineNumber = DEFAULT_LOCATION_LINE;
		if (level == ITestMessage.Level.Error || level == ITestMessage.Level.FatalError) {
			if (testStatus != ITestItem.Status.Aborted) {
				testStatus = ITestItem.Status.Failed;
			}
			
		} else if (level == ITestMessage.Level.Exception) {
			testStatus = ITestItem.Status.Aborted;
		}
	}

	@Override
	public void endElement(String namespaceURI, String localName, String qName) throws SAXException {

		if (qName == XML_NODE_TEST_SUITE) {
			modelUpdater.exitTestSuite();

		} else if (qName == XML_NODE_TEST_CASE) {
			modelUpdater.setTestStatus(testStatus);
			modelUpdater.exitTestCase();
		
		} else if (qName == XML_NODE_TESTING_TIME) {
			modelUpdater.setTestingTime(Integer.parseInt(elementDataStack.peek().toString().trim())/1000);

		} else if (STRING_TO_MESSAGE_LEVEL.containsKey(qName)) {
			addCurrentMessage(STRING_TO_MESSAGE_LEVEL.get(qName));

		} else if (qName == XML_NODE_EXCEPTION) {
			if (fileName != DEFAULT_LOCATION_FILE && !fileName.isEmpty() && lineNumber >= 0) {
				elementDataStack.peek().append(BoostTestsRunnerMessages.BoostXmlLogHandler_exception_suffix);
			}
			addCurrentMessage(ITestMessage.Level.Exception);

		} else if (qName == XML_NODE_TEST_LOG || qName == XML_NODE_LAST_CHECKPOINT) {
			/* just skip, do nothing */
			
		} else {
			logAndThrowErrorForElement(qName);
		}
		elementDataStack.pop();
	}
	
	@Override
	public void characters(char[] ch, int start, int length) {
		StringBuilder builder = elementDataStack.peek();
		for (int i = start; i < start + length; i++) {
			builder.append(ch[i]);
		}
	}
	
	/**
	 * Throws the testing exception for the specified XML tag.
	 * 
	 * @param tagName XML tag name
	 * @throws SAXException the exception that will be thrown
	 */
	private void logAndThrowErrorForElement(String tagName) throws SAXException {
		logAndThrowError(
			MessageFormat.format(BoostTestsRunnerMessages.BoostXmlLogHandler_wrong_tag_name, tagName)
		);
	}
	
	/**
	 * Throws the testing exception with the specified message.
	 * 
	 * @param message the reason
	 * @throws SAXException the exception that will be thrown
	 */
	private void logAndThrowError(String message) throws SAXException {
		SAXException e = new SAXException(message);
		BoostTestsRunnerPlugin.log(e);
		throw e;
	}

	
	@Override
	public void warning(SAXParseException ex) throws SAXException {
		BoostTestsRunnerPlugin.log(ex);
	}

	@Override
	public void error(SAXParseException ex) throws SAXException {
		BoostTestsRunnerPlugin.log(ex);
		throw ex;
	}

	@Override
	public void fatalError(SAXParseException ex) throws SAXException {
		BoostTestsRunnerPlugin.log(ex);
		throw ex;
	}
	
}
