//   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.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.io.UnsupportedEncodingException;
import java.text.DecimalFormat;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;

import org.xml.sax.SAXException;

import de.ugoe.cs.util.StringTools;
import de.ugoe.cs.util.console.Console;

/**
 * <p>
 * This class can be used to compress a bunch of HTML log files to one file, that contains only
 * one full GUI model.
 * </p>
 * 
 * @author Patrick Harms
 * @version 1.0
 * 
 */
public class HTMLLogCompressor extends AbstractDefaultLogParser {
    
    /**
     * <p>
     * The output writer into which the compressed variant of the log files is written
     * </p>
     */
    private PrintWriter outputWriter;
    
    /**
     * <p>
     * the GUI elements, that were already logged and need therefore not be logged again into
     * the same file
     * </p>
     */
    private Set<String> loggedGUIElementIds = new HashSet<String>();

    /**
     * <p>
     * the events that were read
     * </p>
     */
    private List<EventEntry> sortedEvents = new LinkedList<EventEntry>();

    /**
     * <p>
     * called to compress all log files in the provided directory. The method reuses 
     * {@link #compressFilesInDirectory(File)}.
     * </p>
     * 
     * @param directory the directory containing the log files to be compressed
     */
    public void compressFilesInDirectory(String directory) {
        if (directory == null) {
            throw new IllegalArgumentException("directory must not be null");
        }

        compressFilesInDirectory(new File(directory));
    }

    /**
     * <p>
     * called to compress all log files in the provided directory. The directory if processed
     * recursively. For each directory containing at least one log file the log files are
     * replaced by one large log file containing the GUI model as well as all events contained in
     * the compressed log file. If one log file could not be parsed, it is ignored and not
     * compressed with the other ones.
     * </p>
     * 
     * @param directory the directory containing the log files to be compressed
     */
    public void compressFilesInDirectory(File directory) {
        if (directory == null) {
            throw new IllegalArgumentException("directory must not be null");
        }
        
        if (!directory.exists()) {
            throw new IllegalArgumentException("directory must denote an existing directory");
        }
        
        if (!directory.isDirectory()) {
            throw new IllegalArgumentException("directory must denote a directory");
        }
        
        File[] files = directory.listFiles();
        
        if ((files == null) || (files.length == 0)) {
            Console.println("directory is empty");
            return;
        }
        
        File outFile = null;
        
        int logFileIndex = -1;
        do {
            outFile = new File(directory, getLogFileName(directory.getName(), logFileIndex));
            logFileIndex++;
        }
        while (outFile.exists());
        
        List<File> compressedFiles = new LinkedList<File>();
        
        try {
            FileOutputStream fis = new FileOutputStream(outFile);
            outputWriter = new PrintWriter(new OutputStreamWriter(fis, "UTF-8"));
            outputWriter.println("<?xml version=\"1.0\" encoding=\"UTF-8\"?>");
            outputWriter.println("<session>");

            loggedGUIElementIds.clear();
            sortedEvents.clear();

            for (File file : files) {
                if (file.isFile()) {
                    try {
                        super.parseFile(file);
                        Console.println("compressed " + file);
                        compressedFiles.add(file);
                    }
                    catch (Exception e) {
                        Console.printerrln("could not parse and compress file " + file);
                        Console.printerrln(e.getMessage());
                    }
                }
            }
            
            for (EventEntry event : sortedEvents) {
                event.dump();
            }

            outputWriter.println("</session>");
            outputWriter.flush();
        }
        catch (FileNotFoundException e) {
            Console.printerrln("could not create compressed file " + outFile);
            compressedFiles.clear();
        }
        catch (UnsupportedEncodingException e) {
            // this should never happen
            e.printStackTrace();
            compressedFiles.clear();
        }
        finally {
            if (outputWriter != null) {
                outputWriter.close();
                outputWriter = null;
            }
        }
        
        if (compressedFiles.size() > 0) {
            for (File file : compressedFiles) {
                if (!file.delete()) {
                    Console.printerrln("could not delete compressed file " + file);
                }
            }
        
            if (outFile.exists()) {
                File finalOutFile = null;
            
                logFileIndex = -1;
                do {
                    logFileIndex++;
                    finalOutFile =
                        new File(directory, getLogFileName(directory.getName(), logFileIndex));
                }
                while (finalOutFile.exists());

                if (outFile.renameTo(finalOutFile)) {
                    Console.println("created " + finalOutFile);
                }
                else {
                    Console.println
                        ("created " + outFile + " but could not rename to " + finalOutFile);
                }
            }
        }
        else {
            if (!outFile.delete()) {
                Console.printerrln
                    ("could not delete temporary file " + outFile);
            }
        }
    }
    
    /* (non-Javadoc)
     * @see de.ugoe.cs.autoquest.plugin.html.AbstractDefaultLogParser#parseFile(java.lang.String)
     */
    @Override
    public void parseFile(String filename) {
        throw new IllegalStateException("this method must not be called externally");
    }

