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

import static com.android.car.internal.common.CommonConstants.USER_LIFECYCLE_EVENT_TYPE_STARTING;
import static com.android.car.internal.common.CommonConstants.USER_LIFECYCLE_EVENT_TYPE_STOPPED;
import static com.android.car.internal.common.CommonConstants.USER_LIFECYCLE_EVENT_TYPE_STOPPING;
import static com.android.car.internal.common.CommonConstants.USER_LIFECYCLE_EVENT_TYPE_SWITCHING;
import static com.android.car.internal.common.CommonConstants.USER_LIFECYCLE_EVENT_TYPE_UNLOCKED;
import static com.android.car.internal.common.CommonConstants.USER_LIFECYCLE_EVENT_TYPE_UNLOCKING;

import android.annotation.IntDef;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.UserIdInt;
import android.content.pm.UserInfo;
import android.os.RemoteException;
import android.os.Trace;
import android.os.UserHandle;
import android.util.DebugUtils;
import android.util.IndentingPrintWriter;
import android.util.Slog;
import android.util.SparseArray;
import android.util.SparseIntArray;

import com.android.car.internal.ICarSystemServerClient;
import com.android.car.internal.common.CommonConstants.UserLifecycleEventType;
import com.android.internal.annotations.GuardedBy;
import com.android.internal.os.IResultReceiver;
import com.android.internal.util.Preconditions;
import com.android.server.SystemService.TargetUser;
import com.android.server.utils.TimingsTraceAndSlog;

import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.ArrayList;

/**
 * Manages CarService operations requested by CarServiceHelperService.
 *
 * <p>
 * It is used to send and re-send binder calls to CarService when it connects and dies & reconnects.
 * It does not simply queue the operations, because it needs to "replay" some of them on every
 * reconnection.
 */
final class CarServiceProxy {

    /*
     * The logic of re-queue:
     *
     * There are two sparse array - mLastUserLifecycle and mPendingOperations
     *
     * First sparse array - mLastUserLifecycle - is to keep track of the life-cycle events for each
     * user. It would have the last life-cycle event of each running user (typically user 0 and the
     * current user). All life-cycle events seen so far would be replayed on connection and
     * reconnection.
     *
     * Second sparse array - mPendingOperations - would keep all the non-life-cycle events related
     * operations, which are represented by PendintOperation and PendingOperationId.
     * Most operations (like initBootUser and preCreateUsers) just need to be sent only, but some
     * need to be queued (like onUserRemoved).
     */

    // Operation ID for each non life-cycle event calls
    // NOTE: public because of DebugUtils
    public static final int PO_INIT_BOOT_USER = 0;
    public static final int PO_ON_USER_REMOVED = 1;
    public static final int PO_ON_FACTORY_RESET = 2;

    @IntDef(prefix = { "PO_" }, value = {
            PO_INIT_BOOT_USER,
            PO_ON_USER_REMOVED,
            PO_ON_FACTORY_RESET
    })
    @Retention(RetentionPolicy.SOURCE)
    public @interface PendingOperationId{}

    private static final boolean DBG = false;
    private static final String TAG = CarServiceProxy.class.getSimpleName();

    private static final long LIFECYCLE_TIMESTAMP_IGNORE = 0;

    private final Object mLock = new Object();

    @GuardedBy("mLock")
    private boolean mCarServiceCrashed;
    @UserIdInt
    @GuardedBy("mLock")
    private int mLastSwitchedUser = UserHandle.USER_NULL;
    @UserIdInt
    @GuardedBy("mLock")
    private int mPreviousUserOfLastSwitchedUser = UserHandle.USER_NULL;
    // Key: user id, value: life-cycle
    @GuardedBy("mLock")
    private final SparseIntArray mLastUserLifecycle = new SparseIntArray();
    // Key: @PendingOperationId, value: PendingOperation
    @GuardedBy("mLock")
    private final SparseArray<PendingOperation> mPendingOperations = new SparseArray<>();

