/*
 * 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.experimentalcar;

import android.car.experimental.DriverAwarenessEvent;
import android.car.experimental.DriverAwarenessSupplierConfig;
import android.car.experimental.DriverAwarenessSupplierService;
import android.car.experimental.IDriverAwarenessSupplier;
import android.car.experimental.IDriverAwarenessSupplierCallback;
import android.content.Context;
import android.hardware.input.InputManager;
import android.os.Looper;
import android.os.RemoteException;
import android.os.SystemClock;
import android.util.Log;
import android.view.Display;
import android.view.InputChannel;
import android.view.InputEvent;
import android.view.InputEventReceiver;
import android.view.InputMonitor;
import android.view.MotionEvent;

import com.android.internal.annotations.GuardedBy;
import com.android.internal.annotations.VisibleForTesting;

import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;

/**
 * A driver awareness supplier that estimates the driver's current awareness level based on touches
 * on the headunit.
 */
public class TouchDriverAwarenessSupplier extends IDriverAwarenessSupplier.Stub {

    private static final String TAG = "Car.TouchAwarenessSupplier";
    private static final String TOUCH_INPUT_CHANNEL_NAME = "TouchDriverAwarenessInputChannel";

    private static final long MAX_STALENESS = DriverAwarenessSupplierService.NO_STALENESS;

    @VisibleForTesting
    static final float INITIAL_DRIVER_AWARENESS_VALUE = 1.0f;

    private final AtomicInteger mCurrentPermits = new AtomicInteger();
    private final ScheduledExecutorService mRefreshScheduler;
    private final Looper mLooper;
    private final Context mContext;
    private final ITimeSource mTimeSource;
    private final Runnable mRefreshPermitRunnable;
    private final IDriverAwarenessSupplierCallback mDriverAwarenessSupplierCallback;

    private final Object mLock = new Object();

    @GuardedBy("mLock")
    private long mLastEventMillis;

    @GuardedBy("mLock")
    private ScheduledFuture<?> mRefreshScheduleHandle;

    @GuardedBy("mLock")
    private Config mConfig;

    // Main thread only. Hold onto reference to avoid garbage collection
    private InputMonitor mInputMonitor;

    // Main thread only. Hold onto reference to avoid garbage collection
    private InputEventReceiver mInputEventReceiver;

    TouchDriverAwarenessSupplier(Context context,
            IDriverAwarenessSupplierCallback driverAwarenessSupplierCallback, Looper looper) {
        this(context, driverAwarenessSupplierCallback, Executors.newScheduledThreadPool(1),
                looper, new SystemTimeSource());
    }

    @VisibleForTesting
    TouchDriverAwarenessSupplier(
            Context context,
            IDriverAwarenessSupplierCallback driverAwarenessSupplierCallback,
            ScheduledExecutorService refreshScheduler,
            Looper looper,
            ITimeSource timeSource) {
        mContext = context;
        mDriverAwarenessSupplierCallback = driverAwarenessSupplierCallback;
        mRefreshScheduler = refreshScheduler;
        mLooper = looper;
        mTimeSource = timeSource;
        mRefreshPermitRunnable =
                () -> {
                    synchronized (mLock) {
                        handlePermitRefreshLocked(mTimeSource.elapsedRealtime());
                    }
                };
    }


    @Override
    public void onReady() {
        try {
            mDriverAwarenessSupplierCallback.onConfigLoaded(
                    new DriverAwarenessSupplierConfig(MAX_STALENESS));
        } catch (RemoteException e) {
            Log.e(TAG, "Unable to send config - abandoning ready process", e);
            return;
        }
        // send an initial event, as required by the IDriverAwarenessSupplierCallback spec
        try {
            mDriverAwarenessSupplierCallback.onDriverAwarenessUpdated(
                    new DriverAwarenessEvent(mTimeSource.elapsedRealtime(),
                            INITIAL_DRIVER_AWARENESS_VALUE));
        } catch (RemoteException e) {
            Log.e(TAG, "Unable to emit initial awareness event", e);
        }
        synchronized (mLock) {
            mConfig = loadConfig();
            logd("Config loaded: " + mConfig);
            mCurrentPermits.set(mConfig.getMaxPermits());
        }
        startTouchMonitoring();
    }

    @Override
    public void setCallback(IDriverAwarenessSupplierCallback callback) {
        // no-op - the callback is initialized in the constructor
    }

    private Config loadConfig() {
        int maxPermits = mContext.getResources().getInteger(
                R.integer.driverAwarenessTouchModelMaxPermits);
        if (maxPermits <= 0) {
            throw new IllegalArgumentException("driverAwarenessTouchModelMaxPermits must be >0");
        }
        int refreshIntervalMillis = mContext.getResources().getInteger(
                R.integer.driverAwarenessTouchModelPermitRefreshIntervalMs);
        if (refreshIntervalMillis <= 0) {
            throw new IllegalArgumentException(
                    "driverAwarenessTouchModelPermitRefreshIntervalMs must be >0");
        }
        int throttleDurationMillis = mContext.getResources().getInteger(
                R.integer.driverAwarenessTouchModelThrottleMs);
        if (throttleDurationMillis <= 0) {
            throw new IllegalArgumentException("driverAwarenessTouchModelThrottleMs must be >0");
        }
        return new Config(maxPermits, refreshIntervalMillis, throttleDurationMillis);
    }

    /**
     * Starts monitoring touches.
     */
    @VisibleForTesting
    // TODO(b/146802952) handle touch monitoring on multiple displays
    void startTouchMonitoring() {
        InputManager inputManager = (InputManager) mContext.getSystemService(Context.INPUT_SERVICE);
        mInputMonitor = inputManager.monitorGestureInput(
                TOUCH_INPUT_CHANNEL_NAME,
                Display.DEFAULT_DISPLAY);
        mInputEventReceiver = new TouchReceiver(
                mInputMonitor.getInputChannel(),
                mLooper);
    }

