/**
 * @class ST.future.Element
 * A future Element is a class that can be used to interact with an element that will
 * exist at some point. Typically that element does not exist when the future is created.
 *
 * The methods of a future (including its constructor) defer actions in an event queue
 * (based on {@link ST.event.Player}). A simple example would be:
 *
 *      ST.element('@some-div').
 *          click(10, 10);
 *
 * The API of futures is based on chained method calls, sometimes called a "fluent"
 * API. In the above example, the `{@link ST#element}` method accepts the locator and
 * returns a future. The `{@link #click click}` method of the future queues a click
 * event at offset (10, 10).
 *
 * ## Actions
 * Many methods of futures perform actions (such as `click`) on their target. These
 * methods schedule their actions so that they follow previously invoked future methods
 * to ensure that these actions flow in the same order as the test code that requested
 * them.
 *
 * Actions methods use verbs for names.
 *
 * ## States
 * The other main group of methods on futures are state methods. These methods do not
 * affect their target but rather schedule a delay in the test sequence that begins at
 * the proper time (following all previously schedules operations) and finishing when
 * the target arrives in the desired state.
 *
 * For example:
 *
 *      ST.element('@some-div').
 *          click(10, 10).
 *          textLike(/hello/i);
 *
 * The above test will locate our div, click on it and then wait for its `textContent`
 * to match the specified regular expression. The test will complete successfully if
 * the text matches within the default timeout (5 seconds) and will fail otherwise.
 *
 * State methods use nouns or descriptions for names.
 *
 * ## Inspections
 * Because operations on futures all complete asynchronously, it is important not to
 * mix these operations with immediate method calls. Instead, we schedule inspections
 * using the future's `{@link #and}` method.
 *
 *      ST.element('@some-div').
 *          click(10, 10).
 *          and(
 *              // Invoked after the click has played. The ST.Element wrapper for
 *              // the target div is given as a parameter.
 *              function (divEl) {
 *                  expect(divEl.hasCls('foo')).toBe(false);
 *              }
 *          );
 *
 * The functions passed to `and()` are called "inspections" but there are no particular
 * restrictions on what these methods actually do when they are called.
 *
 * ## Waiting
 * There are two basic ways to control the timing of the test sequence. The first is the
 * `and()` method's optional second argument:
 *
 *      ST.element('@some-div').
 *          click(10, 10).
 *          and(function (divEl, done) {
 *              something().then(done);
 *          });
 *
 * When an inspection function is declared to have a second argument, it is called with
 * a completion function typically named "done". If declared as an argument, this function
 * must be called or the test will fail. The inspection function, however, can decide when
 * the function should be called. Once `done` is called, the test sequence can continue.
 *
 * When there is no mechanism that can reasonably be used to determine when a condition
 * is satisfied, there is the `{@link #wait}` method.
 *
 *      ST.element('@some-div').
 *          click(10, 10).
 *          wait(function (divEl) {
 *              return divEl.hasCls('foo');
 *          });
 *
 * In this case, the function passed to `wait()` is called periodically and when it
 * eventually returns `true` the test can proceed. Obviously, the `and()` method and its
 * `done` function are preferrable because they won't need to poll for completion. Which
 * approach is more readily implemented in a given situation will typically determine the
 * best choice, and not this slight performance consideration.
 *
 * ## Components
 *
 * When interacting with Ext JS components, see `{@link ST#component}` or one of the
 * more specific methods such as `{@link ST#panel}`, `{@link ST#grid}`, etc..
 *
 * ### Note
 *
 * This class is not created directly by user code. Instead, it is created automatically
 * by various helper methods, like {@link ST#element} and {@link ST#wait}.
 */
