/**
 * @author Jacky Nguyen <jacky@sencha.com>
 * @private
 */
Ext.define('Ext.fx.runner.CssTransition', {
    extend: 'Ext.fx.runner.Css',

    listenersAttached: false,

    constructor: function() {
        this.requestAnimationFrame = Ext.feature.getSupportedPropertyName(window, 'requestAnimationFrame');

        this.runningData = {};

        this.runningAnimationsData = {};

        return this.callParent(arguments);
    },

    attachListeners: function() {
        this.listenersAttached = true;
        this.getEventDispatcher().addListener('element', '*', 'transitionend', 'onTransitionEnd', this);
    },

    onTransitionEnd: function(e) {
        this.refreshRunningAnimationsData(Ext.get(e.target), [e.browserEvent.propertyName]);
    },

    onAnimationEnd: function(element, animation, isInterrupted) {
        var id = element.getId(),
            endRules = {},
            endData = {
                'transition-property': null,
                'transition-duration': null,
                'transition-timing-function': null,
                'transition-delay': null
            },
            toPropertyNames, i, ln, name;

        endRules[id] = endData;

        if (animation.onBeforeEnd) {
            animation.onBeforeEnd.call(animation.scope || this, element, isInterrupted);
        }

        if (!isInterrupted && !animation.preserveEndState) {
            toPropertyNames = animation.toPropertyNames;

            for (i = 0,ln = toPropertyNames.length; i < ln; i++) {
                name = toPropertyNames[i];
                endData[name] = null;
            }
        }

        if (animation.after) {
            Ext.merge(endData, animation.after);
        }

        this.applyStyles(endRules);

        if (animation.onEnd) {
            animation.onEnd.call(animation.scope || this, element, isInterrupted);
        }
    },

    refreshRunningAnimationsData: function(element, propertyNames, interrupt) {
        var id = element.getId(),
            runningAnimationsData = this.runningAnimationsData,
            animations = runningAnimationsData[id],
            ln, j, subLn, name,
            i, animation, properties;

        if (!animations) {
            return this;
        }

        ln = animations.length;

        if (ln === 0) {
            return this;
        }

        for (i = 0; i < ln; i++) {
            animation = animations[i];
            properties = animation.properties;

            for (j = 0,subLn = propertyNames.length; j < subLn; j++) {
                name = propertyNames[j];

                if (properties[name]) {
                    delete properties[name];
                    animation.length--;
                }
            }

            if (animation.length == 0) {
                animations.splice(i, 1);
                i--;
                ln--;

                this.onAnimationEnd(element, animation.data, interrupt);
            }
        }
    },

   getTestElement: function() {
       var testElement = this.testElement,
           iframe, iframeDocument, iframeStyle;

       if (!testElement) {
           iframe = document.createElement('iframe');
           iframeStyle = iframe.style;
           iframeStyle.visibility = 'hidden !important';
           iframeStyle.width = '0px !important';
           iframeStyle.height = '0px !important';
           iframeStyle.position = 'absolute !important';
           iframeStyle.zIndex = '-1000 !important';

           document.body.appendChild(iframe);
           iframeDocument = iframe.contentDocument;

           this.testElement = testElement = iframeDocument.createElement('div');
           testElement.style.position = 'absolute !important';
           iframeDocument.body.appendChild(testElement);
           this.testElementComputedStyle = window.getComputedStyle(testElement);
       }

       return testElement;
    },

    getCssStyleValue: function(name, value) {
        var testElement = this.getTestElement(),
            computedStyle = this.testElementComputedStyle,
            style = testElement.style;

        style.setProperty(name, value);
        value = computedStyle.getPropertyValue(name);
        style.removeProperty(name);

        return value;
    },

    run: function(animations) {
        var me = this,
            isLengthPropertyMap = this.lengthProperties,
            requestAnimationFrame = this.requestAnimationFrame,
            runningData = this.runningData,
            fromData = {},
            toData = {},
            data = {},
            runningAnimationsData = this.runningAnimationsData,
            previous, element, elementId, from, to, before,
            fromPropertyNames, toPropertyNames, propertyNames,
            doApplyTo, message,
            runningAnimations,
            i, j, ln, animation, propertiesLength, propertiesMap,
            computedStyle, formattedName, name, toFormattedValue,
            computedValue, fromFormattedValue, isLengthProperty;

        if (!this.listenersAttached) {
            this.attachListeners();
        }

        animations = Ext.Array.from(animations);

        for (i = 0,ln = animations.length; i < ln; i++) {
            animation = animations[i];
            animation = Ext.factory(animation, Ext.fx.Animation);
            element = animation.getElement();

            computedStyle = window.getComputedStyle(element.dom);

            elementId = element.getId();

            previous = runningData[elementId];

            animation = Ext.merge({}, animation.getData());

            data[elementId] = animation;

            before = animation.before;
            from = animation.from;
            to = animation.to;

            animation.fromPropertyNames = fromPropertyNames = [];
            animation.toPropertyNames = toPropertyNames = [];

            for (name in to) {
                if (to.hasOwnProperty(name)) {
                    to[name] = toFormattedValue = this.formatValue(to[name], name);
                    formattedName = this.formatName(name);
                    isLengthProperty = isLengthPropertyMap.hasOwnProperty(name);

                    if (!isLengthProperty) {
                        toFormattedValue = this.getCssStyleValue(formattedName, toFormattedValue);
                    }

                    if (from.hasOwnProperty(name)) {
                        from[name] = fromFormattedValue = this.formatValue(from[name], name);

                        if (!isLengthProperty) {
                            fromFormattedValue = this.getCssStyleValue(formattedName, fromFormattedValue);
                        }

                        if (toFormattedValue !== fromFormattedValue) {
                            fromPropertyNames.push(formattedName);
                            toPropertyNames.push(formattedName);
                        }
                    }
                    else {
                        computedValue = computedStyle.getPropertyValue(formattedName);

                        if (toFormattedValue !== computedValue) {
                            toPropertyNames.push(formattedName);
                        }
                    }
                }
            }

            propertyNames = Ext.Array.merge(fromPropertyNames, toPropertyNames);

            propertiesMap = {};
            propertiesLength = toPropertyNames.length;

            for (j = 0; j < propertiesLength; j++) {
                propertiesMap[toPropertyNames[j]] = true;
            }

            if (!(runningAnimations = runningAnimationsData[elementId])) {
                runningAnimationsData[elementId] = runningAnimations = [];
            }

            this.refreshRunningAnimationsData(element, propertyNames, true);

            if (propertiesLength === 0) {
                this.onAnimationEnd(element, animation);
                continue;
            }
            else {
                runningAnimations.push({
                    element: element,
                    properties: propertiesMap,
                    length: propertiesLength,
                    data: animation
                });
            }

            fromData[elementId] = from = Ext.apply(Ext.Object.chain(before), from);

            if (previous) {
                fromPropertyNames = Ext.Array.difference(previous.toPropertyNames, fromPropertyNames);
                toPropertyNames = Ext.Array.merge(fromPropertyNames, toPropertyNames);

                from['transition-property'] = fromPropertyNames;
            }

            toData[elementId] = to = Ext.Object.chain(to);

            to['transition-property'] = toPropertyNames;
            to['transition-duration'] = animation.duration;
            to['transition-timing-function'] = animation.easing;
            to['transition-delay'] = animation.delay;
        }

        Ext.merge(runningData, data);

        if (requestAnimationFrame) {
            window[requestAnimationFrame](function() {
                me.applyStyles(fromData);
                window[requestAnimationFrame](function() {
                    me.applyStyles(toData);
                });
            });
        }
        else {
            message = this.$className;

            this.applyStyles(fromData);

            doApplyTo = function(e) {
                if (e.data === message && e.source === window) {
                    window.removeEventListener('message', doApplyTo, false);
                    me.applyStyles(toData);
                }
            };

            window.addEventListener('message', doApplyTo, false);
            window.postMessage(message, '*');
        }
    }
});