/*
 * Copyright (C) 2010 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package android.view;

import junit.framework.Assert;

import android.test.InstrumentationTestCase;
import android.test.suitebuilder.annotation.MediumTest;
import android.view.animation.AccelerateInterpolator;
import android.view.animation.DecelerateInterpolator;
import android.view.animation.Interpolator;
import android.view.animation.LinearInterpolator;

/**
 * Exercises {@link android.view.VelocityTracker} to compute correct velocity.<br>
 * To launch this test, use :<br>
 * <code>./development/testrunner/runtest.py framework -c android.view.VelocityTest</code>
 */
public class VelocityTest extends InstrumentationTestCase {

    @MediumTest
    public void testInitialCondiditions() {
        VelocityTracker vt = VelocityTracker.obtain();
        assertNotNull(vt);
        vt.recycle();
    }

    /**
     * Test that {@link android.view.VelocityTracker}.clear() clears
     * the previous values after a call to computeCurrentVelocity()
     */
    @MediumTest
    public void testClear() {
        long t = System.currentTimeMillis();
        VelocityTracker vt = VelocityTracker.obtain();
        drag(vt, 100, 200, 100, 200, 10, t, 300);
        vt.computeCurrentVelocity(1);
        assertFalse("Velocity should not be null", vt.getXVelocity() == 0.0f);
        assertFalse("Velocity should not be null", vt.getYVelocity() == 0.0f);
        vt.clear();
        vt.computeCurrentVelocity(1);
        assertEquals(0.0f, vt.getXVelocity());
        assertEquals(0.0f, vt.getYVelocity());
        vt.recycle();
    }

    @MediumTest
    public void testDragAcceleration () {
        long t = System.currentTimeMillis();
        VelocityTracker vt = VelocityTracker.obtain();
        drag(vt, 100, 200, 100, 200, 15, t, 400, new AccelerateInterpolator());
        vt.computeCurrentVelocity(1000);
        assertGreater(250.0f, vt.getXVelocity());
        assertGreater(250.0f, vt.getYVelocity());
        vt.recycle();
    }

    @MediumTest
    public void testDragDeceleration () {
        long t = System.currentTimeMillis();
        VelocityTracker vt = VelocityTracker.obtain();
        drag(vt, 100, 200, 100, 200, 15, t, 400, new DecelerateInterpolator());
        vt.computeCurrentVelocity(1000);
        assertLower(250.0f, vt.getXVelocity());
        assertLower(250.0f, vt.getYVelocity());
        vt.recycle();
    }

    @MediumTest
    public void testDragLinearHorizontal() {
        long t = System.currentTimeMillis();
        VelocityTracker vt = VelocityTracker.obtain();
        // 100px in 400ms => 250px/s
        drag(vt, 100, 200, 200, 200, 15, t, 400);
        vt.computeCurrentVelocity(1000);
        assertEquals(0.0f, vt.getYVelocity());
        assertEqualFuzzy(250.0f, vt.getXVelocity(), 4f);
        vt.recycle();
    }

    @MediumTest
    public void testDragLinearVertical() {
        long t = System.currentTimeMillis();
        VelocityTracker vt = VelocityTracker.obtain();
        // 100px in 400ms => 250px/s
        drag(vt, 200, 200, 100, 200, 15, t, 400);
        vt.computeCurrentVelocity(1000);
        assertEquals(0.0f, vt.getXVelocity());
        assertEqualFuzzy(250.0f, vt.getYVelocity(), 4f);
        vt.recycle();
    }

    /**
     * Test dragging with two points only
     * (velocity must be an exact value)
     */
    @MediumTest
    public void testDragWith2Points () {
        long t = System.currentTimeMillis();
        VelocityTracker vt = VelocityTracker.obtain();
        // 100px, 2 steps, 100ms => 1000px/s
        drag(vt, 100, 200, 100, 200, 2, t, 100);
        vt.computeCurrentVelocity(1000);
        assertEquals(1000.0f, vt.getXVelocity());
        assertEquals(1000.0f, vt.getYVelocity());
        vt.recycle();
    }

    /**
     * Velocity is independent of the number of points used during
     * the same interval
     */
    @MediumTest
    public void testStabilityInNbPoints () {
        long t = System.currentTimeMillis();
        VelocityTracker vt = VelocityTracker.obtain();
        drag(vt, 100, 200, 100, 200, 10, t, 400); // 10 steps over 400ms
        vt.computeCurrentVelocity(1);
        float firstX = vt.getXVelocity();
        float firstY = vt.getYVelocity();
        vt.clear();
        drag(vt, 100, 200, 100, 200, 20, t, 400); // 20 steps over 400ms
        vt.computeCurrentVelocity(1);
        float secondX = vt.getXVelocity();
        float secondY = vt.getYVelocity();
        assertEqualFuzzy(firstX, secondX, 0.1f);
        assertEqualFuzzy(firstY, secondY, 0.1f);
        vt.recycle();
    }

