//   Copyright 2012 Georg-August-Universität Göttingen, Germany
//
//   Licensed under the Apache License, Version 2.0 (the "License");
//   you may not use this file except in compliance with the License.
//   You may obtain a copy of the License at
//
//       http://www.apache.org/licenses/LICENSE-2.0
//
//   Unless required by applicable law or agreed to in writing, software
//   distributed under the License is distributed on an "AS IS" BASIS,
//   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//   See the License for the specific language governing permissions and
//   limitations under the License.

package de.ugoe.cs.autoquest.plugin.html;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.UnsupportedEncodingException;
import java.util.Collection;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;

import javax.xml.parsers.ParserConfigurationException;
import javax.xml.parsers.SAXParser;
import javax.xml.parsers.SAXParserFactory;

import org.xml.sax.Attributes;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;
import org.xml.sax.SAXParseException;
import org.xml.sax.helpers.DefaultHandler;

import de.ugoe.cs.autoquest.eventcore.Event;
import de.ugoe.cs.autoquest.eventcore.IEventType;
import de.ugoe.cs.autoquest.eventcore.guimodel.GUIElementTree;
import de.ugoe.cs.autoquest.eventcore.guimodel.GUIModel;
import de.ugoe.cs.autoquest.eventcore.guimodel.IGUIElement;
import de.ugoe.cs.autoquest.plugin.html.eventcore.HTMLEventTypeFactory;
import de.ugoe.cs.autoquest.plugin.html.guimodel.HTMLGUIElement;
import de.ugoe.cs.autoquest.plugin.html.guimodel.HTMLGUIElementSpec;
import de.ugoe.cs.autoquest.plugin.html.guimodel.HTMLPageElementSpec;
import de.ugoe.cs.autoquest.plugin.html.guimodel.HTMLPageSpec;
import de.ugoe.cs.autoquest.plugin.html.guimodel.HTMLServerSpec;
import de.ugoe.cs.util.console.Console;

/**
 * <p>
 * This class provides the functionality to parse XML log files generated by the HTMLMonitor of
 * autoquest. The result of parsing a file is a collection of event sequences.
 * </p>
 * 
 * @author Fabian Glaser
 * @version 1.0
 * 
 */
public class NewHTMLLogParser extends DefaultHandler {
    /**
     * <p>
     * Constructor. Creates a new HTMLLogParser.
     * </p>
     */
    public NewHTMLLogParser() {
        sequences = new LinkedList<List<Event>>();
    }

    /**
     * <p>
     * Collection of event sequences that is contained in the parsed log file.
     * </p>
     */
    private Collection<List<Event>> sequences;

    /**
     * <p>
     * Internal handle to the parsed GUI structure, stored in a GUIElementTree
     * </p>
     */
    private GUIElementTree<String> currentGUIElementTree;

    /**
     * <p>
     * Path of the GUI element currently being parsed.
     * </p>
     */
    private String currentGUIElementPath;

    /**
     * <p>
     * Path of the parent of the GUI element currently being parsed.
     * </p>
     */
    private String currentParentPath;

    /**
     * <p>
     * Source of the GUI element currently being parsed.
     * </p>
     */
    private String currentEventSource;

    /**
     * <p>
     * Timestamp of the event currently being parsed.
     * </p>
     */
    private Long currentEventTimestamp;

    /**
     * <p>
     * Internal handle to the parameters of the event currently being parsed.
     * </p>
     */
    private Map<String, String> currentEventParameters;

    /**
     * <p>
     * Internal handle to the parameters of the GUI element currently being parsed.
     * </p>
     */
    private Map<String, String> currentGUIElementParameters;
    /**
     * <p>
     * Internal handle to the sequence currently being parsed.
     * </p>
     */
    private List<Event> currentSequence;

    /**
     * <p>
     * Internal handle to type of the event currently being parsed.
     * </p>
     */
    private String currentEventType;

    /**
     * <p>
     * Class of the GUI element currently being parsed.
     * </p>
     */
    private String currentGUIElementClass;