ST.future.Element = ST.define({
    valueProperty: 'el',
 
    statics: {
        addState: function (state, config) {
            if (typeof config === 'function') {
                config = {
                    is: config
                };
            }
 
            // For example: 
            // 
            //      collapsed: { 
            //          is: function () { 
            //              return this.cmp.collapsed; 
            //          }, 
            //          wait: function (fn) { 
            //              var cmp = this.cmp; 
            // 
            //              cmp.on({ 
            //                  collapsed: fn, 
            //                  single: true 
            //              }); 
            // 
            //              return function () { 
            //                  cmp.un({ 
            //                      collapsed: fn, 
            //                      single: true 
            //                  }); 
            //              }; 
            //          } 
            //      }, 
            // 
            //      expanded: { 
            //          is: function () { 
            //              return !this.cmp.collapsed; 
            //          }, 
            //          wait: 'expand'   // generates code like above but expand event 
            //      }, 
            // 
            //      visible: function () { 
            //          return this.el.isVisible(); 
            //      } 
            // 
            // Usage: 
            // 
            //      ST.panel('@thePanel').collapsed(); 
            // 
            var wait = config.wait,
                is = config.is,
                eventName, fn, i;
 
            if (wait) {
                if (typeof wait !== 'function') {
                    eventName = (typeof wait === 'string') ? [wait] : wait;
 
                    wait = function (done) {
                        var me = this,
                            cmp = me.cmp,
                            listener = {}, 
                            args = ST.Array.slice(arguments, 1),
                            fn, timer;
 
                        fn = function () {
                            if (is.apply(me, args)) {
                                if (timer) {
                                    ST.deferCancel(timer);
                                }
 
                                cmp.un(listener);
                                done();
                                done = ST.emptyFn;
                            } else if (!timer) {
                                timer = ST.defer(function () {
                                    timer = null;
 
                                    if (is.apply(me, args)) {
                                        cmp.un(listener);
                                        done();
                                        done = ST.emptyFn;
                                    }
                                }, 0);
                            }
                        }
 
                        for (i=0; i<eventName.length; i++) {
                            listener[eventName[i]] = fn;
                        }
                        
                        cmp.on(listener);
 
                        return function () {
                            cmp.un(listener);
                        };
                    };
                }
 
                fn = function () {
                    var me = this,
                        args = ST.Array.slice(arguments),
                        timeout;
 
                    if (args.length > is.length) {
                        timeout = args.pop();
                    }
 
                    ST.play([{
                        target: me.locator,
                        timeout: timeout,
                        visible: null,
                        available: config.available,
 
                        // With a wait() we can just schedule in a call to advance 
                        // instead of polling the is() method. 
                        fn: function (done) {
                            var event = this,
                                timer, unwait;
 
                            if (is.apply(me, args)) {
                                done();
                            } else {
                                event.setWaiting(state, 'true');
 
                                args.unshift(function () {
                                    if (timer) {
                                        timer = timer();  // call cancelFn 
                                        event.setWaiting(false);
                                        done();
                                    }
                                });
 
                                unwait = wait.apply(me, args);
 
                                timer = ST.timeout(function () {
                                    timer = 0;
                                    unwait();
                                    event._player.doTimeout(event);
                                }, timeout);
                            }
                        }
                    }]);
 
                    return me;
                };
            } else {
                fn = function () {
                    var me = this,
                        args = ST.Array.slice(arguments),
                        timeout;
 
                    if (args.length > is.length) {
                        timeout = args.pop();
                    }
 
                    ST.play([{
                        target: me.locator,
                        timeout: timeout,
                        visible: null,
                        available: config.available,
 
                        // Without wait() we must just poll... which is what ready 
                        // does already. 
                        ready: function () {
                            if (this.animationsDone() && this.targetReady()) {
                                if (!is.apply(me, args)) {
                                    return this.setWaiting(state, 'true');
                                }
                                return this.setWaiting(false);
                            }
                            return false;
                        }
                    }]);
 
                    return me;
                };
            }
 
            this.prototype[state] = fn;
        },
 
        addStates: function (states) {
            for (var name in states) {
                this.addState(name, states[name]);
            }
        }
    },
 
    constructor: function (locator, timeout) {
        var me = this,
            available, root, direction;
 
        if (locator) {
            if (locator.constructor === Object) {
                timeout = locator.timeout;
                available = locator.available;
                root = locator.root;
                direction = locator.direction;
                locator = locator.locator;
            }
 
            if (locator.isComponent || locator.isWidget) {
                locator = locator.el || locator.element; // el=Ext, element=Touch 
            }
        }
 
        me.timeout = timeout;
 
        if (locator) {
            if (locator.dom && !locator.$ST) {
                locator = new ST.Element(locator.dom);
            }
 
            me.locator = ST.play([{
                target: locator,
                visible: null,  // don't worry about visibility... 
                animation: false, // ...or animations at this stage 
                available: available,
                root: root,
                direction: direction,
                timeout: timeout,
 
                fn: function () {
                    me.el = this.targetEl;  // an ST.Element (this is the Playable) 
 
                    if (me._attach) {
                        me._attach();
                    }
                }
            }])[0];
        }
    },
 
    /**
     * Schedules arbitrary actions for later execution. Often these actions are added
     * to the queue following {@link #click} or other interactions in order to test an
     * expectation.
     *
     * For example:
     *
     *      ST.element('@some-div').
     *          click(10, 10).
     *          and(function (el) {
     *              // Runs after the click event. We receive the ST.Element
     *              // wrapper for the "some-div" element.
     *
     *              expect(el.hasCls('foo')).toBe(true);
     *          });
     *
     * Functions that need to perform asynchronous actions can declare a 2nd argument
     * (typically called "done").
     *
     *      ST.element('@some-div').
     *          click(10, 10).
     *          and(
     *              function (el, done) {
     *                  expect(el.hasCls('foo')).toBe(true);
     *
     *                  Ext.Ajax.request({
     *                      ...
     *                      callback: function () {
     *                          done();
     *                      }
     *                  });
     *              }
     *          );
     *
     * Multiple actions can be listed in a single call. Asynchronous actions can override
     * the {@link ST.options#timeout timeout} by specifying a number as the
     * previous argument.
     *
     * For example:
     *
     *      ST.element('@some-div').
     *          click(10, 10).
     *          and(
     *              1000,   // timeout for following async steps in this and()
     *
     *              function (el, done) {
     *                  expect(el.hasCls('foo')).toBe(true);
     *
     *                  Ext.Ajax.request({
     *                      ...
     *                      callback: function () {
     *                          done();
     *                      }
     *                  });
     *              },
     *              function (el) {
     *                  expect(el.hasCls('foo')).toBe(false);
     *              }
     *          );
     *
     * @param {Number/Function...} fnOrTimeout One or more functions to invoke or timeout
     * values. Functions that declare a 2nd argument must call the provided function to
     * indicate that they are complete. Timeout values affect subsequent asynchronous
     * functions and override the {@link ST.options#timeout timeout}. These
     * timeouts only apply to functions passed in the current call.
     *
     * @return {ST.future.Element} this
     * @chainable
     */
    and: function () {
        var me = this,
            events = [],
            timeout; // undefined so we get "player.timeout || ST.options.timeout" 
 
        ST.each(arguments, function (fn) {
            var wrapFn;
 
            if (typeof fn === 'number') {
                timeout = fn;
            }
            else {
                if (fn.length > 1) {
                    wrapFn = function (done) {
                        return fn(me._value(), done);
                    };
                } else {
                    wrapFn = function () {
                        return fn(me._value());
                    };
                }
 
                events.push({
                    fn: wrapFn,
                    timeout: timeout
                });
            }
        });
 
        ST.play(events);
        return me;
    },
 
    _splits: {},
    _decodeArgs: function (args, params, rec) {
        var me = this,
            splits = me._splits,
            array = splits[params],
            n = args.length,
            a = args[0],
            i;
 
        if (=== 1 && a && a.constructor === Object) {
            ST.apply(rec, a);
        } else {
            if (!array) {
                splits[params] = array = params.split(',');
            }
 
            n = Math.min(n, array.length);
 
            for (= 0; i < n; ++i) {
                rec[array[i]] = args[i];
            }
        }
 
        return rec;
    },
 
    /**
     * Schedules a click action at the specified relative coordinates.
     *
     *      ST.element('@some-div').
     *          click(10, 10);
     *
     * Or for a Component:
     *
     *      ST.component('#some-cmp').
     *          click(10, 10);
     *
     * If first argument is an object, it should be a {@link ST.event.Playable playable}
     * config object for a `type="click"` event. In this case, all other arguments are
     * ignored.
     * @param {Number/Object} x The number of pixels from the left edge of the element.
     * @param {Number} y The number of pixels from the top edge of the element.
     * @param {Number} [button=0] The mouse button code for the click.
     * @param {Number} [timeout] The maximum time (in milliseconds) to wait.
     * @return {ST.future.Element} this
     * @chainable
     */
    click: function () {
        var me = this,
            event = me._decodeArgs(arguments, 'x,y,button,timeout', {
                type: 'tap',
                target: me.locator
            });
 
        ST.play([event]);
 
        return me;
    },
 
    emptyRe: /^\s*$/,
 
    /**
     * Waits for this element's `innerHTML` to match the specified value.
     *
     *      ST.element('@some-div').
     *          click(10, 10).
     *          content('Hello <b>world</b>');
     *
     * If first argument is an object, it should be a {@link ST.event.Playable playable}
     * config object (with an additional required `html` property). In this case, all
     * other arguments are ignored.
     * @param {String/Object} html The html to match.
     * @param {Number} [timeout] The maximum time (in milliseconds) to wait.
     * @return {ST.future.Element} this
     * @chainable
     * @since 1.0.1
     */
    content: function () {
        var me = this,
            rec = me._decodeArgs(arguments, 'html,timeout', {
                waitingFor: 'content',
                waitingState: 'match ',
 
                ready: function () {
                    if (html === me.el.dom.innerHTML) {
                        return this.setWaiting(false);
                    }
                    return false;
                }
            }),
            html = rec.html;
 
        rec.waitingState += html;
        delete rec.html;
 
        ST.play([rec]);
 
        return me;
    },
 
    /**
     * Waits for this element's `innerHTML` to be empty.
     *
     *      ST.element('@some-div').
     *          click(10, 10).
     *          contentEmpty();
     *
     * If first argument is an object, it should be a {@link ST.event.Playable playable}
     * config object. In this case, all other arguments are ignored.
     * @param {Number/Object} [timeout] The maximum time (in milliseconds) to wait.
     * @return {ST.future.Element} this
     * @chainable
     */
    contentEmpty: function () {
        var me = this,
            rec = me._decodeArgs(arguments, 'timeout', {
                waitingFor: 'content',
                waitingState: 'empty',
 
                ready: function () {
                    if (me.emptyRe.test(me.el.dom.innerHTML)) {
                        return this.setWaiting(false);
                    }
                    return false;
                }
            });
 
        ST.play([rec]);
 
        return me;
    },
 
    /**
     * Waits for this element's `innerHTML` to match the specified RegExp `pattern`.
     *
     *      ST.element('@some-div').
     *          click(10, 10).
     *          contentLike(/hello/i);
     *
     * If first argument is an object, it should be a {@link ST.event.Playable playable}
     * config object (with an additional required `pattern` property). In this case, all
     * other arguments are ignored.
     * @param {RegExp/String/Object} pattern The pattern to match. If this is a String, it
     * is first promoted to a `RegExp` by called `new RegExp(pattern)`.
     * @param {Number} [timeout] The maximum time (in milliseconds) to wait.
     * @return {ST.future.Element} this
     * @chainable
     */
    contentLike: function () {
        var me = this,
            rec = me._decodeArgs(arguments, 'pattern,timeout', {
                waitingFor: 'content',
                waitingState: 'like ',
 
                ready: function () {
                    if (re.test(me.el.dom.innerHTML)) {
                        return this.setWaiting(false);
                    }
                    return false;
                }
            }),
            pattern = rec.pattern,
            re = (typeof pattern === 'string') ? new RegExp(pattern) : pattern;
 
        rec.waitingState += pattern;
        delete rec.pattern;
 
        ST.play([rec]);
 
        return me;
    },
 
    /**
     * Waits for this element's `innerHTML` to be non-empty.
     *
     *      ST.element('@some-div').
     *          click(10, 10).
     *          contentNotEmpty();
     *
     * If first argument is an object, it should be a {@link ST.event.Playable playable}
     * config object. In this case, all other arguments are ignored.
     * @param {Number/Object} [timeout] The maximum time (in milliseconds) to wait.
     * @return {ST.future.Element} this
     * @chainable
     */
    contentNotEmpty: function () {
        var me = this,
            rec = me._decodeArgs(arguments, 'timeout', {
                waitingFor: 'content',
                waitingState: 'not empty',
 
                ready: function () {
                    if (me.emptyRe.test(me.el.dom.innerHTML)) {
                        return false;
                    }
                    return this.setWaiting(false);
                }
            });
 
        ST.play([rec]);
 
        return me;
    },
 
    /**
     * Waits for this element's `innerHTML` to not match the specified RegExp `pattern`.
     *
     *      ST.element('@some-div').
     *          click(10, 10).
     *          contentNotLike(/world/i);
     *
     * If first argument is an object, it should be a {@link ST.event.Playable playable}
     * config object (with an additional required `pattern` property). In this case, all
     * other arguments are ignored.
     * @param {RegExp/String/Object} pattern The pattern to match. If this is a String, it
     * is first promoted to a `RegExp` by called `new RegExp(pattern)`.
     * @param {Number} [timeout] The maximum time (in milliseconds) to wait.
     * @return {ST.future.Element} this
     * @chainable
     */
    contentNotLike: function () {
        var me = this,
            rec = me._decodeArgs(arguments, 'pattern,timeout', {
                waitingFor: 'content',
                waitingState: 'not like ',
 
                ready: function () {
                    if (!re.test(me.el.dom.innerHTML)) {
                        return this.setWaiting(false);
                    }
                    return false;
                }
            }),
            pattern = rec.pattern,
            re = (typeof pattern === 'string') ? new RegExp(pattern) : pattern;
 
        rec.waitingState += pattern;
        delete rec.pattern;
 
        ST.play([rec]);
 
        return me;
    },
 
    /**
     * Waits for this element's `textContent` to match the specified string.
     *
     *      ST.element('@some-div').
     *          click(10, 10).
     *          text('Hello world');
     *
     * If first argument is an object, it should be a {@link ST.event.Playable playable}
     * config object (with an additional required `text` property). In this case, all
     * other arguments are ignored.
     * @param {String/Object} text The text to match.
     * @param {Number} [timeout] The maximum time (in milliseconds) to wait.
     * @return {ST.future.Element} this
     * @chainable
     * @since 1.0.1
     */
    text: function () {
        var me = this,
            rec = me._decodeArgs(arguments, 'text,timeout', {
                waitingFor: 'text',
                waitingState: 'match ',
 
                ready: function () {
                    var t = me.el.getText();
 
                    if (text === t) {
                        return this.setWaiting(false);
                    }
 
                    return false;
                }
            }),
            text = rec.text;
 
        rec.waitingState += text;
        delete rec.text;
 
        ST.play([rec]);
 
        return me;
    },
 
    /**
     * Waits for this element's `textContent` to be empty.
     *
     *      ST.element('@some-div').
     *          click(10, 10).
     *          textEmpty();
     *
     * If first argument is an object, it should be a {@link ST.event.Playable playable}
     * config object. In this case, all other arguments are ignored.
     * @param {Number/Object} [timeout] The maximum time (in milliseconds) to wait.
     * @return {ST.future.Element} this
     * @chainable
     */
    textEmpty: function () {
        var me = this,
            rec = me._decodeArgs(arguments, 'timeout', {
                waitingFor: 'text',
                waitingState: 'empty',
 
                ready: function () {
                    var text = me.el.getText();
 
                    if (me.emptyRe.test(text)) {
                        return this.setWaiting(false);
                    }
 
                    return false;
                }
            });
 
        ST.play([rec]);
 
        return me;
    },
 
    /**
     * Waits for this element's `textContent` to match the specified RegExp `pattern`.
     *
     *      ST.element('@some-div').
     *          click(10, 10).
     *          textLike(/hello/i);
     *
     * If first argument is an object, it should be a {@link ST.event.Playable playable}
     * config object (with an additional required `pattern` property). In this case, all
     * other arguments are ignored.
     * @param {RegExp/String/Object} pattern The pattern to match. If this is a String, it
     * is first promoted to a `RegExp` by called `new RegExp(pattern)`.
     * @param {Number} [timeout] The maximum time (in milliseconds) to wait.
     * @return {ST.future.Element} this
     * @chainable
     */
    textLike: function () {
        var me = this,
            rec = me._decodeArgs(arguments, 'pattern,timeout', {
                waitingFor: 'text',
                waitingState: 'like ',
 
                ready: function () {
                    var text = me.el.getText();
 
                    if (re.test(text)) {
                        return this.setWaiting(false);
                    }
 
                    return false;
                }
            }),
            pattern = rec.pattern,
            re = (typeof pattern === 'string') ? new RegExp(pattern) : pattern;
 
        rec.waitingState += pattern;
        delete rec.pattern;
 
        ST.play([rec]);
 
        return me;
    },
 
    /**
     * Waits for this element's `textContent` to be non-empty.
     *
     *      ST.element('@some-div').
     *          click(10, 10).
     *          textNotEmpty(200);
     *
     * If first argument is an object, it should be a {@link ST.event.Playable playable}
     * config object. In this case, all other arguments are ignored.
     * @param {Number/Object} [timeout] The maximum time (in milliseconds) to wait.
     * @return {ST.future.Element} this
     * @chainable
     */
    textNotEmpty: function () {
        var me = this,
            rec = me._decodeArgs(arguments, 'timeout', {
                waitingFor: 'text',
                waitingState: 'not empty',
 
                ready: function () {
                    var text = me.el.getText();
 
                    if (me.emptyRe.test(text)) {
                        return false;
                    }
 
                    return this.setWaiting(false);
                }
            });
 
        ST.play([rec]);
 
        return me;
    },
 
    /**
     * Waits for this element's `textContent` to not match the specified RegExp `pattern`.
     *
     *      ST.element('@some-div').
     *          click(10, 10).
     *          textNotLike(/hello/i, 200);
     *
     * If first argument is an object, it should be a {@link ST.event.Playable playable}
     * config object (with an additional required `pattern` property). In this case, all
     * other arguments are ignored.
     * @param {RegExp/String/Object} pattern The pattern to match. If this is a String, it
     * is first promoted to a `RegExp` by called `new RegExp(pattern)`.
     * @param {Number} [timeout] The maximum time (in milliseconds) to wait.
     * @return {ST.future.Element} this
     * @chainable
     */
    textNotLike: function () {
        var me = this,
            rec = me._decodeArgs(arguments, 'pattern,timeout', {
                waitingFor: 'text',
                waitingState: 'not like ',
 
                ready: function () {
                    var text = me.el.getText();
 
                    if (!re.test(text)) {
                        return this.setWaiting(false);
                    }
 
                    return false;
                }
            }),
            pattern = rec.pattern,
            re = (typeof pattern === 'string') ? new RegExp(pattern) : pattern;
 
        rec.waitingState += pattern;
        delete rec.pattern;
 
        ST.play([rec]);
 
        return me;
    },
 
    /**
     * Schedules a "type" action at the specified relative coordinates.  This method
     * assumes you have already achieved correct focus of the target.
     *
     *      ST.element('@some-div/input').
     *          focus().
     *          type('Hello world');
     *
     * If first argument is an object, it should be a {@link ST.event.Playable playable}
     * config object for a `type="type"` event. In this case, all other arguments are
     * ignored.
     * @param {String/Object} text The text to type.
     * @param {Number} [timeout] The maximum time (in milliseconds) to wait for the
     * typing to finish.
     * @return {ST.future.Element} this
     * @chainable
     */
    type: function () {
        var me = this,
            rec = me._decodeArgs(arguments, 'text,timeout', {
                type: 'type',
 
                target: function () {
                    var el = me._getFocusEl();
                    return el && el.dom;
                }
            });
 
        ST.play([rec]);
 
        return me;
    },
 
    /**
     * Schedules the component to receive the focus.
     *
     *      ST.element('@some-div/input').
     *          focus();
     *
     * If first argument is an object, it should be a {@link ST.event.Playable playable}
     * config object. In this case, all other arguments are ignored.
     * @param {Number/Object} [timeout] The maximum time (in milliseconds) to wait.
     * @return {ST.future.Element} this
     * @chainable
     */
    focus: function () {
        var me = this,
            rec = me._decodeArgs(arguments, 'timeout', {
                target: me.locator,
 
                fn: function () {
                    var el = me._getFocusEl();
 
                    if (el) {
                        el.focus();
                    }
                }
            });
 
        ST.play([rec]);
 
        return me.focused(100); // short timeout - focus should be immediate 
    },
 
    /**
     * Schedules a wait a specified amount of time (in milliseconds) or until a provided
     * function returns a truthy value.
     *
     * For example:
     *
     *      ST.element('@some-div').
     *          click(10, 10).
     *
     *          wait(100).  // wait 100ms
     *
     *          and(function (el) {
     *              // Runs after the click event. We receive the ST.Element
     *              // wrapper for the "some-div" element.
     *
     *              expect(el.hasCls('foo')).toBe(true);
     *          });
     *
     * Sometimes the condition on which a wait is based cannot be handles via callbacks
     * or events and must be polled. That is, one must check and re-check at some short
     * interval to determine if the condition is satisfied.
     *
     * For example:
     *
     *      var t = 0;
     *
     *      setTimeout(function () {
     *          t = 1;
     *      }, 1000);
     *
     *      ST.element('@some-div').
     *          click(10, 10).
     *
     *          wait(function (el) {
     *              // this test method ignores the el (ST.Element) argument
     *              // for demonstration purposes.
     *              return t;
     *          }).
     *
     *          and(function (el) {
     *              // Runs after the click event and when t is truthy. We receive the
     *              // ST.Element wrapper for the "some-div" element.
     *
     *              expect(el.hasCls('foo')).toBe(true);
     *          });
     *
     * These can be combined as needed.
     *
     *      ST.element('@some-div').
     *          click(10, 10).
     *
     *          wait(200, // wait 200ms
     *
     *              function (el) {
     *                  return t;  // poll this one until it is truthy
     *              },
     *
     *              300,  // wait 300ms
     *
     *              'Something interest', // message for the next fn's timeout reason
     *
     *              function (el) {
     *                  return el.somethingInteresting();
     *              }
     *          ).
     *
     *          and(function (el) {
     *              expect(el.hasCls('foo')).toBe(true);
     *          });
     *
     * @param {Number/String/Function...} delayOrPollFn One or more millisecond delays,
     * functions to poll for truthy return value or timeout messages for said functions.
     * @return {ST.future.Element} this
     * @chainable
     */
    wait: function () {
        var me = this,
            events = [],
            message;
 
        ST.each(arguments, function (delay) {
            var t = typeof delay,
                m = message;
 
            if (=== 'number') {
                events.push({ delay: delay });
            } else if (=== 'string') {
                message = delay;
            } else if (=== 'function') {
                events.push({
                    waitingFor: message,
                    waitingState: 'truthy',
                    ready: function () {
                        if (delay.call(me, me._value(), me, this)) {
                            return this.setWaiting(false); // not "me" 
                        }
                        return this.setWaiting(|| delay.toString(),
                                               m ? 'ready' : 'truthy');
                    }
                });
 
                message = null;
            } else {
                throw new Error('wait() accepts millisecond delays or functions');
            }
        });
 
        ST.play(events);
        return this;
    },
 
    /** 
     * @method down
     * Returns a descendant `{@link ST.future.Element future element}` that corresponds
     * to the specified selector. 
     *
     *      ST.element('@someElement').
     *          down('span').
     *          and(function (element) {
     *              // span is now available
     *          });
     *
     * If the specified selector for the descendant element cannot be resolved, the request will timeout.
     * @param {String} locator The DOM Query selector to use to search for the descendant
     * @param {Number} [timeout] The maximum time (in milliseconds) to wait.
     * @return {ST.future.Element}
     * @chainable
     */
    down: function (selector, timeout) {
        return this._createRelatedFuture(ST.element, 'down', selector, timeout);
    },
 
    /** 
     * @method up
     * Returns an ancestor `{@link ST.future.Element future element}` that corresponds
     * to the specified selector. 
     *
     *      ST.element('@someElement').
     *          up('div').
     *          and(function (element) {
     *              // div is now available
     *          });
     *
     * If the specified selector for the ancestor element cannot be resolved, the request will timeout.
     * @param {String} selector The DOM Query selector to use to search for the ancestor
     * @param {Number} [timeout] The maximum time (in milliseconds) to wait.
     * @return {ST.future.Element}
     * @chainable
     */
    up: function (selector, timeout) {
        return this._createRelatedFuture(ST.element, 'up', selector, timeout);
    },
 
    /** 
     * @method child
     * Returns a direct child `{@link ST.future.Element future element}` that corresponds
     * to the specified selector. 
     *
     *      ST.element('@someElement').
     *          child('p').
     *          and(function (element) {
     *              // p is now available
     *          });
     *
     * If the specified selector for the child element cannot be resolved, the request will timeout.
     * @param {String} selector The DOM Query selector to use to search for the child component
     * @param {Number} [timeout] The maximum time (in milliseconds) to wait.
     * @return {ST.future.Element}
     * @chainable
     */
    child: function (selector, timeout) {
        return this._createRelatedFuture(ST.element, 'child', selector, timeout);
    },
 
    //----------------------------------------------------------------- 
    // Private 
 
    _createRelatedFuture: function (maker, direction, locator, timeout) {
        var me = this,
            future;
 
        // create the future 
        future = maker({
            locator: locator,
            root: me,
            direction: direction,
            timeout: timeout
        });
 
        // add a flag so we know the requested relationship of these components 
        future.related = this;
 
        return future;
    },
 
    _value: function () {
        var me = this,
            valueProperty = me.valueProperty;
 
        return (valueProperty && me[valueProperty]) || me;
    },
 
    _getFocusEl: function() {
        return this.el;
    }
});
 
