/* Copyright (C) 2005-2011 Fabio Riccardi */

package com.lightcrafts.ui.toolkit.journal;

import com.lightcrafts.utils.xml.XmlDocument;
import com.lightcrafts.utils.xml.XMLException;
import com.lightcrafts.utils.xml.XmlNode;

import javax.swing.*;
import java.awt.*;
import java.awt.event.*;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.LinkedList;
import java.util.Iterator;

/**
 * This class accumulates InputEvents on a single JFrame in a way that their
 * complete history can be serialized and replayed.
 * <p>
 * It works by tapping into the AWT event queue.  Therefore it is only
 * accessible through a singleton instance.  It also assumes that all events
 * occur in a single frame.  It doesn't keep track of window focus at all.
 * <p>
 * To activate the tap, which introduces a small amount of overhead on all
 * event dispatch, call start().  Then, to start accumulating history, press
 * "Scroll Lock".  All mouse and keyboard activity (other than "Scroll Lock")
 * will then accumulate in the history until you press "Scroll Lock" again.
 * To deactivate the tap, removing the dispatch overhead and disabling
 * accumulation, call stop().
 * <p>
 * To replay the history, call replay().  To stream the history somewhere,
 * call write().  To reload a saved history, call read().
 */
public class InputEventJournal implements AWTEventListener {

    /**
     * Since the AWT event queue is a singleton, so is this class.
     */
    public static InputEventJournal Instance = new InputEventJournal();

    // The default key to start and stop event capture is "\".
    private final static int DefaultTapControlKeyCode = KeyEvent.VK_BACK_SLASH;

    private TapQueueControl tapControl;
    private XmlDocument doc;

    private LinkedList listeners;   // JournalListeners
    int eventCount;

    private InputEventJournal() {
        doc = new XmlDocument("InputEventJournal");
        listeners = new LinkedList();
    }

    /**
     * Engage the InputEvent journalling machinery.  This means pushing a
     * custom EventQueue that can siphon off InputEvents, imposing a small
     * amount of overhead on event dispatch.
     */
    public void start() {
        tapControl = new TapQueueControl(this, DefaultTapControlKeyCode);
    }

    /**
     * Test whether the InputEvent journalling machinery is engaged.  Returns
     * true after start() and before stop().
     */
    public boolean isJournaling() {
        return tapControl != null;
    }

    /**
     * Disengage the InputEvent journalling machinery.  This stops all event
     * logging.
     */
    public void stop() {
        tapControl.dispose();
        tapControl = null;
    }

    /**
     * Write out all recorded InputEvents to the given stream, in such a way
     * that they may be retrieved later in read().
     */
    public void write(OutputStream out) throws IOException {
        doc.write(out);
    }

    /**
     * Reinitialize this journal from a saved stream, as generated by write().
     */
    public void read(InputStream in) throws IOException, XMLException {
        doc = new XmlDocument(in);
        // Validate the XML:
        XmlNode root = doc.getRoot();
        XmlNode[] children = root.getChildren();
        JFrame frame = new JFrame();
        for (int n=0; n<children.length; n++) {
            createEvent(frame, children[n]);
        }
    }

    /**
     * Reset this journal, forgetting all logged InputEvents.
     */
    public void clear() {
        doc = new XmlDocument("InputEventJournal");
        eventCount = 0;
    }

    public void replay(final JFrame frame) {
        Runnable runnable = new Runnable() {
            public void run() {
                EventQueue queue =
                    Toolkit.getDefaultToolkit().getSystemEventQueue();
                XmlNode[] nodes = doc.getRoot().getChildren();
                if (nodes.length == 0) {
                    return;
                }
                notifyJournalStarted(true);
                eventCount = 0;
                try {
                    int index = 0;
                    InputEvent event = createEvent(frame, nodes[index++]);
                    long when = event.getWhen();
                    do {
                        queue.postEvent(event);
                        notifyJournalEvent(true);
                        if (nodes.length > index) {
                            event = createEvent(frame, nodes[index++]);
                            Thread.sleep(event.getWhen() - when);
                            when = event.getWhen();
                        }
                    } while (nodes.length > index);
                }
                catch (XMLException e) {
                    System.err.println(
                        "Error decoding event history: " + e.getMessage()
                    );
                }
                catch (InterruptedException e) {
                    System.err.println("History replay interrupted");
                }
                notifyJournalEnded(true);
            }
        };
        Thread thread = new Thread(runnable, "InputEvent Replay");
        thread.start();
    }

    void addJournalListener(JournalListener listener) {
        listeners.add(listener);
    }

    void removeJournalListener(JournalListener listener) {
        listeners.remove(listener);
    }

    private void notifyJournalStarted(boolean replay) {
        for (Iterator i=listeners.iterator(); i.hasNext(); ) {
            JournalListener listener = (JournalListener) i.next();
            listener.journalStarted(replay);
        }
    }

    private void notifyJournalEvent(boolean replay) {
        for (Iterator i=listeners.iterator(); i.hasNext(); ) {
            JournalListener listener = (JournalListener) i.next();
            listener.journalEvent(++eventCount, replay);
        }
    }

    private void notifyJournalEnded(boolean replay) {
        for (Iterator i=listeners.iterator(); i.hasNext(); ) {
            JournalListener listener = (JournalListener) i.next();
            listener.journalEnded(replay);
        }
    }

    public void eventDispatched(AWTEvent event) {
        if (event == TapQueueControl.TapStartEvent) {
            notifyJournalStarted(false);
        }
        else if (event == TapQueueControl.TapEndEvent) {
            notifyJournalEnded(false);
        }
        else {
            addEvent((InputEvent) event);
            notifyJournalEvent(false);
        }
    }

