const Tweener = imports.tweener.tweener;

function installFrameTicker() {
    // Set up Tweener to have a "frame pulse" that the Jasmine clock functions
    // can influence
    let ticker = {
        FRAME_RATE: 50,

        _init : function() {
        },

        start : function() {
            this._currentTime = 0;

            this._timeoutID = setInterval(() => {
                this._currentTime += 1000 / this.FRAME_RATE;
                this.emit('prepare-frame');
            }, Math.floor(1000 / this.FRAME_RATE));
        },

        stop : function() {
            if ('_timeoutID' in this) {
                clearInterval(this._timeoutID);
                delete this._timeoutID;
            }

            this._currentTime = 0;
        },

        getTime : function() {
            return this._currentTime;
        }
    };
    imports.signals.addSignalMethods(ticker);

    Tweener.setFrameTicker(ticker);
}

describe('Tweener', function () {
    beforeAll(function () {
        jasmine.clock().install();
        installFrameTicker();
    });

    afterAll(function () {
        jasmine.clock().uninstall();
    });

    let start, update, overwrite, complete;
    beforeEach(function () {
        start = jasmine.createSpy('start');
        update = jasmine.createSpy('update');
        overwrite = jasmine.createSpy('overwrite');
        complete = jasmine.createSpy('complete');
    });

    it('runs a simple tween', function () {
        var objectA = {
            x: 0,
            y: 0
        };

        var objectB = {
            x: 0,
            y: 0
        };

        Tweener.addTween(objectA, { x: 10, y: 10, time: 1, transition: "linear" });
        Tweener.addTween(objectB, { x: 10, y: 10, time: 1, delay: 0.5, transition: "linear" });

        jasmine.clock().tick(1001);

        expect(objectA.x).toEqual(10);
        expect(objectA.y).toEqual(10);
        expect(objectB.x).toEqual(5);
        expect(objectB.y).toEqual(5);
    });

    it('calls callbacks during the tween', function () {
        Tweener.addTween({}, {
            time: 0.1,
            onStart: start,
            onUpdate: update,
            onComplete: complete,
        });

        jasmine.clock().tick(101);
        expect(start).toHaveBeenCalled();
        expect(update).toHaveBeenCalled();
        expect(complete).toHaveBeenCalled();
    });

    it('can pause tweens', function () {
        var objectA = {
            foo: 0
        };

        var objectB = {
            bar: 0
        };

        var objectC = {
            baaz: 0
        };

        Tweener.addTween(objectA, { foo: 100, time: 0.1 });
        Tweener.addTween(objectC, { baaz: 100, time: 0.1 });
        Tweener.addTween(objectB, { bar: 100, time: 0.1 });

        Tweener.pauseTweens(objectA);
        // This should do nothing
        expect(Tweener.pauseTweens(objectB, 'quux')).toBeFalsy();
        /* Pause and resume should be equal to doing nothing */
        Tweener.pauseTweens(objectC, "baaz");
        Tweener.resumeTweens(objectC, "baaz");

        jasmine.clock().tick(101);

        expect(objectA.foo).toEqual(0);
        expect(objectB.bar).toEqual(100);
        expect(objectC.baaz).toEqual(100);
    });

    it('can remove tweens', function () {
        var object = {
            foo: 0,
            bar: 0,
            baaz: 0
        };

        Tweener.addTween(object, { foo: 50, time: 0.1 });
        Tweener.addTween(object, { bar: 50, time: 0.1 });
        Tweener.addTween(object, { baaz: 50, time: 0.1});

        /* The Tween on property foo should still be run after removing the other two */
        Tweener.removeTweens(object, "bar", "baaz");

        jasmine.clock().tick(101);

        expect(object.foo).toEqual(50);
        expect(object.bar).toEqual(0);
        expect(object.baaz).toEqual(0);
    });

    it('overrides a tween with another one acting on the same object and property at the same time', function () {
        var objectA = {
            foo: 0
        };

        Tweener.addTween(objectA, { foo: 100, time: 0.1 });
        Tweener.addTween(objectA, { foo: 0, time: 0.1 });

        jasmine.clock().tick(101);

        expect(objectA.foo).toEqual(0);
    });

    it('does not override a tween with another one acting not at the same time', function () {
        var objectB = {
            bar: 0
        };

        /* In this case both tweens should be executed, as they don't
         * act on the object at the same time (the second one has a
         * delay equal to the running time of the first one) */
        Tweener.addTween(objectB, { bar: 100, time: 0.1 });
        Tweener.addTween(objectB, { bar: 150, time: 0.1, delay: 0.1 });

        jasmine.clock(0).tick(201);

        expect(objectB.bar).toEqual(150);
    });

    it('can pause and resume all tweens', function () {
        var objectA = {
            foo: 0
        };
        var objectB = {
            bar: 0
        };

        Tweener.addTween(objectA, { foo: 100, time: 0.1 });
        Tweener.addTween(objectB, { bar: 100, time: 0.1 });

        Tweener.pauseAllTweens();

        jasmine.clock().tick(10);

        Tweener.resumeAllTweens();

        jasmine.clock().tick(101);

        expect(objectA.foo).toEqual(100);
        expect(objectB.bar).toEqual(100);
    });

    it('can remove all tweens', function () {
        var objectA = {
            foo: 0
        };
        var objectB = {
            bar: 0
        };

        Tweener.addTween(objectA, { foo: 100, time: 0.1 });
        Tweener.addTween(objectB, { bar: 100, time: 0.1 });

        Tweener.removeAllTweens();

        jasmine.clock().tick(200);

        expect(objectA.foo).toEqual(0);
        expect(objectB.bar).toEqual(0);
    });

    it('runs a tween with a time of 0 immediately', function () {
        var object = {
            foo: 100
        };

        Tweener.addTween(object, { foo: 50, time: 0, delay: 0 });
        Tweener.addTween(object, { foo: 200, time: 0.1,
            onStart: () => {
                /* The immediate tween should set it to 50 before we run */
                expect(object.foo).toEqual(50);
            },
        });

        jasmine.clock().tick(101);

        expect(object.foo).toEqual(200);
    });

    it('can call a callback a certain number of times', function () {
        var object = {
            foo: 0
        };

        Tweener.addCaller(object, {
            onUpdate: () => { object.foo += 1; },
            count: 10,
            time: 0.1,
        });

        jasmine.clock().tick(101);

        expect(object.foo).toEqual(10);
    });

    it('can count the number of tweens on an object', function () {
        var object = {
            foo: 0,
            bar: 0,
            baaz: 0,
            quux: 0
        };

        expect(Tweener.getTweenCount(object)).toEqual(0);

        Tweener.addTween(object, { foo: 100, time: 0.1 });
        expect(Tweener.getTweenCount(object)).toEqual(1);
        Tweener.addTween(object, { bar: 100, time: 0.1 });
        expect(Tweener.getTweenCount(object)).toEqual(2);
        Tweener.addTween(object, { baaz: 100, time: 0.1 });
        expect(Tweener.getTweenCount(object)).toEqual(3);
        Tweener.addTween(object, { quux: 100, time: 0.1 });
        expect(Tweener.getTweenCount(object)).toEqual(4);

        Tweener.removeTweens(object, 'bar', 'baaz');
        expect(Tweener.getTweenCount(object)).toEqual(2);
    });

    it('can register special properties', function () {
        Tweener.registerSpecialProperty(
            'negative_x',
            function(obj) { return -obj.x; },
            function(obj, val) { obj.x = -val; }
        );

        var objectA = {
            x: 0,
            y: 0
        };

        Tweener.addTween(objectA, { negative_x: 10, y: 10, time: 1, transition: "linear" });

        jasmine.clock().tick(1001);

        expect(objectA.x).toEqual(-10);
        expect(objectA.y).toEqual(10);
    });

    it('can register special modifiers for properties', function () {
        Tweener.registerSpecialPropertyModifier('discrete',
                                                discrete_modifier,
                                                discrete_get);
        function discrete_modifier(props) {
            return props.map(function (prop) { return { name: prop, parameters: null }; });
        }
        function discrete_get(begin, end, time, params) {
            return Math.floor(begin + time * (end - begin));
        }

        var objectA = {
            x: 0,
            y: 0,
            xFraction: false,
            yFraction: false
        };

        Tweener.addTween(objectA, { x: 10, y: 10, time: 1,
            discrete: ["x"],
            transition: "linear",
            onUpdate: function() {
                if (objectA.x != Math.floor(objectA.x))
                    objectA.xFraction = true;
                if (objectA.y != Math.floor(objectA.y))
                    objectA.yFraction = true;
            },
        });

        jasmine.clock().tick(1001);

        expect(objectA.x).toEqual(10);
        expect(objectA.y).toEqual(10);
        expect(objectA.xFraction).toBeFalsy();
        expect(objectA.yFraction).toBeTruthy();
    });

    it('can split properties into more than one special property', function () {
        Tweener.registerSpecialPropertySplitter(
            'xnegy',
            function(val) { return [ { name: "x", value: val },
                                     { name: "y", value: -val } ]; }
        );

        var objectA = {
            x: 0,
            y: 0
        };

        Tweener.addTween(objectA, { xnegy: 10, time: 1, transition: "linear" });

        jasmine.clock().tick(1001);

        expect(objectA.x).toEqual(10);
        expect(objectA.y).toEqual(-10);
    });

    it('calls an overwrite callback when a tween is replaced', function () {
        var object = {
            a: 0,
            b: 0,
            c: 0,
            d: 0
        };

        var tweenA = { a: 10, b: 10, c: 10, d: 10, time: 0.1,
            onStart: start,
            onOverwrite: overwrite,
            onComplete: complete,
        };
        var tweenB = { a: 20, b: 20, c: 20, d: 20, time: 0.1,
            onStart: start,
            onOverwrite: overwrite,
            onComplete: complete,
        };

        Tweener.addTween(object, tweenA);
        Tweener.addTween(object, tweenB);

        jasmine.clock().tick(101);

        expect(start).toHaveBeenCalledTimes(1);
        expect(overwrite).toHaveBeenCalledTimes(1);
        expect(complete).toHaveBeenCalledTimes(1);
    });

    it('can still overwrite a tween after it has started', function () {
        var object = {
            a: 0,
            b: 0,
            c: 0,
            d: 0
        };

        var tweenA = { a: 10, b: 10, c: 10, d: 10, time: 0.1,
            onStart: () => {
                start();
                Tweener.addTween(object, tweenB);
            },
            onOverwrite: overwrite,
            onComplete: complete,
        };
        var tweenB = { a: 20, b: 20, c: 20, d: 20, time: 0.1,
            onStart: start,
            onOverwrite: overwrite,
            onComplete: complete,
        };

        Tweener.addTween(object, tweenA);

        jasmine.clock().tick(121);

        expect(start).toHaveBeenCalledTimes(2);
        expect(overwrite).toHaveBeenCalledTimes(1);
        expect(complete).toHaveBeenCalledTimes(1);
    });

    it('stays within min and max values', function () {
        var objectA = {
            x: 0,
            y: 0
        };

        var objectB = {
            x: 0,
            y: 0
        };

        Tweener.addTween(objectA, { x: 300, y: 300, time: 1, max: 255, transition: "linear" });
        Tweener.addTween(objectB, { x: -200, y: -200, time: 1, delay: 0.5, min: 0, transition: "linear" });

        jasmine.clock().tick(1001);

        expect(objectA.x).toEqual(255);
        expect(objectA.y).toEqual(255);
        expect(objectB.x).toEqual(0);
        expect(objectB.y).toEqual(0);
    });
});
