/**
  *
  * @copyright Copyright 2001-2009 Laszlo Systems, Inc.  All Rights Reserved.
  *            Use is subject to license terms.
  *
  * @affects lzbrowser
  * @access public
  * @topic LFC
  * @subtopic Events
  */

/**
  * <p><code>lz.Keys</code> is the single instance of the class
  * <code>lz.KeysService</code>.</p>
  *
  * lz.Keys is a service that provides key handling messages. Objects can also
  * register callbacks to be sent when specific key combinitions are down.
  *
  * <p>Here is a simple example:</p>
  *
  * <example title="lz.Keys">
  * <programlisting>&lt;canvas height="140" debug="true"&gt;
  *   &lt;handler name="onkeydown" reference="lz.Keys" args="k"&gt;
  *     Debug.debug("key %w %s", k, "down");
  *   &lt;/handler&gt;
  *   &lt;handler name="onkeyup" reference="lz.Keys" args="k"&gt;
  *     Debug.debug("key %w %s", k, "up");
  *   &lt;/handler&gt;
  *   &lt;method name="pressA"&gt;
  *     Debug.debug("A pressed");
  *   &lt;/method&gt;
  *   &lt;handler name="oninit"&gt;
  *     del = new LzDelegate(this, "pressA");
  *     lz.Keys.callOnKeyCombo(del, ["A"]);
  *   &lt;/handler&gt;
  * &lt;/canvas&gt;</programlisting></example>
  *
  * @shortdesc Keyboard input service.
  */
public final class LzKeysService extends LzEventable {

    /**
     * The key service.  Also available as the global
     * <code>lz.Keys</code>.
     *
     * @type LzKeysService
     * @keywords readonly
     * @devnote this should be a public getter to enforce readonly
     */
    public static const LzKeys:LzKeysService;

    /** @access private */
    function LzKeysService() {
        super();
        //    if (LzKeysService.LzKeys) {
        //      throw new Error("There can be only one LzKeys");
        //    }
        if ($as3) {
        } else {
            LzKeyboardKernel.setCallback(this, '__keyEvent');
        }

        if ($dhtml) {
            // Need to explicitly register this.  Make sure lz.embed.mousewheel is there for
            // lztest
            if (lz.embed['mousewheel']) {
                lz.embed.mousewheel.setCallback(this, '__mousewheelEvent');
            }
        }
    }

    // Create the singleton
    LzKeysService.LzKeys = new LzKeysService();

    /**
     * A hash where each of the keys that is currently
     * down on the keyboard is set to true.
     * @type Object
     * @access private
     */
    var downKeysHash :Object = {};

    /**
     * An array of currently pressed key codes.
     * @type Array
     * @access private
     */
    var downKeysArray :Array = [];

    /**
     * An object used by callOnKeyCombo() to track key combinations
     * @type Object
     * @access private
     */
    var keycombos :Object = {};

    /**
     * Sent when a key is pressed; sent with keycode
     * for key that was pressed.
     * @lzxtype event
     * @access public
     */
    var onkeydown :LzDeclaredEventClass = LzDeclaredEvent;
    /**
     * Sent whenever a key goes up; sent with keycode
     * for key that was let go.
     * @lzxtype event
     * @access public
     */
    var onkeyup :LzDeclaredEventClass = LzDeclaredEvent;
    /**
     * Sent when the mouse wheel changes state.  Sent with a positive or
     * negative number depending on the direction and amount the wheel moved.
     * @lzxtype event
     * @access public
     */
    var onmousewheeldelta :LzDeclaredEventClass = LzDeclaredEvent;

    /** For mapping key names (control, shift, alt) back to character codes
      * @access private */
    const codemap :Object = {shift: 16, control: 17, alt: 18};

    /** Was the ctrl key set when the last key event came in
      * @access private
      */
    var ctrlKey:Boolean = false;

    /** @access private */
    function __keyEvent (delta:Object, k:Number, type:String, ctrlKey:Boolean=false) :void {
        //Debug.write('__keyEvent', delta, k, type);
        //for each key change, send the corresponding event
        this.ctrlKey = ctrlKey;
        var cm:Object = this.codemap;
        for (var key:String in delta) {
            var down:Boolean = delta[key];
            if (cm[key] != null) k = cm[key];
            if (down) {
                this.gotKeyDown(k);
            } else {
                this.gotKeyUp(k);
            }
        }
    }