    /**
     * <p>
     * Index of the GUI element currently being parsed.
     * </p>
     */
    private String currentGUIElementIndex;

    /**
     * <p>
     * internal handle to the GUI element of the previous event to be potentially reused for the
     * current
     * </p>
     */
    private IGUIElement lastGUIElement;

    /**
     * <p>
     * internal handle to the server specification currently being used.
     * </p>
     */
    private HTMLServerSpec currentServerSpec;

    /**
     * <p>
     * Parses a log file written by the HTMLMonitor and creates a collection of event sequences.
     * </p>
     * 
     * @param filename
     *            name and path of the log file
     */
    public void parseFile(String filename) {
        if (filename == null) {
            throw new IllegalArgumentException("filename must not be null");
        }

        parseFile(new File(filename));
    }

    /**
     * <p>
     * Parses a log file written by the HTMLMonitor and creates a collection of event sequences.
     * </p>
     * 
     * @param file
     *            file to be parsed
     */
    public void parseFile(File file) {
        if (file == null) {
            throw new IllegalArgumentException("file must not be null");
        }
        SAXParserFactory spf = SAXParserFactory.newInstance();
        spf.setValidating(true);
        SAXParser saxParser = null;
        InputSource inputSource = null;
        try {
            saxParser = spf.newSAXParser();
            inputSource =
                new InputSource(new InputStreamReader(new FileInputStream(file), "UTF-8"));
        }
        catch (UnsupportedEncodingException e) {
            Console.printerr("Error parsing file " + file.getName());
            Console.logException(e);
            return;
        }
        catch (ParserConfigurationException e) {
            Console.printerr("Error parsing file " + file.getName());
            Console.logException(e);
            return;
        }
        catch (SAXException e) {
            Console.printerr("Error parsing file " + file.getName());
            Console.logException(e);
        }
        catch (FileNotFoundException e) {
            Console.printerr("Error parsing file " + file.getName());
            Console.logException(e);
        }
        if (inputSource != null) {
            inputSource.setSystemId("file://" + file.getAbsolutePath());
            try {
                if (saxParser == null) {
                    throw new RuntimeException("SaxParser creation failed");
                }
                saxParser.parse(inputSource, this);
            }
            catch (SAXParseException e) {
                Console.printerrln("Failure parsing file in line " + e.getLineNumber() +
                    ", column " + e.getColumnNumber() + ".");
                Console.logException(e);
            }
            catch (SAXException e) {
                Console.printerr("Error parsing file " + file.getName());
                Console.logException(e);
                return;
            }
            catch (IOException e) {
                Console.printerr("Error parsing file " + file.getName());
                Console.logException(e);
                return;
            }
        }
    }

    /*
     * (non-Javadoc)
     * 
     * @see org.xml.sax.helpers.DefaultHandler#startElement(java.lang.String, java.lang.String,
     * java.lang.String, org.xml.sax.Attributes)
     */
    @Override
    public void startElement(String uri, String localName, String qName, Attributes atts)
        throws SAXException
    {
        if (qName.equals("session")) {
            currentSequence = new LinkedList<Event>();
            if (currentGUIElementTree == null)
                currentGUIElementTree = new GUIElementTree<String>();
        }
        else if (qName.equals("component")) {
            currentGUIElementPath = atts.getValue("path");
            currentGUIElementParameters = new HashMap<String, String>();
        }
        else if (qName.equals("event")) {
            currentEventType = atts.getValue("type");
            currentEventParameters = new HashMap<String, String>();
        }
        else if (qName.equals("param")) {
            String paramName = atts.getValue("name");
            if (currentGUIElementPath != null) {
                if ("parent".equals(paramName)) {
                    currentParentPath = atts.getValue("value");
                }
                if ("class".equals(paramName)) {
                    currentGUIElementClass = atts.getValue("value");
                }
                if ("index".equals(paramName)) {
                    currentGUIElementIndex = atts.getValue("value");
                }
                currentGUIElementParameters.put(paramName, atts.getValue("value"));
            }
            else if (currentEventType != null) {
                if ("target".equals(paramName)) {
                    currentEventSource = atts.getValue("value");
                }
                if ("timestamp".equals(paramName)) {
                    currentEventTimestamp = Long.parseLong(atts.getValue("value"));
                }
                currentEventParameters.put(paramName, atts.getValue("value"));
            }
            else {
                throw new SAXException("param tag found where it should not be.");
            }
        }
        else {
            throw new SAXException("unknown tag found: " + qName);
        }

    }