    @GuardedBy("mLock")
    private ICarSystemServerClient mCarService;
    @GuardedBy("mLock")
    private UserHandle mInitialUser;
    private final CarServiceHelperService mCarServiceHelperService;
    private final UserMetrics mUserMetrics = new UserMetrics();

    CarServiceProxy(CarServiceHelperService carServiceHelperService) {
        mCarServiceHelperService = carServiceHelperService;
    }

    /**
     * Handles new CarService Connection.
     */
    void handleCarServiceConnection(ICarSystemServerClient carService) {
        Slog.i(TAG, "CarService connected.");
        TimingsTraceAndSlog t = newTimingsTraceAndSlog();
        t.traceBegin("handleCarServiceConnection");
        synchronized (mLock) {
            mCarService = carService;
            mCarServiceCrashed = false;
            runQueuedOperationLocked(PO_INIT_BOOT_USER);
            runQueuedOperationLocked(PO_ON_USER_REMOVED);
            runQueuedOperationLocked(PO_ON_FACTORY_RESET);
        }
        sendLifeCycleEvents();
        sendInitialUser();
        t.traceEnd();
    }

    private void sendInitialUser() {
        UserHandle initialUser;
        ICarSystemServerClient carService;
        synchronized (mLock) {
            initialUser = mInitialUser;
            carService = mCarService;
        }
        if (initialUser != null && carService != null) {
            try {
                carService.setInitialUser(initialUser);
            } catch (RemoteException e) {
                Slog.w(TAG, "RemoteException from car service while calling setInitialUser.", e);
            }
        } else {
            Slog.i(TAG, "Didn't send Initial User, User: " + initialUser + " , CarService: "
                    + carService);
        }
    }

    void saveInitialUser(UserHandle user) {
        synchronized (mLock) {
            if (user != null || user.getIdentifier() != UserHandle.USER_NULL) {
                mInitialUser = user;
            }
        }
    }

    @GuardedBy("mLock")
    private void runQueuedOperationLocked(@PendingOperationId int operationId) {
        PendingOperation pendingOperation = mPendingOperations.get(operationId);
        if (pendingOperation != null) {
            runLocked(operationId, pendingOperation.value);
            return;
        }
        if (DBG) {
            Slog.d(TAG, "No queued operation of type " + pendingOperationToString(operationId));
        }
    }

    private void sendLifeCycleEvents() {
        int lastSwitchedUser;
        SparseIntArray lastUserLifecycle;

        synchronized (mLock) {
            lastSwitchedUser = mLastSwitchedUser;
            lastUserLifecycle = mLastUserLifecycle.clone();
        }

        // Send user0 events first
        int user0Lifecycle = lastUserLifecycle.get(UserHandle.USER_SYSTEM);
        boolean user0IsCurrent = lastSwitchedUser == UserHandle.USER_SYSTEM;
        // If user0Lifecycle is 0, then no life-cycle event received yet.
        if (user0Lifecycle != 0) {
            sendAllLifecyleToUser(UserHandle.USER_SYSTEM, user0Lifecycle, user0IsCurrent);
        }
        lastUserLifecycle.delete(UserHandle.USER_SYSTEM);

        // Send current user events next
        if (!user0IsCurrent) {
            int currentUserLifecycle = lastUserLifecycle.get(lastSwitchedUser);
            // If currentUserLifecycle is 0, then no life-cycle event received yet.
            if (currentUserLifecycle != 0) {
                sendAllLifecyleToUser(lastSwitchedUser, currentUserLifecycle,
                        /* isCurrentUser= */ true);
            }
        }

        lastUserLifecycle.delete(lastSwitchedUser);

        // Send all other users' events
        for (int i = 0; i < lastUserLifecycle.size(); i++) {
            int userId = lastUserLifecycle.keyAt(i);
            int lifecycle = lastUserLifecycle.valueAt(i);
            sendAllLifecyleToUser(userId, lifecycle, /* isCurrentUser= */ false);
        }
    }

