/*
 * Copyright (C) 2012 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 android.app.SearchManager;
import android.content.ActivityNotFoundException;
import android.content.Context;
import android.content.Intent;
import android.os.Handler;
import android.os.Message;
import android.os.SystemClock;
import android.os.SystemProperties;
import android.os.UserHandle;
import android.util.Log;

/**
 * This class creates DPAD events from TouchNavigation events.
 * 
 * @see ViewRootImpl
 */

//TODO: Make this class an internal class of ViewRootImpl.java
class SimulatedDpad {

    private static final String TAG = "SimulatedDpad";

    // Maximum difference in milliseconds between the down and up of a touch
    // event for it to be considered a tap
    // TODO:Read this value from a configuration file
    private static final int MAX_TAP_TIME = 250;
    // Where the cutoff is for determining an edge swipe
    private static final float EDGE_SWIPE_THRESHOLD = 0.9f;
    private static final int MSG_FLICK = 313;
    // TODO: Pass touch slop from the input device
    private static final int TOUCH_SLOP = 30;
    // The position of the previous TouchNavigation event
    private float mLastTouchNavigationXPosition;
    private float mLastTouchNavigationYPosition;
    // Where the Touch Navigation was initially pressed
    private float mTouchNavigationEnterXPosition;
    private float mTouchNavigationEnterYPosition;
    // When the most recent ACTION_HOVER_ENTER occurred
    private long mLastTouchNavigationStartTimeMs = 0;
    // When the most recent direction key was sent
    private long mLastTouchNavigationKeySendTimeMs = 0;
    // When the most recent touch event of any type occurred
    private long mLastTouchNavigationEventTimeMs = 0;
    // Did the swipe begin in a valid region
    private boolean mEdgeSwipePossible;

    private final Context mContext;

    // How quickly keys were sent;
    private int mKeySendRateMs = 0;
    private int mLastKeySent;
    // Last movement in device screen pixels
    private float mLastMoveX = 0;
    private float mLastMoveY = 0;
    // Offset from the initial touch. Gets reset as direction keys are sent.
    private float mAccumulatedX;
    private float mAccumulatedY;

    // Change in position allowed during tap events
    private float mTouchSlop;
    private float mTouchSlopSquared;
    // Has the TouchSlop constraint been invalidated
    private boolean mAlwaysInTapRegion = true;

    // Information from the most recent event.
    // Used to determine what device sent the event during a fling.
    private int mLastSource;
    private int mLastMetaState;
    private int mLastDeviceId;

    // TODO: Currently using screen dimensions tuned to a Galaxy Nexus, need to
    // read this from a config file instead
    private int mDistancePerTick;
    private int mDistancePerTickSquared;
    // Highest rate that the flinged events can occur at before dying out
    private int mMaxRepeatDelay;
    // The square of the minimum distance needed for a flick to register
    private int mMinFlickDistanceSquared;
    // How quickly the repeated events die off
    private float mFlickDecay;

    public SimulatedDpad(Context context) {
        mDistancePerTick = SystemProperties.getInt("persist.vr_dist_tick", 64);
        mDistancePerTickSquared = mDistancePerTick * mDistancePerTick;
        mMaxRepeatDelay = SystemProperties.getInt("persist.vr_repeat_delay", 300);
        mMinFlickDistanceSquared = SystemProperties.getInt("persist.vr_min_flick", 20);
        mMinFlickDistanceSquared *= mMinFlickDistanceSquared;
        mFlickDecay = Float.parseFloat(SystemProperties.get(
                "persist.sys.vr_flick_decay", "1.3"));
        mTouchSlop = TOUCH_SLOP;
        mTouchSlopSquared = mTouchSlop * mTouchSlop;

        mContext = context;
    }