    /**
     * Called whenever a key is pressed with the Flash keycode corresponding to
     * the down key.
     *
     * @access private
     * @param Number kC: The Flash keycode for the key that is down.
     * @param String info: if "extra" then ignore if you already got one
     */
    function gotKeyDown (kC:Number, info:String = null) :void {
        //Debug.write('gotKeyDown', kC);
        var dkhash:Object = this.downKeysHash;
        var dkeys:Array = this.downKeysArray;

        var firstkeydown:Boolean = !dkhash[ kC ];
        if (firstkeydown) {
            dkhash[ kC ] = true;
            dkeys.push( kC );
            dkeys.sort();
        }
        // send key down event first, so inputtext will get key event
        // before a command that may change the focus
        if (firstkeydown || info != "extra") {
            // won't send repeated key events in player XXX ?

            // check for IME
            if (dkhash[229] != true) {
                if (this.onkeydown.ready) this.onkeydown.sendEvent( kC );
            }
        }
        if (firstkeydown) {
            var cp:Object = this.keycombos;
            for (var i:int = 0; i < dkeys.length && cp != null; i++) {
                cp = cp[ dkeys[ i ] ];
            }

            if (cp != null && 'delegates' in cp) {
                var del:Array = cp.delegates;
                for (var i:int = 0; i < del.length; i++) {
                    del[ i ].execute( dkeys );
                }
            }
        }
    }

    /**
     * Called whenever the a key is released with the Flash keycode
     * corresponding to the up key.
     *
     * @access private
     * @param Number kC: The Flash keycode for the key that was released.
     */
    function gotKeyUp (kC:Number) :void {
        //Debug.write('gotKeyUp', kC);
        var dkhash:Object = this.downKeysHash;
        var dkeys:Array = this.downKeysArray = [];
        var isDown:Boolean = dkhash[ kC ];
        delete dkhash[ kC ];

        if ($dhtml) {
            // not necessary for DHTML
        } else {
            // LPP-7012 we don't get a key up from the 'v' in a command-v sequence on the mac.
            // Workaround is to send all keys up when we see a 'command' (control) key-up event.
            // (Only required for Flash)
            if (kc == this.keyCodes.control) {
                this.downKeysHash = {};
                // Only send a key-up event if we know the key is already down
                if (isDown && this.onkeyup.ready) this.onkeyup.sendEvent( kC );

                // send all additional key-up events
                for (var k:String in dkhash) {
                    if (this.onkeyup.ready) {
                        this.onkeyup.sendEvent( k );
                    }
                }
                return;
            }
        }
        // update downKeysArray before sending onkeyup-event
        for (var k:String in dkhash) {
            dkeys.push( k );
        }

        // Only send a key-up event if we know the key is already down
        if (isDown && this.onkeyup.ready) this.onkeyup.sendEvent( kC );
    }

    /**
     * @return Boolean:  indicating whether the given key(s) are down.
     * @param k: The name of the key to check for downness or an array of
     * key names, e.g. ['shift', 'tab']
     */
    function isKeyDown (k:*) :Boolean {
        if (typeof(k) == "string") {
            return (this.downKeysHash[ this.keyCodes[ k.toLowerCase() ] ] == true);
        } else {
            // an array of keys was passed
            var down:Boolean = true;
            var dkhash:Object = this.downKeysHash;
            var kc:Object = this.keyCodes;
            for (var i:int = 0; i < k.length; i++) {
                down = down && (dkhash[ kc[ k[i].toLowerCase() ] ] == true);
            }
            return down;
        }
    }

