/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

package org.mozilla.gecko.tests;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.Map;
import java.util.StringTokenizer;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import android.app.Instrumentation;
import android.os.SystemClock;
import android.util.Log;
import android.view.MotionEvent;

class MotionEventReplayer {
    private static final String LOGTAG = "RobocopMotionEventReplayer";

    // the inner dimensions of the window on which the motion event capture was taken from
    private static final int CAPTURE_WINDOW_WIDTH = 720;
    private static final int CAPTURE_WINDOW_HEIGHT = 1038;

    private final Instrumentation mInstrumentation;
    private final int mSurfaceOffsetX;
    private final int mSurfaceOffsetY;
    private final int mSurfaceWidth;
    private final int mSurfaceHeight;
    private final Map<String, Integer> mActionTypes;
    private Method mObtainNanoMethod;

    public MotionEventReplayer(Instrumentation inst, int surfaceOffsetX, int surfaceOffsetY, int surfaceWidth, int surfaceHeight) {
        mInstrumentation = inst;
        mSurfaceOffsetX = surfaceOffsetX;
        mSurfaceOffsetY = surfaceOffsetY;
        mSurfaceWidth = surfaceWidth;
        mSurfaceHeight = surfaceHeight;
        Log.i(LOGTAG, "Initialized using offset (" + mSurfaceOffsetX + "," + mSurfaceOffsetY + ")");

        mActionTypes = new HashMap<String, Integer>();
        mActionTypes.put("ACTION_CANCEL", MotionEvent.ACTION_CANCEL);
        mActionTypes.put("ACTION_DOWN", MotionEvent.ACTION_DOWN);
        mActionTypes.put("ACTION_MOVE", MotionEvent.ACTION_MOVE);
        mActionTypes.put("ACTION_POINTER_DOWN", MotionEvent.ACTION_POINTER_DOWN);
        mActionTypes.put("ACTION_POINTER_UP", MotionEvent.ACTION_POINTER_UP);
        mActionTypes.put("ACTION_UP", MotionEvent.ACTION_UP);
    }

    private int parseAction(String action) {
        int index = 0;

        // ACTION_POINTER_DOWN and ACTION_POINTER_UP might be followed by
        // pointer index in parentheses, like ACTION_POINTER_UP(1)
        int beginParen = action.indexOf("(");
        if (beginParen >= 0) {
            int endParen = action.indexOf(")", beginParen + 1);
            index = Integer.parseInt(action.substring(beginParen + 1, endParen));
            action = action.substring(0, beginParen);
        }

        return mActionTypes.get(action) | (index << MotionEvent.ACTION_POINTER_INDEX_SHIFT);
    }

    private int parseInt(String value) {
        if (value == null) {
            return 0;
        }
        if (value.startsWith("0x")) {
            return Integer.parseInt(value.substring(2), 16);
        }
        return Integer.parseInt(value);
    }

    private float scaleX(float value) {
        return value * mSurfaceWidth / CAPTURE_WINDOW_WIDTH;
    }

    private float scaleY(float value) {
        return value * mSurfaceHeight / CAPTURE_WINDOW_HEIGHT;
    }

    public void replayEvents(InputStream eventDescriptions)
            throws IOException, IllegalAccessException, InvocationTargetException, NoSuchMethodException
    {
        // As an example, a line in the input stream might look like:
        //
        // MotionEvent { action=ACTION_DOWN, id[0]=0, x[0]=424.41055, y[0]=825.2412,
        //      toolType[0]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0,
        //      edgeFlags=0x0, pointerCount=1, historySize=0, eventTime=21972329,
        //      downTime=21972329, deviceId=6, source=0x1002 }
        //
        // These can be generated by printing out event.toString() in LayerView's
        // onTouchEvent function on a phone running Ice Cream Sandwich. Different
        // Android versions have different serializations of the motion event, and this
        // code could probably be modified to parse other serializations if needed.
        Pattern p = Pattern.compile("MotionEvent \\{ (.*?) \\}");
        Map<String, String> eventProperties = new HashMap<String, String>();

        boolean firstEvent = true;
        long timeDelta = 0L;
        long lastEventTime = 0L;

        BufferedReader br = new BufferedReader(new InputStreamReader(eventDescriptions));
        try {
            for (String eventStr = br.readLine(); eventStr != null; eventStr = br.readLine()) {
                Matcher m = p.matcher(eventStr);
                if (! m.find()) {
                    // this line doesn't have any MotionEvent data, skip it
                    continue;
                }

                // extract the key-value pairs from the description and store them
                // in the eventProperties table
                StringTokenizer keyValues = new StringTokenizer(m.group(1), ",");
                while (keyValues.hasMoreTokens()) {
                    String keyValue = keyValues.nextToken();
                    String key = keyValue.substring(0, keyValue.indexOf('=')).trim();
                    String value = keyValue.substring(keyValue.indexOf('=') + 1).trim();
                    eventProperties.put(key, value);
                }

                // set up the values we need to build the MotionEvent
                long downTime = Long.parseLong(eventProperties.get("downTime"));
                long eventTime = Long.parseLong(eventProperties.get("eventTime"));
                int action = parseAction(eventProperties.get("action"));
                float pressure = 1.0f;
                float size = 1.0f;
                int metaState = parseInt(eventProperties.get("metaState"));
                float xPrecision = 1.0f;
                float yPrecision = 1.0f;
                int deviceId = 0;
                int edgeFlags = parseInt(eventProperties.get("edgeFlags"));
                int source = parseInt(eventProperties.get("source"));
                int flags = parseInt(eventProperties.get("flags"));

                int pointerCount = parseInt(eventProperties.get("pointerCount"));
                int[] pointerIds = new int[pointerCount];
                Object pointerData;
                MotionEvent.PointerCoords[] pointerCoords = new MotionEvent.PointerCoords[pointerCount];
                for (int i = 0; i < pointerCount; i++) {
                    pointerIds[i] = Integer.parseInt(eventProperties.get("id[" + i + "]"));
                    pointerCoords[i] = new MotionEvent.PointerCoords();
                    pointerCoords[i].x = mSurfaceOffsetX + scaleX(Float.parseFloat(eventProperties.get("x[" + i + "]")));
                    pointerCoords[i].y = mSurfaceOffsetY + scaleY(Float.parseFloat(eventProperties.get("y[" + i + "]")));
                }
                pointerData = pointerCoords;

                // we want to adjust the timestamps on all the generated events so that they line up with
                // the time that this function is executing on-device.
                long now = SystemClock.uptimeMillis();
                if (firstEvent) {
                    timeDelta = now - eventTime;
                    firstEvent = false;
                }
                downTime += timeDelta;
                eventTime += timeDelta;

                // we also generate the events in "real-time" (i.e. have delays between events that
                // correspond to the delays in the event timestamps).
                if (now < eventTime) {
                    try {
                        Thread.sleep(eventTime - now);
                    } catch (InterruptedException ie) {
                    }
                }

                // and finally we dispatch the event
                MotionEvent event;
                event = MotionEvent.obtain(downTime, eventTime, action, pointerCount,
                        pointerIds, (MotionEvent.PointerCoords[])pointerData, metaState,
                        xPrecision, yPrecision, deviceId, edgeFlags, source, flags);
                try {
                    Log.v(LOGTAG, "Injecting " + event.toString());
                    mInstrumentation.sendPointerSync(event);
                } finally {
                    event.recycle();
                    event = null;
                }

                eventProperties.clear();
            }
        } finally {
            br.close();
        }
    }
}