    private final Handler mHandler = new Handler(true /*async*/) {
            @Override
        public void handleMessage(Message msg) {
            switch (msg.what) {
                case MSG_FLICK: {
                    final long time = SystemClock.uptimeMillis();
                    ViewRootImpl viewroot = (ViewRootImpl) msg.obj;
                    // Send the key
                    viewroot.enqueueInputEvent(new KeyEvent(time, time,
                            KeyEvent.ACTION_DOWN, msg.arg2, 0, mLastMetaState,
                            mLastDeviceId, 0, KeyEvent.FLAG_FALLBACK, mLastSource));
                    viewroot.enqueueInputEvent(new KeyEvent(time, time,
                            KeyEvent.ACTION_UP, msg.arg2, 0, mLastMetaState,
                            mLastDeviceId, 0, KeyEvent.FLAG_FALLBACK, mLastSource));

                    // Increase the delay by the decay factor and resend
                    final int delay = (int) Math.ceil(mFlickDecay * msg.arg1);
                    if (delay <= mMaxRepeatDelay) {
                        Message msgCopy = Message.obtain(msg);
                        msgCopy.arg1 = delay;
                        msgCopy.setAsynchronous(true);
                        mHandler.sendMessageDelayed(msgCopy, delay);
                    }
                    break;
                }
            }
        }
    };

