//   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.jfc.commands;

import java.io.BufferedWriter;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.UUID;
import java.util.logging.Level;

import de.ugoe.cs.autoquest.CommandHelpers;
import de.ugoe.cs.autoquest.SequenceInstanceOf;
import de.ugoe.cs.util.console.Command;
import de.ugoe.cs.autoquest.eventcore.Event;
import de.ugoe.cs.autoquest.eventcore.gui.*;
import de.ugoe.cs.autoquest.keyboardmaps.VirtualKey;
import de.ugoe.cs.autoquest.plugin.jfc.guimodel.JFCGUIElement;
import de.ugoe.cs.util.console.Console;
import de.ugoe.cs.util.console.GlobalDataContainer;

// helper class for the tree like structure part within a Jacareto file
class StructureNode {
    private static int nextRef = 0;

    public String content;
    public ArrayList<StructureNode> children;

    public StructureNode(String type) {
        content = "<StructureElement class=\"jacareto.struct." + type + "\">";
        children = new ArrayList<StructureNode>();
    }

    public StructureNode() {
        content = "<Recordable ref=\"" + (nextRef++) + "\" />";
        children = new ArrayList<StructureNode>();
    }

    public StructureNode add(String type) {
        StructureNode node = new StructureNode(type);
        children.add(node);
        return node;
    }

    public void addRecordable() {
        children.add(new StructureNode());
    }

    @Override
    public String toString() {
        String separator = System.getProperty("line.separator");
        String result = content + separator;

        for (StructureNode child : children) {
            result += child.toString();
        }

        if (content.endsWith("/>")) {
            return result;
        }
        return result + "</StructureElement>" + separator;
    }
}

/**
 * <p>
 * Command to create a Jacareto xml replay file from stored sessions.
 * </p>
 * 
 * @author Daniel May
 * @version 1.0
 */
public class CMDgenerateJacaretoReplay implements Command {
    private JFCGUIElement currentFocus;
    private StructureNode structure;

    private StructureNode lastKeySequenceEvent;
    private StructureNode lastKeyTypedEvent;
    private int currentKeyModifiers;

    private HashMap<VirtualKey, Integer> modifiers;
    
    private StructureNode lastMouseClickEvent;
    private StructureNode lastFocusChangeEvent;
    private StructureNode lastItemActionEvent;

    /*
     * (non-Javadoc)
     * 
     * @see de.ugoe.cs.util.console.Command#help()
     */
    @Override
    public String help() {
        return "generateJacaretoReplay <filename> <sequences>";
    }

    /*
     * (non-Javadoc)
     * 
     * @see de.ugoe.cs.util.console.Command#run(java.util.List)
     */
    @SuppressWarnings("unchecked")
    @Override
    public void run(List<Object> parameters) {
        String filename;
        String sequencesName;
        try {
            filename = (String) parameters.get(0);
            sequencesName = (String) parameters.get(1);
        }
        catch (Exception e) {
            throw new IllegalArgumentException();
        }

        Collection<List<Event>> sequences = null;
        Object dataObject = GlobalDataContainer.getInstance().getData(sequencesName);
        if (dataObject == null) {
            CommandHelpers.objectNotFoundMessage(sequencesName);
            return;
        }
        if (!SequenceInstanceOf.isCollectionOfSequences(dataObject)) {
            CommandHelpers.objectNotType(sequencesName, "Collection<List<Event<?>>>");
            return;
        }

        sequences = (Collection<List<Event>>) dataObject;

        // map which maps VirtualKeys back to awt key modifier codes
        modifiers = new HashMap<>();
        modifiers.put(VirtualKey.SHIFT, 1);
        modifiers.put(VirtualKey.CONTROL, 2);
        modifiers.put(VirtualKey.ALT, 8);
        modifiers.put(VirtualKey.ALT_GRAPH, 32);
        currentKeyModifiers = 0;
        
        writeJacaretoXML(sequences, filename);
    }

    private void writeLine(BufferedWriter writer, String line) throws IOException {
        writer.write(line);
        writer.newLine();
    }