    /**
     * Refreshes permits on the interval specified by {@code R.integer
     * .driverAwarenessTouchModelPermitRefreshIntervalMs}.
     */
    @GuardedBy("mLock")
    private void schedulePermitRefreshLocked() {
        logd("Scheduling permit refresh interval (ms): "
                + mConfig.getPermitRefreshIntervalMillis());
        mRefreshScheduleHandle = mRefreshScheduler.scheduleAtFixedRate(
                mRefreshPermitRunnable,
                mConfig.getPermitRefreshIntervalMillis(),
                mConfig.getPermitRefreshIntervalMillis(),
                TimeUnit.MILLISECONDS);
    }

    /**
     * Stops the scheduler for refreshing the number of permits.
     */
    @GuardedBy("mLock")
    private void stopPermitRefreshLocked() {
        logd("Stopping permit refresh");
        if (mRefreshScheduleHandle != null) {
            mRefreshScheduleHandle.cancel(true);
            mRefreshScheduleHandle = null;
        }
    }

    /**
     * Consume a single permit if the event should not be throttled.
     */
    @VisibleForTesting
    @GuardedBy("mLock")
    void consumePermitLocked(long timestamp) {
        long timeSinceLastEvent = timestamp - mLastEventMillis;
        boolean isEventAccepted = timeSinceLastEvent >= mConfig.getThrottleDurationMillis();
        if (!isEventAccepted) {
            logd("Ignoring consumePermit request: event throttled");
            return;
        }
        mLastEventMillis = timestamp;
        int curPermits = mCurrentPermits.updateAndGet(cur -> Math.max(0, cur - 1));
        logd("Permit consumed to: " + curPermits);

        if (mRefreshScheduleHandle == null) {
            schedulePermitRefreshLocked();
        }

        try {
            mDriverAwarenessSupplierCallback.onDriverAwarenessUpdated(
                    new DriverAwarenessEvent(timestamp,
                            (float) curPermits / mConfig.getMaxPermits()));
        } catch (RemoteException e) {
            Log.e(TAG, "Unable to emit awareness event", e);
        }
    }

    @VisibleForTesting
    @GuardedBy("mLock")
    void handlePermitRefreshLocked(long timestamp) {
        int curPermits = mCurrentPermits.updateAndGet(
                cur -> Math.min(cur + 1, mConfig.getMaxPermits()));
        logd("Permit refreshed to: " + curPermits);
        if (curPermits == mConfig.getMaxPermits()) {
            stopPermitRefreshLocked();
        }
        try {
            mDriverAwarenessSupplierCallback.onDriverAwarenessUpdated(
                    new DriverAwarenessEvent(timestamp,
                            (float) curPermits / mConfig.getMaxPermits()));
        } catch (RemoteException e) {
            Log.e(TAG, "Unable to emit awareness event", e);
        }
    }

    private static void logd(String message) {
        if (Log.isLoggable(TAG, Log.DEBUG)) {
            Log.d(TAG, message);
        }
    }

    /**
     * Receiver of all touch events. This receiver filters out all events except {@link
     * MotionEvent#ACTION_UP} events.
     */
    private class TouchReceiver extends InputEventReceiver {

        /**
         * Creates an input event receiver bound to the specified input channel.
         *
         * @param inputChannel The input channel.
         * @param looper       The looper to use when invoking callbacks.
         */
        TouchReceiver(InputChannel inputChannel, Looper looper) {
            super(inputChannel, looper);
        }

        @Override
        public void onInputEvent(InputEvent event) {
            boolean handled = false;
            try {
                if (!(event instanceof MotionEvent)) {
                    return;
                }

                MotionEvent motionEvent = (MotionEvent) event;
                if (motionEvent.getActionMasked() == MotionEvent.ACTION_UP) {
                    logd("ACTION_UP touch received");
                    synchronized (mLock) {
                        consumePermitLocked(SystemClock.elapsedRealtime());
                    }
                    handled = true;
                }
            } finally {
                finishInputEvent(event, handled);
            }
        }
    }

    /**
     * Configuration for a {@link TouchDriverAwarenessSupplier}.
     */
    private static class Config {

        private final int mMaxPermits;
        private final int mPermitRefreshIntervalMillis;
        private final int mThrottleDurationMillis;

        /**
         * Creates an instance of {@link Config}.
         *
         * @param maxPermits                  the maximum number of permits in the user's
         *                                    attention buffer. A user's number of permits will
         *                                    never refresh to a value higher than this.
         * @param permitRefreshIntervalMillis the refresh interval in milliseconds for refreshing
         *                                    permits
         * @param throttleDurationMillis      the duration in milliseconds representing the window
         *                                    that permit consumption is ignored after an event.
         */
        private Config(
                int maxPermits,
                int permitRefreshIntervalMillis,
                int throttleDurationMillis) {
            mMaxPermits = maxPermits;
            mPermitRefreshIntervalMillis = permitRefreshIntervalMillis;
            mThrottleDurationMillis = throttleDurationMillis;
        }

        int getMaxPermits() {
            return mMaxPermits;
        }

        int getPermitRefreshIntervalMillis() {
            return mPermitRefreshIntervalMillis;
        }

        int getThrottleDurationMillis() {
            return mThrottleDurationMillis;
        }

        @Override
        public String toString() {
            return String.format(
                    "Config{mMaxPermits=%s, mPermitRefreshIntervalMillis=%s, "
                            + "mThrottleDurationMillis=%s}",
                    mMaxPermits,
                    mPermitRefreshIntervalMillis,
                    mThrottleDurationMillis);
        }
    }
}