    public void updateTouchNavigation(ViewRootImpl viewroot, MotionEvent event,
            boolean synthesizeNewKeys) {
        if (!synthesizeNewKeys) {
            mHandler.removeMessages(MSG_FLICK);
        }
        InputDevice device = event.getDevice();
        if (device == null) {
            return;
        }
        // Store what time the TouchNavigation event occurred
        final long time = SystemClock.uptimeMillis();
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                mLastTouchNavigationStartTimeMs = time;
                mAlwaysInTapRegion = true;
                mTouchNavigationEnterXPosition = event.getX();
                mTouchNavigationEnterYPosition = event.getY();
                mAccumulatedX = 0;
                mAccumulatedY = 0;
                mLastMoveX = 0;
                mLastMoveY = 0;
                if (device.getMotionRange(MotionEvent.AXIS_Y).getMax()
                        * EDGE_SWIPE_THRESHOLD < event.getY()) {
                    // Did the swipe begin in a valid region
                    mEdgeSwipePossible = true;
                }
                // Clear any flings
                if (synthesizeNewKeys) {
                    mHandler.removeMessages(MSG_FLICK);
                }
                break;
            case MotionEvent.ACTION_MOVE:
                // Determine whether the move is slop or an intentional move
                float deltaX = event.getX() - mTouchNavigationEnterXPosition;
                float deltaY = event.getY() - mTouchNavigationEnterYPosition;
                if (mTouchSlopSquared < deltaX * deltaX + deltaY * deltaY) {
                    mAlwaysInTapRegion = false;
                }
                // Checks if the swipe has crossed the midpoint
                // and if our swipe gesture is complete
                if (event.getY() < (device.getMotionRange(MotionEvent.AXIS_Y).getMax()
                        * .5) && mEdgeSwipePossible) {
                    mEdgeSwipePossible = false;

                    Intent intent =
                            ((SearchManager)mContext.getSystemService(Context.SEARCH_SERVICE))
                            .getAssistIntent(mContext, false, UserHandle.USER_CURRENT_OR_SELF);
                    if (intent != null) {
                        intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
                        try {
                            mContext.startActivity(intent);
                        } catch (ActivityNotFoundException e){
                            Log.e(TAG, "Could not start search activity");
                        }
                    } else {
                        Log.e(TAG, "Could not find a search activity");
                    }
                }
                // Find the difference in position between the two most recent
                // TouchNavigation events
                mLastMoveX = event.getX() - mLastTouchNavigationXPosition;
                mLastMoveY = event.getY() - mLastTouchNavigationYPosition;
                mAccumulatedX += mLastMoveX;
                mAccumulatedY += mLastMoveY;
                float mAccumulatedXSquared = mAccumulatedX * mAccumulatedX;
                float mAccumulatedYSquared = mAccumulatedY * mAccumulatedY;
                // Determine if we've moved far enough to send a key press
                if (mAccumulatedXSquared > mDistancePerTickSquared ||
                        mAccumulatedYSquared > mDistancePerTickSquared) {
                    float dominantAxis;
                    float sign;
                    boolean isXAxis;
                    int key;
                    int repeatCount = 0;
                    // Determine dominant axis
                    if (mAccumulatedXSquared > mAccumulatedYSquared) {
                        dominantAxis = mAccumulatedX;
                        isXAxis = true;
                    } else {
                        dominantAxis = mAccumulatedY;
                        isXAxis = false;
                    }
                    // Determine sign of axis
                    sign = (dominantAxis > 0) ? 1 : -1;
                    // Determine key to send
                    if (isXAxis) {
                        key = (sign == 1) ? KeyEvent.KEYCODE_DPAD_RIGHT :
                                KeyEvent.KEYCODE_DPAD_LEFT;
                    } else {
                        key = (sign == 1) ? KeyEvent.KEYCODE_DPAD_DOWN : KeyEvent.KEYCODE_DPAD_UP;
                    }
                    // Send key until maximum distance constraint is satisfied
                    while (dominantAxis * dominantAxis > mDistancePerTickSquared) {
                        repeatCount++;
                        dominantAxis -= sign * mDistancePerTick;
                        if (synthesizeNewKeys) {
                            viewroot.enqueueInputEvent(new KeyEvent(time, time,
                                    KeyEvent.ACTION_DOWN, key, 0, event.getMetaState(),
                                    event.getDeviceId(), 0, KeyEvent.FLAG_FALLBACK,
                                    event.getSource()));
                            viewroot.enqueueInputEvent(new KeyEvent(time, time,
                                    KeyEvent.ACTION_UP, key, 0, event.getMetaState(),
                                    event.getDeviceId(), 0, KeyEvent.FLAG_FALLBACK,
                                    event.getSource()));
                        }
                    }
                    // Save new axis values
                    mAccumulatedX = isXAxis ? dominantAxis : 0;
                    mAccumulatedY = isXAxis ? 0 : dominantAxis;

                    mLastKeySent = key;
                    mKeySendRateMs = (int) (time - mLastTouchNavigationKeySendTimeMs) / repeatCount;
                    mLastTouchNavigationKeySendTimeMs = time;
                }
                break;
            case MotionEvent.ACTION_UP:
                if (time - mLastTouchNavigationStartTimeMs < MAX_TAP_TIME && mAlwaysInTapRegion) {
                    if (synthesizeNewKeys) {
                        viewroot.enqueueInputEvent(new KeyEvent(mLastTouchNavigationStartTimeMs,
                                    time, KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_DPAD_CENTER, 0,
                                    event.getMetaState(), event.getDeviceId(), 0,
                                    KeyEvent.FLAG_FALLBACK, event.getSource()));
                        viewroot.enqueueInputEvent(new KeyEvent(mLastTouchNavigationStartTimeMs,
                                    time, KeyEvent.ACTION_UP, KeyEvent.KEYCODE_DPAD_CENTER, 0,
                                    event.getMetaState(), event.getDeviceId(), 0,
                                    KeyEvent.FLAG_FALLBACK, event.getSource()));
                    }
                } else {
                    float xMoveSquared = mLastMoveX * mLastMoveX;
                    float yMoveSquared = mLastMoveY * mLastMoveY;
                    // Determine whether the last gesture was a fling.
                    if (mMinFlickDistanceSquared <= xMoveSquared + yMoveSquared &&
                            time - mLastTouchNavigationEventTimeMs <= MAX_TAP_TIME &&
                            mKeySendRateMs <= mMaxRepeatDelay && mKeySendRateMs > 0) {
                        mLastDeviceId = event.getDeviceId();
                        mLastSource = event.getSource();
                        mLastMetaState = event.getMetaState();

                        if (synthesizeNewKeys) {
                            Message message = Message.obtain(mHandler, MSG_FLICK,
                                    mKeySendRateMs, mLastKeySent, viewroot);
                            message.setAsynchronous(true);
                            mHandler.sendMessageDelayed(message, mKeySendRateMs);
                        }
                    }
                }
                mEdgeSwipePossible = false;
                break;
        }

        // Store touch event position and time
        mLastTouchNavigationEventTimeMs = time;
        mLastTouchNavigationXPosition = event.getX();
        mLastTouchNavigationYPosition = event.getY();
    }
}
