//   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.android;

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 java.util.logging.Level;

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.gui.CharacterTyped;
import de.ugoe.cs.autoquest.eventcore.gui.IInteraction;
import de.ugoe.cs.autoquest.eventcore.gui.TouchSingle;
import de.ugoe.cs.autoquest.eventcore.guimodel.GUIElementTree;
import de.ugoe.cs.autoquest.eventcore.guimodel.GUIModel;
import de.ugoe.cs.autoquest.eventcore.guimodel.GUIModelException;
import de.ugoe.cs.autoquest.eventcore.guimodel.IGUIElement;
import de.ugoe.cs.autoquest.plugin.android.guimodel.ANDROIDGUIElement;
import de.ugoe.cs.autoquest.plugin.android.guimodel.ANDROIDGUIElementSpec;
import de.ugoe.cs.util.console.Console;

/**
 * <p>
 * This class provides functionality to parse XML log files generated by the AndroidMonitor of
 * AutoQUEST. The result of parsing a file is a collection of event sequences.
 * </p>
 * 
 * @author Florian Unger
 * @version 1.0
 */
public class AndroidLogParser extends DefaultHandler {

    /*
     * (non-Javadoc)
     * 
     * int java.lang.Object.hashCode() is used in the Androidmonitor Long is used to internally
     * handle and compare the number. e.g. currentGUIElementHash != null
     */
    /**
     * <p>
     * Internal handle to the id of the event that is currently being parsed.
     * </p>
     */
    private String currentEventId = null;

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

    /**
     * <p>
     * Internal handle to the source of the event that is currently being parsed.
     * </p>
     */
    private Long currentEventSource;

    /**
     * <p>
     * Internal handle to the timestamp of the event that is currently being parsed.
     */
    private Long currentEventTimestamp = -1l;

    /**
     * 
     * <p>
     * Internal handle to the hashcode of the GUI element, that is currently parsed.
     * </p>
     */
    private Long currentGUIElementHash = null;

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

    /**
     * <p>
     * Internal handle to the specification currently parsed for a GUI element
     * </p>
     */
    private ANDROIDGUIElementSpec currentGUIElementSpec;

    /**
     * 
     * <p>
     * Internal handle to the hashcode of the parent of the GUI element, that is currently parsed.
     * </p>
     */
    private Long currentParentHash;

    /**
     * <p>
     * Internal handle to the event sequence that is currently being parsed.
     * </p>
     */
    private List<Event> currentSequence;

    /**
     * <p>
     * internal handle to the class ancestors
     * </p>
     */
    private List<String> currentTypeHierarchy;

    /**
     * <p>
     * Map that holds events that had no registered target GUI element during parsing. Keys are the
     * IDs of the unregistered targets.
     * </p>
     */
    // private Map<Long, List<Event>> eventsWithoutTargets;

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

    /**
	 * 
	 */
    private Boolean showSteps = false;

    /**
     * <p>
     * Constructor. Creates a new AndroidLogParser.
     * </p>
     */
    public AndroidLogParser() {
        sequences = new LinkedList<List<Event>>();
        currentSequence = null;
    }

    // TODO create a constructor which creates a new AndroidLogParser with a
    // specific event filter.