    private void sendAllLifecyleToUser(@UserIdInt int userId, int lifecycle,
            boolean isCurrentUser) {
        if (DBG) {
            Slog.d(TAG, "sendAllLifecyleToUser, user:" + userId + " lifecycle:" + lifecycle);
        }
        if (lifecycle >= USER_LIFECYCLE_EVENT_TYPE_STARTING) {
            sendUserLifecycleEvent(USER_LIFECYCLE_EVENT_TYPE_STARTING, UserHandle.USER_NULL,
                    userId);
        }

        if (isCurrentUser && userId != UserHandle.USER_SYSTEM) {
            synchronized (mLock) {
                sendUserLifecycleEvent(USER_LIFECYCLE_EVENT_TYPE_SWITCHING,
                        mPreviousUserOfLastSwitchedUser, userId);
            }
        }

        if (lifecycle >= USER_LIFECYCLE_EVENT_TYPE_UNLOCKING) {
            sendUserLifecycleEvent(USER_LIFECYCLE_EVENT_TYPE_UNLOCKING, UserHandle.USER_NULL,
                    userId);
        }

        if (lifecycle >= USER_LIFECYCLE_EVENT_TYPE_UNLOCKED) {
            sendUserLifecycleEvent(USER_LIFECYCLE_EVENT_TYPE_UNLOCKED, UserHandle.USER_NULL,
                    userId);
        }
    }

    /**
     * Initializes boot user.
     */
    void initBootUser() {
        if (DBG) Slog.d(TAG, "initBootUser()");

        saveOrRun(PO_INIT_BOOT_USER);
    }

    // TODO(b/173664653): add unit test
    /**
     * Callback to indifcate the given user was removed.
     */
    void onUserRemoved(@NonNull UserInfo user) {
        if (DBG) Slog.d(TAG, "onUserRemoved(): " + user.toFullString());

        saveOrRun(PO_ON_USER_REMOVED, user);
    }

    // TODO(b/173664653): add unit test
    /**
     * Callback to ask user to confirm if it's ok to factory reset the device.
     */
    void onFactoryReset(@NonNull IResultReceiver callback) {
        if (DBG) Slog.d(TAG, "onFactoryReset(): " + callback);

        saveOrRun(PO_ON_FACTORY_RESET, callback);
    }

    private void saveOrRun(@PendingOperationId int operationId) {
        saveOrRun(operationId, /* value= */ null);
    }

    private void saveOrRun(@PendingOperationId int operationId, @Nullable Object value) {
        synchronized (mLock) {
            if (mCarService == null) {
                if (DBG) {
                    Slog.d(TAG, "CarService null. Operation "
                            + pendingOperationToString(operationId)
                            + (value == null ? "" : "(" + value + ")") + " deferred.");
                }
                savePendingOperationLocked(operationId, value);
                return;
            }
            if (operationId == PO_ON_FACTORY_RESET) {
                // Must always persist it, so it's sent again if CarService is crashed before
                // the next reboot or suspension-to-ram
                savePendingOperationLocked(operationId, value);
            }
            runLocked(operationId, value);
        }
    }

    @GuardedBy("mLock")
    private void runLocked(@PendingOperationId int operationId, @Nullable Object value) {
        if (DBG) Slog.d(TAG, "runLocked(): " + pendingOperationToString(operationId) + "/" + value);
        try {
            if (isServiceCrashedLoggedLocked(operationId)) {
                return;
            }
            sendCarServiceActionLocked(operationId, value);
            if (operationId == PO_ON_FACTORY_RESET) {
                if (DBG) Slog.d(TAG, "NOT removing " + pendingOperationToString(operationId));
                return;
            }
            if (DBG) Slog.d(TAG, "removing " + pendingOperationToString(operationId));
            mPendingOperations.delete(operationId);
        } catch (RemoteException e) {
            Slog.w(TAG, "RemoteException from car service", e);
            handleCarServiceCrash();
        }
    }

