/*
 * Copyright (C) 2021 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.cluster;

import static android.car.builtin.app.ActivityManagerHelper.createActivityOptions;
import static android.content.Intent.ACTION_MAIN;

import static com.android.car.PermissionHelper.checkHasDumpPermissionGranted;
import static com.android.car.hal.ClusterHalService.DISPLAY_OFF;
import static com.android.car.hal.ClusterHalService.DISPLAY_ON;
import static com.android.car.hal.ClusterHalService.DONT_CARE;
import static com.android.car.internal.ExcludeFromCodeCoverageGeneratedReport.DUMP_INFO;

import android.app.ActivityOptions;
import android.car.Car;
import android.car.CarOccupantZoneManager;
import android.car.ICarOccupantZoneCallback;
import android.car.builtin.os.UserManagerHelper;
import android.car.builtin.util.Slogf;
import android.car.cluster.ClusterHomeManager;
import android.car.cluster.ClusterState;
import android.car.cluster.IClusterHomeService;
import android.car.cluster.IClusterNavigationStateListener;
import android.car.cluster.IClusterStateListener;
import android.car.cluster.navigation.NavigationState.NavigationStateProto;
import android.car.navigation.CarNavigationInstrumentCluster;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.graphics.Insets;
import android.graphics.Point;
import android.graphics.Rect;
import android.hardware.display.DisplayManager;
import android.os.Bundle;
import android.os.RemoteCallbackList;
import android.os.RemoteException;
import android.text.TextUtils;
import android.util.proto.ProtoOutputStream;
import android.view.Display;
import android.view.SurfaceControl;

import com.android.car.CarLog;
import com.android.car.CarOccupantZoneService;
import com.android.car.CarServiceBase;
import com.android.car.R;
import com.android.car.am.FixedActivityService;
import com.android.car.hal.ClusterHalService;
import com.android.car.internal.ExcludeFromCodeCoverageGeneratedReport;
import com.android.car.internal.util.IndentingPrintWriter;

/**
 * Service responsible for interactions between ClusterOS and ClusterHome.
 */