    /**
     * Velocity is independent of the time when the events occurs,
     * it only depends on delays between the events.
     */
    @MediumTest
    public void testStabilityInTime () {
        long t = System.currentTimeMillis();
        VelocityTracker vt = VelocityTracker.obtain();
        drag(vt, 100, 200, 100, 200, 10, t, 400);
        vt.computeCurrentVelocity(1);
        float firstX = vt.getXVelocity();
        float firstY = vt.getYVelocity();
        vt.clear();
        drag(vt, 100, 200, 100, 200, 10, t + 3600*1000, 400); // on hour later
        vt.computeCurrentVelocity(1);
        float secondX = vt.getXVelocity();
        float secondY = vt.getYVelocity();
        assertEqualFuzzy(firstX, secondX, 0.1f);
        assertEqualFuzzy(firstY, secondY, 0.1f);
        vt.recycle();
    }

    /**
     * Velocity is independent of the position of the events,
     * it only depends on their relative distance.
     */
    @MediumTest
    public void testStabilityInSpace () {
        long t = System.currentTimeMillis();
        VelocityTracker vt = VelocityTracker.obtain();
        drag(vt, 100, 200, 100, 200, 10, t, 400);
        vt.computeCurrentVelocity(1);
        float firstX = vt.getXVelocity();
        float firstY = vt.getYVelocity();
        vt.clear();
        drag(vt, 200, 300, 200, 300, 10, t, 400); // 100px further
        vt.computeCurrentVelocity(1);
        float secondX = vt.getXVelocity();
        float secondY = vt.getYVelocity();
        assertEqualFuzzy(firstX, secondX, 0.1f);
        assertEqualFuzzy(firstY, secondY, 0.1f);
        vt.recycle();
    }

    /**
     * Test that calls to {@link android.view.VelocityTracker}.computeCurrentVelocity()
     * will output same values when using the same data.
     */
    @MediumTest
    public void testStabilityOfComputation() {
        long t = System.currentTimeMillis();
        VelocityTracker vt = VelocityTracker.obtain();
        drag(vt, 100, 200, 100, 200, 10, t, 300);
        vt.computeCurrentVelocity(1);
        float firstX = vt.getXVelocity();
        float firstY = vt.getYVelocity();
        vt.computeCurrentVelocity(1);
        float secondX = vt.getXVelocity();
        float secondY = vt.getYVelocity();
        assertEquals(firstX, secondX);
        assertEquals(firstY, secondY);
        vt.recycle();
    }

    /**
     * Test the units parameter of {@link android.view.VelocityTracker}.computeCurrentVelocity()
     */
    @MediumTest
    public void testStabilityOfUnits() {
        long t = System.currentTimeMillis();
        VelocityTracker vt = VelocityTracker.obtain();
        drag(vt, 100, 200, 100, 200, 10, t, 300);
        vt.computeCurrentVelocity(1);
        float firstX = vt.getXVelocity();
        float firstY = vt.getYVelocity();
        vt.computeCurrentVelocity(1000);
        float secondX = vt.getXVelocity();
        float secondY = vt.getYVelocity();
        assertEqualFuzzy(firstX, secondX / 1000.0f, 0.1f);
        assertEqualFuzzy(firstY, secondY / 1000.0f, 0.1f);
        vt.recycle();
    }

    /**
     * Simulate a drag by giving directly MotionEvents to
     * the VelocityTracker using a linear interpolator
     */
    private void drag(VelocityTracker vt, int startX, int endX, int startY, int endY, int steps,
            long startime, int duration) {
        drag(vt, startX, endX, startY, endY, steps, startime, duration, new LinearInterpolator());
    }

    /**
     * Simulate a drag by giving directly MotionEvents to
     * the VelocityTracker using a given interpolator
     */
    private void drag(VelocityTracker vt, int startX, int endX, int startY, int endY, int steps,
            long startime, int duration, Interpolator interpolator) {
        addMotionEvent(vt, startX, startY, startime, MotionEvent.ACTION_DOWN);
        float dt = duration / (float)steps;
        int distX = endX - startX;
        int distY = endY - startY;
        for (int i=1; i<steps-1; i++) {
            float ii = interpolator.getInterpolation(i / (float)steps);
            int x = (int) (startX + distX * ii);
            int y = (int) (startY + distY * ii);
            long time = startime + (int) (i * dt);
            addMotionEvent(vt, x, y, time, MotionEvent.ACTION_MOVE);
        }
        addMotionEvent(vt, endX, endY, startime + duration, MotionEvent.ACTION_UP);
    }

    private void addMotionEvent(VelocityTracker vt, int x, int y, long time, int action) {
        MotionEvent me = MotionEvent.obtain(time, time, action, x, y, 0);
        vt.addMovement(me);
        me.recycle();
    }

    /**
     * Float imprecision of the average computations and filtering
     * (removing last MotionEvent for N > 3) implies that tests
     *  accepts some approximated values.
     */
    private void assertEqualFuzzy(float expected, float actual, float threshold) {
        boolean fuzzyEqual = actual >= expected - threshold && actual <= expected + threshold;
        Assert.assertTrue("Expected: <"+expected+"> but was: <"+actual+
                "> while accepting a variation of: <"+threshold+">", fuzzyEqual);
    }

    private void assertGreater(float minExpected, float actual) {
        Assert.assertTrue("Expected: minimum <"+minExpected+"> but was: <"+actual+">",
                actual > minExpected);
    }

    private void assertLower(float maxExpected, float actual) {
        Assert.assertTrue("Expected: maximum <"+maxExpected+"> but was: <"+actual+">",
                actual < maxExpected);
    }
}