    /**
     * Instructs the service to call the given delegate whenever the
     * given key combination is pressed.
     * The names for recognized special keys are (case-insensitive):
     * <ul>
     * <li>numbpad0 </li>
     * <li>numbpad1 </li>
     * <li>numbpad2 </li>
     * <li>numbpad3 </li>
     * <li>numbpad4 </li>
     * <li>numbpad5 </li>
     * <li>numbpad6 </li>
     * <li>numbpad7 </li>
     * <li>numbpad8 </li>
     * <li>numbpad9 </li>
     * <li>multiply </li>
     * <li>enter </li>
     * <li>subtract </li>
     * <li>decimal </li>
     * <li>divide </li>
     * <li>f1 </li>
     * <li>f2 </li>
     * <li>f3 </li>
     * <li>f4 </li>
     * <li>f5 </li>
     * <li>f6 </li>
     * <li>f7 </li>
     * <li>f8 </li>
     * <li>f9 </li>
     * <li>f10 </li>
     * <li>f11 </li>
     * <li>f12 </li>
     * <li>backspace </li>
     * <li>tab </li>
     * <li>clear </li>
     * <li>enter </li>
     * <li>shift </li>
     * <li>control </li>
     * <li>alt </li>
     * <li>capslock </li>
     * <li>esc </li>
     * <li>spacebar </li>
     * <li>pageup </li>
     * <li>pagedown </li>
     * <li>end </li>
     * <li>home </li>
     * <li>leftarrow </li>
     * <li>uparrow </li>
     * <li>rightarrow </li>
     * <li>downarrow </li>
     * <li>insert </li>
     * <li>help </li>
     * <li>numlock </li>
     * <li>add </li>
     * <li>delete </li>
     * <li>0 </li>
     * <li>1 </li>
     * <li>2 </li>
     * <li>3 </li>
     * <li>4 </li>
     * <li>5 </li>
     * <li>6 </li>
     * <li>7 </li>
     * <li>8 </li>
     * <li>9 </li>
     * <li>[ </li>
     * <li>@ </li>
     * <li># </li>
     * <li>$ </li>
     * <li>% </li>
     * <li>^ </li>
     * <li>&amp; </li>
     * <li>* </li>
     * <li>* </li>
     * <li>( </li>
     * <li>) </li>
     * <li>; </li>
     * <li>: </li>
     * <li>= </li>
     * <li>+ </li>
     * <li>- </li>
     * <li>_ </li>
     * <li>/ </li>
     * <li>? </li>
     * <li>~ </li>
     * <li>[ </li>
     * <li>{ </li>
     * <li>\ </li>
     * <li>| </li>
     * <li>] </li>
     * <li>} </li>
     * <li>" </li>
     * <li>' </li>
     *  </ul>
     * @param LzDelegate d: The delegate to be called when the keycombo is down.
     * @param Array kCArr: Array of strings indicating which keys constitute the
     * keycombo. This array may be in any order.
     * @devnote TODO: [20081012 anba] (LPP-7037) change param d to LzDelegatable
     */
    function callOnKeyCombo (d:*, kCArr:Array) :void {
        var kc:Object = this.keyCodes;
        var kcSorted:Array = [];
        for (var i:int = 0; i < kCArr.length; i++) {
            kcSorted.push( kc[ kCArr[ i ].toLowerCase() ] );
        }
        kcSorted.sort();

        var cp:Object = this.keycombos;
        for (var i:int = 0; i < kcSorted.length; i++) {
            var cpnext:Object = cp[ kcSorted[ i ] ];
            if (cpnext == null) {
                cp[ kcSorted[ i ] ] = cpnext = {delegates: []};
            }
            cp = cpnext;
        }
        cp.delegates.push( d );
    }

    /**
     * Removes the request to call the delegate on the keycombo.
     * @param LzDelegate d: The delegate that was to be called when the
     * keycombo was down.
     * @param Array kCArr: An array of strings indicating which keys
     * constituted the keycombo.
     * @devnote TODO: [20081012 anba] (LPP-7037) change param d to LzDelegatable
     */
    function removeKeyComboCall (d:*, kCArr:Array) :* {
        var kc:Object = this.keyCodes;
        var kcSorted:Array = [];
        for (var i:int = 0; i < kCArr.length; i++) {
            kcSorted.push( kc[ kCArr[ i ].toLowerCase() ] );
        }
        kcSorted.sort();

        var cp:Object = this.keycombos;
        for (var i:int = 0; i < kcSorted.length; i++) {
            cp = cp[ kcSorted[ i ] ];
            if (cp == null) {
                return false; //error
            }
        }
        for (var i:int = cp.delegates.length - 1; i >= 0; i--) {
            if (cp.delegates[ i ] == d) {
                cp.delegates.splice( i, 1 );
            }
        }
    }

