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

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.List;

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 specification for
 * events. It parses the file and adds component specifications to the source, that do not have
 * them. The parent components are reused from the last GUI element the user worked with. 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. 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 source of the last parsed event
     * </p>
     */
    private Source lastSource;

    /**
     * <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 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")) {
            //harmonizeToStringComponents();

            currentSession.dump(outFile);
            currentSession = null;
        }
        else if (qName.equals("newsession")) {
            //harmonizeToStringComponents();
            
            currentSession.dump(outFile);
            currentSession = null;
        }
        else if (qName.equals("event")) {
            currentSession.events.add(currentEvent);
            currentEvent = null;
        }
        else if (qName.equals("source")) {
            if (currentSource.components.size() == 0) {
                correctEventSource(currentSource);
            }
            
            lastSource = currentSource;
            currentEvent.source = 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);
        }

    }

    /**
     * 
     */
    /*private void harmonizeToStringComponents() {
        List<SourceGroup> groups = determineSourceGroups();
        
        for (SourceGroup group : groups) {
            group.dump(System.out);
        }
    }*/

    /**
     *
     */
    /*private List<SourceGroup> determineSourceGroups() {
        List<SourceGroup> groups = new ArrayList<SourceGroup>();
        
        for (Event event : currentSession.events) {
            Source source = event.source;
            Component lastComponent = source.components.get(source.components.size() - 1);
            if (lastComponent instanceof ComponentFromToString) {
                SourceGroup group = getSourceGroup(groups, source);
                List<ComponentFromToString> sourcesWithSameXCoordinate =
                    group.leafComponents.get(((ComponentFromToString) lastComponent).x);
                
                if (sourcesWithSameXCoordinate == null) {
                    sourcesWithSameXCoordinate = new ArrayList<ComponentFromToString>();
                    group.leafComponents.put
                        (((ComponentFromToString) lastComponent).x, sourcesWithSameXCoordinate);
                }
                
                if (!sourcesWithSameXCoordinate.contains(lastComponent)) {
                    sourcesWithSameXCoordinate.add(((ComponentFromToString) lastComponent));
                }
            }
        }
        
        return groups;
    }*/

    /**
     * 
     */
    /*private SourceGroup getSourceGroup(List<SourceGroup> groups, Source source) {
        SourceGroup resultingGroup = null;
        
        for (SourceGroup candidate : groups) {
            if (candidate.parentComponents.size() == (source.components.size() - 1)) {
                boolean allComponentsMatch = true;
            
                for (int i = 0; i < candidate.parentComponents.size(); i++) {
                    if (!candidate.parentComponents.get(i).equals(source.components.get(i))) {
                        allComponentsMatch = false;
                        break;
                    }
                }
                
                if (allComponentsMatch) {
                    resultingGroup = candidate;
                    break;
                }
            }
        }
        
        if (resultingGroup == null) {
            resultingGroup = new SourceGroup();
            
            for (int i = 0; i < (source.components.size() - 1); i++) {
                resultingGroup.parentComponents.add(source.components.get(i));
            }
            
            groups.add(resultingGroup);
        }
        
        return resultingGroup;
    }*/

    /**
     * <p>
     * corrects the source of an event. It copies all parameters of the source of the 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 source the source to be corrected
     */
    private void correctEventSource(Source source) {
        for (String[] parameterOfLastSource : lastSource.params) {
            boolean foundParameter = false;
            for (String[] parameter : source.params) {
                if (parameter[0].equals(parameterOfLastSource[0])) {
                    foundParameter = true;
                    break;
                }
            }
            
            if (!foundParameter) {
                source.params.add(parameterOfLastSource);
            }
        }
        
        for (Component component : lastSource.components) {
            if (!(component instanceof ComponentFromToString)) {
                source.components.add(component);
            }
        }
        
        source.components.add(getComponentFromToString(source));
    }

    /**
     * <p>
     * determines a component based on the <code>toString</code> parameter of the provided 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 source the source to extract the <code>toString</code> parameter from
     * 
     * @return the component parsed from the <code>toString</code> parameter
     */
    private Component getComponentFromToString(Source source) {
        String toStringValue = null;
        
        for (String[] parameter : source.params) {
            if ("toString".equals(parameter[0])) {
                toStringValue = parameter[1];
                break;
            }
        }
        
        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;
        }*/
    }

    /**
     * 
     */
    /*private class SourceGroup {
        protected List<Component> parentComponents = new ArrayList<Component>();
        protected Map<Integer, List<ComponentFromToString>> leafComponents =
            new HashMap<Integer, List<ComponentFromToString>>();

        private void dump(PrintStream out) {
            out.println("source group:");
            
            for (Map.Entry<Integer, List<ComponentFromToString>> entry : leafComponents.entrySet())
            {
                out.print("    x-coordinate = ");
                out.println(entry.getKey());
                for (ComponentFromToString leafComponent : entry.getValue()) {
                    leafComponent.dump(out);
                }
                
                out.println();
            }
        }
    }*/

}