    @GuardedBy("mLock")
    private void savePendingOperationLocked(@PendingOperationId int operationId,
            @Nullable Object value) {
        PendingOperation pendingOperation = mPendingOperations.get(operationId);

        if (pendingOperation == null) {
            pendingOperation = new PendingOperation(operationId, value);
            if (DBG) Slog.d(TAG, "Created " + pendingOperation);
            mPendingOperations.put(operationId, pendingOperation);
            return;
        }
        switch (operationId) {
            case PO_ON_USER_REMOVED:
                Preconditions.checkArgument((value instanceof UserInfo),
                        "invalid value passed to ON_USER_REMOVED", value);
                if (pendingOperation.value instanceof ArrayList) {
                    if (DBG) Slog.d(TAG, "Adding " + value + " to existing " + pendingOperation);
                    ((ArrayList) pendingOperation.value).add(value);
                } else if (pendingOperation.value instanceof UserInfo) {
                    ArrayList<Object> list = new ArrayList<>(2);
                    list.add(pendingOperation.value);
                    list.add(value);
                    if (DBG) Slog.d(TAG, "Converting " + pendingOperation.value + " to " + list);
                    pendingOperation.value = list;
                } else {
                    throw new IllegalStateException("Invalid value for ON_USER_REMOVED: " + value);
                }
                break;
            case PO_ON_FACTORY_RESET:
                PendingOperation newOperation = new PendingOperation(operationId, value);
                if (DBG) Slog.d(TAG, "Replacing " + pendingOperation + " by " + newOperation);
                mPendingOperations.put(operationId, newOperation);
                break;
            default:
                if (DBG) {
                    Slog.d(TAG, "Already saved operation of type "
                            + pendingOperationToString(operationId));
                }
        }
    }

    @GuardedBy("mLock")
    private void sendCarServiceActionLocked(@PendingOperationId int operationId,
            @Nullable Object value) throws RemoteException {
        if (DBG) {
            Slog.d(TAG, "sendCarServiceActionLocked: Operation "
                    + pendingOperationToString(operationId));
        }
        switch (operationId) {
            case PO_INIT_BOOT_USER:
                mCarService.initBootUser();
                break;
            case PO_ON_USER_REMOVED:
                if (value instanceof ArrayList) {
                    ArrayList<Object> list = (ArrayList<Object>) value;
                    if (DBG) Slog.d(TAG, "Sending " + list.size() + " onUserRemoved() calls");
                    for (Object user: list) {
                        onUserRemovedLocked(user);
                    }
                } else {
                    onUserRemovedLocked(value);
                }
                break;
            case PO_ON_FACTORY_RESET:
                mCarService.onFactoryReset((IResultReceiver) value);
                break;
            default:
                Slog.wtf(TAG, "Invalid Operation. OperationId -" + operationId);
        }
    }

    @GuardedBy("mLock")
    private void onUserRemovedLocked(@NonNull Object value) throws RemoteException {
        Preconditions.checkArgument((value instanceof UserInfo),
                "Invalid value for ON_USER_REMOVED: %s", value);
        UserInfo user = (UserInfo) value;
        if (DBG) Slog.d(TAG, "Sending onUserRemoved(): " + user.toFullString());
        mCarService.onUserRemoved(user);
    }

    /**
     * Sends user life-cycle events to CarService.
     */
    void sendUserLifecycleEvent(@UserLifecycleEventType int eventType, @Nullable TargetUser from,
            @NonNull TargetUser to) {
        long now = System.currentTimeMillis();
        int fromId = from == null ? UserHandle.USER_NULL : from.getUserIdentifier();
        int toId = to.getUserIdentifier();
        mUserMetrics.onEvent(eventType, now, fromId, toId);

        synchronized (mLock) {
            if (eventType == USER_LIFECYCLE_EVENT_TYPE_SWITCHING) {
                mLastSwitchedUser = to.getUserIdentifier();
                mPreviousUserOfLastSwitchedUser = from.getUserIdentifier();
                mLastUserLifecycle.put(to.getUserIdentifier(), eventType);
            } else if (eventType == USER_LIFECYCLE_EVENT_TYPE_STOPPING
                    || eventType == USER_LIFECYCLE_EVENT_TYPE_STOPPED) {
                mLastUserLifecycle.delete(to.getUserIdentifier());
            } else {
                mLastUserLifecycle.put(to.getUserIdentifier(), eventType);
            }
            if (mCarService == null) {
                if (DBG) {
                    Slog.d(TAG, "CarService null. sendUserLifecycleEvent() deferred for lifecycle"
                            + " event " + eventType + " for user " + to);
                }
                return;
            }
        }
        sendUserLifecycleEvent(eventType, fromId, toId);
    }

