/**
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 *
 * Copyright 2009-2010 Brian Ziman
 * <http://www.brianziman.com/>
 *
 * Please contact me if you find flaws in this software.
 */
package com.brianziman.ui;

import java.awt.event.KeyListener;
import java.awt.event.KeyEvent;
import java.util.concurrent.*;
import java.util.*;

/**
 * <p>
 * This class wraps your KeyAdapter and suppresses spurious repeated events.
 * </p>
 * <p>
 * Written by <a href="http://www.brianziman.com/">Brian Ziman</a> and available
 * for download and comments at <a href="http://www.brianziman.com/r/post/stuff-200910092100">http://www.brianziman.com/r/post/stuff-200910092100</a>.
 * </p>
 * <h1>Rationale</h1>
 * <p>
 * When running in an X11 environment, it appears as though either Java or
 * the windowing system stupidly generate lots of repeated key events when 
 * you hold a key down. As it is impractical to get users to reconfigure their
 * systems, and Sun seems unable to fix this issue (having been reported nearly
 * ten years ago). This adapter is an attempt to filter out the useless events.
 * </p>
 * <p>
 * Many people have whined about this, but few have fixed it. Some have tried,
 * but their solutions are not as clean as my OCD nature requires. Here are
 * some examples of what this is meant to handle:
 * </p>
 * <ul>
 * <li><a href="http://bugs.sun.com/view_bug.do?bug_id=4153069">http://bugs.sun.com/view_bug.do?bug_id=4153069</a></li>
 * <li><a href="http://forums.sun.com/thread.jspa?threadID=5382946">http://forums.sun.com/thread.jspa?threadID=5382946</a></li>
 * <li><a href="http://stackoverflow.com/questions/1457071/how-to-know-when-a-user-has-really-released-a-key-in-java">http://stackoverflow.com/questions/1457071/how-to-know-when-a-user-has-really-released-a-key-in-java</a></li>
 * </ul>
 * <p>
 *
 * The goal is to generate a KeyPressed event when a key is pressed,
 * and to generate a KeyReleased event when a key is released.  And 
 * ONLY when a key is pressed or released.
 * </p>
 * <h1>Compatibility</h1>
 * <p>
 * The class is tested with Java 1.6.0 on Ubuntu 8.10, but should theoretically
 * work on any platform, even if that platform issues events properly anyway.
 * That is, this SHOULD work in a cross platform manner.  If it doesn't,
 * please let me know and I may fix it.
 * </p>
 * <h1>Usage</h1>
 * <p>
 * You don't actually call any of the methods on this class, directly. When Java
 * thinks it is receiving an event, it will call the methods in this class,
 * which will, in turn, decide whether an event has actually occurred, and
 * call YOUR methods when you probably intended them to be called.
 * </p>
 * <p>
 * Replace:
 * </p>
 * <pre>
 * addKeyListener(new KeyAdapter() {
 *      public void keyPressed(KeyEvent e) { ... }
 *      public void keyReleased(KeyEvent e) { ... }
 * });
 * </pre>
 * <p>
 * with
 * </p>
 * <pre>
 * addKeyListener(new UsefulKeyAdapter(new KeyAdapter() {
 *      public void keyPressed(KeyEvent e) { ... }
 *      public void keyReleased(KeyEvent e) { ... }
 * }));
 * </pre>
 * <p>
 * Don't use this for keyTyped() events.  If you are interested in these
 * sorts of events, the default behaviour is probably what you actually
 * want.
 * </p>
 * <p>
 * See <a href="http://java.sun.com/docs/books/tutorial/uiswing/events/keylistener.html">How to Write a Key Listener</a> on Sun's web site for more details.
 * </p>
 * <h1>Algorithm:</h1>
 * <p>This algorithm queues up keyboard events as the system reports them,
 *    and then uses a separate thread to handle the desired events, and
 *    disregard the spurious events.</p>
 * <p>Testing has indicated that there is zero delay between the key released
 *    event and the subsequent key pressed event when that pair are generated
 *    spuriously, and that is the case in which they are ignored. The default
 *    tolerance is 10 ms, as it is possible that the system will not always
 *    generate spurious key events instantly, but that is still substantially
 *    faster than a physical keyboard is able to perform multiple physical
 *    actions.</p>
 * <p>This code uses an ArrayBlockingQueue so the thread will not spin while
 *    waiting for input. Most events are handled immediately, and no event
 *    will be delayed more than the specified maximum age (default 10 ms).
 * </ul>
 * <h1>License</h1>
 * <p>
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 * </p>
 * <p>
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 * </p>
 * <p>
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <a href="http://www.gnu.org/licenses/">http://www.gnu.org/licenses/</a>.
 * </p>
 * <p>
 * Copyright 2009-2010 <a href="http://www.brianziman.com/">Brian Ziman</a>
 * </p>
 * <p>
 * Please contact me if you find flaws in this software, or have any other
 * comments or suggestions.
 * </p>
 * <h1>Version History</h1>
 * <ul>
 * <li>1/4/2010 - Reworked algorithm to eliminate spinning by using java.util.concurrent framework, and to improve reliability.
 * <li>10/9/2009 - Initial Revision</li>
 * </ul>
 */
