/**
 * @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',

    requires: ['Ext.fx.easing.Linear'],

    config: {
        element: null,

        easing: null,

        easingX: null,

        easingY: null,

        fps: 60
    },

    /**
     * @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
     */

    constructor: function(config) {
        var element;

        this.doAnimationFrame = Ext.Function.bind(this.doAnimationFrame, this);

        this.x = 0;

        this.y = 0;

        this.activeEasingX = null;

        this.activeEasingY = null;

        this.initialConfig = config;

        if (config && config.element) {
            element = config.element;
            this.setElement(element);
        }
    },

    applyElement: function(element) {
        if (!element) {
            return;
        }

        return Ext.get(element);
    },

    updateElement: function(element) {
        this.initConfig(this.initialConfig);
        this.refresh();
    },

    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);
    },

    updateFps: function(fps) {
        this.animationInterval = 1000 / fps;
    },

    doTranslate: function(x, y) {
        if (typeof x == 'number') {
            this.x = x;
        }

        if (typeof y == 'number') {
            this.y = y;
        }

        return this;
    },

    translate: function(x, y, animation) {
        if (!this.getElement().dom) {
            return;
        }
        if (Ext.isObject(x)) {
            throw new Error();
        }

        this.stopAnimation();

        if (animation) {
            return this.translateAnimated(x, y, animation);
        }

        return this.doTranslate(x, y);
    },

    animate: function(easingX, easingY) {
        this.activeEasingX = easingX;
        this.activeEasingY = easingY;

        this.isAnimating = true;
        this.animationTimer = setInterval(this.doAnimationFrame, this.animationInterval);

        this.fireEvent('animationstart', this, this.x, this.y);

        return this;
    },

    translateAnimated: function(x, y, animation) {
        if (Ext.isObject(x)) {
            throw new Error();
        }

        if (!Ext.isObject(animation)) {
            animation = {};
        }

        var now = Ext.Date.now(),
            easing = animation.easing,
            easingX = (typeof x == 'number') ? (animation.easingX || this.getEasingX() || easing || true) : null,
            easingY = (typeof y == 'number') ? (animation.easingY || this.getEasingY() || easing || true) : null;

        if (easingX) {
            easingX = this.factoryEasing(easingX);
            easingX.setStartTime(now);
            easingX.setStartValue(this.x);
            easingX.setEndValue(x);

            if ('duration' in animation) {
                easingX.setDuration(animation.duration);
            }
        }

        if (easingY) {
            easingY = this.factoryEasing(easingY);
            easingY.setStartTime(now);
            easingY.setStartValue(this.y);
            easingY.setEndValue(y);

            if ('duration' in animation) {
                easingY.setDuration(animation.duration);
            }
        }

        return this.animate(easingX, easingY);
    },

    doAnimationFrame: function() {
        var easingX = this.activeEasingX,
            easingY = this.activeEasingY,
            element = this.getElement(),
            x, y;

        if (!this.isAnimating || !element.dom) {
            return;
        }

        if (easingX === null && easingY === null) {
            this.stopAnimation();
            return;
        }

        if (easingX !== null) {
            this.x = x = Math.round(easingX.getValue());

            if (easingX.isEnded) {
                this.activeEasingX = null;
                this.fireEvent('axisanimationend', this, 'x', x);
            }
        }
        else {
            x = this.x;
        }

        if (easingY !== null) {
            this.y = y = Math.round(easingY.getValue());

            if (easingY.isEnded) {
                this.activeEasingY = null;
                this.fireEvent('axisanimationend', this, 'y', y);
            }
        }
        else {
            y = this.y;
        }

        this.doTranslate(x, y);
        this.fireEvent('animationframe', this, x, y);
    },

    stopAnimation: function() {
        if (!this.isAnimating) {
            return;
        }

        this.activeEasingX = null;
        this.activeEasingY = null;

        this.isAnimating = false;

        clearInterval(this.animationTimer);
        this.fireEvent('animationend', this, this.x, this.y);
    },

    refresh: function() {
        this.translate(this.x, this.y);
    }
});