/*
 * Copyright (C) 2020 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.commands.uinput;

import android.os.Handler;
import android.os.HandlerThread;
import android.os.Looper;
import android.os.Message;
import android.os.Parcel;
import android.os.SystemClock;
import android.util.Log;
import android.util.SparseArray;

import com.android.internal.os.SomeArgs;

import org.json.JSONException;
import org.json.JSONObject;

import java.io.IOException;
import java.io.OutputStream;

import src.com.android.commands.uinput.InputAbsInfo;

/**
 * Device class defines uinput device interfaces of device operations, for device open, close,
 * configuration, events injection.
 */
public class Device {
    private static final String TAG = "UinputDevice";

    private static final int MSG_OPEN_UINPUT_DEVICE = 1;
    private static final int MSG_CLOSE_UINPUT_DEVICE = 2;
    private static final int MSG_INJECT_EVENT = 3;

    private final int mId;
    private final HandlerThread mThread;
    private final DeviceHandler mHandler;
    // mConfiguration is sparse array of ioctl code and array of values.
    private final SparseArray<int[]> mConfiguration;
    private final SparseArray<InputAbsInfo> mAbsInfo;
    private final OutputStream mOutputStream;
    private final Object mCond = new Object();
    private long mTimeToSend;

    static {
        System.loadLibrary("uinputcommand_jni");
    }

    private static native long nativeOpenUinputDevice(String name, int id, int vid, int pid,
            int bus, int ffEffectsMax, DeviceCallback callback);
    private static native void nativeCloseUinputDevice(long ptr);
    private static native void nativeInjectEvent(long ptr, int type, int code, int value);
    private static native void nativeConfigure(int handle, int code, int[] configs);
    private static native void nativeSetAbsInfo(int handle, int axisCode, Parcel axisParcel);

    public Device(int id, String name, int vid, int pid, int bus,
            SparseArray<int[]> configuration, int ffEffectsMax,
            SparseArray<InputAbsInfo> absInfo) {
        mId = id;
        mThread = new HandlerThread("UinputDeviceHandler");
        mThread.start();
        mHandler = new DeviceHandler(mThread.getLooper());
        mConfiguration = configuration;
        mAbsInfo = absInfo;
        mOutputStream = System.out;
        SomeArgs args = SomeArgs.obtain();
        args.argi1 = id;
        args.argi2 = vid;
        args.argi3 = pid;
        args.argi4 = bus;
        args.argi5 = ffEffectsMax;
        if (name != null) {
            args.arg1 = name;
        } else {
            args.arg1 = id + ":" + vid + ":" + pid;
        }

        mHandler.obtainMessage(MSG_OPEN_UINPUT_DEVICE, args).sendToTarget();
        mTimeToSend = SystemClock.uptimeMillis();
    }

    /**
     * Inject uinput events to device
     *
     * @param events  Array of raw uinput events.
     */
    public void injectEvent(int[] events) {
        // if two messages are sent at identical time, they will be processed in order received
        Message msg = mHandler.obtainMessage(MSG_INJECT_EVENT, events);
        mHandler.sendMessageAtTime(msg, mTimeToSend);
    }

    /**
     * Impose a delay to the device for execution.
     *
     * @param delay  Time to delay in unit of milliseconds.
     */
    public void addDelay(int delay) {
        mTimeToSend = Math.max(SystemClock.uptimeMillis(), mTimeToSend) + delay;
    }

    /**
     * Close an uinput device.
     *
     */
    public void close() {
        Message msg = mHandler.obtainMessage(MSG_CLOSE_UINPUT_DEVICE);
        mHandler.sendMessageAtTime(msg, Math.max(SystemClock.uptimeMillis(), mTimeToSend) + 1);
        try {
            synchronized (mCond) {
                mCond.wait();
            }
        } catch (InterruptedException ignore) {
        }
    }

    private class DeviceHandler extends Handler {
        private long mPtr;
        private int mBarrierToken;

        DeviceHandler(Looper looper) {
            super(looper);
        }

        @Override
        public void handleMessage(Message msg) {
            switch (msg.what) {
                case MSG_OPEN_UINPUT_DEVICE:
                    SomeArgs args = (SomeArgs) msg.obj;
                    mPtr = nativeOpenUinputDevice((String) args.arg1, args.argi1, args.argi2,
                            args.argi3, args.argi4, args.argi5,
                            new DeviceCallback());
                    break;
                case MSG_INJECT_EVENT:
                    if (mPtr != 0) {
                        int[] events = (int[]) msg.obj;
                        for (int pos = 0; pos + 2 < events.length; pos += 3) {
                            nativeInjectEvent(mPtr, events[pos], events[pos + 1], events[pos + 2]);
                        }
                    }
                    break;
                case MSG_CLOSE_UINPUT_DEVICE:
                    if (mPtr != 0) {
                        nativeCloseUinputDevice(mPtr);
                        getLooper().quitSafely();
                        mPtr = 0;
                    } else {
                        Log.e(TAG, "Tried to close already closed device.");
                    }
                    Log.i(TAG, "Device closed.");
                    synchronized (mCond) {
                        mCond.notify();
                    }
                    break;
                default:
                    throw new IllegalArgumentException("Unknown device message");
            }
        }

        public void pauseEvents() {
            mBarrierToken = getLooper().myQueue().postSyncBarrier();
        }

        public void resumeEvents() {
            getLooper().myQueue().removeSyncBarrier(mBarrierToken);
            mBarrierToken = 0;
        }
    }

    private class DeviceCallback {
        public void onDeviceOpen() {
            mHandler.resumeEvents();
        }

        public void onDeviceConfigure(int handle) {
            for (int i = 0; i < mConfiguration.size(); i++) {
                int key = mConfiguration.keyAt(i);
                int[] data = mConfiguration.get(key);
                nativeConfigure(handle, key, data);
            }

            if (mAbsInfo != null) {
                for (int i = 0; i < mAbsInfo.size(); i++) {
                    int key = mAbsInfo.keyAt(i);
                    InputAbsInfo info = mAbsInfo.get(key);
                    Parcel parcel = Parcel.obtain();
                    info.writeToParcel(parcel, 0);
                    parcel.setDataPosition(0);
                    nativeSetAbsInfo(handle, key, parcel);
                }
            }
        }

        public void onDeviceVibrating(int value) {
            JSONObject json = new JSONObject();
            try {
                json.put("reason", "vibrating");
                json.put("id", mId);
                json.put("status", value);
            } catch (JSONException e) {
                throw new RuntimeException("Could not create JSON object ", e);
            }
            try {
                mOutputStream.write(json.toString().getBytes());
                mOutputStream.flush();
            } catch (IOException e) {
                throw new RuntimeException(e);
            }
        }

        public void onDeviceError() {
            Log.e(TAG, "Device error occurred, closing /dev/uinput");
            Message msg = mHandler.obtainMessage(MSG_CLOSE_UINPUT_DEVICE);
            msg.setAsynchronous(true);
            msg.sendToTarget();
        }
    }
}
