/*
 * Copyright (C) 2013 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.google.android.glass.sample.apidemo.touchpad;

import com.google.android.glass.sample.apidemo.R;

import android.content.Context;
import android.util.AttributeSet;
import android.util.Log;
import android.view.Gravity;
import android.view.InputDevice;
import android.view.MotionEvent;
import android.widget.RelativeLayout;
import android.widget.TextView;

/**
 * Provides a visual representation of the Glass touchpad, with colored and labeled circles that
 * represent the locations of the user's fingers when they are on the touchpad.
 */
public class TouchpadView extends RelativeLayout {

    private static final String TAG = TouchpadView.class.getSimpleName();

    // The size of the finger trace drawn at the location of one of the user's fingers.
    private static final int FINGER_TRACE_SIZE = 50;

    // The drawable ids used to draw the user's three fingers.
    private static final int[] FINGER_RES_IDS = {
        R.drawable.finger_trace_0, R.drawable.finger_trace_1, R.drawable.finger_trace_2 };

    // The duration, in milliseconds, of the animation used to fade out a finger trace when the
    // user's finger is lifted from the touchpad.
    private static final int FADE_OUT_DURATION_MILLIS = 100;

    // The views used to display the location of a finger on the touchpad.
    private final TextView[] mFingerTraceViews = new TextView[FINGER_RES_IDS.length];

    // The horizontal and vertical hardware resolutions of the touchpad. These are used to
    // calculate the aspect ratio of the view when it is measured.
    private float mTouchpadHardwareWidth;
    private float mTouchpadHardwareHeight;

    public TouchpadView(Context context) {
        this(context, null, 0);
    }

    public TouchpadView(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public TouchpadView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);

        setFocusable(true);
        setFocusableInTouchMode(true);
        setClipChildren(false);

        lookupTouchpadHardwareResolution();

