/**
 * @private
 *
 * The abstract class. Sub-classes are expected, at the very least, to implement translation logics
 * inside the 'translate' method
 */
Ext.define('Ext.util.translatable.Abstract', {
    extend: 'Ext.Evented',
 
    mixins: [
        'Ext.mixin.Factoryable'
    ],
 
    factoryConfig: {
        type: 'translatable',
        defaultType: 'csstransform'
    },
 
    requires: ['Ext.fx.easing.Linear'],
 
    config: {
        easing: null,
 
        easingX: {
            duration: 300
        },
 
        easingY: {
            duration: 300
        },
 
        offsetX: 0,
 
        offsetY: 0
    },
 
    /**
     * @event animationstart
     * Fires whenever the animation is started
     * @param {Ext.util.translatable.Abstract} this 
     * @param {Number} x The current translation on the x axis
     * @param {Number} y The current translation on the y axis
     */
 
    /**
     * @event animationframe
     * Fires for each animation frame
     * @param {Ext.util.translatable.Abstract} this 
     * @param {Number} x The new translation on the x axis
     * @param {Number} y The new translation on the y axis
     */
 
    /**
     * @event animationend
     * Fires whenever the animation is ended
     * @param {Ext.util.translatable.Abstract} this 
     * @param {Number} x The current translation on the x axis
     * @param {Number} y The current translation on the y axis
     */
 
    /**
     * @property {Number} x
     * @private
     * The last translated x value
     */
    x: 0,
 
    /**
     * @property {Number} y
     * @private
     * The last translated y value
     */
    y: 0,
 
    activeEasingX: null,
 
    activeEasingY: null,
 
    isAnimating: false,
 
    isTranslatable: true,
 
    constructor: function(config) {
        this.callParent([config]);
        
        // this.position is simply an internal reusable object for GC purposes and should
        // not be accessed directly as it's values are not kept in sync.  always use
        // getPosition() to get the position
        this.position = { x: 0, y: 0 };
    },
 
    factoryEasing: function(easing) {
        return Ext.factory(easing, Ext.fx.easing.Linear, null, 'easing');
    },
 
    applyEasing: function(easing) {
        if (!this.getEasingX()) {
            this.setEasingX(this.factoryEasing(easing));
        }
 
        if (!this.getEasingY()) {
            this.setEasingY(this.factoryEasing(easing));
        }
    },
 
    applyEasingX: function(easing) {
        return this.factoryEasing(easing);
    },
 
    applyEasingY: function(easing) {
        return this.factoryEasing(easing);
    },
 
    updateOffsetX: function() {
        var me = this;
 
        if (!me.isConfiguring && !me.isAnimating) {
            me.translateXY(me.x, me.y);
        }
    },
 
    updateOffsetY: function() {
        var me = this;
 
        if (!me.isConfiguring && !me.isAnimating) {
            me.translateXY(me.x, me.y);
        }
    },
 
    translate: function(x, y, animation) {
        var me = this;
 
        if (animation) {
            return me.translateAnimated(x, y, animation);
        }
 
        if (me.isAnimating) {
            me.stopAnimation();
        }
 
        if (!isNaN(x) && typeof x === 'number') {
            me.x = x;
        }
        else {
            x = me.x;
        }
 
        if (!isNaN(y) && typeof y === 'number') {
            me.y = y;
        }
        else {
            y = me.y;
        }
 
        me.translateXY(x, y);
    },
 
    translateXY: function(x, y) {
        var me = this;
 
        if (!me.destroyed) {
            me.doTranslate(+ me.getOffsetX(), y + me.getOffsetY());
 
            if (me.hasListeners.translate) {
                me.fireEvent('translate', me, x, y);
            }
        }
    },
 
    translateAxis: function(axis, value, animation) {
        var x, y;
 
        if (axis === 'x') {
            x = value;
        }
        else {
            y = value;
        }
 
        return this.translate(x, y, animation);
    },
 
    /**
     * Returns the translatable object's current position.
     * @return {Object} position An object with x and y properties
     */
    getPosition: function() {
        var me = this,
            position = me.position;
 
        position.x = -me.x;
        position.y = -me.y;
 
        return position;
    },
 
    animate: function(easingX, easingY) {
        var me = this;
 
        me.activeEasingX = easingX;
        me.activeEasingY = easingY;
 
        me.isAnimating = true;
 
        if (me.ownerCmp) {
            me.ownerCmp.isTranslating = true;
        }
 
        me.lastX = null;
        me.lastY = null;
 
        Ext.AnimationQueue.start(me.doAnimationFrame, me);
 
        me.fireEvent('animationstart', me, me.x, me.y);
 
        return me;
    },
 
    translateAnimated: function(x, y, animation) {
        var me = this,
            now, easing, easingX, easingY;
 
        if (!Ext.isObject(animation)) {
            animation = {};
        }
 
        if (me.isAnimating) {
            me.stopAnimation();
        }
 
        // Callback must be called in stopAnimation
        me.callback = animation.callback;
        me.callbackScope = animation.scope;
 
        now = Ext.Date.now();
        easing = animation.easing;
        
        easingX = (typeof x === 'number')
            ? (animation.easingX || easing || me.getEasingX() || true)
            : null;
        
        easingY = (typeof y === 'number')
            ? (animation.easingY || easing || me.getEasingY() || true)
            : null;
 
        if (easingX) {
            easingX = me.factoryEasing(easingX);
            easingX.setStartTime(now);
            easingX.setStartValue(me.x);
            easingX.setEndValue(x);
 
            if ('duration' in animation) {
                easingX.setDuration(animation.duration);
            }
        }
 
        if (easingY) {
            easingY = me.factoryEasing(easingY);
            easingY.setStartTime(now);
            easingY.setStartValue(me.y);
            easingY.setEndValue(y);
 
            if ('duration' in animation) {
                easingY.setDuration(animation.duration);
            }
        }
 
        return me.animate(easingX, easingY);
    },
 
    doAnimationFrame: function() {
        var me = this,
            easingX = me.activeEasingX,
            easingY = me.activeEasingY,
            now = Date.now(),
            x, y;
 
        if (!me.isAnimating) {
            return;
        }
 
        me.lastRun = now;
 
        if (easingX === null && easingY === null) {
            me.stopAnimation();
 
            return;
        }
 
        if (easingX !== null) {
            me.x = x = Math.round(easingX.getValue());
 
            if (easingX.isEnded) {
                me.activeEasingX = null;
                me.fireEvent('axisanimationend', me, 'x', x);
            }
        }
        else {
            x = me.x;
        }
 
        if (easingY !== null) {
            me.y = y = Math.round(easingY.getValue());
 
            if (easingY.isEnded) {
                me.activeEasingY = null;
                me.fireEvent('axisanimationend', me, 'y', y);
            }
        }
        else {
            y = me.y;
        }
 
        if (me.lastX !== x || me.lastY !== y) {
            me.translateXY(x, y);
 
            me.lastX = x;
            me.lastY = y;
        }
 
        me.fireEvent('animationframe', me, x, y);
    },
 
    stopAnimation: function() {
        var me = this;
 
        if (!me.isAnimating) {
            return;
        }
 
        me.activeEasingX = null;
        me.activeEasingY = null;
 
        me.isAnimating = false;
 
        if (me.ownerCmp) {
            me.ownerCmp.isTranslating = false;
        }
 
        Ext.AnimationQueue.stop(me.doAnimationFrame, me);
        
        me.fireEvent('animationend', me, me.x, me.y);
 
        if (me.callback) {
            me.callback.call(me.callbackScope);
            me.callback = null;
        }
    },
 
    refresh: function() {
        this.translate(this.x, this.y);
    },
 
    resolveListenerScope: function() {
        var ownerCmp = this.ownerCmp,
            a = arguments;
 
        if (ownerCmp) {
            return ownerCmp.resolveListenerScope.apply(ownerCmp, a);
        }
 
        return this.callParent(a);
    },
 
    destroy: function() {
        var me = this;
        
        me.destroying = true;
        
        if (me.isAnimating) {
            me.stopAnimation();
        }
 
        me.callParent();
        
        me.destroying = false;
        me.destroyed = true;
    }
});