    private void writeJacaretoHead(BufferedWriter writer) throws IOException {
        writeLine(writer, "<?xml version=\"1.0\" encoding=\"iso-8859-1\"?>");
        writeLine(writer, "<JacaretoStructure>");
        writeLine(writer, "<Record>");

        // TODO: This header content is basically copy+paste from a
        // specific jacareto replay file right now.
        // Some things such as screen resolution and especially the
        // application starter details need to be changed for general cases.
        writeLine(writer,
                  "<Calendar procTime=\"0\" duration=\"0\" year=\"2014\" month=\"8\" date=\"11\" hour=\"14\" min=\"43\" sec=\"41\" uuid=\"06831ba1-f28a-4e05-b46e-ce9d8f9ffa0f\" />");
        writeLine(writer,
                  "<SystemInfo procTime=\"0\" duration=\"0\" screenWidth=\"2646\" screenHeight=\"1024\" javaVersion=\"1.7.0_65\" lookAndFeel=\"javax.swing.plaf.metal.MetalLookAndFeel\" uuid=\"720f430f-52cf-4d8b-9fbe-58434f766efe\" />");
        writeLine(writer,
                  "<KeyboardState procTime=\"0\" duration=\"0\" isNumLockOn=\"false\" isScrollLockOn=\"false\" isCapsLockOn=\"false\" applyIsNumLockOn=\"true\" applyIsScrollLockOn=\"true\" applyIsCapsLockOn=\"true\" uuid=\"28146f79-9fc7-49f9-b4a8-5866a7625683\" />");
        writeLine(writer, "<ComponentMode numberPopupMenues=\"true\" />");
        writeLine(writer,
                  "<ApplicationStarter procTime=\"0\" duration=\"0\" name=\"HelloWorldSwing\" class=\"HelloWorldSwing\" initclass=\"\" basepath=\"/home/daniel/project/autoquest-jfcmonitor\" classpathext=\"${basepath}/helloswing.jar;${basepath}/.;\" detectDuration=\"false\" captureparams=\"\" replayparams=\"\" uuid=\"a7b7d7b9-caa9-4d6d-b052-cf74d353275e\" />");
    }

    private void writeJacaretoEvents(BufferedWriter writer, Collection<List<Event>> sequences)
        throws IOException
    {
        structure = new StructureNode("RootElement");
        // reference the elements that we included in the header
        structure.addRecordable(); // Calendar
        structure.addRecordable(); // SystemInfo
        structure.addRecordable(); // KeyboardState
        structure.addRecordable(); // ComponentMode
        structure.addRecordable(); // ApplicationStarter

        for (List<Event> sequence : sequences) {
            for (Iterator<Event> eventIter = sequence.iterator(); eventIter.hasNext();) {
                Event event = eventIter.next();

                if (event.getType() instanceof MouseButtonDown) {
                    lastKeySequenceEvent = null;

                    lastMouseClickEvent = new StructureNode("MouseClick");
                    writeMouseClickEvent(writer, event, 501);
                }
                else if (event.getType() instanceof MouseButtonUp) {
                    lastKeySequenceEvent = null;

                    writeMouseClickEvent(writer, event, 502);
                }
                else if (event.getType() instanceof MouseClick) {
                    lastKeySequenceEvent = null;

                    writeMouseClickEvent(writer, event, 500);
                    // FIXME: don't always write an item action
                    writeItemActionEvent(writer, event);
                    // FIXME: don't write it all here because there
                    // might be no item action at all
                    if (lastFocusChangeEvent == null) {
                        // write structure sequentially
                        structure.children.add(lastMouseClickEvent);
                        structure.children.add(lastItemActionEvent);
                    }
                    else {
                        // with nested structure
                        structure.children.add(lastItemActionEvent);
                        lastItemActionEvent.children.add(0, lastFocusChangeEvent);
                        lastFocusChangeEvent.children.add(0, lastMouseClickEvent);

                        lastFocusChangeEvent = null;
                    }
                }
                else if (event.getType() instanceof KeyboardFocusChange) {
                    lastKeySequenceEvent = null;

                    writeFocusChangeEvent(writer, event);
                }
                else if (event.getType() instanceof KeyPressed) {
                    if (lastKeySequenceEvent == null) {
                        lastKeySequenceEvent = structure.add("KeySequence");
                    }
                    lastKeyTypedEvent = lastKeySequenceEvent.add("KeyTyped");
                    writeKeyEvent(writer, event, 401);
                }
                else if (event.getType() instanceof KeyReleased) {
                    writeKeyEvent(writer, event, 402);
                }
            }
        }
    }