ST.future.Element.addStates({
    /**
     * Waits for this element to have a specified CSS class.
     *
     *      ST.element('@someEl').
     *          hasCls('foo').
     *          and(function (el) {
     *              // el is now does has a "foo" class
     *          });
     *
     * @param {String} cls The class name to test.
     * @param {Number} [timeout] The maximum time (in milliseconds) to wait.
     * @return {ST.future.Element} this
     * @chainable
     */
    hasCls: function (cls) {
        return this.el.hasCls(cls);
    },
 
    /**
     * Waits for this element to become hidden.
     *
     *      ST.element('@someEl').
     *          hidden().
     *          and(function (el) {
     *              // el is now hidden
     *          });
     *
     * @param {Number} [timeout] The maximum time (in milliseconds) to wait.
     * @return {ST.future.Element} this
     * @chainable
     */
    hidden: function () {
        return !this.el.isVisible();
    },
 
    /**
     * Waits for this element to not have a specified CSS class.
     *
     *      ST.element('@someEl').
     *          missingCls('foo').
     *          and(function (el) {
     *              // el is now does not have a "foo" class
     *          });
     *
     * @param {String} cls The class name to test.
     * @param {Number} [timeout] The maximum time (in milliseconds) to wait.
     * @return {ST.future.Element} this
     * @chainable
     */
    missingCls: function (cls) {
        return !this.el.hasCls(cls);
    },
 
    /**
     * Waits for this element to be removed from the document.
     *
     *      ST.element('@someEl').
     *          removed().
     *          and(function (el) {
     *              // el is now removed from the document
     *          });
     *
     * @method removed
     * @param {Number} [timeout] The maximum time (in milliseconds) to wait.
     * @return {ST.future.Element} this
     * @chainable
     */
    removed: {
        is: function () {
            return !ST.fly(document.body).contains(this.el);
        },
        available: false
    },
 
    /**
     * Waits for this element to become visible.
     *
     * Event injection methods automatically wait for target visibility, however, if
     * using `and` sequences explicitly waiting for visibility may be necessary.
     *
     *      ST.element('@someEl').
     *          visible().
     *          and(function (el) {
     *              // el is now visible
     *          });
     *
     * @param {Number} [timeout] The maximum time (in milliseconds) to wait.
     * @return {ST.future.Element} this
     * @chainable
     */
    visible: function () {
        return this.el.isVisible();
    },
 
    /**
     * Waits for this element to become focused.
     *
     *      ST.element('@someEl').
     *          focused().
     *          and(function (el) {
     *              // el is now hidden
     *          });
     *
     * @param {Number} [timeout] The maximum time (in milliseconds) to wait.
     * @return {ST.future.Element} this
     * @chainable
     */
    focused: function() {
        return document.activeElement === this._getFocusEl().dom;
    },
 
    /**
     * Waits for this element to become blurred.
     *
     *      ST.element('@someEl').
     *          focused().
     *          and(function (el) {
     *              // el is now hidden
     *          });
     *
     * @param {Number} [timeout] The maximum time (in milliseconds) to wait.
     * @return {ST.future.Element} this
     * @chainable
     */
    blurred: function() {
        return document.activeElement !== this._getFocusEl().dom;
    },
 
 
    /**
     * Takes a snapshot of the viewport and compares it to the associated baseline image.
     *
     *      ST.element('@someEl').
     *          click(10, 10).
     *          screenshot();
     *
     * @param {String} name 
     * @param {Number} [timeout] The maximum time (in milliseconds) to wait.
     * @method screenshot
     * @return {ST.future.Element} this
     * @chainable
     */
    screenshot: function(name, timeout) {
        ST.screenshot(name, null, timeout);
        return this;
    }
 
    /*
    * snapshot: function () {}
    * */
});
 
