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

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.Iterator;
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.Locator;
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.HierarchicalEventTargetModel;
import de.ugoe.cs.autoquest.eventcore.HierarchicalEventTargetTree;
import de.ugoe.cs.autoquest.plugin.genericevents.eventCore.GenericEventTarget;
import de.ugoe.cs.autoquest.plugin.genericevents.eventCore.GenericEventTargetFactory;
import de.ugoe.cs.autoquest.plugin.genericevents.eventCore.GenericEventTargetSpec;
import de.ugoe.cs.util.console.Console;

/**
 * <p>
 * This class provides the functionality to parse XML log files generated by monitors of
 * AutoQUEST. The result of parsing a file is a collection of event sequences and a target model.
 * This class must be extended by implementing a subclass and the abstract method to complete
 * the implementation.
 * </p>
 * 
 * @author Patrick Harms
 * @version 1.0
 * 
 */
public abstract class AbstractDefaultLogParser extends DefaultHandler {
    
    /**
     * <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 target structure, stored in a GUIElementTree
     * </p>
     */
    private HierarchicalEventTargetTree<String, GenericEventTarget, GenericEventTargetSpec> targetTree;

    /**
     * <p>
     * Id of the event target currently being parsed.
     * </p>
     */
    private String currentTargetId;

    /**
     * <p>
     * the buffer for event targets already parsed but not processed yet (because e.g. the parent
     * target has not been parsed yet)
     * </p>
     */
    private List<BufferEntry> targetBuffer;

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

    /**
     * <p>
     * the buffer for events already parsed but not processed yet (because e.g. the target
     * has not been parsed yet)
     * </p>
     */
    private List<BufferEntry> eventBuffer;

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

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

    /**
     * <p>
     * the handle to the locator for correctly throwing exceptions with location information
     * </p>
     */
    private Locator locator;

    /**
     * <p>
     * Constructor. Creates a new logParser.
     * </p>
     */
    public AbstractDefaultLogParser() {
        sequences = new LinkedList<List<Event>>();
        
        targetTree = new HierarchicalEventTargetTree<String, GenericEventTarget, GenericEventTargetSpec>
            (new HierarchicalEventTargetModel<GenericEventTarget>(false),
             new GenericEventTargetFactory());
        
        targetBuffer = new LinkedList<BufferEntry>();
        eventBuffer = new LinkedList<BufferEntry>();
        currentSequence = new LinkedList<Event>();
    }

    /**
     * <p>
     * Parses a log file written by an AutoQUEST monitor and creates a collection of event
     * sequences.
     * </p>
     * 
     * @param filename
     *            name and path of the log file
     *
     * @throws SAXException in the case, the file could not be parsed
     */
    public void parseFile(String filename) throws SAXException {
        if (filename == null) {
            throw new IllegalArgumentException("filename must not be null");
        }

        parseFile(new File(filename));
    }

    /**
     * <p>
     * Parses a log file written by an AutoQUEST monitor and creates a collection of event
     * sequences.
     * </p>
     * 
     * @param file
     *            file to be parsed
     *
     * @throws SAXException in the case, the file could not be parsed
     */
    public void parseFile(File file) throws SAXException {
        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.printerrln("Error parsing file " + file.getName());
            Console.logException(e);
            throw new SAXParseException("Error parsing file " + file.getName(), locator, e);
        }
        catch (ParserConfigurationException e) {
            Console.printerrln("Error parsing file " + file.getName());
            Console.logException(e);
            throw new SAXParseException("Error parsing file " + file.getName(), locator, e);
        }
        catch (FileNotFoundException e) {
            Console.printerrln("Error parsing file " + file.getName());
            Console.logException(e);
            throw new SAXParseException("Error parsing file " + file.getName(), locator, e);
        }
        
        // we parse a new file. So clear the buffers.
        targetBuffer.clear();
        eventBuffer.clear();
        
        if (inputSource != null) {
            inputSource.setSystemId("file://" + file.getAbsolutePath());
            try {
                if (saxParser == null) {
                    throw new RuntimeException("SaxParser creation failed");
                }
                saxParser.parse(inputSource, this);
            }
            catch (IOException e) {
                Console.printerrln("Error parsing file " + file.getName());
                Console.logException(e);
                throw new SAXParseException("Error parsing file " + file.getName(), locator, e);
            }
            catch (SAXParseException e) {
                if ("XML document structures must start and end within the same entity.".equals
                    (e.getMessage()))
                {
                    // this only denotes, that the final session tag is missing, because the
                    // file wasn't completed. Ignore this but complete the session
                    eventBuffer.add(new BufferEntry("sessionSwitch", null));
                    processEvents();
                }
                else {
                    throw e;
                }
            }
        }
        
        if (targetBuffer.size() > 0) {
            Console.println(targetBuffer.size() + " GUI elements not processed");
        }
        