    /**
     * @access private
     */
    function enableEnter (onroff:Boolean) :void {
        //Debug.write("enableEnter: "+onroff);
        // SWF-specific
        if ($debug) Debug.write('lz.Keys.enableEnter not yet defined');
        // TODO [hqm 2008-01] What is the way to do this in AS3 ??
        /*   _root.entercontrol.gotoAndStop( onroff ? 1 : 2 );
         */
    }

    /**
      * The amount the mouse wheel last moved.  Use the
      * onmousewheeldelta event to learn when this value changes.
      * @type Number
      * @lzxtype Number
      * @lzxdefault 0
      * @keywords readonly
      */
    var mousewheeldelta :Number = 0;

    /** @access private */
    function __mousewheelEvent (d:Number) :void {
        this.mousewheeldelta = d;
        if (this.onmousewheeldelta.ready) this.onmousewheeldelta.sendEvent(d);
    }

    /** Called when the last focusable element is reached
      * @access private
      */
    function gotLastFocus (ignore:*) :void {
        LzKeyboardKernel.gotLastFocus();
    }

    /**
     * A hash that maps key names to key codes.
     * @type Object
     * @access private
     */
    const keyCodes :Object = {
    /* numerical keys and non-alphanum keys */
    '0' : 48, ')' : 48,    ';'  : 186, ':'  : 186,
    '1' : 49, '!' : 49,    '='  : 187, '+'  : 187,
    '2' : 50, '@' : 50,    '<'  : 188, ','  : 188,
    '3' : 51, '#' : 51,    '-'  : 189, '_'  : 189,
    '4' : 52, '$' : 52,    '>'  : 190, '.'  : 190,
    '5' : 53, '%' : 53,    '/'  : 191, '?'  : 191,
    '6' : 54, '^' : 54,    '`'  : 192, '~'  : 192,
    '7' : 55, '&' : 55,    '['  : 219, '{'  : 219,
    '8' : 56, '*' : 56,    '\\' : 220, '|'  : 220,
    '9' : 57, '(' : 57,    ']'  : 221, '}'  : 221,
                           '\"' : 222, '\'' : 222,
    /* alphabetical keys */
    a : 65, b : 66, c : 67, d : 68, e : 69, f : 70, g : 71,
    h : 72, i : 73, j : 74, k : 75, l : 76, m : 77, n : 78,
    o : 79, p : 80, q : 81, r : 82, s : 83, t : 84, u : 85,
    v : 86, w : 87, x : 88, y : 89, z : 90,

    /* numpad keys */
    numbpad0 : 96,  numbpad1 : 97,  numbpad2 : 98,  numbpad3 : 99,  numbpad4 : 100,
    numbpad5 : 101, numbpad6 : 102, numbpad7 : 103, numbpad8 : 104, numbpad9 : 105,
    multiply : 106, 'add'    : 107, subtract : 109, decimal  : 110, divide   : 111,

    /* function keys */
    f1 : 112, f2 : 113, f3 : 114, f4  : 115, f5  : 116, f6  : 117,
    f7 : 118, f8 : 119, f9 : 120, f10 : 121, f11 : 122, f12 : 123,

    /* special keys */
    backspace : 8,
    tab : 9,
    clear : 12,
    enter : 13,
    shift : 16,
    control : 17,
    alt : 18,
    'pause' : 19, 'break' : 19,
    capslock : 20,
    esc : 27,
    spacebar : 32,
    pageup : 33,
    pagedown : 34,
    end : 35,
    home : 36,
    leftarrow : 37,
    uparrow : 38,
    rightarrow : 39,
    downarrow : 40,
    insert : 45,
    'delete' : 46,
    help : 47,
    numlock : 144,
    screenlock : 145,
    'IME' : 229
    }

}
lz.KeysService = LzKeysService;  // publish

/**
  * lz.Keys is a shortcut for <a href="LzKeysService.html">LzKeysService.LzKeys</a>.
  */
lz.Keys = LzKeysService.LzKeys;

