/**
 * @extends Ext.ux.event.Driver
 * This class manages the playback of an array of "event descriptors". For details on the
 * contents of an "event descriptor", see {@link Ext.ux.event.Recorder}. The events recorded by the
 * {@link Ext.ux.event.Recorder} class are designed to serve as input for this class.
 * 
 * The simplest use of this class is to instantiate it with an {@link #eventQueue} and call
 * {@link #method-start}. Like so:
 *
 *      var player = Ext.create('Ext.ux.event.Player', {
 *          eventQueue: [ ... ],
 *          speed: 2,  // play at 2x speed
 *          listeners: {
 *              stop: function() {
 *                  player = null; // all done
 *              }
 *          }
 *      });
 *
 *      player.start();
 *
 * A more complex use would be to incorporate keyframe generation after playing certain
 * events.
 *
 *      var player = Ext.create('Ext.ux.event.Player', {
 *          eventQueue: [ ... ],
 *          keyFrameEvents: {
 *              click: true
 *          },
 *          listeners: {
 *              stop: function() {
 *                  // play has completed... probably time for another keyframe...
 *                  player = null;
 *              },
 *              keyframe: onKeyFrame
 *          }
 *      });
 *
 *      player.start();
 *
 * If a keyframe can be handled immediately (synchronously), the listener would be:
 *
 *      function onKeyFrame () {
 *          handleKeyFrame();
 *      }
 *
 *  If the keyframe event is always handled asynchronously, then the event listener is only
 *  a bit more:
 *
 *      function onKeyFrame (p, eventDescriptor) {
 *          eventDescriptor.defer(); // pause event playback...
 *
 *          handleKeyFrame(function() {
 *              eventDescriptor.finish(); // ...resume event playback
 *          });
 *      }
 *
 * Finally, if the keyframe could be either handled synchronously or asynchronously (perhaps
 * differently by browser), a slightly more complex listener is required.
 *
 *      function onKeyFrame (p, eventDescriptor) {
 *          var async;
 *
 *          handleKeyFrame(function() {
 *              // either this callback is being called immediately by handleKeyFrame (in
 *              // which case async is undefined) or it is being called later (in which case
 *              // async will be true).
 *
 *              if (async) {
 *                  eventDescriptor.finish();
 *              }
 *              else {
 *                  async = false;
 *              }
 *          });
 *
 *          // either the callback was called (and async is now false) or it was not
 *          // called (and async remains undefined).
 *
 *          if (async !== false) {
 *              eventDescriptor.defer();
 *              async = true; // let the callback know that we have gone async
 *          }
 *      }
 */