        if (eventBuffer.size() > 0) {
            Console.printerrln(eventBuffer.size() + " events not processed");
        }
    }

    /**
     * <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 target model that is obtained from parsing log files.
     * </p>
     * 
     * @return GUIModel
     */
    public HierarchicalEventTargetModel<GenericEventTarget> getHierarchicalEventTargetModel() {
        return targetTree.getHierarchicalEventTargetModel();
    }

    /* (non-Javadoc)
     * @see org.xml.sax.helpers.DefaultHandler#setDocumentLocator(org.xml.sax.Locator)
     */
    @Override
    public void setDocumentLocator(Locator locator) {
        this.locator = locator;
    }

    /*
     * (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")) {
            // do nothing
        }
        else if (qName.equals("target")) {
            currentTargetId = atts.getValue("id");
            currentParameters = new HashMap<String, String>();
        }
        else if (qName.equals("event")) {
            currentEventType = atts.getValue("type");
            currentParameters = new HashMap<String, String>();
        }
        else if (qName.equals("param")) {
            String paramName = atts.getValue("name");
            if ((currentTargetId != null) || (currentEventType != null)) {
                currentParameters.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") && (eventBuffer != null)) {
            eventBuffer.add(new BufferEntry("sessionSwitch", null));
            processTargets();
            processEvents();
        }
        else if (qName.equals("target") && (currentTargetId != null)) {
            targetBuffer.add(0, new BufferEntry(currentTargetId, currentParameters));
            currentTargetId = null;
            currentParameters = null;
        }
        else if (qName.equals("event") && (currentEventType != null)) {
            eventBuffer.add(new BufferEntry(currentEventType, currentParameters));
            currentEventType = null;
            currentParameters = null;
        }
    }

    /**
     * <p>
     * called to handle parsed targets
     * </p>
     *
     * @param id         the id of the parsed target
     * @param parameters all parameters parsed for the target
     * 
     * @return true, if the target could be handled. In this case this method is not called
     *         again for the same target. Otherwise the method is called later again. This
     *         may be required, if a child target is parsed before the parent target
     */
    protected abstract boolean handleTarget(String id, Map<String, String> parameters) 
        throws SAXException;
    
    /**
     * <p>
     * called to handle parsed events
     * </p>
     *
     * @param type       the type of the parsed event
     * @param parameters the parameters of the parsed event
     * 
     * @return true, if the event could be handled. In this case this method is not called
     *         again for the same event. Otherwise the method is called later again. This
     *         may be required, if e.g. the target of the event is not yet parsed
     */
    protected abstract boolean handleEvent(String type, Map<String, String> parameters)
        throws SAXException;
    
    /**
     * <p>
     * returns the tree of parsed targets, which consists of the ids of the targets
     * </p>
     *
     * @return as described
     */
    protected HierarchicalEventTargetTree<String, GenericEventTarget, GenericEventTargetSpec> getTargetTree() {
        return targetTree;
    }

    /**
     * <p>
     * adds an event to the currently parsed sequence of events
     * </p>
     *
     * @param event
     */
    protected void addToSequence(Event event) {
        currentSequence.add(event);
    }

    /**
     * <p>
     * this method internally processes targets, that have been parsed but not processed yet.
     * I.e., for such targets, either the method {@link #handleTarget(String, Map)} has
     * not been called yet, or it returned false for the previous calls. In this case, the method
     * is called (again). Furthermore, the processing of events is initiated by a call to
     * {@link #processEvents()}.
     * </p>
     */
    private void processTargets() throws SAXException {
        int processedElements = 0;
        boolean processedElement;
        
        do {
            processedElement = false;
            
            // search for the next GUI element that can be processed (use an iterator on the 
            // linked list, as this is most effective)
            Iterator<BufferEntry> iterator = targetBuffer.iterator();
            while (iterator.hasNext()) {
                BufferEntry entry = iterator.next();
                
                if (handleTarget(entry.id, entry.parameters)) {
                    iterator.remove();
                    processedElements++;
                    processedElement = true;
                }
            }
        }
        while (processedElement);
        
        if (processedElements > 0) {
            processEvents();
        }
    }

    /**
     * <p>
     * this method internally processes events, that have been parsed but not processed yet.
     * I.e., for such events, either the method {@link #handleEvent(String, Map)} has
     * not been called yet, or it returned false for the previous calls. In this case, the method
     * is called (again).
     * </p>
     */
    private void processEvents() throws SAXException {
        boolean processedEvent;
        
        do {
            processedEvent = false;
            // check, if the next event can be processed
            if (eventBuffer.size() > 0) {
                BufferEntry entry = eventBuffer.get(0);
                
                if ((entry != null) && (entry.id != null) && (entry.parameters != null)) {
                    
                    processedEvent = handleEvent(entry.id, entry.parameters);

                    if (processedEvent) {
                        eventBuffer.remove(0);
                    }
                }
                else {
                    // the entry signals a session switch. Close the current session and start the
                    // next one
                    if (currentSequence.size() > 0) {
                        sequences.add(currentSequence);
                        currentSequence = new LinkedList<Event>();
                    }
                    eventBuffer.remove(0);
                    processedEvent = true;
                }
            }
        }
        while (processedEvent);
    }

    /**
     * <p>
     * This class is used internally for storing events and targets in lists.
     * </p>
     */
    private static class BufferEntry {
        
        /** */
        private String id;
        
        /** */
        private Map<String, String> parameters;
        
        /**
         * 
         */
        private BufferEntry(String id, Map<String, String> parameters) {
            this.id = id;
            this.parameters = parameters;
        }
    }

}
