/**
 * 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 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.*;

/**
 * <p>
 * This class wraps your KeyAdapter and supresses spurious repeated events.
 * </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... 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><b>On KeyPressed</b></p>
 * <ul>
 * <li>If it's not in the map, add "NEW" to the map, and issue a 
 *     KeyPressed event.</li>
 * <li>Otherwise, if it's "OLD", change it to "NEW".</li>
 * <li>Otherwise, do nothing.</li>
 * </ul>
 * <p><b>On KeyReleased</b></p>
 * <ul>
 * <li>Queue it up for handling after a brief 10 ms moment,
 *     and flag the entry "OLD" in the map.</li>
 * </ul>
 * <p><b>Queue Thread</b></p>
 * <ul>
 * <li>Whenever an entry in the queue is available, dequeue it.</li>
 * <li>If the entry is marked "OLD" in the map, issue a KeyReleased event,
 *     and delete it from the map.</li>
 * <li>Otherwise, do nothing.</li>
 * </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 <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>
 *
 */
public class UsefulKeyAdapter implements KeyListener {
    private static final Object NEW = new Object(); // dummy object
    private static final Object OLD = new Object(); // dummy object

    private final KeyListener gKeyListener;
    private final ConcurrentMap<Integer,Object> gMap = new ConcurrentHashMap<Integer,Object>();
 
    private final DelayQueue<DelayedKey> gQueue = new DelayQueue<DelayedKey>();

    private final Thread gRunner;
    private boolean gRunning = true;

    public UsefulKeyAdapter(KeyListener gKeyListener) {
        this.gKeyListener = gKeyListener;
        final UsefulKeyAdapter adapter = this;

        gRunner = new Thread() {
            public void run() {
                try {
                    while(adapter.gRunning) {
                        DelayedKey dk = gQueue.take();
                        Object o = gMap.get(dk.event.getKeyCode());
                        if(o == OLD) {
                            gMap.remove(dk.event.getKeyCode());
                            adapter.gKeyListener.keyReleased(dk.event);
                        } // else do nothing
                    }
                } catch (InterruptedException e) {
                    // interrupt is called when we're finalized
                }
            }
        };
        gRunner.setName("UsefulKeyAdapter-Thread");
        gRunner.setDaemon(true);
        gRunner.start();

    }

    /**
     * @override
     */
    public void keyPressed(KeyEvent e) {
        Object o = gMap.get(e.getKeyCode());
        if(o == OLD) { // not present or OLD
            gMap.put(e.getKeyCode(), NEW);
        } else if(o == null) {
            gMap.put(e.getKeyCode(), NEW);
            gKeyListener.keyPressed(e);
        } // else do nothing
    }

    /**
     * In my testing, the fastest I can release a key and repress it,
     * is about 40 ms.  Also in my testing, I found that the auto-repeat
     * delay is 0 ms between bogus release and the next bogus press.
     * I assume that no keyboard is more than three times as responsive
     * as mine, and that no computer will use an auto-repeat algorithm
     * slower than 10ms.  If you find these assumptions in error, please
     * let me know.
     * @override
     */
    public void keyReleased(KeyEvent e) {
        gMap.put(e.getKeyCode(), OLD);
        DelayedKey dk = new DelayedKey(e, 10, TimeUnit.MILLISECONDS);
        gQueue.offer(dk);
    }

    /**
     * Don't use the UsefulKeyAdapter for keyTyped events.
     * @override
     */
    public void keyTyped(KeyEvent e) {
        gKeyListener.keyTyped(e);
    }

    private static final class DelayedKey implements Delayed {
        KeyEvent event;
        long expire;
        public DelayedKey(KeyEvent event, long delay, TimeUnit unit) {
            this.event = event;
            expire = event.getWhen() + unit.toMillis(delay);
        }
        public long getDelay(TimeUnit unit) {
            long delay = expire - System.currentTimeMillis();
            return unit.convert(delay, TimeUnit.MILLISECONDS);
            
        }
        public int compareTo(Delayed d) {
            if(getDelay(TimeUnit.MILLISECONDS) < d.getDelay(TimeUnit.MILLISECONDS)) return -1;
            if(getDelay(TimeUnit.MILLISECONDS) > d.getDelay(TimeUnit.MILLISECONDS)) return 1;
            return 0;
        }
    }


    /**
     * 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();
    }

    

}
