package de.ugoe.cs.quest.plugin.jfc;

import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintStream;
import java.io.UnsupportedEncodingException;
import java.util.ArrayList;
import java.util.HashMap;
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.util.StringTools;
import de.ugoe.cs.util.console.Console;

/**
 * <p>
 * corrects older JFC log files which sometimes do not contain correct source specifications for
 * events. It parses the file and adds component specifications to the sources, that do not have
 * them. For each invalid source it checks, if there is another source with the same
 * <code>toString</code> parameter but a complete list of components. If one is found, it is reused
 * as source for the event with the wrong source. If none is found, a new component list is
 * generated. This contains as parent components the components of the source of the previous event.
 * The leaf component is parsed from the <code>toString</code> parameter that is provided with the
 * source specifications. The resulting leaf nodes are not fully correct. They may pretend to
 * be equal although they are not. Furthermore they may resist at a position in the GUI tree where
 * they are not in reality. But more correctness is not achievable based on the
 * <code>toString</code> parameter. 
 * </p>
 * 
 * @version $Revision: $ $Date: 05.09.2012$
 * @author 2012, last modified by $Author: pharms$
 */
public class JFCTraceCorrector  extends DefaultHandler {

    /**
     * <p>
     * the file to write the result into
     * </p>
     */
    private PrintStream outFile;
    
    /**
     * <p>
     * the currently parsed event
     * </p>
     */
    private Event currentEvent;

    /**
     * <p>
     * the currently parsed source of the currently parsed event
     * </p>
     */
    private Source currentSource;

    /**
     * <p>
     * the list of all sources parsed in a file identified through their <code>toString</code>
     * representation
     * </p>
     */
    private Map<String, List<Source>> allSources = new HashMap<String, List<Source>>();

    /**
     * <p>
     * the currently parsed component of the currently parsed source of the currently parsed event
     * </p>
     */
    private Component currentComponent;

    /**
     * <p>
     * the currently parsed session
     * </p>
     */
    private Session currentSession;

    /**
     * <p>
     * corrects the given file and returns the name of the file into which the result was written
     * </p>
     * 
     * @param filename the name of the file to be corrected
     * 
     * @return the name of the file with the corrected logfile
     * 
     * @throws IllegalArgumentException if the filename is null
     */
    public String correctFile(String filename) throws IllegalArgumentException {
        if (filename == null) {
            throw new IllegalArgumentException("filename must not be null");
        }

        return correctFile(new File(filename)).getAbsolutePath();
    }

    /**
     * <p>
     * corrects the given file, stores the result in the second provided file and returns the
     * name of the file into which the result was written
     * </p>
     * 
     * @param filename   the name of the file to be corrected
     * @param resultFile the name of the file into which the corrected log shall be written
     * 
     * @return the name of the file with the corrected logfile
     * 
     * @throws IllegalArgumentException if the filename or resultFile is null
     */
    public String correctFile(String filename, String resultFile) throws IllegalArgumentException {
        if ((filename == null) | (resultFile == null)) {
            throw new IllegalArgumentException("filename and resultFile must not be null");
        }

        return correctFile(new File(filename), new File(resultFile)).getAbsolutePath();
    }

    /**
     * <p>
     * corrects the given file and returns the file into which the result was written. The name
     * of the resulting file is contains the suffix "_corrected" before the dot.
     * </p>
     * 
     * @param file the file to be corrected
     * 
     * @return the file containing the corrected logfile
     * 
     * @throws IllegalArgumentException if the file is null
     */
    public File correctFile(File file) throws IllegalArgumentException {
        if (file == null) {
            throw new IllegalArgumentException("file must not be null");
        }

        int index = file.getName().lastIndexOf('.');
        String fileName =
            file.getName().substring(0, index) + "_corrected" + file.getName().substring(index);

        File resultFile = new File(file.getParentFile(), fileName);

        return correctFile(file, resultFile);
    }