Ext.define('Ext.ux.event.Player', function(Player) {
/* eslint-disable indent, vars-on-top, one-var */
var defaults = {},
    mouseEvents = {},
    keyEvents = {},
    doc,
 
    // HTML events supported
    uiEvents = {},
 
    // events that bubble by default
    bubbleEvents = {
        // scroll: 1,
        resize: 1,
        reset: 1,
        submit: 1,
        change: 1,
        select: 1,
        error: 1,
        abort: 1
    };
 
Ext.each([ 'click', 'dblclick', 'mouseover', 'mouseout', 'mousedown', 'mouseup', 'mousemove' ],
    function(type) {
        bubbleEvents[type] = defaults[type] = mouseEvents[type] = {
            bubbles: true,
            cancelable: (type !== "mousemove"), // mousemove cannot be cancelled
            detail: 1,
            screenX: 0,
            screenY: 0,
            clientX: 0,
            clientY: 0,
            ctrlKey: false,
            altKey: false,
            shiftKey: false,
            metaKey: false,
            button: 0
        };
    });
 
Ext.each([ 'keydown', 'keyup', 'keypress' ],
    function(type) {
        bubbleEvents[type] = defaults[type] = keyEvents[type] = {
            bubbles: true,
            cancelable: true,
            ctrlKey: false,
            altKey: false,
            shiftKey: false,
            metaKey: false,
            keyCode: 0,
            charCode: 0
        };
    });
 
Ext.each([ 'blur', 'change', 'focus', 'resize', 'scroll', 'select' ],
    function(type) {
        defaults[type] = uiEvents[type] = {
            bubbles: (type in bubbleEvents),
            cancelable: false,
            detail: 1
        };
    });
 
var inputSpecialKeys = {
        8: function(target, start, end) { // backspace: 8,
            if (start < end) {
                target.value = target.value.substring(0, start) +
                               target.value.substring(end);
            }
            else if (start > 0) {
                target.value = target.value.substring(0, --start) +
                               target.value.substring(end);
            }
 
            this.setTextSelection(target, start, start);
        },
        46: function(target, start, end) { // delete: 46
            if (start < end) {
                target.value = target.value.substring(0, start) +
                               target.value.substring(end);
            }
            else if (start < target.value.length - 1) {
                target.value = target.value.substring(0, start) +
                               target.value.substring(start + 1);
            }
 
            this.setTextSelection(target, start, start);
        }
    };
 
return {
    extend: 'Ext.ux.event.Driver',
 
    /**
     * @cfg {Array} eventQueue The event queue to playback. This must be provided before
     * the {@link #method-start} method is called.
     */
 
    /**
     * @cfg {Object} keyFrameEvents An object that describes the events that should generate
     * keyframe events. For example, `{ click: true }` would generate keyframe events after
     * each `click` event.
     */
    keyFrameEvents: {
        click: true
    },
 
    /**
     * @cfg {Boolean} pauseForAnimations True to pause event playback during animations, false
     * to ignore animations. Default is true.
     */
    pauseForAnimations: true,
 
    /**
     * @cfg {Number} speed The playback speed multiplier. Default is 1.0 (to playback at the
     * recorded speed). A value of 2 would playback at 2x speed.
     */
    speed: 1.0,
 
    stallTime: 0,
 
    _inputSpecialKeys: {
        INPUT: inputSpecialKeys,
 
        TEXTAREA: Ext.apply({
            // 13: function(target, start, end) { // enter: 8,
                // TODO ?
            // }
        }, inputSpecialKeys)
    },
 
    tagPathRegEx: /(\w+)(?:\[(\d+)\])?/,
    xpathRe: /^[#~](?:[a-z][-a-z0-9_]*)(?:\/[a-z]+)*$/i,
 
    /**
     * @event beforeplay
     * Fires before an event is played.
     * @param {Ext.ux.event.Player} this 
     * @param {Object} eventDescriptor The event descriptor about to be played.
     */
 
    /**
     * @event keyframe
     * Fires when this player reaches a keyframe. Typically, this is after events
     * like `click` are injected and any resulting animations have been completed.
     * @param {Ext.ux.event.Player} this 
     * @param {Object} eventDescriptor The keyframe event descriptor.
     */
 
    constructor: function(config) {
        var me = this;
 
        me.callParent(arguments);
 
        me.timerFn = function() {
            me.onTick();
        };
 
        me.attachTo = me.attachTo || window;
        me.counter = 0;
 
        doc = me.attachTo.document;
    },
 
    /**
     * Returns the element given is XPath-like description.
     * @param {String} xpath The XPath-like description of the element.
     * @return {HTMLElement} 
     */
    getElementFromXPath: function(xpath) {
        var me = this,
            parts = xpath.split('/'),
            regex = me.tagPathRegEx,
            i, n, m, count, tag, child,
            el = me.attachTo.document;
 
        el = (parts[0] === '~') ? el.body : el.getElementById(parts[0].substring(1)); // remove '#'
 
        for (= 1, n = parts.length; el && i < n; ++i) {
            m = regex.exec(parts[i]);
            count = m[2] ? parseInt(m[2], 10) : 1;
            tag = m[1].toUpperCase();
 
            for (child = el.firstChild; child; child = child.nextSibling) {
                if (child.tagName === tag) {
                    if (count === 1) {
                        break;
                    }
 
                    --count;
                }
            }
 
            el = child;
        }
 
        return el;
    },
 
    locateElement: function(locator) {
        var cmp, cq, dq, el, parts;
 
        if (this.xpathRe.test(locator)) {
            el = this.getElementFromXPath(locator);
        }
        else {
            parts = locator.split('=>');
            cq = Ext.String.trim(parts[0]);
            dq = Ext.String.trim(parts[1]);
 
            if (cq) {
                cmp = Ext.first(cq);
                el = cmp && cmp.el;
            }
            else {
                el = Ext.getBody();
            }
 
            if (dq && el) {
                el = Ext.query(dq, true, el);
                el = el && el[0];
            }
        }
 
        return el;
    },
 
    // Moving across a line break only counts as moving one character in a TextRange, whereas
    // a line break in the textarea value is two characters. This function corrects for that
    // by converting a text offset into a range character offset by subtracting one character
    // for every line break in the textarea prior to the offset
    offsetToRangeCharacterMove: function(el, offset) {
        return offset - (el.value.slice(0, offset).split("\r\n").length - 1);
    },
 
    setTextSelection: function(el, startOffset, endOffset) {
        var range, startCharMove;
 
        // See https://code.google.com/p/rangyinputs/source/browse/trunk/rangyinputs_jquery.js
        if (startOffset < 0) {
            startOffset += el.value.length;
        }
 
        if (endOffset == null) {
            endOffset = startOffset;
        }
 
        if (endOffset < 0) {
            endOffset += el.value.length;
        }
 
        if (typeof el.selectionStart === "number") {
            el.selectionStart = startOffset;
            el.selectionEnd = endOffset;
        }
        else {
            range = el.createTextRange();
            startCharMove = this.offsetToRangeCharacterMove(el, startOffset);
 
            range.collapse(true);
 
            if (startOffset === endOffset) {
                range.move("character", startCharMove);
            }
            else {
                range.moveEnd("character", this.offsetToRangeCharacterMove(el, endOffset));
                range.moveStart("character", startCharMove);
            }
 
            range.select();
        }
    },
 
    getTimeIndex: function() {
        var t = this.getTimestamp() - this.stallTime;
 
        return t * this.speed;
    },
 
    makeToken: function(eventDescriptor, signal) {
        var me = this,
            t0;
 
        eventDescriptor[signal] = true;
 
        eventDescriptor.defer = function() {
            eventDescriptor[signal] = false;
            t0 = me.getTime();
        };
 
        eventDescriptor.finish = function() {
            eventDescriptor[signal] = true;
            me.stallTime += me.getTime() - t0;
 
            me.schedule();
        };
    },
 
    /**
     * This method is called after an event has been played to prepare for the next event.
     * @param {Object} eventDescriptor The descriptor of the event just played.
     */
    nextEvent: function(eventDescriptor) {
        var me = this,
            index = ++me.queueIndex;
 
        // keyframe events are inserted after a keyFrameEvent is played.
        if (me.keyFrameEvents[eventDescriptor.type]) {
            Ext.Array.insert(me.eventQueue, index, [
                { keyframe: true, ts: eventDescriptor.ts }
            ]);
        }
    },
 
    /**
     * This method returns the event descriptor at the front of the queue. This does not
     * dequeue the event. Repeated calls return the same object (until {@link #nextEvent}
     * is called).
     */
    peekEvent: function() {
        var ev = this.eventQueue[this.queueIndex] || null;
 
        if (ev && ev.seq === undefined) {
            ev.seq = this.counter++;
        }
 
        return ev;
    },
 
    /**
     * Replaces an event in the queue with an array of events. This is often used to roll
     * up a multi-step pseudo-event and expand it just-in-time to be played. The process
     * for doing this in a derived class would be this:
     * 
     *      Ext.define('My.Player', {
     *          extend: 'Ext.ux.event.Player',
     *
     *          peekEvent: function() {
     *              var event = this.callParent();
     *
     *              if (event.multiStepSpecial) {
     *                  this.replaceEvent(null, [
     *                      ... expand to actual events
     *                  ]);
     *
     *                  event = this.callParent(); // get the new next event
     *              }
     *
     *              return event;
     *          }
     *      });
     * 
     * This method ensures that the `beforeplay` hook (if any) from the replaced event is
     * placed on the first new event and the `afterplay` hook (if any) is placed on the
     * last new event.
     * 
     * @param {Number} index The queue index to replace. Pass `null` to replace the event
     * at the current `queueIndex`.
     * @param {Event[]} events The array of events with which to replace the specified
     * event.
     */
    replaceEvent: function(index, events) {
        for (var t, i = 0, n = events.length; i < n; ++i) {
            if (i) {
                t = events[- 1];
                delete t.afterplay;
                delete t.screenshot;
 
                delete events[i].beforeplay;
            }
        }
 
        Ext.Array.replace(this.eventQueue, (index == null) ? this.queueIndex : index, 1, events);
    },
 
    /**
     * This method dequeues and injects events until it has arrived at the time index. If
     * no events are ready (based on the time index), this method does nothing.
     * @return {Boolean} True if there is more to do; false if not (at least for now).
     */
    processEvents: function() {
        var me = this,
            animations = me.pauseForAnimations && me.attachTo.Ext.fx.Manager.items,
            eventDescriptor;
 
        while ((eventDescriptor = me.peekEvent()) !== null) {
            if (animations && animations.getCount()) {
                return true;
            }
 
            if (eventDescriptor.keyframe) {
                if (!me.processKeyFrame(eventDescriptor)) {
                    return false;
                }
 
                me.nextEvent(eventDescriptor);
            }
            else if (eventDescriptor.ts <= me.getTimeIndex() &&
                       me.fireEvent('beforeplay', me, eventDescriptor) !== false &&
                       me.playEvent(eventDescriptor)) {
                me.nextEvent(eventDescriptor);
            }
            else {
                return true;
            }
        }
 
        me.stop();
 
        return false;
    },
 
    /**
     * This method is called when a keyframe is reached. This will fire the keyframe event.
     * If the keyframe has been handled, true is returned. Otherwise, false is returned.
     * @param {Object} eventDescriptor The event descriptor of the keyframe.
     * @return {Boolean} True if the keyframe was handled, false if not.
     */
    processKeyFrame: function(eventDescriptor) {
        var me = this;
 
        // only fire keyframe event (and setup the eventDescriptor) once...
        if (!eventDescriptor.defer) {
            me.makeToken(eventDescriptor, 'done');
            me.fireEvent('keyframe', me, eventDescriptor);
        }
 
        return eventDescriptor.done;
    },
 
    /**
     * Called to inject the given event on the specified target.
     * @param {HTMLElement} target The target of the event.
     * @param {Object} event The event to inject. The properties of this object should be
     * those of standard DOM events but vary based on the `type` property. For details on
     * event types and their properties, see the class documentation.
     */
    injectEvent: function(target, event) {
        var me = this,
            type = event.type,
            options = Ext.apply({}, event, defaults[type]),
            handler;
 
        if (type === 'type') {
            handler = me._inputSpecialKeys[target.tagName];
 
            if (handler) {
                return me.injectTypeInputEvent(target, event, handler);
            }
 
            return me.injectTypeEvent(target, event);
        }
 
        if (type === 'focus' && target.focus) {
            target.focus();
 
            return true;
        }
 
        if (type === 'blur' && target.blur) {
            target.blur();
 
            return true;
        }
 
        if (type === 'scroll') {
            target.scrollLeft = event.pos[0];
            target.scrollTop = event.pos[1];
 
            return true;
        }
 
        if (type === 'mduclick') {
            return me.injectEvent(target, Ext.applyIf({ type: 'mousedown' }, event)) &&
                   me.injectEvent(target, Ext.applyIf({ type: 'mouseup' }, event)) &&
                   me.injectEvent(target, Ext.applyIf({ type: 'click' }, event));
        }
 
        if (mouseEvents[type]) {
            return Player.injectMouseEvent(target, options, me.attachTo);
        }
 
        if (keyEvents[type]) {
            return Player.injectKeyEvent(target, options, me.attachTo);
        }
 
        if (uiEvents[type]) {
            return Player.injectUIEvent(target, type,
                options.bubbles,
                options.cancelable,
                options.view || me.attachTo,
                options.detail);
        }
 
        return false;
    },
 
    injectTypeEvent: function(target, event) {
        var me = this,
            text = event.text,
            xlat = [],
            ch, chUp, i, n, upper;
 
        if (text) {
            delete event.text;
            upper = text.toUpperCase();
 
            for (= 0, n = text.length; i < n; ++i) {
                ch = text.charCodeAt(i);
                chUp = upper.charCodeAt(i);
 
                xlat.push(
                    Ext.applyIf({ type: 'keydown', charCode: chUp, keyCode: chUp }, event),
                    Ext.applyIf({ type: 'keypress', charCode: ch, keyCode: ch }, event),
                    Ext.applyIf({ type: 'keyup', charCode: chUp, keyCode: chUp }, event)
                );
            }
        }
        else {
            xlat.push(
                Ext.applyIf({ type: 'keydown', charCode: event.keyCode }, event),
                Ext.applyIf({ type: 'keyup', charCode: event.keyCode }, event)
            );
        }
 
        for (= 0, n = xlat.length; i < n; ++i) {
            me.injectEvent(target, xlat[i]);
        }
 
        return true;
    },
 
    injectTypeInputEvent: function(target, event, handler) {
        var me = this,
            text = event.text,
            sel, n;
 
        if (handler) {
            sel = me.getTextSelection(target);
 
            if (text) {
                n = sel[0];
                target.value = target.value.substring(0, n) + text +
                               target.value.substring(sel[1]);
                n += text.length;
                me.setTextSelection(target, n, n);
            }
            else {
                if (!(handler = handler[event.keyCode])) {
                    // no handler for the special key for this element
                    if ('caret' in event) {
                        me.setTextSelection(target, event.caret, event.caret);
                    }
                    else if (event.selection) {
                        me.setTextSelection(target, event.selection[0], event.selection[1]);
                    }
 
                    return me.injectTypeEvent(target, event);
                }
 
                handler.call(this, target, sel[0], sel[1]);
 
                return true;
            }
        }
 
        return true;
    },
 
    playEvent: function(eventDescriptor) {
        var me = this,
            target = me.locateElement(eventDescriptor.target),
            now = me.getTimeIndex(),
            timeout = eventDescriptor.timeout,
            event;
 
        if (eventDescriptor.startedAt === undefined) {
            eventDescriptor.startedAt = now;
        }
 
        if (!target) {
            // not present (yet)... wait for element present...
            if (timeout !== null) {
                timeout = timeout || 3e4; // 30 sec
 
                if (now - eventDescriptor.startedAt > timeout) {
                    me.playEventHook(eventDescriptor, 'timeout', 'onEventTimeout');
                }
            }
 
            return false;
        }
 
        if (!me.playEventHook(eventDescriptor, 'beforeplay')) {
            return false;
        }
 
        if (!eventDescriptor.injected) {
            eventDescriptor.injected = true;
            event = me.translateEvent(eventDescriptor, target);
            me.injectEvent(target, event);
        }
 
        return me.playEventHook(eventDescriptor, 'afterplay');
    },
 
    playEventHook: function(eventDescriptor, hookName, hookHandler) {
        var me = this,
            doneName = hookName + '.done',
            firedName = hookName + '.fired',
            hook = hookHandler || eventDescriptor[hookName];
 
        if (hook && !eventDescriptor[doneName]) {
            if (!eventDescriptor[firedName]) {
                eventDescriptor[firedName] = true;
                me.makeToken(eventDescriptor, doneName);
 
                if (me.eventScope && Ext.isString(hook)) {
                    hook = me.eventScope[hook];
                }
 
                if (hook) {
                    hook.call(me.eventScope || me, eventDescriptor);
                }
            }
 
            return false;
        }
 
        return true;
    },
 
    schedule: function() {
        var me = this;
 
        if (!me.timer) {
            me.timer = Ext.defer(me.timerFn, 10);
        }
    },
 
    _translateAcross: [
        'type',
        'button',
        'charCode',
        'keyCode',
        'caret',
        'pos',
        'text',
        'selection'
    ],
 
    translateEvent: function(eventDescriptor, target) {
        var me = this,
            event = {},
            modKeys = eventDescriptor.modKeys || '',
            names = me._translateAcross,
            i = names.length,
            name, xy;
 
        while (i--) {
            name = names[i];
 
            if (name in eventDescriptor) {
                event[name] = eventDescriptor[name];
            }
        }
 
        event.altKey = modKeys.indexOf('A') > 0;
        event.ctrlKey = modKeys.indexOf('C') > 0;
        event.metaKey = modKeys.indexOf('M') > 0;
        event.shiftKey = modKeys.indexOf('S') > 0;
 
        if (target && 'x' in eventDescriptor) {
            xy = Ext.fly(target).getXY();
            xy[0] += eventDescriptor.x;
            xy[1] += eventDescriptor.y;
        }
        else if ('x' in eventDescriptor) {
            xy = [ eventDescriptor.x, eventDescriptor.y ];
        }
        else if ('px' in eventDescriptor) {
            xy = [ eventDescriptor.px, eventDescriptor.py ];
        }
 
        if (xy) {
            event.clientX = event.screenX = xy[0];
            event.clientY = event.screenY = xy[1];
        }
 
        if (eventDescriptor.key) {
            event.keyCode = me.specialKeysByName[eventDescriptor.key];
        }
 
        if (eventDescriptor.type === 'wheel') {
            if ('onwheel' in me.attachTo.document) {
                event.wheelX = eventDescriptor.dx;
                event.wheelY = eventDescriptor.dy;
            }
            else {
                event.type = 'mousewheel';
                event.wheelDeltaX = -40 * eventDescriptor.dx;
                event.wheelDeltaY = event.wheelDelta = -40 * eventDescriptor.dy;
            }
        }
 
        return event;
    },
 
    //---------------------------------
    // Driver overrides
 
    onStart: function() {
        var me = this;
 
        me.queueIndex = 0;
        me.schedule();
    },
 
    onStop: function() {
        var me = this;
 
        if (me.timer) {
            Ext.undefer(me.timer);
            me.timer = null;
        }
    },
 
    //---------------------------------
 
    onTick: function() {
        var me = this;
 
        me.timer = null;
 
        if (me.processEvents()) {
            me.schedule();
        }
    },
 
    statics: {
        ieButtonCodeMap: {
            0: 1,
            1: 4,
            2: 2
        },
 
        /**
         * Injects a key event using the given event information to populate the event
         * object.
         * 
         * **Note:** `keydown` causes Safari 2.x to crash.
         * 
         * @param {HTMLElement} target The target of the given event.
         * @param {Object} options Object object containing all of the event injection
         * options.
         * @param {String} options.type The type of event to fire. This can be any one of
         * the following: `keyup`, `keydown` and `keypress`.
         * @param {Boolean} [options.bubbles=true] `tru` if the event can be bubbled up.
         * DOM Level 3 specifies that all key events bubble by default.
         * @param {Boolean} [options.cancelable=true] `true` if the event can be canceled
         * using `preventDefault`. DOM Level 3 specifies that all key events can be
         * cancelled.
         * @param {Boolean} [options.ctrlKey=false] `true` if one of the CTRL keys is
         * pressed while the event is firing.
         * @param {Boolean} [options.altKey=false] `true` if one of the ALT keys is
         * pressed while the event is firing.
         * @param {Boolean} [options.shiftKey=false] `true` if one of the SHIFT keys is
         * pressed while the event is firing.
         * @param {Boolean} [options.metaKey=false] `true` if one of the META keys is
         * pressed while the event is firing.
         * @param {Number} [options.keyCode=0] The code for the key that is in use.
         * @param {Number} [options.charCode=0] The Unicode code for the character 
         * associated with the key being used.
         * @param {Window} [view=window] The view containing the target. This is typically
         * the window object.
         * @private
         */
        injectKeyEvent: function(target, options, view) {
            var type = options.type,
                customEvent = null;
 
            if (type === 'textevent') {
                type = 'keypress';
            }
 
            view = view || window;
 
            // check for DOM-compliant browsers first
            if (doc.createEvent) {
                try {
                    customEvent = doc.createEvent("KeyEvents");
 
                    // Interesting problem: Firefox implemented a non-standard
                    // version of initKeyEvent() based on DOM Level 2 specs.
                    // Key event was removed from DOM Level 2 and re-introduced
                    // in DOM Level 3 with a different interface. Firefox is the
                    // only browser with any implementation of Key Events, so for
                    // now, assume it's Firefox if the above line doesn't error.
 
                    // @TODO: Decipher between Firefox's implementation and a correct one.
                    customEvent.initKeyEvent(type, options.bubbles, options.cancelable,
                            view, options.ctrlKey, options.altKey, options.shiftKey,
                            options.metaKey, options.keyCode, options.charCode);
 
                }
                catch (ex) {
                    // If it got here, that means key events aren't officially supported. 
                    // Safari/WebKit is a real problem now. WebKit 522 won't let you
                    // set keyCode, charCode, or other properties if you use a
                    // UIEvent, so we first must try to create a generic event. The
                    // fun part is that this will throw an error on Safari 2.x. The
                    // end result is that we need another try...catch statement just to
                    // deal with this mess.
 
                    try {
                        // try to create generic event - will fail in Safari 2.x
                        customEvent = doc.createEvent("Events");
 
                    }
                    catch (uierror) {
                        // the above failed, so create a UIEvent for Safari 2.x
                        customEvent = doc.createEvent("UIEvents");
 
                    }
                    finally {
                        customEvent.initEvent(type, options.bubbles, options.cancelable);
 
                        customEvent.view = view;
                        customEvent.altKey = options.altKey;
                        customEvent.ctrlKey = options.ctrlKey;
                        customEvent.shiftKey = options.shiftKey;
                        customEvent.metaKey = options.metaKey;
                        customEvent.keyCode = options.keyCode;
                        customEvent.charCode = options.charCode;
                    }
                }
 
                target.dispatchEvent(customEvent);
 
            }
            else if (doc.createEventObject) { // IE
                customEvent = doc.createEventObject();
 
                customEvent.bubbles = options.bubbles;
                customEvent.cancelable = options.cancelable;
                customEvent.view = view;
                customEvent.ctrlKey = options.ctrlKey;
                customEvent.altKey = options.altKey;
                customEvent.shiftKey = options.shiftKey;
                customEvent.metaKey = options.metaKey;
 
                // IE doesn't support charCode explicitly. CharCode should
                // take precedence over any keyCode value for accurate
                // representation.
 
                customEvent.keyCode = (options.charCode > 0) ? options.charCode : options.keyCode;
 
                target.fireEvent("on" + type, customEvent);
 
            }
            else {
                return false;
            }
 
            return true;
        },
 
        /**
         * Injects a mouse event using the given event information to populate the event
         * object.
         *
         * @param {HTMLElement} target The target of the given event.
         * @param {Object} options Object object containing all of the event injection
         * options.
         * @param {String} options.type The type of event to fire. This can be any one of
         * the following: `click`, `dblclick`, `mousedown`, `mouseup`, `mouseout`,
         * `mouseover` and `mousemove`.
         * @param {Boolean} [options.bubbles=true] `tru` if the event can be bubbled up.
         * DOM Level 2 specifies that all mouse events bubble by default.
         * @param {Boolean} [options.cancelable=true] `true` if the event can be canceled
         * using `preventDefault`. DOM Level 2 specifies that all mouse events except
         * `mousemove` can be cancelled. This defaults to `false` for `mousemove`.
         * @param {Boolean} [options.ctrlKey=false] `true` if one of the CTRL keys is
         * pressed while the event is firing.
         * @param {Boolean} [options.altKey=false] `true` if one of the ALT keys is
         * pressed while the event is firing.
         * @param {Boolean} [options.shiftKey=false] `true` if one of the SHIFT keys is
         * pressed while the event is firing.
         * @param {Boolean} [options.metaKey=false] `true` if one of the META keys is
         * pressed while the event is firing.
         * @param {Number} [options.detail=1] The number of times the mouse button has 
         * been used.
         * @param {Number} [options.screenX=0] The x-coordinate on the screen at which point
         * the event occurred.
         * @param {Number} [options.screenY=0] The y-coordinate on the screen at which point
         * the event occurred.
         * @param {Number} [options.clientX=0] The x-coordinate on the client at which point
         * the event occurred.
         * @param {Number} [options.clientY=0] The y-coordinate on the client at which point
         * the event occurred.
         * @param {Number} [options.button=0] The button being pressed while the event is
         * executing. The value should be 0 for the primary mouse button (typically the
         * left button), 1 for the tertiary mouse button (typically the middle button),
         * and 2 for the secondary mouse button (typically the right button).
         * @param {HTMLElement} [options.relatedTarget=null] For `mouseout` events, this
         * is the element that the mouse has moved to. For `mouseover` events, this is
         * the element that the mouse has moved from. This argument is ignored for all
         * other events.
         * @param {Window} [view=window] The view containing the target. This is typically
         * the window object.
         * @private
         */
        injectMouseEvent: function(target, options, view) {
            var type = options.type,
                customEvent = null;
 
            view = view || window;
 
            // check for DOM-compliant browsers first
            if (doc.createEvent) {
                customEvent = doc.createEvent("MouseEvents");
 
                // Safari 2.x (WebKit 418) still doesn't implement initMouseEvent()
                if (customEvent.initMouseEvent) {
                    customEvent.initMouseEvent(type, options.bubbles, options.cancelable,
                            view, options.detail, options.screenX, options.screenY,
                            options.clientX, options.clientY, options.ctrlKey,
                            options.altKey, options.shiftKey, options.metaKey,
                            options.button, options.relatedTarget);
                }
                else { // Safari
                    // the closest thing available in Safari 2.x is UIEvents
                    customEvent = doc.createEvent("UIEvents");
 
                    customEvent.initEvent(type, options.bubbles, options.cancelable);
 
                    customEvent.view = view;
                    customEvent.detail = options.detail;
                    customEvent.screenX = options.screenX;
                    customEvent.screenY = options.screenY;
                    customEvent.clientX = options.clientX;
                    customEvent.clientY = options.clientY;
                    customEvent.ctrlKey = options.ctrlKey;
                    customEvent.altKey = options.altKey;
                    customEvent.metaKey = options.metaKey;
                    customEvent.shiftKey = options.shiftKey;
                    customEvent.button = options.button;
                    customEvent.relatedTarget = options.relatedTarget;
                }
 
                /*
                 * Check to see if relatedTarget has been assigned. Firefox
                 * versions less than 2.0 don't allow it to be assigned via
                 * initMouseEvent() and the property is readonly after event
                 * creation, so in order to keep YAHOO.util.getRelatedTarget()
                 * working, assign to the IE proprietary toElement property
                 * for mouseout event and fromElement property for mouseover
                 * event.
                 */
                if (options.relatedTarget && !customEvent.relatedTarget) {
                    if (type === "mouseout") {
                        customEvent.toElement = options.relatedTarget;
                    }
                    else if (type === "mouseover") {
                        customEvent.fromElement = options.relatedTarget;
                    }
                }
 
                target.dispatchEvent(customEvent);
 
            }
            else if (doc.createEventObject) { // IE
                customEvent = doc.createEventObject();
 
                customEvent.bubbles = options.bubbles;
                customEvent.cancelable = options.cancelable;
                customEvent.view = view;
                customEvent.detail = options.detail;
                customEvent.screenX = options.screenX;
                customEvent.screenY = options.screenY;
                customEvent.clientX = options.clientX;
                customEvent.clientY = options.clientY;
                customEvent.ctrlKey = options.ctrlKey;
                customEvent.altKey = options.altKey;
                customEvent.metaKey = options.metaKey;
                customEvent.shiftKey = options.shiftKey;
                customEvent.button = Player.ieButtonCodeMap[options.button] || 0;
 
                /*
                 * Have to use relatedTarget because IE won't allow assignment
                 * to toElement or fromElement on generic events. This keeps
                 * YAHOO.util.customEvent.getRelatedTarget() functional.
                 */
                customEvent.relatedTarget = options.relatedTarget;
 
                target.fireEvent('on' + type, customEvent);
            }
            else {
                return false;
            }
 
            return true;
        },
 
        /**
         * Injects a UI event using the given event information to populate the event
         * object.
         * 
         * @param {HTMLElement} target The target of the given event.
         * @param {Object} options 
         * @param {String} options.type The type of event to fire. This can be any one of
         * the following: `click`, `dblclick`, `mousedown`, `mouseup`, `mouseout`,
         * `mouseover` and `mousemove`.
         * @param {Boolean} [options.bubbles=true] `tru` if the event can be bubbled up.
         * DOM Level 2 specifies that all mouse events bubble by default.
         * @param {Boolean} [options.cancelable=true] `true` if the event can be canceled
         * using `preventDefault`. DOM Level 2 specifies that all mouse events except
         * `mousemove` can be canceled. This defaults to `false` for `mousemove`.
         * @param {Number} [options.detail=1] The number of times the mouse button has been
         * used.
         * @param {Window} [view=window] The view containing the target. This is typically
         * the window object.
         * @private
         */
        injectUIEvent: function(target, options, view) {
            var customEvent = null;
 
            view = view || window;
 
            // check for DOM-compliant browsers first
            if (doc.createEvent) {
                // just a generic UI Event object is needed
                customEvent = doc.createEvent("UIEvents");
 
                customEvent.initUIEvent(options.type, options.bubbles, options.cancelable,
                        view, options.detail);
 
                target.dispatchEvent(customEvent);
 
            }
            else if (doc.createEventObject) { // IE
                customEvent = doc.createEventObject();
 
                customEvent.bubbles = options.bubbles;
                customEvent.cancelable = options.cancelable;
                customEvent.view = view;
                customEvent.detail = options.detail;
 
                target.fireEvent("on" + options.type, customEvent);
 
            }
            else {
                return false;
            }
 
            return true;
        }
    } // statics
};
});