    private void addEvent(InputEvent e) {
        if (e instanceof MouseWheelEvent) {
            addEvent((MouseWheelEvent) e);
        }
        else if (e instanceof KeyEvent) {
            addEvent((KeyEvent) e);
        }
        else if (e instanceof MouseEvent) {
            addEvent((MouseEvent) e);
        }
        else {
            // Can't think of another kind of InputEvent.
            assert false;
        }
    }

    private void addEvent(MouseEvent e) {
        XmlNode node = doc.getRoot().addChild("MouseEvent");
        node.setAttribute("id", toString(e.getID()));
        node.setAttribute("when", toString(e.getWhen()));
        node.setAttribute("modifiers", toString(e.getModifiers()));
        node.setAttribute("x", toString(e.getX()));
        node.setAttribute("y", toString(e.getY()));
        node.setAttribute("clickCount", toString(e.getClickCount()));
        node.setAttribute("popup", toString(e.isPopupTrigger()));
        node.setAttribute("button", toString(e.getButton()));
    }

    private void addEvent(MouseWheelEvent e) {
        XmlNode node = doc.getRoot().addChild("MouseWheelEvent");
        node.setAttribute("id", toString(e.getID()));
        node.setAttribute("when", toString(e.getWhen()));
        node.setAttribute("modifiers", toString(e.getModifiers()));
        node.setAttribute("x", toString(e.getX()));
        node.setAttribute("y", toString(e.getY()));
        node.setAttribute("clickCount", toString(e.getClickCount()));
        node.setAttribute("popup", toString(e.isPopupTrigger()));
        node.setAttribute("type", toString(e.getScrollType()));
        node.setAttribute("amount", toString(e.getScrollAmount()));
        node.setAttribute("rotation", toString(e.getWheelRotation()));
    }

    private void addEvent(KeyEvent e) {
        XmlNode node = doc.getRoot().addChild("KeyEvent");
        node.setAttribute("id", toString(e.getID()));
        node.setAttribute("when", toString(e.getWhen()));
        node.setAttribute("modifiers", toString(e.getModifiers()));
        node.setAttribute("keyCode", toString(e.getKeyCode()));
        node.setAttribute("keyChar", toString(e.getKeyChar()));
        node.setAttribute("keyLoc", toString(e.getKeyLocation()));
    }

    private InputEvent createEvent(JFrame frame, XmlNode node)
        throws XMLException
    {
        String name = node.getName();
        if (name.equals("MouseEvent")) {
            return createMouseEvent(frame, node);
        }
        else if (name.equals("MouseWheelEvent")) {
            return createMouseWheelEvent(frame, node);
        }
        else if (name.equals("KeyEvent")) {
            return createKeyEvent(frame, node);
        }
        else {
            // Can't think of another kind of InputEvent.
            assert false;
            return null;
        }
    }

    private InputEvent createMouseEvent(JFrame frame, XmlNode node)
        throws XMLException
    {
        int id = parseInt(node.getAttribute("id"));
        long when = parseLong(node.getAttribute("when"));
        int modifiers = parseInt(node.getAttribute("modifiers"));
        int x = parseInt(node.getAttribute("x"));
        int y = parseInt(node.getAttribute("y"));
        int clickCount = parseInt(node.getAttribute("clickCount"));
        boolean popup = parseBoolean(node.getAttribute("popup"));
        int button = parseInt(node.getAttribute("button"));

        MouseEvent e = new MouseEvent(
            frame, id, when, modifiers, x, y, clickCount, popup, button
        );
        return e;
    }

    private InputEvent createMouseWheelEvent(JFrame frame, XmlNode node)
        throws XMLException
    {
        int id = parseInt(node.getAttribute("id"));
        long when = parseLong(node.getAttribute("when"));
        int modifiers = parseInt(node.getAttribute("modifiers"));
        int x = parseInt(node.getAttribute("x"));
        int y = parseInt(node.getAttribute("y"));
        int clickCount = parseInt(node.getAttribute("clickCount"));
        boolean popup = parseBoolean(node.getAttribute("popup"));
        int type = parseInt(node.getAttribute("type"));
        int amount = parseInt(node.getAttribute("amount"));
        int rotation = parseInt(node.getAttribute("rotation"));

        MouseWheelEvent e = new MouseWheelEvent(
            frame, id, when, modifiers, x, y, clickCount, popup, type,
            amount, rotation
        );
        return e;
    }

    private InputEvent createKeyEvent(JFrame frame, XmlNode node)
        throws XMLException
    {
        int id = parseInt(node.getAttribute("id"));
        long when = parseLong(node.getAttribute("when"));
        int modifiers = parseInt(node.getAttribute("modifiers"));
        int keyCode = parseInt(node.getAttribute("keyCode"));
        char keyChar = parseChar(node.getAttribute("keyChar"));
        int keyLoc = parseInt(node.getAttribute("keyLoc"));
        
        KeyEvent e = new KeyEvent(
            frame, id, when, modifiers, keyCode, keyChar, keyLoc
        );
        return e;
    }

    private static String toString(char c) {
        return toString((int) c);
    }

    private static String toString(int i) {
        return Integer.toString(i);
    }

    private static String toString(long l) {
        return Long.toString(l);
    }

    private static String toString(boolean b) {
        return Boolean.toString(b);
    }

    private static char parseChar(String s) {
        return (char) parseInt(s);
    }

    private static int parseInt(String s) {
        return Integer.parseInt(s);
    }

    private static long parseLong(String s) {
        return Long.parseLong(s);
    }

    private static boolean parseBoolean(String s) {
        return Boolean.valueOf(s).booleanValue();
    }
}