    /* (non-Javadoc)
     * @see de.ugoe.cs.autoquest.plugin.html.AbstractDefaultLogParser#parseFile(java.io.File)
     */
    @Override
    public void parseFile(File file) {
        throw new IllegalStateException("this method must not be called externally");
    }

    /* (non-Javadoc)
     * @see de.ugoe.cs.autoquest.plugin.html.AbstractDefaultLogParser#handleGUIElement(String, Map)
     */
    @Override
    protected boolean handleGUIElement(String id, Map<String, String> parameters)
        throws SAXException
    {
        if (!loggedGUIElementIds.contains(id)) {
            outputWriter.print("<component id=\"");
            outputWriter.print(id);
            outputWriter.println("\">");
        
            for (Map.Entry<String, String> param : parameters.entrySet()) {
                dumpParam(param.getKey(), param.getValue());
            }
            
            outputWriter.println("</component>");
        
            loggedGUIElementIds.add(id);
        }
        
        return true;
    }

    /* (non-Javadoc)
     * @see de.ugoe.cs.autoquest.plugin.html.AbstractDefaultLogParser#handleEvent(String,Map)
     */
    @Override
    protected boolean handleEvent(String type, Map<String, String> parameters) throws SAXException {
        String timestampStr = parameters.get("timestamp");
        
        long timestamp = Long.MAX_VALUE;
        
        if (timestampStr != null) {
            timestamp = Long.parseLong(timestampStr);
        }
        
        EventEntry newEvent = new EventEntry(type, parameters, timestamp);
        
        int start = 0;
        int end = sortedEvents.size();
        int center = 0;
        long centerTimestamp;
        
        while (start != end) {
            center = start + ((end - start) / 2);
            
            if ((center != start) || (center != end)) {
                centerTimestamp = sortedEvents.get(center).timestamp;
            
                if (centerTimestamp < newEvent.timestamp) {
                    start = Math.max(center, start + 1);
                }
                else if (centerTimestamp > newEvent.timestamp) {
                    end = Math.min(center, end - 1);
                }
                else {
                    // add the event directly where the center is, as the timestamps of the center
                    // and the new event are equal
                    end = center;
                    start = end;
                    break;
                }
            }
            else {
                // add the event to the position denoted by the add index
                break;
            }
        }
        
        sortedEvents.add(start, newEvent);

        return true;
    }

    /**
     * <p>
     * dumps a parameter with the given name and value to the log file. The result is a
     * tag named param with a name attribute and a value attribute. The value is transformed
     * to a String if it is no String already. Furthermore, an XML entity replacement is performed
     * if required.
     * </p>
     *
     * @param name  the name of the parameter to be dumped
     * @param value the value of the parameter to be dumped
     */
    private void dumpParam(String name, Object value) {
        if (value == null) {
            return;
        }
        
        String val;
        
        if (value instanceof String) {
            val = (String) value;
        }
        else {
            val = String.valueOf(value);
        }
        
        outputWriter.print(" <param name=\"");
        outputWriter.print(name);
        outputWriter.print("\" value=\"");
        outputWriter.print(StringTools.xmlEntityReplacement(val));
        outputWriter.println("\"/>");
    }


    /**
     * <p>
     * used to calculate a log file name. If the provided index is smaller 0, then no index
     * is added to the file name. A filename is e.g. "htmlmonitor_12345_001.log". 
     * </p>
     *
     * @param index the index of the log file or -1 one, if no index shall be added
     * 
     * @return the file name as described
     */
    private String getLogFileName(String clientId, int index) {
        String result = "htmlmonitor_" + clientId;
        
        if (index >= 0) {
            result += "_" + new DecimalFormat("000" ).format(index);
        }
        
        result += ".log";
        
        return result;
    }
    
    /**
     * <p>
     * this class is used internally for storing events in a sorted list together with the
     * timestamps, being the sort criteria. 
     * </p>
     */
    private class EventEntry {
        
        /**
         * <p>
         * the type of the event
         * </p>
         */
        private String type;
        
        /**
         * <p>
         * the parameters of the event
         * </p>
         */
        private Map<String, String> parameters;
        
        /**
         * <p>
         * the timestamp of the event
         * </p>
         */
        private long timestamp;

        /**
         * <p>
         * creates a new event entry with event type, parameters and the timestamp
         * </p>
         */
        private EventEntry(String type, Map<String, String> parameters, long timestamp) {
            this.type = type;
            this.parameters = parameters;
            this.timestamp = timestamp;
        }
        
        /**
         * <p>
         * convenience method for dumping the event into the compressed log file
         * </p>
         */
        private void dump() {
            outputWriter.print("<event type=\"");
            outputWriter.print(type);
            outputWriter.println("\">");
            
            for (Map.Entry<String, String> param : parameters.entrySet()) {
                dumpParam(param.getKey(), param.getValue());
            }
            
            outputWriter.println("</event>");
        }
    }
}