    private void writeJacaretoTail(BufferedWriter writer) throws IOException {
        writeLine(writer, "</Record>");

        // write the recording's structure
        writeLine(writer, "<Structure>");
        writer.write(structure.toString());
        // close root element
        writeLine(writer, "</Structure>");
    }

    private void writeJacaretoXML(Collection<List<Event>> sequences, String filename) {
        BufferedWriter writer = new BufferedWriter(openReplayFile(filename + ".xml"));

        try {
            writeJacaretoHead(writer);
            writeJacaretoEvents(writer, sequences);
            writeJacaretoTail(writer);
            writeLine(writer, "</JacaretoStructure>");

            writer.flush();
            writer.close();
        }
        catch (IOException e) {
            Console.printerrln("Unable to write Jacareto replay file " + filename);
        }
    }

    /**
     * <p>
     * Helper function that opens the replay file for writing.
     * </p>
     * 
     * @param filename
     *            name and path of the replay file
     * @param encoding
     *            file encoding, empty string for platform default
     * @return {@link OutputStreamWriter} that writes to the replay file
     */
    private OutputStreamWriter openReplayFile(String filename) {
        File file = new File(filename);
        boolean fileCreated;
        try {
            fileCreated = file.createNewFile();
            if (!fileCreated) {
                Console.traceln(Level.INFO, "Created logfile " + filename);
            }
            else {
                Console.traceln(Level.INFO, "Overwrote existing logfile " + filename);
            }
        }
        catch (IOException e) {
            Console.printerrln("Unable to create file " + filename);
            Console.logException(e);
        }
        OutputStreamWriter writer = null;
        try {
            writer = new OutputStreamWriter(new FileOutputStream(file));
        }
        catch (IOException e) {
            Console.printerrln("Unable to open file for writing (read-only file):" + filename);
            Console.logException(e);
        }
        return writer;
    }

    private void writeItemActionEvent(BufferedWriter writer, Event event) throws IOException {
        JFCGUIElement target = (JFCGUIElement) event.getTarget();
        MouseButtonInteraction info = (MouseButtonInteraction) event.getType();

        //@formatter:off
        writeLine(writer,
            "<ItemEvent "
            + "procTime=\"0\" "
            + "duration=\"0\" "
            + "source=\"" + target.getJacaretoHierarchy() + "\" "
            + "class=\"" + target.getSpecification().getType() + "\" "
            + "uuid=\"" + UUID.randomUUID() + "\" "
            + "ID=\"701\" "
            + "item=\"\" "
            + "stateChange=\"1\" />"
        );
        writeLine(writer,
            "<ActionEvent "
            + "procTime=\"0\" "
            + "duration=\"0\" "
            + "source=\"" + target.getJacaretoHierarchy() + "\" "
            + "class=\"" + target.getSpecification().getType() + "\" "
            + "uuid=\"" + UUID.randomUUID() + "\" "
            + "ID=\"1001\" "
            + "command=" + target.getName() + " "
            + "modifiers=\"" + getButtonModifier(info) + "\" />"
        );
        //@formatter:on
        lastItemActionEvent = new StructureNode("ItemStateChange");
        lastItemActionEvent.addRecordable();
        lastItemActionEvent.addRecordable();
    }

    private void writeFocusChangeEvent(BufferedWriter writer, Event event) throws IOException {
        KeyboardFocusChange info = (KeyboardFocusChange) event.getType();
        JFCGUIElement target = (JFCGUIElement) event.getTarget();

        if (currentFocus != null) {
            lastFocusChangeEvent = new StructureNode("FocusChange");

            // focus lost on old target
            writeFocusEvent(writer, info, currentFocus, 1005);
            // focus gained on new target
            writeFocusEvent(writer, info, target, 1004);
        }
        else {
            // TODO: it seems like Jacareto wants a window activation before
            // the first focus event but that is not the case in autoquest,
            // skip for now
        }

        currentFocus = target;
    }

    private void writeFocusEvent(BufferedWriter writer,
                                 KeyboardFocusChange info,
                                 JFCGUIElement target,
                                 int jacId) throws IOException
    {
        //@formatter:off
        writeLine(writer,
            "<FocusEvent "
            + "procTime=\"0\" "
            + "duration=\"0\" "
            + "source=\"" + target.getJacaretoHierarchy() + "\" "
            + "class=\"" + target.getSpecification().getType() + "\" "
            + "uuid=\"" + UUID.randomUUID() + "\" "
            + "ID=\"" + jacId + "\" "
            + "component=\"null\" "
            + "root=\"" + target.getJacaretoRoot() + "\" "
            + "xPos=\"0\" "
            + "yPos=\"0\" "
            + "width=\"0\" "
            + "height=\"0\" "
            + "isTemporary=\"false\" />" 
        );
        //@formatter:on
        lastFocusChangeEvent.addRecordable();
    }