    private void sendUserLifecycleEvent(@UserLifecycleEventType int eventType,
            @UserIdInt int fromId, @UserIdInt int toId) {
        if (DBG) {
            Slog.d(TAG, "sendUserLifecycleEvent():" + " eventType=" + eventType + ", fromId="
                    + fromId + ", toId=" + toId);
        }
        try {
            synchronized (mLock) {
                if (isServiceCrashedLoggedLocked("sendUserLifecycleEvent")) return;
                mCarService.onUserLifecycleEvent(eventType, fromId, toId);
            }
        } catch (RemoteException e) {
            Slog.w(TAG, "RemoteException from car service", e);
            handleCarServiceCrash();
        }
    }

    private void handleCarServiceCrash() {
        synchronized (mLock) {
            mCarServiceCrashed = true;
            mCarService = null;
        }
        Slog.w(TAG, "CarServiceCrashed. No more car service calls before reconnection.");
        mCarServiceHelperService.handleCarServiceCrash();
    }

    private TimingsTraceAndSlog newTimingsTraceAndSlog() {
        return new TimingsTraceAndSlog(TAG, Trace.TRACE_TAG_SYSTEM_SERVER);
    }

    @GuardedBy("mLock")
    private boolean isServiceCrashedLoggedLocked(@PendingOperationId int operationId) {
        return isServiceCrashedLoggedLocked(pendingOperationToString(operationId));
    }

    @GuardedBy("mLock")
    private boolean isServiceCrashedLoggedLocked(@NonNull String operation) {
        if (mCarServiceCrashed) {
            Slog.w(TAG, "CarServiceCrashed. " + operation + " will be executed after reconnection");
            return true;
        }
        return false;
    }

    /**
     * Dump
     */
    void dump(IndentingPrintWriter writer) {
        writer.println("CarServiceProxy");
        writer.increaseIndent();
        writer.printf("mLastSwitchedUser=%s\n", mLastSwitchedUser);
        writer.printf("mInitialUser=%s\n", mInitialUser);
        writer.printf("mLastUserLifecycle:\n");
        int user0Lifecycle = mLastUserLifecycle.get(UserHandle.USER_SYSTEM, 0);
        if (user0Lifecycle != 0) {
            writer.printf("SystemUser Lifecycle Event:%s\n", user0Lifecycle);
        } else {
            writer.println("SystemUser not initialized");
        }

        int lastUserLifecycle = mLastUserLifecycle.get(mLastSwitchedUser, 0);
        if (mLastSwitchedUser != UserHandle.USER_SYSTEM && user0Lifecycle != 0) {
            writer.printf("last user (%s) Lifecycle Event:%s\n",
                    mLastSwitchedUser, lastUserLifecycle);
        }

        int size = mPendingOperations.size();
        if (size == 0) {
            writer.println("No pending operations");
        } else {
            writer.printf("%d pending operation%s:\n", size, size == 1 ? "" : "s");
            writer.increaseIndent();
            for (int i = 0; i < size; i++) {
                writer.println(mPendingOperations.valueAt(i));
            }
            writer.decreaseIndent();
        }
        writer.decreaseIndent();
        dumpUserMetrics(writer);
    }

    /**
     * Dump User metrics
     */
    void dumpUserMetrics(IndentingPrintWriter writer) {
        mUserMetrics.dump(writer);
    }

    private final class PendingOperation {
        public final int id;
        public @Nullable Object value;

        PendingOperation(int id, @Nullable Object value) {
            this.id = id;
            this.value = value;
        }

        @Override
        public String toString() {
            return "PendingOperation[" + pendingOperationToString(id)
                + (value == null ? "" : ": " + value) + "]";
        }
    }

    @NonNull
    private String pendingOperationToString(@PendingOperationId int operationType) {
        return DebugUtils.constantToString(CarServiceProxy.class, "PO_" , operationType);
    }
}
