/*
 * Copyright (C) 2016 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 static android.support.test.espresso.matcher.ViewMatchers.isAssignableFrom;
import static android.support.test.espresso.matcher.ViewMatchers.isCompletelyDisplayed;

import static com.google.common.base.Preconditions.checkNotNull;

import static org.hamcrest.Matchers.allOf;

import android.os.SystemClock;
import android.support.test.espresso.InjectEventSecurityException;
import android.support.test.espresso.PerformException;
import android.support.test.espresso.UiController;
import android.support.test.espresso.ViewAction;
import android.support.test.espresso.action.Swiper;
import android.support.test.espresso.util.HumanReadables;
import android.view.MotionEvent;
import android.view.View;

import org.hamcrest.Matcher;

/**
 * Pinch and zooms on a View using touch events.
 * <br>
 * View constraints:
 * <ul>
 * <li>must be displayed on screen
 * <ul>
 */
public class PinchZoomAction implements ViewAction {
    public static Swiper.Status sendPinchZoomAction(UiController uiController,
                                                    float[] firstFingerStartCoords,
                                                    float[] firstFingerEndCoords,
                                                    float[] secondFingerStartCoords,
                                                    float[] secondFingerEndCoords,
                                                    float[] precision) {
        checkNotNull(uiController);
        checkNotNull(firstFingerStartCoords);
        checkNotNull(firstFingerEndCoords);
        checkNotNull(secondFingerStartCoords);
        checkNotNull(secondFingerEndCoords);
        checkNotNull(precision);

        // Specify the touch properties for the finger events.
        final MotionEvent.PointerProperties pp1 = new MotionEvent.PointerProperties();
        pp1.id = 0;
        pp1.toolType = MotionEvent.TOOL_TYPE_FINGER;
        final MotionEvent.PointerProperties pp2 = new MotionEvent.PointerProperties();
        pp2.id = 1;
        pp2.toolType = MotionEvent.TOOL_TYPE_FINGER;
        MotionEvent.PointerProperties[] pointerProperties =
                new MotionEvent.PointerProperties[]{pp1, pp2};

        // Specify the motion properties of the two touch points.
        final MotionEvent.PointerCoords pc1 = new MotionEvent.PointerCoords();
        pc1.x = firstFingerStartCoords[0];
        pc1.y = firstFingerStartCoords[1];
        pc1.pressure = 1;
        pc1.size = 1;
        final MotionEvent.PointerCoords pc2 = new MotionEvent.PointerCoords();
        pc2.x = secondFingerStartCoords[0];
        pc2.y = secondFingerEndCoords[1];
        pc2.pressure = 1;
        pc2.size = 1;

        final long startTime = SystemClock.uptimeMillis();
        long eventTime = startTime;
        final MotionEvent.PointerCoords[] pointerCoords = new MotionEvent.PointerCoords[]{pc1, pc2};

        final MotionEvent firstFingerEvent = MotionEvent.obtain(startTime,
                eventTime, MotionEvent.ACTION_DOWN, 1, pointerProperties, pointerCoords,
                0, 0, 1, 1, 0, 0, 0, 0);

        eventTime = SystemClock.uptimeMillis();
        final MotionEvent secondFingerEvent = MotionEvent.obtain(startTime, eventTime,
                MotionEvent.ACTION_POINTER_DOWN +
                        (pp2.id << MotionEvent.ACTION_POINTER_INDEX_SHIFT),
                2, pointerProperties, pointerCoords, 0, 0, 1, 1, 0, 0, 0, 0);

        try {
            uiController.injectMotionEvent(firstFingerEvent);
        } catch (InjectEventSecurityException e) {
            throw new PerformException.Builder()
                    .withActionDescription("First finger down event")
                    .withViewDescription("Scale gesture detector")
                    .withCause(e)
                    .build();
        }

        try {
            uiController.injectMotionEvent(secondFingerEvent);
        } catch (InjectEventSecurityException e) {
            throw new PerformException.Builder()
                    .withActionDescription("Second finger down event")
                    .withViewDescription("Scale gesture detector")
                    .withCause(e)
                    .build();
        }

        // Specify the coordinates of the two touch points.
        final float[][] stepsFirstFinger = interpolate(firstFingerStartCoords,
                firstFingerEndCoords);
        final float[][] stepsSecondFinger = interpolate(secondFingerStartCoords,
                secondFingerEndCoords);

        // Loop until the end points of the two fingers are reached.
        for (int i = 0; i < PINCH_STEP_COUNT; i++) {
            eventTime = SystemClock.uptimeMillis();

            pc1.x = stepsFirstFinger[i][0];
            pc1.y = stepsFirstFinger[i][1];
            pc2.x = stepsSecondFinger[i][0];
            pc2.y = stepsSecondFinger[i][1];

            final MotionEvent event = MotionEvent.obtain(startTime, eventTime,
                    MotionEvent.ACTION_MOVE, 2, pointerProperties, pointerCoords,
                    0, 0, 1, 1, 0, 0, 0, 0);

            try {
                uiController.injectMotionEvent(event);
            } catch (InjectEventSecurityException e) {
                throw new PerformException.Builder()
                        .withActionDescription("Move event")
                        .withViewDescription("Scale gesture event")
                        .withCause(e)
                        .build();
            }

           uiController.loopMainThreadForAtLeast(800);
        }

        eventTime = SystemClock.uptimeMillis();

        // Send the up event for the second finger.
        final MotionEvent secondFingerUpEvent = MotionEvent.obtain(startTime, eventTime,
                MotionEvent.ACTION_POINTER_UP, 2, pointerProperties, pointerCoords,
                0, 0, 1, 1, 0, 0, 0, 0);
        try {
            uiController.injectMotionEvent(secondFingerUpEvent);
        } catch (InjectEventSecurityException e) {
            throw new PerformException.Builder()
                    .withActionDescription("Second finger up event")
                    .withViewDescription("Scale gesture detector")
                    .withCause(e)
                    .build();
        }

        eventTime = SystemClock.uptimeMillis();
        // Send the up event for the first finger.
        final MotionEvent firstFingerUpEvent = MotionEvent.obtain(startTime, eventTime,
                MotionEvent.ACTION_POINTER_UP, 1, pointerProperties, pointerCoords,
                0, 0, 1, 1, 0, 0, 0, 0);
        try {
            uiController.injectMotionEvent(firstFingerUpEvent);
        } catch (InjectEventSecurityException e) {
            throw new PerformException.Builder()
                    .withActionDescription("First finger up event")
                    .withViewDescription("Scale gesture detector")
                    .withCause(e)
                    .build();
        }
        return Swiper.Status.SUCCESS;
    }