    /**
     * <p>
     * Parses a log file written by the JFCMonitor 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 JFCMonitor and creates a collection of event sequences.
     * </p>
     * 
     * @param file
     *            name and path of the log file
     */
    public void parseFile(File file) {
        if (file == null) {
            throw new IllegalArgumentException("file must not be null");
        }

        SAXParserFactory spf = SAXParserFactory.newInstance();
        // set true to validate that the file is well defined
        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);
            return;
        }
        catch (FileNotFoundException e) {
            Console.printerr("Error parsing file + " + file.getName());
            Console.logException(e);
            return;
        }

        if (inputSource != null) {
            inputSource.setSystemId("file://" + file.getAbsolutePath());
            try {
                // called a second time to be sure that no error happens
                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);
                return;
            }
            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;
            }
        }
    }

    /**
     * <p>
     * Returns the collection of event sequences that is obtained from parsing log files.
     * </p>
     * 
     * @return collection of event sequences
     */
    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();
    }

    /*
     * (non-Javadoc)
     * 
     * @see org.xml.sax.helpers.DefaultHandler#startElement(java.lang.String, java.lang.String,
     * java.lang.String, org.xml.sax.Attributes)
     */
    public void startElement(String uri, String localName, String qName, Attributes atts)
        throws SAXException
    {
        showSteps("start element: " + qName, true);
        if (qName.equals("sessions")) {
            currentSequence = new LinkedList<Event>();
            if (currentGUIElementTree == null) {
                currentGUIElementTree = new GUIElementTree<Long>();
            }

        }

        if (qName.equals("component")) {
            currentGUIElementHash = Long.parseLong(atts.getValue("hash"));
            currentGUIElementSpec = new ANDROIDGUIElementSpec();
            currentGUIElementSpec.setHashCode((int) currentGUIElementHash.longValue());

        }
        else if (qName.equals("event")) {
            currentEventId = atts.getValue("id");
            currentEventParameters = new HashMap<String, String>();

        }
        else if (qName.equals("param")) {
            if (currentGUIElementHash != null) {
                if ("class".equals(atts.getValue("name"))) {
                    currentGUIElementSpec.setType(atts.getValue("value"));
                }
                else if ("path".equals(atts.getValue("name"))) {
                    currentGUIElementSpec.setPath(atts.getValue("value"));
                }
                else if ("id".equals(atts.getValue("name"))) {
                    currentGUIElementSpec.setIndex(Integer.parseInt(atts.getValue("value")));
                }
                else if ("parent".equals(atts.getValue("name"))) {
                    currentParentHash = Long.parseLong(atts.getValue("value"));
                }
            }
            else if (currentEventId != null) {
                if ("source".equals(atts.getValue("name"))) {
                    currentEventSource = Long.parseLong(atts.getValue("value"));
                }
                if ("timestamp".equals(atts.getValue("name"))) {
                    currentEventTimestamp = Long.parseLong(atts.getValue("value"));
                }
                currentEventParameters.put(atts.getValue("name"), atts.getValue("value"));
            }
        }
        else if (qName.equals("ancestor")) {
            currentTypeHierarchy.add(atts.getValue("name"));
        }
        else if (qName.equals("ancestors")) {
            currentTypeHierarchy = new LinkedList<String>();
        }
    }

    /*
     * (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 {

        showSteps("end element: " + qName, true);

        if (qName.equals("sessions")) {
            if (currentSequence != null) {
                sequences.add(currentSequence);
            }
        }
        else if (qName.equals("ancestors")) {
            currentGUIElementSpec.setTypeHierarchy(currentTypeHierarchy);
        }
        else if (qName.equals("component") && currentGUIElementHash != null) {
            try {
                currentGUIElementTree.add(currentGUIElementHash, currentParentHash,
                                          currentGUIElementSpec);
            }
            catch (GUIModelException e) {
                throw new SAXException("could not handle GUI element with hash " +
                    currentGUIElementHash + ": " + e.getMessage(), e);
            }
            currentGUIElementHash = null;
            currentParentHash = null;
            currentTypeHierarchy = null;
        }
        else if (currentEventId != null && qName.equals("event")) {

            IGUIElement currentGUIElement;
            currentGUIElement = currentGUIElementTree.find(currentEventSource);
            Event event;

            // up to now only onClick events are implemented and each
            // onclick event is processed as a mouse click
            if (currentGUIElement == null) {

            }
            else {

                event =
                    new Event(instantiateInteraction(currentEventId, currentEventParameters),
                              currentGUIElement);
                ANDROIDGUIElement currentEventTarget = (ANDROIDGUIElement) event.getTarget();
                currentEventTarget.markUsed();
                event.setTimestamp(currentEventTimestamp);
                currentSequence.add(event);
            }
            currentEventParameters = null;
            currentEventId = null;
            currentEventTimestamp = -1l;
        }
    }

    /**
     * <p>
     * depending on the event id and the event parameters, this method instantiates the concrete
     * interaction, that took place, i.e. the event type
     * </p>
     * 
     * @param eventId
     *            the id of the event
     * @param eventParameters
     *            the parameters provided for the event
     * 
     * @return as described
     * 
     * @throws SAXException
     *             thrown if the provided event id is unknown
     */
    private IInteraction instantiateInteraction(String event, Map<String, String> eventParameters)
        throws SAXException
    {

        switch (event)
        {
            case "onClick":

                float x = Float.parseFloat(currentEventParameters.get("X"));
                float y = Float.parseFloat(currentEventParameters.get("Y"));

                return new TouchSingle(x, y);

            case "text":
                return new CharacterTyped(currentEventParameters.get("message"));

            default:
                throw new SAXException("unhandled event id " + event);
        }

    }

    private void showSteps(String message, Boolean ln) {
        if (showSteps) {
            if (ln) {
                Console.traceln(Level.INFO, message);
            }
            else {
                Console.trace(Level.INFO, message);
            }

        }
    }

    public void setShowSteps(Boolean showSteps) {
        this.showSteps = showSteps;
    }

}