/**
 * Returns a {@link ST.future.Element future element} used to queue operations for
 * when that element becomes available (not necessiarly `visible`).
 *
 * Once a future is returned from this method, it is typically used to describe some
 * sequence of actions, wait for state transitions and perform inspections.
 *
 *      ST.element('@someEl').
 *          click(10, 10).
 *          textLike(/hello/i).
 *          and(function (el) { ... });
 *
 * @param {String} locator See {@link ST.Locator} for supported syntax.
 * @param {Number} [timeout] The maximum time (in milliseconds) to wait for the element.
 * @return {ST.future.Element}
 * @method element
 * @chainable
 * @member ST
 */
ST.element = function (locator, timeout) {
    return new ST.future.Element(locator, timeout);
};
 
/**
 * Schedules a wait for an element to be absent or missing from the DOM. This is typically
 * used after some future action that should cause a removal.
 *
 *      ST.button('@okButton').click();
 *      ST.absent('@confirmationWindow'); // the window owning the OK button
 *
 * This method is similar to `{@link ST.future.Element#removed}` but the difference
 * is that this method does not first wait for the specified element to be found. This
 * difference makes this method suitable for checking for things that should not be
 * present in the first place.
 *
 * @param {String} locator See {@link ST.Locator} for supported syntax.
 * @param {Number} [timeout] The maximum time (in milliseconds) to wait for the element
 * to be removed.
 * @method absent
 * @return {ST.future.Element}
 * @chainable
 * @member ST
 */
