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

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.SortedMap;
import java.util.TreeMap;
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.guimodel.GUIElementTree;
import de.ugoe.cs.autoquest.eventcore.guimodel.GUIModel;
import de.ugoe.cs.autoquest.plugin.mfc.eventcore.WindowsMessage;
import de.ugoe.cs.autoquest.plugin.mfc.eventcore.WindowsMessageType;
import de.ugoe.cs.autoquest.plugin.mfc.guimodel.MFCGUIElement;
import de.ugoe.cs.util.StringTools;
import de.ugoe.cs.util.console.Console;

/**
 * <p>
 * This class provides functionality to parse XML log files generated by the MFCUsageMonitor of
 * EventBench. The result of parsing a file is a collection of event sequences. It uses the
 * {@link SequenceSplitter} and the {@link EventGenerator} as well as custom defined
 * {@link MessageHandler} for the parsing.
 * </p>
 * 
 * @author Steffen Herbold
 * @author Fabian Glaser
 * @version 1.0
 */
public class MFCLogParser extends DefaultHandler {

    /**
     * <p>
     * If a custom message handler is used, this field contains its handle. Otherwise this field is
     * {@code null}.
     * </p>
     */
    private MessageHandler currentHandler;

    /**
     * <p>
     * internal handle to the GUI element tree
     * </p>
     */
    private GUIElementTree<Long> guiElementTree;

    /**
     * <p>
     * the type of the currently parsed message
     * </p>
     */
    private WindowsMessageType currentMessageType;
    
    /**
     * <p>
     * the parameters of the currently parsed message
     * </p>
     */
    private Map<String, Object> currentMessageParameters = new HashMap<String, Object>();
    
    /**
     * <p>
     * {@link SequenceSplitter} instance used by the {@link MFCLogParser}.
     * </p>
     */
    private SequenceSplitter sequenceSplitter;

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

    /**
     * <p>
     * Debugging variable that allows the analysis which message type occurs how often in the log
     * file. Can be used to enhance the message filter.
     * </p>
     */
    private SortedMap<WindowsMessageType, Integer> typeCounter;

    /**
     * <p>
     * Debugging variable that enables the counting of the occurrences of each message. Used in
     * combination with {@link #typeCounter}.
     * </p>
     */
    private boolean countMessageOccurences;

    /**
     * <p>
     * Constructor. Creates a new LogParser that does not count message occurrences.
     * </p>
     */
    public MFCLogParser() {
        this(false);
    }

    /**
     * <p>
     * Constructor. Creates a new LogParser.
     * </p>
     * 
     * @param countMessageOccurences
     *            if true, the occurrences of each message type in the log is counted.
     */
    public MFCLogParser(boolean countMessageOccurences) {
        sequences = new LinkedList<List<Event>>();
        currentHandler = null;
        this.countMessageOccurences = countMessageOccurences;
        if (countMessageOccurences) {
            typeCounter = new TreeMap<WindowsMessageType, Integer>();
        }
    }

    /**
     * <p>
     * Parses a log file written by the MFCMonitor 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 MFCMonitor 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();
        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);
        }
        catch (ParserConfigurationException e) {
            Console.printerr("Error parsing file " + file.getName());
            Console.logException(e);
        }
        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);
            }
            catch (IOException e) {
                Console.printerr("Error parsing file " + file.getName());
                Console.logException(e);
            }
        }
        
        if (countMessageOccurences) {
            Console.println("Message statistics:");
            Console.println
                (typeCounter.toString().replace(" ", StringTools.ENDLINE).replaceAll("[\\{\\}]", ""));
        }
    }
    
    /**
     * <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 collection of event sequences
     */
    public GUIModel getGuiModel() {
        if( guiElementTree!=null ) {
            return guiElementTree.getGUIModel();
        } else {
            return null;
        }
    }

    /*
     * (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")) {
            Console.traceln(Level.FINE, "start of session");
            // in some logs, the session end may be marked in between the log. This is because
            // of thread problems. So instead of creating a new GUI model, preserve it.
            if (guiElementTree == null) {
                guiElementTree = new GUIElementTree<Long>();
            }
            sequenceSplitter = new SequenceSplitter(guiElementTree);
        }
        else if (qName.equals("msg")) {
            currentMessageType = WindowsMessageType.parseMessageType(atts.getValue("type"));

            if (countMessageOccurences) {
                Integer currentCount = typeCounter.get(currentMessageType);
                if (currentCount == null) {
                    typeCounter.put(currentMessageType, 1);
                }
                else {
                    typeCounter.put(currentMessageType, currentCount + 1);
                }
            }

            if (currentMessageType == WindowsMessageType.WM_CREATE) {
                currentHandler = new HandlerCreate(guiElementTree);
                currentHandler.onStartElement();
            }
            else if (currentMessageType == WindowsMessageType.WM_DESTROY) {
                currentHandler = new HandlerDestroy(guiElementTree);
                currentHandler.onStartElement();
            }
            else if (currentMessageType == WindowsMessageType.WM_SETTEXT) {
                currentHandler = new HandlerSetText(guiElementTree);
                currentHandler.onStartElement();
            }
        }
        else if (qName.equals("param")) {
            if (currentHandler != null) {
                currentHandler.onParameter(atts.getValue("name"), atts.getValue("value"));
            }
            else {
                // provide the parameters directly in the correct type
                String paramName = atts.getValue("name");
                if (("window.hwnd".equals(paramName)) ||
                    ("source".equals(paramName)) ||
                    ("LPARAM".equals(paramName)) ||
                    ("WPARAM".equals(paramName)) ||
                    ("scrollPos".equals(paramName)) ||
                    ("scrollBarHandle".equals(paramName)))
                {
                    Long paramValue = Long.parseLong(atts.getValue("value"));
                    currentMessageParameters.put(paramName, paramValue);
                }
                else {
                    currentMessageParameters.put(paramName, atts.getValue("value"));
                }
            }
        }
    }

    /*
     * (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("msg")) {
            if (currentHandler != null) {
                currentHandler.onEndElement();
                currentHandler = null;
            }
            else {
                try {
                    long hwnd = (Long) currentMessageParameters.get("window.hwnd");
                    MFCGUIElement target = (MFCGUIElement) guiElementTree.find(hwnd);
                    target.markUsed();
                    
                    WindowsMessage message = new WindowsMessage
                        (currentMessageType, target, currentMessageParameters);
                    
                    sequenceSplitter.addMessage(message);
                }
                catch (IllegalArgumentException e) {
                    Console.traceln(Level.WARNING, e.getMessage() + " WindowsMessage " + currentMessageType +
                                    " ignored.");
                }
            }
        }
        else if (qName.equals("session")) {
            sequenceSplitter.endSession();
            List<Event> seq = sequenceSplitter.getSequence();
            if (seq != null && !seq.isEmpty()) {
                sequences.add(seq);
            }
            Console.traceln(Level.FINE, "end of session");
        }
    }

}
