(function($){
    // for scoping our entry into $.data()
    var rand = Math.floor(Math.random()*9999999);
    
    // onCancel/onComplete are optional
    $.fn.timeout = function( id, delay, onTimeout, onCancel, onComplete ) {
        
        // try a timeout for each matched element. use the matched element for the context in the timeouts
        function doTimeout(isInterval) {
            var $e = $(this);
            
            // if this timeout is already active, cancel it
            if(!isInterval) { $e.cancelTimeout(id); }
            
            // set the timeout for real
            var toid = setTimeout(function() {
            
                // if the try function returns true, treat this as an interval
                if(onTimeout.call($e[0]) === true) {
                    doTimeout.call($e[0], true);
                }
                
                // call the finally function if the timeout/interval is terminating
                else if(onComplete) {
                    onComplete.call($e[0]);
                }
                
                // cleanup
                $e.removeData(id);
            }, delay);
            
            // store the data for this timeout
            $e.data(id + ".to" + rand, {
                'toid': toid,
                'onTimeout': onTimeout,
                'onCancel': onCancel,
                'onComplete': onComplete
            });
        }
        return $(this).each(doTimeout);
    };
    
    function endTimeout( ids, calls ) {
        // ids can be an array or a string of ids delimited by spaces
        ids = $.isArray(ids) ? ids : ids.split(/\s+/);
        
        // clear timeouts, call the appropriate functions for each id, and cleanup
        return $(this).each(function() {
            var i, j, $e = $(this);
            for(i in ids) {
                if(ids.hasOwnProperty(i)) {
                    var id = ids[i];
                    var to = $e.data(id + ".to" + rand);
                    if(to) {
                        clearTimeout(to.toid);
                        for(j=0; calls && j < calls.length; j++) {
                          if(to[calls[j]]) { to[calls[j]].call( $e[0] ); }
                        }
                        $e.removeData(id);
                    }
                }
            }
        });
    }
    
    // cancel all timeouts listed in ids
    $.fn.cancelTimeout = function( ids ) {
        return endTimeout.call(this, ids, ["onCancel", "onComplete"] );
    };
    
    // force all timeouts listed in ids
    $.fn.forceTimeout = function( ids ) {
        return endTimeout.call(this, ids, ["onTimeout", "onComplete"] );
    };

    // global scope -- just use window as the object
    $.timeout = function() {
        return $.fn.timeout.apply(window, arguments);
    };
    
    $.cancelTimeout = function() {
        return $.fn.cancelTimeout.apply(window, arguments);
    };
    
    $.forceTimeout = function() {
        return $.fn.forceTimeout.apply(window, arguments);
    };
    
}(jQuery));