ST.absent = function (locator, timeout) {
    return new ST.future.Element({
        locator: locator,
        timeout: timeout,
        available: false
    });
};
 
/**
 * Returns a limited-use {@link ST.future.Element future} that can be used only to
 * {@link ST.future.Element#wait wait} and perform some
 * {@link ST.future.Element#and manual steps}.
 * @return {ST.future.Element}
 * @method wait
 * @member ST
 */
ST.wait = function () {
    var future = new ST.future.Element();
 
    return future.wait.apply(future, arguments);
};
 
ST.future.classes = [];
 
ST.future.define = function (componentName, body) {
    if (!body.extend) {
        body.extend = ST.future.Element;
    }
 
    var states = body.states;
 
    delete body.states;
 
    var cls = ST.define(body),
        parts = componentName.split('.'),
        methodScope = ST,
        classScope = ST.future,
        name;
 
    if (states) {
        cls.addStates(states);  // an inherited static method 
    }
 
    while (parts.length > 1) {
        name = parts.shift();
 
        if (!classScope[name]) {
            classScope[name] = {};
        }
        if (!methodScope[name]) {
            methodScope[name] = {};
        }
    }
 
    name = parts[0];
 
    // track the classes being defined 
    ST.future.classes.push(cls);
 
    if (body.factoryable === false) {
        delete cls.prototype.factoryable;
    } else {
        methodScope[ST.decapitalize(name)] = function (locator, timeout) {
            return new cls(locator, timeout);
        };
    }
 
    return classScope[ST.capitalize(name)] = cls;
};