public class UsefulKeyAdapter implements KeyListener, Runnable {

    private final KeyListener gKeyListener;

    // if we queue up more than 100 events, we're screwed!
    private final BlockingQueue<KeyEvent> gQueue = new ArrayBlockingQueue<KeyEvent>(100);

    private final Thread gRunner;
    private boolean gRunning = true;
    private int gMaxAge; // default 10 ms

    /**
     * Creates a KeyListener implementation that delegates event handling
     * to the specified KeyListener, after filtering out spurious key pressed
     * and key released events. In a sequence of events, events are considered
     * spurious if a key pressed event immediately follows a key released event,
     * for the same key, and within the specified maximum age. The
     * default maximum age is 10 ms.
     */
    public UsefulKeyAdapter(KeyListener keyListener) {
        this(keyListener, 10);
    }

    /**
     * <p>The run loop processes events as they are queued up.</p>
     * <p>New algorithm:</p>
     * <p>All events get queued up in sequence.</p>
     * <p>
     * A separate thread pulls events off in a tight loop.</p>
     * <p>If the head event is a key released event:</p>
     * <ul><li>pull another event (waiting up to 10 ms for one to appear)</li>
     *     <li>if the next event is a key pressed, suppress both,</li>
     *     <li>otherwise dispatch both.</li>
     * </ul>
     */
    public void run() {
        while(gRunning) {
            try {
                KeyEvent ev = gQueue.take(); // block until event is available
                if(ev.getID() == KeyEvent.KEY_RELEASED) {
                    KeyEvent ev2 = gQueue.poll(gMaxAge, TimeUnit.MILLISECONDS);
                    if(!matches(ev, ev2)) {
                        dispatchEvent(ev);
                        dispatchEvent(ev2); // might be null, that's okay.
                    } // otherwise suppress them both
                } else {
                    dispatchEvent(ev); // no need to suppress anything else
                }
            } catch (InterruptedException ie) {
                // might be triggered when adapter is shut down.
            }
        }
    }

    /**
     * Creates a KeyListener implementation that delegates event handling
     * to the specified KeyListener, after filtering out spurious key pressed
     * and key released events. In a sequence of events, events are considered
     * spurious if a key pressed event immediately follows a key released event,
     * for the same key, and within the specified maximum age. The
     * default maximum age is 10 ms.
     */
    public UsefulKeyAdapter(KeyListener keyListener, int maxAge) {
        this.gKeyListener = keyListener;
        this.gMaxAge = maxAge;
        final UsefulKeyAdapter adapter = this;

        gRunner = new Thread(this);
        gRunner.setName("UsefulKeyAdapter-Thread");
        gRunner.setDaemon(true);
        gRunner.start();

    }

    /**
     * Delegates the key pressed event.
     */
    public void keyPressed(KeyEvent e) {
        gQueue.offer(e);
    }

    /**
     * Delegates the key released event.
     */
    public void keyReleased(KeyEvent e) {
        gQueue.offer(e);
    }
    /**
     * Don't use the UsefulKeyAdapter for keyTyped events.
     */
    public void keyTyped(KeyEvent e) {
        gKeyListener.keyTyped(e);
    }

    private void dispatchEvent(KeyEvent e) {
        if(e == null) return; // do nothing
        if(e.getID() == KeyEvent.KEY_PRESSED) gKeyListener.keyPressed(e);
        else if(e.getID() == KeyEvent.KEY_RELEASED) gKeyListener.keyReleased(e);
    }
    /** If two events represent the same key and within 10 ms */
    private boolean matches(KeyEvent e1, KeyEvent e2) {
        if(e1 == e2) return true;
        if(e2 == null) return false;
        return (e1.getID() == KeyEvent.KEY_RELEASED)
            && (e2.getID() == KeyEvent.KEY_PRESSED)
            && e1.getKeyCode() == e2.getKeyCode()
            && e1.getKeyChar() == e2.getKeyChar()
            && e2.getWhen() <= e1.getWhen() + gMaxAge;
    }


    /**
     * Disable the background thread when the adapter goes out of scope.
     * Not guaranteed to be called, but it's polite to clean up resources.
     */
    public void finalize() {
        gRunning = false;
        gRunner.interrupt();
    }

    

}