    private void writeMouseClickEvent(BufferedWriter writer, Event event, int jacId)
        throws IOException
    {
        MouseButtonInteraction info = (MouseButtonInteraction) event.getType();
        JFCGUIElement target = (JFCGUIElement) event.getTarget();
        int clickCount = event.getType() instanceof MouseDoubleClick ? 2 : 1;

        // TODO: change procTime and duration to adequate values
        //@formatter:off
        writeLine(writer,
            "<MouseEvent "
            + "procTime=\"0\" "
            + "duration=\"150\" "
            + "source=\"" + target.getJacaretoHierarchy() + "\" "
            + "class=\"" + target.getSpecification().getType() + "\" "
            + "uuid=\"" + UUID.randomUUID() + "\" "
            + "ID=\"" + jacId + "\" "
            + "component=\"null\" "
            + "root=\"" + target.getJacaretoRoot() + "\" "
            + "xPos=\"0\" "
            + "yPos=\"0\" "
            + "width=\"0\" "
            + "height=\"0\" "
            + "when=\"" + event.getTimestamp() + "\" " 
            + "isConsumed=\"false\">" 
        );
        writeLine(writer,
            "<MouseInfo "
            + "xPosition=\"" + info.getX() + "\" "
            + "yPosition=\"" + info.getY() + "\" "
            + "rootX=\"0\" "
            + "rootY=\"0\" "
            + "clickCount=\"" + clickCount + "\" "
            + "modifiers=\"" + getButtonModifier(info) + "\" "
            + "isPopupTrigger=\"false\" />"
        );
        writeLine(writer, "</MouseEvent>");
        //@formatter:on

        lastMouseClickEvent.addRecordable();
    }

    private int getButtonModifier(MouseButtonInteraction info) {
        switch (info.getButton())
        {
            case LEFT:
                return 16;
            case MIDDLE:
                return 8;
            case RIGHT:
                return 4;
            default:
                // TODO: handle unknown mouse button
                return -1;
        }
    }

    private void writeKeyEvent(BufferedWriter writer, Event event, int jacId) throws IOException {
        KeyInteraction info = (KeyInteraction) event.getType();
        JFCGUIElement target = (JFCGUIElement) event.getTarget();
        int keyCode = info.getKey().getVirtualKeyCode();

        applyKeyModifier(info.getKey(), jacId == 401);
        
        //@formatter:off
        writeLine(writer,
            "<KeyEvent "
            + "procTime=\"0\" "
            + "duration=\"150\" "
            + "source=\"" + target.getJacaretoHierarchy() + "\" "
            + "class=\"" + target.getSpecification().getType() + "\" "
            + "uuid=\"" + UUID.randomUUID() + "\" "
            + "ID=\"" + jacId + "\" "
            + "component=\"null\" "
            + "root=\"" + target.getJacaretoRoot() + "\" "
            + "xPos=\"0\" "
            + "yPos=\"0\" "
            + "width=\"0\" "
            + "height=\"0\" "
            + "when=\"" + event.getTimestamp() + "\" " 
            + "isConsumed=\"false\">" 
        );
        writeLine(writer,
            "<KeyInfo "
            + "keyCode=\"" + keyCode + "\" "
            + "keyChar=\"" + getKeyChar(keyCode) + "\" "
            + "modifiers=\"" + currentKeyModifiers + "\" />"
        );
        
        writeLine(writer, "</KeyEvent>");
        
        lastKeyTypedEvent.addRecordable();
    }
    
    private String getKeyChar (int keyCode) {
        if (keyCode >= 32 && keyCode < 127) {
            return String.valueOf((char)keyCode);
        }
        else {
            return "_NO_LEGAL_XML_CHAR";
        }
    }
    
    private void applyKeyModifier (VirtualKey key, boolean set) {
        Integer modifier = modifiers.get(key);
        if (modifier != null) {
            if (set) {
                currentKeyModifiers |= modifier;
            }
            else {
                currentKeyModifiers &= ~modifier;
            }
        }
    }
}
