/*
 * Copyright (C) 2019 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 com.android.server.accessibility.gestures;

import static android.view.MotionEvent.INVALID_POINTER_ID;

import static com.android.server.accessibility.gestures.GestureUtils.getActionIndex;

import android.content.Context;
import android.os.Handler;
import android.view.MotionEvent;
import android.view.ViewConfiguration;

/**
 * This class matches second-finger multi-tap gestures. A second-finger multi-tap gesture is where
 * one finger is held down and a second finger executes the taps. The number of taps for each
 * instance is specified in the constructor.
 */
class SecondFingerMultiTap extends GestureMatcher {
    final int mTargetTaps;
    int mDoubleTapSlop;
    int mTouchSlop;
    int mTapTimeout;
    int mDoubleTapTimeout;
    int mCurrentTaps;
    int mSecondFingerPointerId;
    float mBaseX;
    float mBaseY;

    SecondFingerMultiTap(
            Context context, int taps, int gesture, GestureMatcher.StateChangeListener listener) {
        super(gesture, new Handler(context.getMainLooper()), listener);
        mTargetTaps = taps;
        mDoubleTapSlop = ViewConfiguration.get(context).getScaledDoubleTapSlop();
        mTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop();
        mTapTimeout = ViewConfiguration.getTapTimeout();
        mDoubleTapTimeout = ViewConfiguration.getDoubleTapTimeout();
        clear();
    }

    @Override
    protected void clear() {
        mCurrentTaps = 0;
        mBaseX = Float.NaN;
        mBaseY = Float.NaN;
        mSecondFingerPointerId = INVALID_POINTER_ID;
        super.clear();
    }

    @Override
    protected void onPointerDown(MotionEvent event, MotionEvent rawEvent, int policyFlags) {
        if (event.getPointerCount() > 2) {
            cancelGesture(event, rawEvent, policyFlags);
            return;
        }
        // Second finger has gone down.
        int index = getActionIndex(event);
        mSecondFingerPointerId = event.getPointerId(index);
        cancelAfterTapTimeout(event, rawEvent, policyFlags);
        if (Float.isNaN(mBaseX) && Float.isNaN(mBaseY)) {
            mBaseX = event.getX();
            mBaseY = event.getY();
        }
        if (!isSecondFingerInsideSlop(rawEvent, mDoubleTapSlop)) {
            cancelGesture(event, rawEvent, policyFlags);
        }
        mBaseX = event.getX();
        mBaseY = event.getY();
    }

    @Override
    protected void onPointerUp(MotionEvent event, MotionEvent rawEvent, int policyFlags) {
        if (event.getPointerCount() > 2) {
            cancelGesture(event, rawEvent, policyFlags);
            return;
        }
        cancelAfterDoubleTapTimeout(event, rawEvent, policyFlags);
        if (!isSecondFingerInsideSlop(rawEvent, mTouchSlop)) {
            cancelGesture(event, rawEvent, policyFlags);
        }
        if (getState() == STATE_GESTURE_STARTED || getState() == STATE_CLEAR) {
            mCurrentTaps++;
            if (mCurrentTaps == mTargetTaps) {
                // Done.
                completeGesture(event, rawEvent, policyFlags);
                return;
            }
            // Needs more taps.
            cancelAfterDoubleTapTimeout(event, rawEvent, policyFlags);
        } else {
            // Nonsensical event stream.
            cancelGesture(event, rawEvent, policyFlags);
        }
    }

    @Override
    protected void onMove(MotionEvent event, MotionEvent rawEvent, int policyFlags) {
        switch (event.getPointerCount()) {
            case 1:
                // We don't need to track anything about one-finger movements.
                break;
            case 2:
                if (!isSecondFingerInsideSlop(rawEvent, mTouchSlop)) {
                    cancelGesture(event, rawEvent, policyFlags);
                }
                break;
            default:
                // More than two fingers means we stop tracking.
                cancelGesture(event, rawEvent, policyFlags);
                break;
        }
    }

    @Override
    protected void onUp(MotionEvent event, MotionEvent rawEvent, int policyFlags) {
        // Cancel early when possible, or it will take precedence over two-finger double tap.
        cancelGesture(event, rawEvent, policyFlags);
    }

    @Override
    public String getGestureName() {
        switch (mTargetTaps) {
            case 2:
                return "Second Finger Double Tap";
            case 3:
                return "Second Finger Triple Tap";
            default:
                return "Second Finger " + Integer.toString(mTargetTaps) + " Taps";
        }
    }

    private boolean isSecondFingerInsideSlop(MotionEvent rawEvent, int slop) {
        int pointerIndex = rawEvent.findPointerIndex(mSecondFingerPointerId);
        if (pointerIndex == -1) {
            return false;
        }
        final float deltaX = mBaseX - rawEvent.getX(pointerIndex);
        final float deltaY = mBaseY - rawEvent.getY(pointerIndex);
        if (deltaX == 0 && deltaY == 0) {
            return true;
        }
        final double moveDelta = Math.hypot(deltaX, deltaY);
        return moveDelta <= slop;
    }

    @Override
    public String toString() {
        return super.toString()
                + ", Taps:"
                + mCurrentTaps
                + ", mBaseX: "
                + Float.toString(mBaseX)
                + ", mBaseY: "
                + Float.toString(mBaseY);
    }
}