    /*
     * (non-Javadoc)
     * 
     * @see org.xml.sax.helpers.DefaultHandler#endElement(java.lang.String, java.lang.String,
     * java.lang.String)
     */
    @Override
    public void endElement(String uri, String localName, String qName) throws SAXException {
        if (qName.equals("session")) {
            if (currentSequence != null && !currentSequence.isEmpty()) {
                sequences.add(currentSequence);
            }
            currentSequence = null;
        }
        else if (qName.equals("component") && currentGUIElementPath != null) {
            HTMLGUIElementSpec guiElementSpec =
                getGUIElementSpec(currentGUIElementClass, currentGUIElementParameters);
            currentGUIElementTree.add(currentGUIElementPath, currentParentPath, guiElementSpec);

            currentParentPath = null;
            currentGUIElementPath = null;
            currentGUIElementParameters = null;
        }
        else if (qName.equals("event")) {
            IGUIElement currentGUIElement;
            currentGUIElement = currentGUIElementTree.find(currentEventSource);

            IEventType eventType =
                HTMLEventTypeFactory.getInstance().getEventType(currentEventType,
                                                                currentEventParameters,
                                                                currentGUIElement);
            Event event =
                new Event(eventType, (currentGUIElement == null ? lastGUIElement
                    : currentGUIElement));

            event.setTimestamp(currentEventTimestamp);
            HTMLGUIElement currentEventTarget = (HTMLGUIElement) event.getTarget();
            currentEventTarget.markUsed();
            currentSequence.add(event);

            currentEventSource = null;
            currentEventTimestamp = -1l;
            currentEventParameters = null;
            currentEventType = null;

            if (currentGUIElement != null) {
                lastGUIElement = currentGUIElement;
            }

            currentGUIElement = null;
        }
    }

    /**
     * <p>
     * Returns a collection of event sequences that was obtained from parsing log files.
     * </p>
     * 
     * @return
     */
    public Collection<List<Event>> getSequences() {
        return sequences;
    }

    /**
     * <p>
     * Returns the GUI model that is obtained from parsing log files.
     * </p>
     * 
     * @return GUIModel
     */
    public GUIModel getGuiModel() {
        return currentGUIElementTree.getGUIModel();
    }

    /**
     * Returns the HTMLGUIElementSpecification for a GUI Element described
     * by its class name and its parameters.
     * @param guiElementClass
     * @param guiElementParameters
     * @return
     */
    private HTMLGUIElementSpec getGUIElementSpec(String guiElementClass,
                                                 Map<String, String> guiElementParameters)
    {
        HTMLGUIElementSpec specification = null;
        if ("server".equals(guiElementClass)) {
            // TODO: add correct port handling
            specification = new HTMLServerSpec(guiElementParameters.get("htmlId"), 0);
            currentServerSpec = (HTMLServerSpec) specification;
        }

        else {
            String id = guiElementParameters.get("htmlId");
            if (id == null) {
                HTMLPageElementSpec parentSpec =
                    (HTMLPageElementSpec) currentGUIElementTree.find(currentParentPath)
                        .getSpecification();
                id = parentSpec.getPage().getPagePath();
            }

            int index = -1;
            String indexStr = guiElementParameters.get("index");

            if ((indexStr != null) && (!"".equals(indexStr))) {
                index = Integer.parseInt(indexStr);
            }
            String title = guiElementParameters.get("title");
            HTMLPageSpec page = new HTMLPageSpec(currentServerSpec, id, title);
            specification = new HTMLPageElementSpec(page, guiElementClass, id, index);
        }

        return specification;
    }
}
