(function($) {

var classAnimationActions = ['add', 'remove', 'toggle'],
    shorthandStyles = {
        border: 1,
        borderBottom: 1,
        borderColor: 1,
        borderLeft: 1,
        borderRight: 1,
        borderTop: 1,
        borderWidth: 1,
        margin: 1,
        padding: 1
    };

function getElementStyles() {
    var style = document.defaultView
            ? document.defaultView.getComputedStyle(this, null)
            : this.currentStyle,
        newStyle = {},
        key,
        camelCase;

    // webkit enumerates style porperties
    if (style && style.length && style[0] && style[style[0]]) {
        var len = style.length;
        while (len--) {
            key = style[len];
            if (typeof style[key] == 'string') {
                camelCase = key.replace(/\-(\w)/g, function(all, letter) {
                    return letter.toUpperCase();
                });
                newStyle[camelCase] = style[key];
            }
        }
    } else {
        for (key in style) {
            if (typeof style[key] === 'string') {
                newStyle[key] = style[key];
            }
        }
    }
    
    return newStyle;
}

function filterStyles(styles) {
    var name, value;
    for (name in styles) {
        value = styles[name];
        if (
            // ignore null and undefined values
            value == null ||
            // ignore functions (when does this occur?)
            $.isFunction(value) ||
            // shorthand styles that need to be expanded
            name in shorthandStyles ||
            // ignore scrollbars (break in IE)
            (/scrollbar/).test(name) ||
    
            // only colors or values that can be converted to numbers
            (!(/color/i).test(name) && isNaN(parseFloat(value)))
        ) {
            delete styles[name];
        }
    }
    
    return styles;
}

function styleDifference(oldStyle, newStyle) {
    var diff = { _: 0 }, // http://dev.jquery.com/ticket/5459
        name;
    
    for (name in newStyle) {
        if (oldStyle[name] != newStyle[name]) {
            diff[name] = newStyle[name];
        }
    }
    
    return diff;
}

function animateClass(value, duration, easing, callback) {
    if ($.isFunction(easing)) {
        callback = easing;
        easing = null;
    }
    
    return this.each(function() {
    
        var that = $(this).stop(false, true),
            originalStyleAttr = that.attr('style') || ' ',
            originalStyle = filterStyles(getElementStyles.call(this)),
            newStyle,
            className = that.attr('className'),
            children = $('*', that).stop(false, true);
            
        children.each(function() {
            var e = $(this);
            e.data('old-style-attr', e.attr('style') || ' ');
            e.data('old-style', filterStyles(getElementStyles.call(this)));
        });
    
        $.each(classAnimationActions, function(i, action) {
            if (value[action]) {
                that[action + 'Class'](value[action]);
            }
        });
        newStyle = filterStyles(getElementStyles.call(this));
        
        children.each(function() {
            $(this).data('new-style', filterStyles(getElementStyles.call(this)));
        });
        
        that.attr('className', className);
        
        children.each(function() {
            var e = $(this);
            e.animate(styleDifference(e.data('old-style'), e.data('new-style')), duration, easing, function() {
                // work around bug in IE by clearing the cssText before setting it
                if (typeof e.attr('style') == 'object') {
                    e.attr('style').cssText = '';
                    e.attr('style').cssText = e.data('old-style-attr');
                } else {
                    e.attr('style', e.data('old-style-attr'));
                }
            });
        });
    
        that.animate(styleDifference(originalStyle, newStyle), duration, easing, function() {
            $.each(classAnimationActions, function(i, action) {
                if (value[action]) { that[action + 'Class'](value[action]); }
            });
            // work around bug in IE by clearing the cssText before setting it
            if (typeof that.attr('style') == 'object') {
                that.attr('style').cssText = '';
                that.attr('style').cssText = originalStyleAttr;
            } else {
                that.attr('style', originalStyleAttr);
            }
            if (callback) { callback.apply(this, arguments); }
        });
    });
};

$.fn.extend({
    _addClass: $.fn.addClass,
    addClass: function(classNames, speed, easing, callback) {
        return speed ? animateClass.apply(this, [{ add: classNames },speed,easing,callback]) : this._addClass(classNames);
    },
    
    _removeClass: $.fn.removeClass,
    removeClass: function(classNames,speed,easing,callback) {
        return speed ? animateClass.apply(this, [{ remove: classNames },speed,easing,callback]) : this._removeClass(classNames);
    },
    
    _toggleClass: $.fn.toggleClass,
    toggleClass: function(classNames, force, speed, easing, callback) {
        if ( typeof force == "boolean" || force === undefined ) {
                if ( !speed ) {
                        // without speed parameter;
                        return this._toggleClass(classNames, force);
                } else {
                        return animateClass.apply(this, [(force?{add:classNames}:{remove:classNames}),speed,easing,callback]);
                }
        } else {
                // without switch parameter;
                return animateClass.apply(this, [{ toggle: classNames },force,speed,easing]);
        }
    },
    
    switchClass: function(remove,add,speed,easing,callback) {
        return animateClass.apply(this, [{ add: add, remove: remove },speed,easing,callback]);
    }
});

})(jQuery);