public final class ClusterHomeService extends IClusterHomeService.Stub
        implements CarServiceBase, ClusterNavigationService.ClusterNavigationServiceCallback,
        ClusterHalService.ClusterHalEventCallback {
    private static final String TAG = CarLog.TAG_CLUSTER;
    private static final int DEFAULT_MIN_UPDATE_INTERVAL_MILLIS = 1000;
    private static final String NAV_STATE_PROTO_BUNDLE_KEY = "navstate2";

    private final Context mContext;
    private final ClusterHalService mClusterHalService;
    private final ClusterNavigationService mClusterNavigationService;
    private final CarOccupantZoneService mOccupantZoneService;
    private final FixedActivityService mFixedActivityService;
    private final ComponentName mClusterHomeActivity;
    private final ClusterHealthMonitor mClusterHealthMonitor;

    private boolean mServiceEnabled;

    private int mClusterDisplayId = Display.INVALID_DISPLAY;

    private int mOnOff = DISPLAY_OFF;
    private Rect mBounds = new Rect();
    private Insets mInsets = Insets.NONE;
    private int mUiType = ClusterHomeManager.UI_TYPE_CLUSTER_HOME;
    private Intent mLastIntent;
    private int mLastIntentUserId = UserManagerHelper.USER_SYSTEM;

    private final RemoteCallbackList<IClusterStateListener> mClientListeners =
            new RemoteCallbackList<>();

    private final RemoteCallbackList<IClusterNavigationStateListener> mClientNavigationListeners =
            new RemoteCallbackList<>();

    public ClusterHomeService(Context context, ClusterHalService clusterHalService,
            ClusterNavigationService navigationService,
            CarOccupantZoneService occupantZoneService,
            FixedActivityService fixedActivityService) {
        mContext = context;
        mClusterHalService = clusterHalService;
        mClusterNavigationService = navigationService;
        mOccupantZoneService = occupantZoneService;
        mFixedActivityService = fixedActivityService;
        mClusterHomeActivity = ComponentName.unflattenFromString(
                mContext.getString(R.string.config_clusterHomeActivity));
        mClusterHealthMonitor = new ClusterHealthMonitor(mContext, mClusterHalService);
        mLastIntent = new Intent(ACTION_MAIN).setComponent(mClusterHomeActivity);
    }

    @Override
    public void init() {
        Slogf.d(TAG, "initClusterHomeService");
        if (TextUtils.isEmpty(mClusterHomeActivity.getPackageName())
                || TextUtils.isEmpty(mClusterHomeActivity.getClassName())) {
            Slogf.i(TAG, "Improper ClusterHomeActivity: %s", mClusterHomeActivity);
            return;
        }
        if (!mClusterHalService.isServiceEnabled()) {
            Slogf.e(TAG, "ClusterHomeService is disabled. To enable, it must be either in LIGHT "
                    + "mode, or all core properties must be defined in FULL mode.");
            return;
        }
        // In FULL mode mOnOff is set to 'OFF', and can be changed by the CLUSTER_DISPLAY_STATE
        // property. In LIGHT mode, we set it to 'ON' because the CLUSTER_DISPLAY_STATE property may
        // not be available, and we do not subscribe to it.
        if (mClusterHalService.isLightMode()) {
            mOnOff = DISPLAY_ON;
        }

        mServiceEnabled = true;
        mClusterHalService.setCallback(this);
        mClusterNavigationService.setClusterServiceCallback(this);

        mOccupantZoneService.registerCallback(mOccupantZoneCallback);

        initClusterDisplay();
    }

    private void initClusterDisplay() {
        int clusterDisplayId = mOccupantZoneService.getDisplayIdForDriver(
                CarOccupantZoneManager.DISPLAY_TYPE_INSTRUMENT_CLUSTER);
        Slogf.d(TAG, "initClusterDisplay: displayId=%d", clusterDisplayId);
        if (clusterDisplayId == Display.INVALID_DISPLAY) {
            Slogf.i(TAG, "No cluster display is defined");
        }
        if (clusterDisplayId == mClusterDisplayId) {
            return;  // Skip if the cluster display isn't changed.
        }
        mClusterDisplayId = clusterDisplayId;
        sendDisplayState(ClusterHomeManager.CONFIG_DISPLAY_ID);
        if (clusterDisplayId == Display.INVALID_DISPLAY) {
            return;
        }

        // Initialize mBounds only once.
        if (mBounds.right == 0 && mBounds.bottom == 0 && mBounds.left == 0 && mBounds.top == 0) {
            DisplayManager displayManager = mContext.getSystemService(DisplayManager.class);
            Display clusterDisplay = displayManager.getDisplay(clusterDisplayId);
            Point size = new Point();
            clusterDisplay.getRealSize(size);
            mBounds.right = size.x;
            mBounds.bottom = size.y;
            Slogf.d(TAG, "Found cluster displayId=%d, bounds=%s", clusterDisplayId, mBounds);
        }

        ActivityOptions activityOptions = ActivityOptions.makeBasic()
                .setLaunchDisplayId(clusterDisplayId);
        mFixedActivityService.startFixedActivityModeForDisplayAndUser(
                mLastIntent, activityOptions, clusterDisplayId, mLastIntentUserId);
    }

    private final ICarOccupantZoneCallback mOccupantZoneCallback =
            new ICarOccupantZoneCallback.Stub() {
                @Override
                public void onOccupantZoneConfigChanged(int flags) throws RemoteException {
                    if ((flags & CarOccupantZoneManager.ZONE_CONFIG_CHANGE_FLAG_DISPLAY) != 0
                            || (flags & CarOccupantZoneManager.ZONE_CONFIG_CHANGE_FLAG_USER) != 0) {
                        initClusterDisplay();
                    }
                }
            };

    @Override
    public void release() {
        Slogf.d(TAG, "releaseClusterHomeService");
        mOccupantZoneService.unregisterCallback(mOccupantZoneCallback);
        mClusterHalService.setCallback(null);
        mClusterNavigationService.setClusterServiceCallback(null);
        mClientListeners.kill();
        mClientNavigationListeners.kill();
    }

    @Override
    @ExcludeFromCodeCoverageGeneratedReport(reason = DUMP_INFO)
    public void dump(IndentingPrintWriter writer) {
        checkHasDumpPermissionGranted(mContext, "dump()");
        writer.println("*ClusterHomeService*");

        writer.increaseIndent();
        writer.printf("mServiceEnabled: %b\n", mServiceEnabled);
        writer.printf("isLightMode: %b\n", mClusterHalService.isLightMode());
        writer.printf("mClusterDisplayId: %d\n", mClusterDisplayId);
        writer.printf("mClusterHomeActivity: %s\n", mClusterHomeActivity);
        writer.printf("mOnOff: %d\n", mOnOff);
        writer.printf("mBounds: %s\n", mBounds);
        writer.printf("mInsets: %s\n", mInsets);
        writer.printf("mUiType: %d\n", mUiType);
        writer.printf("mLastIntent: %s\n", mLastIntent);
        writer.printf("mLastIntentUserId: %d\n", mLastIntentUserId);
        mClusterHealthMonitor.dump(writer);
        writer.decreaseIndent();
    }

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

    // ClusterHalEventListener starts
    @Override
    public void onSwitchUi(int uiType) {
        Slogf.d(TAG, "onSwitchUi: uiType=%d", uiType);
        int changes = 0;
        if (mUiType != uiType) {
            mUiType = uiType;
            changes |= ClusterHomeManager.CONFIG_UI_TYPE;
        }
        sendDisplayState(changes);
    }

    @Override
    public void onDisplayState(int onOff, Rect bounds, Insets insets) {
        Slogf.d(TAG, "onDisplayState: onOff=%d, bounds=%s, insets=%s", onOff, bounds, insets);
        int changes = 0;
        if (onOff != DONT_CARE && mOnOff != onOff) {
            mOnOff = onOff;
            changes |= ClusterHomeManager.CONFIG_DISPLAY_ON_OFF;
        }
        if (bounds != null && !mBounds.equals(bounds)) {
            mBounds = bounds;
            changes |= ClusterHomeManager.CONFIG_DISPLAY_BOUNDS;
        }
        if (insets != null && !mInsets.equals(insets)) {
            mInsets = insets;
            changes |= ClusterHomeManager.CONFIG_DISPLAY_INSETS;
        }
        sendDisplayState(changes);
    }
    // ClusterHalEventListener ends

    private void sendDisplayState(int changes) {
        ClusterState state = createClusterState();
        int n = mClientListeners.beginBroadcast();
        for (int i = 0; i < n; i++) {
            IClusterStateListener callback = mClientListeners.getBroadcastItem(i);
            try {
                callback.onClusterStateChanged(state, changes);
            } catch (RemoteException ignores) {
                // ignore
            }
        }
        mClientListeners.finishBroadcast();
    }

    // ClusterNavigationServiceCallback starts
    @Override
    public void onNavigationStateChanged(Bundle bundle) {
        byte[] protoBytes = bundle.getByteArray(NAV_STATE_PROTO_BUNDLE_KEY);

        sendNavigationState(protoBytes);
    }

    private void sendNavigationState(byte[] protoBytes) {
        final int n = mClientNavigationListeners.beginBroadcast();
        for (int i = 0; i < n; i++) {
            IClusterNavigationStateListener callback =
                    mClientNavigationListeners.getBroadcastItem(i);
            try {
                callback.onNavigationStateChanged(protoBytes);
            } catch (RemoteException ignores) {
                // ignore
            }
        }
        mClientNavigationListeners.finishBroadcast();

        if (!mClusterHalService.isNavigationStateSupported()) {
            Slogf.d(TAG, "No Cluster NavigationState HAL property");
            return;
        }
        mClusterHalService.sendNavigationState(protoBytes);
    }

    @Override
    public CarNavigationInstrumentCluster getInstrumentClusterInfo() {
        return CarNavigationInstrumentCluster.createCluster(DEFAULT_MIN_UPDATE_INTERVAL_MILLIS);
    }

    @Override
    public void notifyNavContextOwnerChanged(ClusterNavigationService.ContextOwner owner) {
        Slogf.d(TAG, "notifyNavContextOwnerChanged: owner=%s", owner);
        // Sends the empty NavigationStateProto to clear out the last direction
        // when the app context owner is changed or the navigation is finished.
        NavigationStateProto emptyProto = NavigationStateProto.newBuilder()
                .setServiceStatus(NavigationStateProto.ServiceStatus.NORMAL).build();
        sendNavigationState(emptyProto.toByteArray());
    }
    // ClusterNavigationServiceCallback ends

    // IClusterHomeService starts
    @Override
    public void reportState(int uiTypeMain, int uiTypeSub, byte[] uiAvailability) {
        Slogf.d(TAG, "reportState: main=%d, sub=%d", uiTypeMain, uiTypeSub);
        enforcePermission(Car.PERMISSION_CAR_INSTRUMENT_CLUSTER_CONTROL);
        if (!mServiceEnabled) throw new IllegalStateException("Service is not enabled");

        mUiType = uiTypeMain;
        mClusterHalService.reportState(mOnOff, mBounds, mInsets,
                uiTypeMain, uiTypeSub, uiAvailability);
    }

    @Override
    public void requestDisplay(int uiType) {
        Slogf.d(TAG, "requestDisplay: uiType=%d", uiType);
        enforcePermission(Car.PERMISSION_CAR_INSTRUMENT_CLUSTER_CONTROL);
        if (!mServiceEnabled) throw new IllegalStateException("Service is not enabled");

        mClusterHalService.requestDisplay(uiType);
    }

    @Override
    public boolean startFixedActivityModeAsUser(Intent intent,
            Bundle activityOptionsBundle, int userId) {
        Slogf.d(TAG, "startFixedActivityModeAsUser: intent=%s, userId=%d", intent, userId);
        enforcePermission(Car.PERMISSION_CAR_INSTRUMENT_CLUSTER_CONTROL);
        if (!mServiceEnabled) throw new IllegalStateException("Service is not enabled");
        if (mClusterDisplayId == Display.INVALID_DISPLAY) {
            Slogf.e(TAG, "Cluster display is not ready.");
            return false;
        }

        ActivityOptions activityOptions = activityOptionsBundle != null
                ? createActivityOptions(activityOptionsBundle)
                : ActivityOptions.makeBasic();
        activityOptions.setLaunchDisplayId(mClusterDisplayId);
        mLastIntent = intent;
        mLastIntentUserId = userId;
        return mFixedActivityService.startFixedActivityModeForDisplayAndUser(
                intent, activityOptions, mClusterDisplayId, userId);
    }

    @Override
    public void stopFixedActivityMode() {
        enforcePermission(Car.PERMISSION_CAR_INSTRUMENT_CLUSTER_CONTROL);
        if (!mServiceEnabled) throw new IllegalStateException("Service is not enabled");
        if (mClusterDisplayId == Display.INVALID_DISPLAY) {
            Slogf.e(TAG, "Cluster display is not ready.");
            return;
        }

        mFixedActivityService.stopFixedActivityMode(mClusterDisplayId);
    }

    @Override
    public void registerClusterStateListener(IClusterStateListener listener) {
        enforcePermission(Car.PERMISSION_CAR_INSTRUMENT_CLUSTER_CONTROL);
        if (!mServiceEnabled) throw new IllegalStateException("Service is not enabled");

        mClientListeners.register(listener);
    }

    @Override
    public void unregisterClusterStateListener(IClusterStateListener listener) {
        enforcePermission(Car.PERMISSION_CAR_INSTRUMENT_CLUSTER_CONTROL);
        if (!mServiceEnabled) throw new IllegalStateException("Service is not enabled");

        mClientListeners.unregister(listener);
    }

    @Override
    public void registerClusterNavigationStateListener(IClusterNavigationStateListener listener) {
        enforcePermission(Car.PERMISSION_CAR_MONITOR_CLUSTER_NAVIGATION_STATE);
        if (!mServiceEnabled) throw new IllegalStateException("Service is not enabled");

        mClientNavigationListeners.register(listener);
    }

    @Override
    public void unregisterClusterNavigationStateListener(IClusterNavigationStateListener listener) {
        enforcePermission(Car.PERMISSION_CAR_MONITOR_CLUSTER_NAVIGATION_STATE);
        if (!mServiceEnabled) throw new IllegalStateException("Service is not enabled");

        mClientNavigationListeners.unregister(listener);
    }

    @Override
    public ClusterState getClusterState() {
        Slogf.d(TAG, "getClusterState");
        enforcePermission(Car.PERMISSION_CAR_INSTRUMENT_CLUSTER_CONTROL);
        if (!mServiceEnabled) throw new IllegalStateException("Service is not enabled");
        return createClusterState();
    }

    @Override
    public void sendHeartbeat(long epochTimeNs, byte[] appMetadata) {
        enforcePermission(Car.PERMISSION_CAR_INSTRUMENT_CLUSTER_CONTROL);
        mClusterHealthMonitor.sendHeartbeat(epochTimeNs, appMetadata);
    }

    @Override
    public void startVisibilityMonitoring(SurfaceControl surface) {
        enforcePermission(Car.PERMISSION_CAR_INSTRUMENT_CLUSTER_CONTROL);
        mClusterHealthMonitor.startVisibilityMonitoring(surface);
    }

    @Override
    public void stopVisibilityMonitoring() {
        enforcePermission(Car.PERMISSION_CAR_INSTRUMENT_CLUSTER_CONTROL);
        mClusterHealthMonitor.stopVisibilityMonitoring();
    }
    // IClusterHomeService ends

    private void enforcePermission(String permissionName) {
        if (mContext.checkCallingOrSelfPermission(permissionName)
                != PackageManager.PERMISSION_GRANTED) {
            throw new SecurityException("requires permission " + permissionName);
        }
    }

    private ClusterState createClusterState() {
        ClusterState state = new ClusterState();
        state.on = mOnOff == DISPLAY_ON;
        state.bounds = mBounds;
        state.insets = mInsets;
        state.uiType = mUiType;
        state.displayId = mClusterDisplayId;
        return state;
    }
}