        for (int i = 0; i < mFingerTraceViews.length; i++) {
            mFingerTraceViews[i] = createFingerTraceView(i);
            addView(mFingerTraceViews[i]);
        }
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        // Constrains the view's dimensions to have the same aspect ratio as the actual hardware
        // touchpad.
        int newHeight = (int) (MeasureSpec.getSize(widthMeasureSpec) / mTouchpadHardwareWidth *
                mTouchpadHardwareHeight);
        int newHeightMeasureSpec = MeasureSpec.makeMeasureSpec(newHeight, MeasureSpec.EXACTLY);
        super.onMeasure(widthMeasureSpec, newHeightMeasureSpec);
    }

    /**
     * Processes all the pointers that are part of the motion event and displays the finger traces
     * at the proper positions in the view.
     * <p>
     * Since this view is only intended to render motion events and not consume them, we always
     * return false so that the events bubble up to the activity and the gesture detector has a
     * chance to handle them.
     */
    @Override
    public boolean onGenericMotionEvent(MotionEvent event) {
        logVerbose(printToString(event));
        for (int i = 0; i < event.getPointerCount(); i++) {
            int pointerId = event.getPointerId(i);
            float x = event.getX(i);
            float y = event.getY(i);

            switch (event.getActionMasked()) {
                case MotionEvent.ACTION_DOWN:
                case MotionEvent.ACTION_POINTER_DOWN:
                case MotionEvent.ACTION_MOVE:
                    moveFingerTrace(pointerId, x, y);
                    break;
                case MotionEvent.ACTION_UP:
                case MotionEvent.ACTION_POINTER_UP:
                case MotionEvent.ACTION_CANCEL:
                    hideFingerTrace(pointerId);
                    break;
            }
        }

        return false;
    }

    /** Looks up the hardware resolution of the Glass touchpad. */
    private void lookupTouchpadHardwareResolution() {
        int[] deviceIds = InputDevice.getDeviceIds();
        for (int deviceId : deviceIds) {
            InputDevice device = InputDevice.getDevice(deviceId);
            if ((device.getSources() & InputDevice.SOURCE_TOUCHPAD) != 0) {
                logVerbose("Touchpad motion range: x-axis [%d, %d] y-axis [%d, %d]",
                        device.getMotionRange(MotionEvent.AXIS_X).getMin(),
                        device.getMotionRange(MotionEvent.AXIS_X).getMax(),
                        device.getMotionRange(MotionEvent.AXIS_Y).getMin(),
                        device.getMotionRange(MotionEvent.AXIS_Y).getMax());

                mTouchpadHardwareWidth = device.getMotionRange(MotionEvent.AXIS_X).getRange();
                mTouchpadHardwareHeight = device.getMotionRange(MotionEvent.AXIS_Y).getRange();
                // Stop after we've seen the first touchpad device, because there might be multiple
                // devices in this list if the user is currently screencasting with MyGlass. The
                // first one will always be the hardware touchpad.
                break;
            }
        }
    }

    /**
     * Creates a new view that will be used to display the specified pointer id on the touchpad
     * view.
     *
     * @param pointerId the id of the pointer that this finger trace view will represent; used to
     *     determine its color and text
     * @return the {@code TextView} that was created
     */
    private TextView createFingerTraceView(int pointerId) {
        TextView fingerTraceView = new TextView(getContext());
        fingerTraceView.setBackgroundResource(FINGER_RES_IDS[pointerId]);
        fingerTraceView.setText(Integer.toString(pointerId));
        fingerTraceView.setGravity(Gravity.CENTER);
        fingerTraceView.setTextAppearance(getContext(),
                android.R.style.TextAppearance_DeviceDefault_Small);
        fingerTraceView.setAlpha(0);

        RelativeLayout.LayoutParams lp = new RelativeLayout.LayoutParams(
                FINGER_TRACE_SIZE, FINGER_TRACE_SIZE);
        lp.addRule(RelativeLayout.ALIGN_PARENT_TOP);
        lp.addRule(RelativeLayout.ALIGN_PARENT_LEFT);
        // The right and bottom margin here are required so that the view doesn't get "squished"
        // as it touches the right or bottom side of the touchpad view.
        lp.rightMargin = -2 * FINGER_TRACE_SIZE;
        lp.bottomMargin = -2 * FINGER_TRACE_SIZE;
        fingerTraceView.setLayoutParams(lp);

        return fingerTraceView;
    }

    /**
     * Moves the finger trace associated with the specified pointer id to a new location in the
     * view.
     *
     * @param pointerId the pointer id of the finger trace to move
     * @param point the new location of the finger trace
     */
    private void moveFingerTrace(int pointerId, float x, float y) {
        TextView fingerTraceView = mFingerTraceViews[pointerId];

        // Cancel any current animations on the view and bring it back to full opacity.
        fingerTraceView.animate().cancel();
        fingerTraceView.setScaleX(1.0f);
        fingerTraceView.setScaleY(1.0f);
        fingerTraceView.setAlpha(1);

        // Reposition the finger trace by updating the layout margins of its view.
        RelativeLayout.LayoutParams lp =
                (RelativeLayout.LayoutParams) fingerTraceView.getLayoutParams();
        int viewX = (int) (x / mTouchpadHardwareWidth * getWidth());
        int viewY = (int) (y / mTouchpadHardwareHeight * getHeight());
        lp.leftMargin = viewX - FINGER_TRACE_SIZE / 2;
        lp.topMargin = viewY - FINGER_TRACE_SIZE / 2;
        fingerTraceView.setLayoutParams(lp);
    }

    /**
     * Hides the finger trace associated with the specified pointer id. Traces are faded away
     * instead of immediately hidden in order to reduce flickering due to intermittence in the
     * touchpad.
     *
     * @param pointerId the pointer id whose finger trace should be hidden
     */
    private void hideFingerTrace(int pointerId) {
        TextView fingerTraceView = mFingerTraceViews[pointerId];
        fingerTraceView.animate()
                .scaleX(3.0f)
                .scaleY(3.0f)
                .setDuration(FADE_OUT_DURATION_MILLIS).alpha(0);
    }

    /**
     * Helper method to debug the motion events.
     */
    private static String printToString(MotionEvent ev) {
        int historySize = ev.getHistorySize();
        int pointerCount = ev.getPointerCount();
        StringBuilder resultString = new StringBuilder();
        for (int h = 0; h < historySize; h++) {
            resultString.append(String.format("At time %d:", ev.getHistoricalEventTime(h)));
            for (int p = 0; p < pointerCount; p++) {
                resultString.append(String.format("  pointer %d %s: (%f, %f)",
                    ev.getPointerId(p), MotionEvent.actionToString(ev.getActionMasked()),
                    ev.getHistoricalX(p, h), ev.getHistoricalY(p, h)));
            }
        }
        resultString.append(String.format("At time %d:", ev.getEventTime()));
        for (int p = 0; p < pointerCount; p++) {
            resultString.append(String.format("  pointer %d %s: (%f, %f)",
                ev.getPointerId(p), MotionEvent.actionToString(ev.getActionMasked()),
                ev.getX(p), ev.getY(p)));
        }
        return resultString.toString();
    }

    /**
     * Helper method to print to the main logcat stream.
     * <p>
     * In order to turn the debugging on, execute following command.
     * <pre> $ adb shell setprop log.tag.[TAG] VERBOSE </pre>
     */
    private void logVerbose(String format, Object... args) {
        if (Log.isLoggable(TAG, Log.VERBOSE)) {
              Log.v(TAG, String.format(format, args));
        }
    }
}