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

import static com.android.car.internal.ExcludeFromCodeCoverageGeneratedReport.DUMP_INFO;

import android.annotation.NonNull;
import android.car.Car;
import android.car.builtin.os.ServiceManagerHelper;
import android.car.builtin.util.Slogf;
import android.car.occupantawareness.IOccupantAwarenessEventCallback;
import android.car.occupantawareness.OccupantAwarenessDetection;
import android.car.occupantawareness.OccupantAwarenessDetection.VehicleOccupantRole;
import android.car.occupantawareness.SystemStatusEvent;
import android.car.occupantawareness.SystemStatusEvent.DetectionTypeFlags;
import android.content.Context;
import android.hardware.automotive.occupant_awareness.IOccupantAwareness;
import android.hardware.automotive.occupant_awareness.IOccupantAwarenessClientCallback;
import android.os.RemoteCallbackList;
import android.os.RemoteException;
import android.util.Log;
import android.util.proto.ProtoOutputStream;

import com.android.car.internal.ExcludeFromCodeCoverageGeneratedReport;
import com.android.car.internal.util.IndentingPrintWriter;
import com.android.internal.annotations.GuardedBy;
import com.android.internal.annotations.VisibleForTesting;

import java.lang.ref.WeakReference;

/**
 * A service that listens to an Occupant Awareness Detection system across a HAL boundary and
 * exposes the data to system clients in Android via a {@link
 * android.car.occupantawareness.OccupantAwarenessManager}.
 *
 * <p>The service exposes the following detections types:
 *
 * <h1>Presence Detection</h1>
 *
 * Detects whether a person is present for each seat location.
 *
 * <h1>Gaze Detection</h1>
 *
 * Detects where an occupant is looking and for how long they have been looking at the specified
 * target.
 *
 * <h1>Driver Monitoring</h1>
 *
 * Detects whether a driver is looking on or off-road and for how long they have been looking there.
 */
