/**
 * @private
 */
Ext.define('Ext.fx.runner.CssTransition', {
    extend: 'Ext.fx.runner.Css',
    requires: ['Ext.AnimationQueue'],
    alternateClassName: 'Ext.Animator',
    singleton: true,

    listenersAttached: false,

    constructor: function() {
        this.runningAnimationsData = {};

        return this.callParent(arguments);
    },

    attachListeners: function() {
        this.listenersAttached = true;

        Ext.getWin().on('transitionend', 'onTransitionEnd', this);
    },

    onTransitionEnd: function(e) {
        var target = e.target,
            id = target.id;

        if (id && this.runningAnimationsData.hasOwnProperty(id)) {
            this.refreshRunningAnimationsData(Ext.get(target), [e.browserEvent.propertyName]);
        }
    },

    onAnimationEnd: function(element, data, animation, isInterrupted, isReplaced) {
        var id = element.getId(),
            runningData = this.runningAnimationsData[id],
            endRules = {},
            endData = {},
            runningNameMap, toPropertyNames, i, ln, name;

        animation.un('stop', 'onAnimationStop', this);

        if (runningData) {
            runningNameMap = runningData.nameMap;
        }

        endRules[id] = endData;

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

        animation.fireEvent('animationbeforeend', animation, element, isInterrupted);
        this.fireEvent('animationbeforeend', this, animation, element, isInterrupted);

        if (isReplaced || (!isInterrupted && !data.preserveEndState)) {
            toPropertyNames = data.toPropertyNames;

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

                if (runningNameMap && !runningNameMap.hasOwnProperty(name)) {
                    endData[name] = null;
                }
            }
        }

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

        this.applyStyles(endRules);

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

        animation.fireEvent('animationend', animation, element, isInterrupted);
        this.fireEvent('animationend', this, animation, element, isInterrupted);
        Ext.AnimationQueue.stop(Ext.emptyFn, animation);
    },

    onAllAnimationsEnd: function(element) {
        var id = element.getId(),
            endRules = {};

        delete this.runningAnimationsData[id];

        endRules[id] = {
            'transition-property': null,
            'transition-duration': null,
            'transition-timing-function': null,
            'transition-delay': null
        };

        this.applyStyles(endRules);
        this.fireEvent('animationallend', this, element);
    },

    hasRunningAnimations: function(element) {
        var id = element.getId(),
            runningAnimationsData = this.runningAnimationsData;

        return runningAnimationsData.hasOwnProperty(id) && runningAnimationsData[id].sessions.length > 0;
    },

    refreshRunningAnimationsData: function(element, propertyNames, interrupt, replace) {
        var id = element.getId(),
            runningAnimationsData = this.runningAnimationsData,
            runningData = runningAnimationsData[id];

        if (!runningData) {
            return;
        }

        var nameMap = runningData.nameMap,
            nameList = runningData.nameList,
            sessions = runningData.sessions,
            ln, j, subLn, name,
            i, session, map, list,
            hasCompletedSession = false;

        interrupt = Boolean(interrupt);
        replace = Boolean(replace);

        if (!sessions) {
            return this;
        }

        ln = sessions.length;

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

        if (replace) {
            runningData.nameMap = {};
            nameList.length = 0;

            for (i = 0; i < ln; i++) {
                session = sessions[i];
                this.onAnimationEnd(element, session.data, session.animation, interrupt, replace);
            }

            sessions.length = 0;
        }
        else {
            for (i = 0; i < ln; i++) {
                session = sessions[i];
                map = session.map;
                list = session.list;

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

                    if (map[name]) {
                        delete map[name];
                        Ext.Array.remove(list, name);
                        session.length--;
                        if (--nameMap[name] == 0) {
                            delete nameMap[name];
                            Ext.Array.remove(nameList, name);
                        }
                    }
                }

                if (session.length == 0) {
                    sessions.splice(i, 1);
                    i--;
                    ln--;

                    hasCompletedSession = true;
                    this.onAnimationEnd(element, session.data, session.animation, interrupt);
                }
            }
        }

        if (!replace && !interrupt && sessions.length == 0 && hasCompletedSession) {
            this.onAllAnimationsEnd(element);
        }
    },

    getRunningData: function(id) {
        var runningAnimationsData = this.runningAnimationsData;

        if (!runningAnimationsData.hasOwnProperty(id)) {
            runningAnimationsData[id] = {
                nameMap: {},
                nameList: [],
                sessions: []
            };
        }

        return runningAnimationsData[id];
    },

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

        if (!testElement) {
            iframe = document.createElement('iframe');
            //<debug>
            // Set an attribute that tells the test runner to ignore this node when checking
            // for dom cleanup
            iframe.setAttribute('data-sticky', true);
            //</debug>
            iframe.setAttribute('tabIndex', -1);
            iframeStyle = iframe.style;
            iframeStyle.setProperty('visibility', 'hidden', 'important');
            iframeStyle.setProperty('width', '0px', 'important');
            iframeStyle.setProperty('height', '0px', 'important');
            iframeStyle.setProperty('position', 'absolute', 'important');
            iframeStyle.setProperty('border', '0px', 'important');
            iframeStyle.setProperty('zIndex', '-1000', 'important');

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

            iframeDocument.open();
            iframeDocument.writeln('</body>');
            iframeDocument.close();

            this.testElement = testElement = iframeDocument.createElement('div');
            testElement.style.setProperty('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);

        if (Ext.browser.is.Firefox) {
            // We force a repaint of the element in Firefox to make sure the computedStyle to be updated
            testElement.offsetHeight;
        }

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

        return value;
    },

    run: function(animations) {
        var me = this,
            isLengthPropertyMap = me.lengthProperties,
            fromData = {},
            toData = {},
            data = {},
            element, elementId, from, to, before,
            fromPropertyNames, toPropertyNames,
            doApplyTo, message,
            runningData, elementData,
            i, j, ln, animation, propertiesLength, sessionNameMap,
            computedStyle, formattedName, name, toFormattedValue,
            computedValue, fromFormattedValue, isLengthProperty,
            runningNameMap, runningNameList, runningSessions, runningSession;

        if (!me.listenersAttached) {
            me.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();

            // Empty function to prevent idleTasks from running while we animate.
            Ext.AnimationQueue.start(Ext.emptyFn, animation);

            computedStyle = window.getComputedStyle(element.dom);

            elementId = element.getId();

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

            if (animation.onBeforeStart) {
                animation.onBeforeStart.call(animation.scope || me, element);
            }
            animation.fireEvent('animationstart', animation);
            me.fireEvent('animationstart', me, animation);

            data[elementId] = data;

            before = data.before;
            from = data.from;
            to = data.to;

            data.fromPropertyNames = fromPropertyNames = [];
            data.toPropertyNames = toPropertyNames = [];

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

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

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

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

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

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

            propertiesLength = toPropertyNames.length;

            if (propertiesLength === 0) {
                me.onAnimationEnd(element, data, animation);
                continue;
            }

            runningData = me.getRunningData(elementId);
            runningSessions = runningData.sessions;

            if (runningSessions.length > 0) {
                me.refreshRunningAnimationsData(
                    element, Ext.Array.merge(fromPropertyNames, toPropertyNames), true, data.replacePrevious
                );
            }

            runningNameMap = runningData.nameMap;
            runningNameList = runningData.nameList;

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

                if (!runningNameMap.hasOwnProperty(name)) {
                    runningNameMap[name] = 1;
                    runningNameList.push(name);
                }
                else {
                    runningNameMap[name]++;
                }
            }

            runningSession = {
                element: element,
                map: sessionNameMap,
                list: toPropertyNames.slice(),
                length: propertiesLength,
                data: data,
                animation: animation
            };
            runningSessions.push(runningSession);

            animation.on('stop', 'onAnimationStop', me);

            elementData = Ext.apply({}, before);
            Ext.apply(elementData, from);

            if (runningNameList.length > 0) {
                fromPropertyNames = Ext.Array.difference(runningNameList, fromPropertyNames);
                toPropertyNames = Ext.Array.merge(fromPropertyNames, toPropertyNames);
                elementData['transition-property'] = fromPropertyNames;
            }

            fromData[elementId] = elementData;
            toData[elementId] = Ext.apply({}, to);

            toData[elementId]['transition-property'] = toPropertyNames;
            toData[elementId]['transition-duration'] = data.duration;
            toData[elementId]['transition-timing-function'] = data.easing;
            toData[elementId]['transition-delay'] = data.delay;

            animation.startTime = Date.now();
        }

        message = me.$className;

        me.applyStyles(fromData);

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

        if (window.requestAnimationFrame) {
            window.requestAnimationFrame(function() {
                window.addEventListener('message', doApplyTo, false);
                window.postMessage(message, '*');
            });
        }else {
            Ext.defer(function() {
                window.addEventListener('message', doApplyTo, false);
                window.postMessage(message, '*');
            }, 1)
        }
    },

    onAnimationStop: function(animation) {
        var runningAnimationsData = this.runningAnimationsData,
            id, runningData, sessions, i, ln, session;

        for (id in runningAnimationsData) {
            if (runningAnimationsData.hasOwnProperty(id)) {
                runningData = runningAnimationsData[id];
                sessions = runningData.sessions;

                for (i = 0,ln = sessions.length; i < ln; i++) {
                    session = sessions[i];
                    if (session.animation === animation) {
                        this.refreshRunningAnimationsData(session.element, session.list.slice(), false);
                    }
                }
            }
        }
    }
});