    /**
     * <p>
     * corrects the given file, stores the result in the second provided file and returns the
     * file into which the result was written
     * </p>
     * 
     * @param file       the file to be corrected
     * @param resultFile the file into which the corrected log shall be written
     * 
     * @return the file with the corrected logfile
     * 
     * @throws IllegalArgumentException if the file or resultFile is null or if they are equal
     */
    public File correctFile(File file, File resultFile) throws IllegalArgumentException {
        if ((file == null) || (resultFile == null)) {
            throw new IllegalArgumentException("file and result file must not be null");
        }
        
        if (file.getAbsolutePath().equals(resultFile.getAbsolutePath())) {
            throw new IllegalArgumentException("file and result file must not be equal");
        }
        
        try {
            outFile = new PrintStream(new BufferedOutputStream(new FileOutputStream(resultFile)));
            outFile.println("<?xml version=\"1.0\" encoding=\"UTF-8\" ?>");
        }
        catch (FileNotFoundException e1) {
            throw new IllegalArgumentException("could not create a corrected file name " +
                                               resultFile + " next to " + file);
        }
        

        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 null;
        }
        catch (ParserConfigurationException e) {
            Console.printerr("Error parsing file + " + file.getName());
            Console.logException(e);
            return null;
        }
        catch (SAXException e) {
            Console.printerr("Error parsing file + " + file.getName());
            Console.logException(e);
            return null;
        }
        catch (FileNotFoundException e) {
            Console.printerr("Error parsing file + " + file.getName());
            Console.logException(e);
            return null;
        }
        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);
                return null;
            }
            catch (SAXException e) {
                Console.printerr("Error parsing file + " + file.getName());
                Console.logException(e);
                return null;
            }
            catch (IOException e) {
                Console.printerr("Error parsing file + " + file.getName());
                Console.logException(e);
                return null;
            }
        }
        
        if (outFile != null) {
            outFile.close();
        }
        
        return resultFile;
    }

    /*
     * (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
    {
        if (qName.equals("sessions")) {
            currentSession = new Session();
            currentSession.type = "sessions";
        }
        else if (qName.equals("newsession")) {
            if (currentSession != null) {
                currentSession.dump(outFile);
            }
            
            currentSession = new Session();
            currentSession.type = "newsession";
        }
        else if (qName.equals("event")) {
            currentEvent = new Event();
            currentEvent.id = atts.getValue("id");
        }
        else if (qName.equals("source")) {
            currentSource = new Source();
        }
        else if (qName.equals("component")) {
            currentComponent = new Component();
        }
        else if (qName.equals("param")) {
            if (currentComponent != null) {
                currentComponent.params.add
                    (new String[] {atts.getValue("name"), atts.getValue("value") });
            }
            else if (currentSource != null) {
                currentSource.params.add
                    (new String[] {atts.getValue("name"), atts.getValue("value") });
            }
            else if (currentEvent != null) {
                currentEvent.params.add
                    (new String[] {atts.getValue("name"), atts.getValue("value") });
            }
            else {
                throw new SAXException("parameter occurred at an unexpected place");
            }
        }
        else {
            throw new SAXException("unexpected tag " + 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("sessions")) {
            correctSources(currentSession);

            currentSession.dump(outFile);
            currentSession = null;
            allSources.clear();
        }
        else if (qName.equals("newsession")) {
            correctSources(currentSession);
            
            currentSession.dump(outFile);
            currentSession = null;
            allSources.clear();
        }
        else if (qName.equals("event")) {
            currentSession.events.add(currentEvent);
            currentEvent = null;
        }
        else if (qName.equals("source")) {
            currentEvent.source = getUniqueSource(currentSource);
            currentSource = null;
        }
        else if (qName.equals("component")) {
            currentSource.components.add(currentComponent);
            currentComponent = null;
        }
        else if (!qName.equals("param")) {
            throw new SAXException("unexpected closing tag " + qName);
        }

    }

    /**
     * <p>
     * stores a parsed source for later correction or reuse
     * </p>
     *
     * @param source the source to store
     */
    private Source getUniqueSource(Source source) {
        String toStringValue = getToStringParam(source);

        Source existingSource = findSource(toStringValue);
        
        if (existingSource == null) {
            List<Source> sources = allSources.get(toStringValue);
        
            if (sources == null) {
                sources = new ArrayList<Source>();
                allSources.put(toStringValue, sources);
            }
            
            sources.add(source);
            existingSource = source;
        }
        
        return existingSource;
    }

    /**
     * <p>
     * convenience method to find a source based on its <code>toString</code> parameter value.
     * The method only returns sources, which match the provided <code>toString</code>
     * representation and which have a valid list of components.
     * </p>
     *
     * @param toStringValue the value of the <code>toString</code> parameter the source to find
     *                      must have
     * 
     * @return the source matching the parameter and having a valid list of components or null if
     *         none is found
     */
    private Source findSource(String toStringValue) {
        Source existingSource = null;
        
        List<Source> candidates = allSources.get(toStringValue);
        
        if (candidates != null) {
            for (Source candidate : candidates) {
                if (toStringValue.equals(getToStringParam(candidate)) &&
                    (candidate.components != null) && (candidate.components.size() > 0))
                {
                    existingSource = candidate;
                    break;
                }
            }
        }
        
        return existingSource;
    }

    /**
     * <p>
     * corrects all wrong sources in the events of the session. For each wrong resource, the
     * {@link #correctEventSource(Event, Source)} method is called.
     * </p>
     *
     * @param session the session of which the events shall be corrected
     */
    private void correctSources(Session session) {
        Source previousSource = null;
        for (Event event : session.events) {
            if ((event.source == null) || (event.source.components == null) ||
                (event.source.components.size() == 0))
            {
                correctEventSource(event, previousSource);
            }
            
            previousSource = event.source;
        }
    }

    /**
     * <p>
     * corrects the source of an event. It first searches for a correct source with an equal
     * <code>toString</code> parameter. If there is any, this is reused. Otherwise it creates a
     * new correct source. For this, it copies all parameters of the source of the provided previous
     * event if they are not included in the source already. Furthermore, it adds all components
     * of the source of the previous event. At last, it adds a further component based on the
     * information found in the <code>toString</code> parameter of the source.
     * </p>
     * 
     * @param event          the event of which the source must be corrected
     * @param previousSource the source of the previous event to be potentially reused partially
     */
    private void correctEventSource(Event event, Source previousSource) {
        String toStringValue = null;
        
        if ((event.source != null) && (event.source.params != null)) {
            toStringValue = getToStringParam(event.source);
        }
        
        Source existingSource = null;
        
        if (toStringValue != null) {
            existingSource = findSource(toStringValue);
        }
        
        if (existingSource != null) {
            event.source = existingSource;
        }
        else {
            if (previousSource != null) {
                for (String[] parameterOfPreviousSource : previousSource.params) {
                    boolean foundParameter = false;
                    for (String[] parameter : event.source.params) {
                        if (parameter[0].equals(parameterOfPreviousSource[0])) {
                            foundParameter = true;
                            break;
                        }
                    }

                    if (!foundParameter) {
                        event.source.params.add(parameterOfPreviousSource);
                    }
                }
    
                for (Component component : previousSource.components) {
                    if (!(component instanceof ComponentFromToString)) {
                        event.source.components.add(component);
                    }
                }
            }

            event.source.components.add(getComponentFromToString(toStringValue));
        }
    }

    /**
     * <p>
     * retrieves the value of the <code>toString</code> parameter of the provided source.
     * </p>
     *
     * @param source the source to read the parameter of
     * @return the value of the parameter
     */
    private String getToStringParam(Source source) {
        String value = null;
        
        for (String[] param : source.params) {
            if (("toString".equals(param[0])) && (param[1] != null) && (!"".equals(param[1]))) {
                value = param[1];
                break;
            }
        }
        
        return value;
    }

    /**
     * <p>
     * determines a component based on the <code>toString</code> parameter of a source.
     * For this, it parses the parameter value and tries to determine several infos such as the
     * type and the name of it. The resulting components are not always distinguishable. This is,
     * because the <code>toString</code> parameter does not contain sufficient information for
     * correct identification.
     * </p>
     * 
     * @param toStringValue the <code>toString</code> parameter of a source
     * 
     * @return the component parsed from the <code>toString</code> parameter
     */
    private Component getComponentFromToString(String toStringValue) {
        ComponentFromToString component = new ComponentFromToString();
        
        // search for the beginning of the parameters section. Up to this position we find the class
        int start = toStringValue.indexOf('[');
        String clazz = toStringValue.substring(0, start);
        
        // the first parameters are x and y coordinate as well as the size. The size is one
        // parameter, where with and height are separated with an 'x'
        start = toStringValue.indexOf(',', start) + 1;
        int end = toStringValue.indexOf(',', start);
        
        component.x = Integer.parseInt(toStringValue.substring(start, end));
        
        start = end + 1;
        end = toStringValue.indexOf(',', start);

        component.y = Integer.parseInt(toStringValue.substring(start, end));

        start = end + 1;
        end = toStringValue.indexOf('x', start);

        component.width = Integer.parseInt(toStringValue.substring(start, end));

        start = end + 1;
        end = toStringValue.indexOf(',', start);

        component.height = Integer.parseInt(toStringValue.substring(start, end));

        // no start parsing the rest of the parameters and extract those having a key and a 
        // value and whose key is text, defaultIcon, or an alignment
        int intermediate;
        start = end + 1;

        String title = null;
        String icon = null;
        String alignment = null;
        
        do {
            end = toStringValue.indexOf(',', start);
            intermediate = toStringValue.indexOf('[', start);
            
            if ((intermediate >= 0) && (intermediate < end)) {
                // the value of the parameter itself contains brackets. So try to determine the 
                // real end of the parameter
                end = toStringValue.indexOf(']', intermediate);
                end = toStringValue.indexOf(',', end);
            }
            
            if (end < 0) {
                //we reached the end of the stream. So the the end to the "end"
                end = toStringValue.lastIndexOf(']');
            }
            
            intermediate = toStringValue.indexOf('=', start);
            
            if ((intermediate >= 0) && (intermediate < end)) {
                // this is a key value pair, so store the the parameter
                String key = toStringValue.substring(start, intermediate);
                String value = toStringValue.substring(intermediate + 1, end);
                
                if ("text".equals(key)) {
                    title = value;
                }
                else if ("defaultIcon".equals(key)) {
                    icon = value;
                }
                else if ("alignmentX".equals(key) || "alignmentY".equals(key)) {
                    if (alignment == null) {
                        alignment = value;
                    }
                    else {
                        alignment += "/" + value;
                    }
                }
            }
            /*else {
                // this is a simple value, for now simply ignore it
                String key = toStringValue.substring(start, end);
                if (!"invalid".equals(key)) {
                    componentHash += key.hashCode();
                    component.params.add(new String[] { key, "true" });
                }
            }*/
            
            start = end + 1;
        }
        while (start < toStringValue.lastIndexOf(']'));
        
        // finish the component specification by setting the parameters
        if ((title == null) || "".equals(title) || "null".equals(title)) {
            if ((icon == null) || "".equals(icon) || "null".equals(icon)) {
                title = clazz.substring(clazz.lastIndexOf('.') + 1) + "(";
                
                // to be able to distinguish some elements, that usually have no name and icon, try
                // to include some of their specific identifying information in their name.
                if ("org.tigris.gef.presentation.FigTextEditor".equals(clazz) ||
                    "org.argouml.core.propertypanels.ui.UMLTextField".equals(clazz))
                {
                    title += "height " + component.height + ", ";
                }
                else if ("org.argouml.core.propertypanels.ui.UMLLinkedList".equals(clazz) ||
                         "org.argouml.core.propertypanels.ui.LabelledComponent".equals(clazz))
                {
                    title += "position " + component.x + "/" + component.y + ", ";
                }
                
                title += "alignment " + alignment + ")";
            }
            else {
                title = icon;
            }
        }
        
        component.params.add(new String[] { "title", title } );
        component.params.add(new String[] { "class", clazz } );
        component.params.add(new String[] { "icon", icon } );
        component.params.add(new String[] { "index", "-1" });
        
        int hashCode = clazz.hashCode() + title.hashCode();
        
        if (hashCode < 0) {
            hashCode = -hashCode;
        }
        
        component.params.add(new String[] { "hash", Integer.toString(hashCode, 16) });

        return component;
    }    

    /**
     * <p>
     * used to dump a list of parameters to the provided print stream
     * </p>
     */
    private void dumpParams(PrintStream out, List<String[]> params, String indent) {
        for (String[] param : params) {
            out.print(indent);
            out.print("<param name=\"");
            out.print(StringTools.xmlEntityReplacement(param[0]));
            out.print("\" value=\"");
            out.print(StringTools.xmlEntityReplacement(param[1]));
            out.println("\" />");
        }
        
    }
    
    /**
     * <p>
     * used to carry all events of a session and to dump it to the output file
     * </p> 
     */
    private class Session {
        private String type;
        private List<Event> events = new ArrayList<Event>();
        
        public void dump(PrintStream out) {
            out.print("<");
            out.print(type);
            out.println(">");

            for (Event event : events) {
                event.dump(out);
            }
            
            out.print("</");
            out.print(type);
            out.println(">");
        }
    }

    /**
     * <p>
     * used to carry all information about an event and to dump it to the output file
     * </p> 
     */
    private class Event {
        private String id;
        private List<String[]> params = new ArrayList<String[]>();
        private Source source;
        
        private void dump(PrintStream out) {
            out.print("<event id=\"");
            out.print(StringTools.xmlEntityReplacement(id));
            out.println("\">");
            
            dumpParams(out, params, " ");
            source.dump(out);
            
            out.println("</event>");
        }
    }

    /**
     * <p>
     * used to carry all information about a source of an event and to dump it to the output file
     * </p> 
     */
    private class Source {
        private List<String[]> params = new ArrayList<String[]>();
        private List<Component> components = new ArrayList<Component>();

        private void dump(PrintStream out) {
            out.println(" <source>");
            
            dumpParams(out, params, "  ");
            
            for (Component component : components) {
                component.dump(out);
            }
            
            out.println(" </source>");
        }
    }
    
    /**
     * <p>
     * used to carry all information about a component of a source and to dump it to the output file
     * </p> 
     */
    private class Component {
        protected List<String[]> params = new ArrayList<String[]>();
        
        protected void dump(PrintStream out) {
            out.println("  <component>");
            dumpParams(out, params, "   ");
            out.println("  </component>");
        }

        public boolean equals(Object other) {
            if (this == other) {
                return true;
            }
            
            if (!(other instanceof Component)) {
                return false;
            }
            
            Component otherComp = (Component) other;
            
            boolean allParamsEqual = (params.size() == otherComp.params.size());
            
            if (allParamsEqual) {
                for (int i = 0; i < params.size(); i++) {
                    if (!params.get(i)[0].equals(otherComp.params.get(i)[0]) &&
                        !params.get(i)[1].equals(otherComp.params.get(i)[1]))
                    {
                        allParamsEqual = false;
                        break;
                    }
                }
            }
            
            return allParamsEqual;
        }
    }

    /**
     * <p>
     * represents a specific component, which was read from the toString parameter of a source
     * </p> 
     */
    private class ComponentFromToString extends Component {
        private int x;
        private int y;
        private int width;
        private int height;
        
        @Override
        protected void dump(PrintStream out) {
            out.println("  <component>");
            
            out.print("   ");
            out.print("<param name=\"x\" value=\"");
            out.print(x);
            out.println("\" />");

            out.print("   ");
            out.print("<param name=\"y\" value=\"");
            out.print(y);
            out.println("\" />");

            out.print("   ");
            out.print("<param name=\"width\" value=\"");
            out.print(width);
            out.println("\" />");

            out.print("   ");
            out.print("<param name=\"height\" value=\"");
            out.print(height);
            out.println("\" />");

            dumpParams(out, params, "   ");
            out.println("  </component>");
        }
        
        /*public boolean equals(Object other) {
            if (this == other) {
                return true;
            }
            
            if (!(other instanceof ComponentFromToString)) {
                return false;
            }
            
            ComponentFromToString otherComp = (ComponentFromToString) other;
            
            // ignore the coordinates and the width as it may change over time
            boolean allParamsEqual =
                (height == otherComp.height) && (params.size() == otherComp.params.size());
            
            if (allParamsEqual) {
                for (int i = 0; i < params.size(); i++) {
                    if (!"x".equals(params.get(i)[0]) && !"y".equals(params.get(i)[0]) &&
                        !"width".equals(params.get(i)[0]) && !"height".equals(params.get(i)[0]) &&
                        !"index".equals(params.get(i)[0]) && !"hash".equals(params.get(i)[0]) &&
                        !params.get(i)[0].equals(otherComp.params.get(i)[0]) &&
                        !params.get(i)[1].equals(otherComp.params.get(i)[1]))
                    {
                        allParamsEqual = false;
                        break;
                    }
                }
            }
            
            return allParamsEqual;
        }*/
    }

}