public class OccupantAwarenessService
        extends android.car.occupantawareness.IOccupantAwarenessManager.Stub
        implements CarServiceBase {
    private static final String TAG = CarLog.tagFor(OccupantAwarenessService.class);
    private static final boolean DBG = Slogf.isLoggable(TAG, Log.DEBUG);

    // HAL service identifier name.
    @VisibleForTesting
    static final String OAS_SERVICE_ID =
            "android.hardware.automotive.occupant_awareness.IOccupantAwareness/default";

    private final Object mLock = new Object();
    private final Context mContext;

    @GuardedBy("mLock")
    private IOccupantAwareness mOasHal;

    private final ChangeListenerToHalService mHalListener = new ChangeListenerToHalService(this);

    private static class ChangeCallbackList
            extends RemoteCallbackList<IOccupantAwarenessEventCallback> {
        private final WeakReference<OccupantAwarenessService> mOasService;

        ChangeCallbackList(OccupantAwarenessService oasService) {
            mOasService = new WeakReference<>(oasService);
        }

        /** Handle callback death. */
        @Override
        public void onCallbackDied(IOccupantAwarenessEventCallback listener) {
            Slogf.i(TAG, "binderDied: " + listener.asBinder());

            OccupantAwarenessService service = mOasService.get();
            if (service != null) {
                service.handleClientDisconnected();
            }
        }
    }

    private final ChangeCallbackList mListeners = new ChangeCallbackList(this);

    /** Creates an OccupantAwarenessService instance given a {@link Context}. */
    public OccupantAwarenessService(Context context) {
        mContext = context;
    }

    /** Creates an OccupantAwarenessService instance given a {@link Context}. */
    @VisibleForTesting
    OccupantAwarenessService(Context context, IOccupantAwareness oasInterface) {
        mContext = context;
        mOasHal = oasInterface;
    }

    @Override
    public void init() {
        logd("Initializing service");
        connectToHalServiceIfNotConnected(true);
    }

    @Override
    public void release() {
        logd("Will stop detection and disconnect listeners");
        stopDetectionGraph();
        mListeners.kill();
    }

    @Override
    @ExcludeFromCodeCoverageGeneratedReport(reason = DUMP_INFO)
    public void dump(IndentingPrintWriter writer) {
        writer.println("*OccupantAwarenessService*");
        synchronized (mLock) {
            writer.println(String.format(
                    "%s to HAL service", mOasHal == null ? "NOT connected" : "Connected"));
        }
        writer.println(
                String.format(
                        "%d change listeners subscribed.",
                        mListeners.getRegisteredCallbackCount()));
    }

    @Override
    @ExcludeFromCodeCoverageGeneratedReport(reason = DUMP_INFO)
    public void dumpProto(ProtoOutputStream proto) {}

    /** Attempts to connect to the HAL service if it is not already connected. */
    private void connectToHalServiceIfNotConnected(boolean forceConnect) {
        logd("connectToHalServiceIfNotConnected()");

        synchronized (mLock) {
            // If already connected, nothing more needs to be done.
            if (mOasHal != null && !forceConnect) {
                logd("Client is already connected, nothing more to do");
                return;
            }

            // Attempt to find the HAL service.
            if (mOasHal == null) {
                logd("Attempting to connect to client at: " + OAS_SERVICE_ID);
                mOasHal =
                        android.hardware.automotive.occupant_awareness.IOccupantAwareness.Stub
                                .asInterface(ServiceManagerHelper.getService(OAS_SERVICE_ID));

                if (mOasHal == null) {
                    Slogf.e(TAG, "Failed to find OAS hal_service at: [" + OAS_SERVICE_ID + "]");
                    return;
                }
            }

            // Register for callbacks.
            try {
                mOasHal.setCallback(mHalListener);
            } catch (RemoteException e) {
                mOasHal = null;
                Slogf.e(TAG, "Failed to set callback: " + e);
                return;
            }

            logd("Successfully connected to hal_service at: [" + OAS_SERVICE_ID + "]");
        }
    }

    /** Sends a message via the HAL to start the detection graph. */
    private void startDetectionGraph() {
        logd("Attempting to start detection graph");

        // Grab a copy of 'mOasHal' to avoid sitting on the lock longer than is necessary.
        IOccupantAwareness hal;
        synchronized (mLock) {
            hal = mOasHal;
        }

        if (hal != null) {
            try {
                hal.startDetection();
            } catch (RemoteException e) {
                Slogf.e(TAG, "startDetection() HAL invocation failed: " + e, e);

                synchronized (mLock) {
                    mOasHal = null;
                }
            }
        } else {
            Slogf.e(TAG, "No HAL is connected. Cannot request graph start");
        }
    }

    /** Sends a message via the HAL to stop the detection graph. */
    private void stopDetectionGraph() {
        logd("Attempting to stop detection graph.");

        // Grab a copy of 'mOasHal' to avoid sitting on the lock longer than is necessary.
        IOccupantAwareness hal;
        synchronized (mLock) {
            hal = mOasHal;
        }

        if (hal != null) {
            try {
                hal.stopDetection();
            } catch (RemoteException e) {
                Slogf.e(TAG, "stopDetection() HAL invocation failed: " + e, e);

                synchronized (mLock) {
                    mOasHal = null;
                }
            }
        } else {
            Slogf.e(TAG, "No HAL is connected. Cannot request graph stop");
        }
    }

    /**
     * Gets the vehicle capabilities for a given role.
     *
     * <p>Capabilities are static for a given vehicle configuration and need only be queried once
     * per vehicle. Once capability is determined, clients should query system status to see if the
     * subsystem is currently ready to serve.
     *
     * <p>Requires {@link android.car.Car.PERMISSION_READ_CAR_OCCUPANT_AWARENESS_STATE} read
     * permissions to access.
     *
     * @param role {@link VehicleOccupantRole} to query for.
     * @return Flags indicating supported capabilities for the role.
     */
    public @DetectionTypeFlags int getCapabilityForRole(@VehicleOccupantRole int role) {
        CarServiceUtils.assertPermission(mContext,
                Car.PERMISSION_READ_CAR_OCCUPANT_AWARENESS_STATE);

        connectToHalServiceIfNotConnected(false);

        // Grab a copy of 'mOasHal' to avoid sitting on the lock longer than is necessary.
        IOccupantAwareness hal;
        synchronized (mLock) {
            hal = mOasHal;
        }

        if (hal != null) {
            try {
                return hal.getCapabilityForRole(role);
            } catch (RemoteException e) {

                Slogf.e(TAG, "getCapabilityForRole() HAL invocation failed: " + e, e);

                synchronized (mLock) {
                    mOasHal = null;
                }

                return SystemStatusEvent.DETECTION_TYPE_NONE;
            }
        } else {
            Slogf.e(TAG, "getCapabilityForRole(): No HAL interface has been provided. Cannot get"
                    + " capabilities");
            return SystemStatusEvent.DETECTION_TYPE_NONE;
        }
    }

    /**
     * Registers a {@link IOccupantAwarenessEventCallback} to be notified for changes in the system
     * state.
     *
     * <p>Requires {@link android.car.Car.PERMISSION_READ_CAR_OCCUPANT_AWARENESS_STATE} read
     * permissions to access.
     *
     * @param listener {@link IOccupantAwarenessEventCallback} listener to register.
     */
    @Override
    public void registerEventListener(@NonNull IOccupantAwarenessEventCallback listener) {
        CarServiceUtils.assertPermission(mContext,
                Car.PERMISSION_READ_CAR_OCCUPANT_AWARENESS_STATE);

        connectToHalServiceIfNotConnected(false);

        synchronized (mLock) {
            if (mOasHal == null) {
                Slogf.e(TAG, "Attempting to register a listener, but could not connect to HAL.");
                return;
            }

            logd("Registering a new listener");
            mListeners.register(listener);

            // After the first client connects, request that the detection graph start.
            if (mListeners.getRegisteredCallbackCount() == 1) {
                startDetectionGraph();
            }
        }
    }

    /**
     * Unregister the given {@link IOccupantAwarenessEventCallback} listener from receiving events.
     *
     * <p>Requires {@link android.car.Car.PERMISSION_READ_CAR_OCCUPANT_AWARENESS_STATE} read
     * permissions to access.
     *
     * @param listener {@link IOccupantAwarenessEventCallback} client to unregister.
     */
    @Override
    public void unregisterEventListener(@NonNull IOccupantAwarenessEventCallback listener) {
        CarServiceUtils.assertPermission(mContext,
                Car.PERMISSION_READ_CAR_OCCUPANT_AWARENESS_STATE);

        connectToHalServiceIfNotConnected(false);

        synchronized (mLock) {
            mListeners.unregister(listener);
        }

        // When the last client disconnects, request that the detection graph stop.
        handleClientDisconnected();
    }

    /** Processes a detection event and propagates it to registered clients. */
    @VisibleForTesting
    void processStatusEvent(@NonNull SystemStatusEvent statusEvent) {
        int idx = mListeners.beginBroadcast();
        while (idx-- > 0) {
            IOccupantAwarenessEventCallback listener = mListeners.getBroadcastItem(idx);
            try {
                listener.onStatusChanged(statusEvent);
            } catch (RemoteException e) {
                // It's likely the connection snapped. Let binder death handle the situation.
                Slogf.e(TAG, "onStatusChanged() invocation failed: " + e, e);
            }
        }
        mListeners.finishBroadcast();
    }

    /** Processes a detection event and propagates it to registered clients. */
    @VisibleForTesting
    void processDetectionEvent(@NonNull OccupantAwarenessDetection detection) {
        int idx = mListeners.beginBroadcast();
        while (idx-- > 0) {
            IOccupantAwarenessEventCallback listener = mListeners.getBroadcastItem(idx);
            try {
                listener.onDetectionEvent(detection);
            } catch (RemoteException e) {
                // It's likely the connection snapped. Let binder death handle the situation.
                Slogf.e(TAG, "onDetectionEvent() invocation failed: " + e, e);
            }
        }
        mListeners.finishBroadcast();
    }

    /** Handle client disconnections, possibly stopping the detection graph. */
    void handleClientDisconnected() {
        // If the last client disconnects, requests that the graph stops.
        synchronized (mLock) {
            if (mListeners.getRegisteredCallbackCount() == 0) {
                stopDetectionGraph();
            }
        }
    }

    private static void logd(String msg) {
        if (DBG) {
            Slogf.d(TAG, msg);
        }
    }

    /**
     * Class that implements the listener interface and gets called back from the {@link
     * android.hardware.automotive.occupant_awareness.IOccupantAwarenessClientCallback} across the
     * binder interface.
     */
    private static class ChangeListenerToHalService extends IOccupantAwarenessClientCallback.Stub {
        private final WeakReference<OccupantAwarenessService> mOasService;

        ChangeListenerToHalService(OccupantAwarenessService oasService) {
            mOasService = new WeakReference<>(oasService);
        }

        @Override
        public void onSystemStatusChanged(int inputDetectionFlags, byte inputStatus) {
            OccupantAwarenessService service = mOasService.get();
            if (service != null) {
                service.processStatusEvent(
                        OccupantAwarenessUtils.convertToStatusEvent(
                                inputDetectionFlags, inputStatus));
            }
        }

        @Override
        public void onDetectionEvent(
                android.hardware.automotive.occupant_awareness.OccupantDetections detections) {
            OccupantAwarenessService service = mOasService.get();
            if (service != null) {
                for (android.hardware.automotive.occupant_awareness.OccupantDetection detection :
                        detections.detections) {
                    service.processDetectionEvent(
                            OccupantAwarenessUtils.convertToDetectionEvent(
                                    detections.timeStampMillis, detection));
                }
            }
        }

        @Override
        public int getInterfaceVersion() {
            return this.VERSION;
        }

        @Override
        public String getInterfaceHash() {
            return this.HASH;
        }
    }
}