    private static float[][] interpolate(float[] start, float[] end) {
        float[][] res = new float[PINCH_STEP_COUNT][2];

        for (int i = 0; i < PINCH_STEP_COUNT; i++) {
            res[i][0] = start[0] + (end[0] - start[0]) * i / (PINCH_STEP_COUNT - 1f);
            res[i][1] = start[1] + (end[1] - start[1]) * i / (PINCH_STEP_COUNT - 1f);
        }

        return res;
    }

    /** The number of move events to send for each pinch. */
    private static final int PINCH_STEP_COUNT = 10;

    private final Class<? extends View> mViewClass;
    private final float[] mFirstFingerStartCoords;
    private final float[] mFirstFingerEndCoords;
    private final float[] mSecondFingerStartCoords;
    private final float[] mSecondFingerEndCoords;

    public PinchZoomAction(float[] firstFingerStartCoords,
                           float[] firstFingerEndCoords,
                           float[] secondFingerStartCoords,
                           float[] secondFingerEndCoords,
                           Class<? extends View> viewClass) {
        mFirstFingerStartCoords = firstFingerStartCoords;
        mFirstFingerEndCoords = firstFingerEndCoords;
        mSecondFingerStartCoords = secondFingerStartCoords;
        mSecondFingerEndCoords = secondFingerEndCoords;
        mViewClass = viewClass;
    }

    @Override
    @SuppressWarnings("unchecked")
    public Matcher<View> getConstraints() {
        return allOf(isCompletelyDisplayed(), isAssignableFrom(mViewClass));
    }

    @Override
    public void perform(UiController uiController, View view) {
        checkNotNull(uiController);
        checkNotNull(view);
        Swiper.Status status;
        final float[] precision = {1.0f, 1.0f, 1.0f, 1.0f};

        try {
            status = sendPinchZoomAction(uiController, this.mFirstFingerStartCoords,
                this.mFirstFingerEndCoords, this.mSecondFingerStartCoords,
                this.mSecondFingerEndCoords, precision);
        } catch (RuntimeException re) {
            throw new PerformException.Builder()
                    .withActionDescription(getDescription())
                    .withViewDescription(HumanReadables.describe(view))
                    .withCause(re)
                    .build();
        }
        if (status == Swiper.Status.FAILURE) {
            throw new PerformException.Builder()
                    .withActionDescription(getDescription())
                    .withViewDescription(HumanReadables.describe(view))
                    .withCause(new RuntimeException(getDescription() + " failed"))
                    .build();
        }
    }

    @Override
    public String getDescription() {
        return "Pinch Zoom Action";
    }
}
