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

import static androidx.core.util.Preconditions.checkNotNull;

import static com.android.documentsui.DevicePolicyResources.Drawables.Style.SOLID_COLORED;
import static com.android.documentsui.DevicePolicyResources.Drawables.WORK_PROFILE_ICON;
import static com.android.documentsui.DevicePolicyResources.Strings.PERSONAL_TAB;
import static com.android.documentsui.DevicePolicyResources.Strings.WORK_TAB;
import static com.android.documentsui.base.SharedMinimal.DEBUG;

import android.Manifest;
import android.annotation.SuppressLint;
import android.app.ActivityManager;
import android.app.admin.DevicePolicyManager;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.pm.PackageManager;
import android.content.pm.UserProperties;
import android.graphics.drawable.Drawable;
import android.os.Build;
import android.os.UserHandle;
import android.os.UserManager;
import android.util.Log;

import androidx.annotation.GuardedBy;
import androidx.annotation.RequiresApi;
import androidx.annotation.VisibleForTesting;

import com.android.documentsui.base.Features;
import com.android.documentsui.base.UserId;
import com.android.documentsui.util.CrossProfileUtils;
import com.android.documentsui.util.VersionUtils;
import com.android.modules.utils.build.SdkLevel;

import com.google.common.base.Objects;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

@RequiresApi(Build.VERSION_CODES.S)
public interface UserManagerState {

    /**
     * Returns the {@link UserId} of each profile which should be queried for documents. This will
     * always
     * include {@link UserId#CURRENT_USER}.
     */
    List<UserId> getUserIds();

    /**
     * Returns mapping between the {@link UserId} and the label for the profile
     */
    Map<UserId, String> getUserIdToLabelMap();

    /**
     * Returns mapping between the {@link UserId} and the drawable badge for the profile
     *
     * returns {@code null} for non-profile userId
     */
    Map<UserId, Drawable> getUserIdToBadgeMap();

    /**
     * Returns a map of {@link UserId} to boolean value indicating whether
     * the {@link UserId}.CURRENT_USER can forward {@link Intent} to that {@link UserId}
     */
    Map<UserId, Boolean> getCanForwardToProfileIdMap(Intent intent);

    /**
     * Updates the state of the list of userIds and all the associated maps according the intent
     * received in broadcast
     *
     * @param userId {@link UserId} for the profile for which the availability status changed
     * @param action {@link Intent}.ACTION_PROFILE_UNAVAILABLE and {@link
     *     Intent}.ACTION_PROFILE_AVAILABLE, {@link Intent}.ACTION_PROFILE_ADDED} and {@link
     *     Intent}.ACTION_PROFILE_REMOVED}
     */
    void onProfileActionStatusChange(String action, UserId userId);

    /**
     * Sets the intent that triggered the launch of the DocsUI
     */
    void setCurrentStateIntent(Intent intent);

    /** Returns true if there are hidden profiles */
    boolean areHiddenInQuietModeProfilesPresent();

    /**
     * Creates an implementation of {@link UserManagerState}.
     */
    // TODO: b/314746383 Make this class a singleton
    static UserManagerState create(Context context) {
        return new RuntimeUserManagerState(context);
    }

    /**
     * Implementation of {@link UserManagerState}
     */
    final class RuntimeUserManagerState implements UserManagerState {

        private static final String TAG = "UserManagerState";
        private final Context mContext;
        private final UserId mCurrentUser;
        private final boolean mIsDeviceSupported;
        private final UserManager mUserManager;
        private final ConfigStore mConfigStore;
        /**
         * List of all the {@link UserId} that have the {@link UserProperties.ShowInSharingSurfaces}
         * set as `SHOW_IN_SHARING_SURFACES_SEPARATE` OR it is a system/personal user
         */
        @GuardedBy("mUserIds")
        private final List<UserId> mUserIds = new ArrayList<>();
        /**
         * Mapping between the {@link UserId} to the corresponding profile label
         */
        @GuardedBy("mUserIdToLabelMap")
        private final Map<UserId, String> mUserIdToLabelMap = new HashMap<>();
        /**
         * Mapping between the {@link UserId} to the corresponding profile badge
         */
        @GuardedBy("mUserIdToBadgeMap")
        private final Map<UserId, Drawable> mUserIdToBadgeMap = new HashMap<>();
        /**
         * Map containing {@link UserId}, other than that of the current user, as key and boolean
         * denoting whether it is accessible by the current user or not as value
         */
        @GuardedBy("mCanFrowardToProfileIdMap")
        private final Map<UserId, Boolean> mCanFrowardToProfileIdMap = new HashMap<>();

        private Intent mCurrentStateIntent;

        private final BroadcastReceiver mIntentReceiver = new BroadcastReceiver() {
            @Override
            public void onReceive(Context context, Intent intent) {
                synchronized (mUserIds) {
                    mUserIds.clear();
                }
                synchronized (mUserIdToLabelMap) {
                    mUserIdToLabelMap.clear();
                }
                synchronized (mUserIdToBadgeMap) {
                    mUserIdToBadgeMap.clear();
                }
                synchronized (mCanFrowardToProfileIdMap) {
                    mCanFrowardToProfileIdMap.clear();
                }
            }
        };


        private RuntimeUserManagerState(Context context) {
            this(context, UserId.CURRENT_USER,
                    Features.CROSS_PROFILE_TABS && isDeviceSupported(context),
                    DocumentsApplication.getConfigStore());
        }

        @VisibleForTesting
        RuntimeUserManagerState(Context context, UserId currentUser, boolean isDeviceSupported,
                ConfigStore configStore) {
            mContext = context.getApplicationContext();
            mCurrentUser = checkNotNull(currentUser);
            mIsDeviceSupported = isDeviceSupported;
            mUserManager = mContext.getSystemService(UserManager.class);
            mConfigStore = configStore;

            IntentFilter filter = new IntentFilter();
            filter.addAction(Intent.ACTION_MANAGED_PROFILE_ADDED);
            filter.addAction(Intent.ACTION_MANAGED_PROFILE_REMOVED);
            if (SdkLevel.isAtLeastV() && mConfigStore.isPrivateSpaceInDocsUIEnabled()) {
                filter.addAction(Intent.ACTION_PROFILE_ADDED);
                filter.addAction(Intent.ACTION_PROFILE_REMOVED);
            }
            mContext.registerReceiver(mIntentReceiver, filter);
        }

        @Override
        public List<UserId> getUserIds() {
            synchronized (mUserIds) {
                if (mUserIds.isEmpty()) {
                    mUserIds.addAll(getUserIdsInternal());
                }
                return mUserIds;
            }
        }

        @Override
        public Map<UserId, String> getUserIdToLabelMap() {
            synchronized (mUserIdToLabelMap) {
                if (mUserIdToLabelMap.isEmpty()) {
                    getUserIdToLabelMapInternal();
                }
                return mUserIdToLabelMap;
            }
        }

        @Override
        public Map<UserId, Drawable> getUserIdToBadgeMap() {
            synchronized (mUserIdToBadgeMap) {
                if (mUserIdToBadgeMap.isEmpty()) {
                    getUserIdToBadgeMapInternal();
                }
                return mUserIdToBadgeMap;
            }
        }

        @Override
        public Map<UserId, Boolean> getCanForwardToProfileIdMap(Intent intent) {
            synchronized (mCanFrowardToProfileIdMap) {
                if (mCanFrowardToProfileIdMap.isEmpty()) {
                    getCanForwardToProfileIdMapInternal(intent);
                }
                return mCanFrowardToProfileIdMap;
            }
        }

        @Override
        @SuppressLint("NewApi")
        public void onProfileActionStatusChange(String action, UserId userId) {
            if (!SdkLevel.isAtLeastV()) return;
            UserProperties userProperties = mUserManager.getUserProperties(
                    UserHandle.of(userId.getIdentifier()));
            if (userProperties.getShowInQuietMode() != UserProperties.SHOW_IN_QUIET_MODE_HIDDEN) {
                return;
            }
            if (Intent.ACTION_PROFILE_UNAVAILABLE.equals(action)
                    || Intent.ACTION_PROFILE_REMOVED.equals(action)) {
                synchronized (mUserIds) {
                    mUserIds.remove(userId);
                }
            } else if (Intent.ACTION_PROFILE_AVAILABLE.equals(action)
                    || Intent.ACTION_PROFILE_ADDED.equals(action)) {
                synchronized (mUserIds) {
                    if (!mUserIds.contains(userId)) {
                        mUserIds.add(userId);
                    }
                }
                synchronized (mUserIdToLabelMap) {
                    if (!mUserIdToLabelMap.containsKey(userId)) {
                        mUserIdToLabelMap.put(userId, getProfileLabel(userId));
                    }
                }
                synchronized (mUserIdToBadgeMap) {
                    if (!mUserIdToBadgeMap.containsKey(userId)) {
                        mUserIdToBadgeMap.put(userId, getProfileBadge(userId));
                    }
                }
                synchronized (mCanFrowardToProfileIdMap) {
                    if (!mCanFrowardToProfileIdMap.containsKey(userId)) {
                        if (userId.getIdentifier() == ActivityManager.getCurrentUser()
                                || isCrossProfileContentSharingStrategyDelegatedFromParent(
                                UserHandle.of(userId.getIdentifier()))
                                || CrossProfileUtils.getCrossProfileResolveInfo(mCurrentUser,
                                mContext.getPackageManager(), mCurrentStateIntent, mContext,
                                mConfigStore.isPrivateSpaceInDocsUIEnabled()) != null) {
                            mCanFrowardToProfileIdMap.put(userId, true);
                        } else {
                            mCanFrowardToProfileIdMap.put(userId, false);
                        }

                    }
                }
            } else {
                Log.e(TAG, "Unexpected action received: " + action);
            }
        }

        @Override
        public void setCurrentStateIntent(Intent intent) {
            mCurrentStateIntent = intent;
        }

        @Override
        public boolean areHiddenInQuietModeProfilesPresent() {
            if (!SdkLevel.isAtLeastV()) {
                return false;
            }

            for (UserId userId : getUserIds()) {
                if (mUserManager
                                .getUserProperties(UserHandle.of(userId.getIdentifier()))
                                .getShowInQuietMode()
                        == UserProperties.SHOW_IN_QUIET_MODE_HIDDEN) {
                    return true;
                }
            }
            return false;
        }

        private List<UserId> getUserIdsInternal() {
            final List<UserId> result = new ArrayList<>();

            if (!mIsDeviceSupported) {
                result.add(mCurrentUser);
                return result;
            }

            if (mUserManager == null) {
                Log.e(TAG, "cannot obtain user manager");
                result.add(mCurrentUser);
                return result;
            }

            final List<UserHandle> userProfiles = mUserManager.getUserProfiles();
            if (userProfiles.size() < 2) {
                result.add(mCurrentUser);
                return result;
            }

            if (SdkLevel.isAtLeastV()) {
                getUserIdsInternalPostV(userProfiles, result);
            } else {
                getUserIdsInternalPreV(userProfiles, result);
            }
            return result;
        }

        @SuppressLint("NewApi")
        private void getUserIdsInternalPostV(List<UserHandle> userProfiles, List<UserId> result) {
            for (UserHandle userHandle : userProfiles) {
                if (userHandle.getIdentifier() == ActivityManager.getCurrentUser()) {
                    result.add(UserId.of(userHandle));
                } else {
                    // Out of all the profiles returned by user manager the profiles that are
                    // returned should satisfy both the following conditions:
                    // 1. It has user property SHOW_IN_SHARING_SURFACES_SEPARATE
                    // 2. Quite mode is not enabled, if it is enabled then the profile's user
                    //    property is not SHOW_IN_QUIET_MODE_HIDDEN
                    if (isProfileAllowed(userHandle)) {
                        result.add(UserId.of(userHandle));
                    }
                }
            }
            if (result.isEmpty()) {
                result.add(mCurrentUser);
            }
        }

        @SuppressLint("NewApi")
        private boolean isProfileAllowed(UserHandle userHandle) {
            final UserProperties userProperties =
                    mUserManager.getUserProperties(userHandle);
            if (userProperties.getShowInSharingSurfaces()
                    == UserProperties.SHOW_IN_SHARING_SURFACES_SEPARATE) {
                return !UserId.of(userHandle).isQuietModeEnabled(mContext)
                        || userProperties.getShowInQuietMode()
                        != UserProperties.SHOW_IN_QUIET_MODE_HIDDEN;
            }
            return false;
        }

        private void getUserIdsInternalPreV(List<UserHandle> userProfiles, List<UserId> result) {
            result.add(mCurrentUser);
            UserId systemUser = null;
            UserId managedUser = null;
            for (UserHandle userHandle : userProfiles) {
                if (userHandle.isSystem()) {
                    systemUser = UserId.of(userHandle);
                } else if (mUserManager.isManagedProfile(userHandle.getIdentifier())) {
                    managedUser = UserId.of(userHandle);
                }
            }
            if (mCurrentUser.isSystem() && managedUser != null) {
                result.add(managedUser);
            } else if (mCurrentUser.isManagedProfile(mUserManager) && systemUser != null) {
                result.add(0, systemUser);
            } else {
                if (DEBUG) {
                    Log.w(TAG, "The current user " + UserId.CURRENT_USER
                            + " is neither system nor managed user. has system user: "
                            + (systemUser != null));
                }
            }
        }

        private void getUserIdToLabelMapInternal() {
            if (SdkLevel.isAtLeastV()) {
                getUserIdToLabelMapInternalPostV();
            } else {
                getUserIdToLabelMapInternalPreV();
            }
        }

        @SuppressLint("NewApi")
        private void getUserIdToLabelMapInternalPostV() {
            if (mUserManager == null) {
                Log.e(TAG, "cannot obtain user manager");
                return;
            }
            List<UserId> userIds = getUserIds();
            for (UserId userId : userIds) {
                synchronized (mUserIdToLabelMap) {
                    mUserIdToLabelMap.put(userId, getProfileLabel(userId));
                }
            }
        }

        private void getUserIdToLabelMapInternalPreV() {
            if (mUserManager == null) {
                Log.e(TAG, "cannot obtain user manager");
                return;
            }
            List<UserId> userIds = getUserIds();
            for (UserId userId : userIds) {
                if (mUserManager.isManagedProfile(userId.getIdentifier())) {
                    synchronized (mUserIdToLabelMap) {
                        mUserIdToLabelMap.put(userId,
                                getEnterpriseString(WORK_TAB, R.string.work_tab));
                    }
                } else {
                    synchronized (mUserIdToLabelMap) {
                        mUserIdToLabelMap.put(userId,
                                getEnterpriseString(PERSONAL_TAB, R.string.personal_tab));
                    }
                }
            }
        }

        @SuppressLint("NewApi")
        private String getProfileLabel(UserId userId) {
            if (userId.getIdentifier() == ActivityManager.getCurrentUser()) {
                return getEnterpriseString(PERSONAL_TAB, R.string.personal_tab);
            }
            try {
                Context userContext = mContext.createContextAsUser(
                        UserHandle.of(userId.getIdentifier()), 0 /* flags */);
                UserManager userManagerAsUser = userContext.getSystemService(UserManager.class);
                if (userManagerAsUser == null) {
                    Log.e(TAG, "cannot obtain user manager");
                    return null;
                }
                return userManagerAsUser.getProfileLabel();
            } catch (Exception e) {
                Log.e(TAG, "Exception occurred while trying to get profile label:\n" + e);
                return null;
            }
        }

        private String getEnterpriseString(String updatableStringId, int defaultStringId) {
            if (SdkLevel.isAtLeastT()) {
                return getUpdatableEnterpriseString(updatableStringId, defaultStringId);
            } else {
                return mContext.getString(defaultStringId);
            }
        }

        @RequiresApi(Build.VERSION_CODES.TIRAMISU)
        private String getUpdatableEnterpriseString(String updatableStringId, int defaultStringId) {
            DevicePolicyManager dpm = mContext.getSystemService(DevicePolicyManager.class);
            if (Objects.equal(dpm, null)) {
                Log.e(TAG, "can not get device policy manager");
                return mContext.getString(defaultStringId);
            }
            return dpm.getResources().getString(
                    updatableStringId,
                    () -> mContext.getString(defaultStringId));
        }

        private void getUserIdToBadgeMapInternal() {
            if (SdkLevel.isAtLeastV()) {
                getUserIdToBadgeMapInternalPostV();
            } else {
                getUserIdToBadgeMapInternalPreV();
            }
        }

        @SuppressLint("NewApi")
        private void getUserIdToBadgeMapInternalPostV() {
            if (mUserManager == null) {
                Log.e(TAG, "cannot obtain user manager");
                return;
            }
            List<UserId> userIds = getUserIds();
            for (UserId userId : userIds) {
                synchronized (mUserIdToBadgeMap) {
                    mUserIdToBadgeMap.put(userId, getProfileBadge(userId));
                }
            }
        }

        private void getUserIdToBadgeMapInternalPreV() {
            if (!SdkLevel.isAtLeastR()) return;
            if (mUserManager == null) {
                Log.e(TAG, "cannot obtain user manager");
                return;
            }
            List<UserId> userIds = getUserIds();
            for (UserId userId : userIds) {
                if (mUserManager.isManagedProfile(userId.getIdentifier())) {
                    synchronized (mUserIdToBadgeMap) {
                        mUserIdToBadgeMap.put(userId,
                                SdkLevel.isAtLeastT() ? getWorkProfileBadge()
                                        : mContext.getDrawable(R.drawable.ic_briefcase));
                    }
                }
            }
        }

        @SuppressLint("NewApi")
        private Drawable getProfileBadge(UserId userId) {
            if (userId.getIdentifier() == ActivityManager.getCurrentUser()) {
                return null;
            }
            try {
                Context userContext = mContext.createContextAsUser(
                        UserHandle.of(userId.getIdentifier()), 0 /* flags */);
                UserManager userManagerAsUser = userContext.getSystemService(UserManager.class);
                if (userManagerAsUser == null) {
                    Log.e(TAG, "cannot obtain user manager");
                    return null;
                }
                return userManagerAsUser.getUserBadge();
            } catch (Exception e) {
                Log.e(TAG, "Exception occurred while trying to get profile badge:\n" + e);
                return null;
            }
        }

        @RequiresApi(Build.VERSION_CODES.TIRAMISU)
        private Drawable getWorkProfileBadge() {
            DevicePolicyManager dpm = mContext.getSystemService(DevicePolicyManager.class);
            Drawable drawable = dpm.getResources().getDrawable(WORK_PROFILE_ICON, SOLID_COLORED,
                    () ->
                            mContext.getDrawable(R.drawable.ic_briefcase));
            return drawable;
        }

        private void getCanForwardToProfileIdMapInternal(Intent intent) {
            // Versions less than V will not have the user properties required to determine whether
            // cross profile check is delegated from parent or not
            if (!SdkLevel.isAtLeastV()) {
                getCanForwardToProfileIdMapPreV(intent);
                return;
            }
            if (mUserManager == null) {
                Log.e(TAG, "can not get user manager");
                return;
            }

            List<UserId> parentOrDelegatedFromParent = new ArrayList<>();
            List<UserId> canForwardToProfileIds = new ArrayList<>();
            List<UserId> noDelegation = new ArrayList<>();

            List<UserId> userIds = getUserIds();
            for (UserId userId : userIds) {
                final UserHandle userHandle = UserHandle.of(userId.getIdentifier());
                // Parent (personal) profile and all the child profiles that delegate cross profile
                // content sharing check to parent can share among each other
                if (userId.getIdentifier() == ActivityManager.getCurrentUser()
                        || isCrossProfileContentSharingStrategyDelegatedFromParent(userHandle)) {
                    parentOrDelegatedFromParent.add(userId);
                } else {
                    noDelegation.add(userId);
                }
            }

            if (noDelegation.size() > 1) {
                Log.e(TAG, "There cannot be more than one profile delegating cross profile "
                        + "content sharing check from self.");
            }

            /*
             * Cross profile resolve info need to be checked in the following 2 cases:
             * 1. current user is either parent or delegates check to parent and the target user
             *    does not delegate to parent
             * 2. current user does not delegate check to the parent and the target user is the
             *    parent profile
             */
            UserId needToCheck = null;
            if (parentOrDelegatedFromParent.contains(mCurrentUser)
                    && !noDelegation.isEmpty()) {
                needToCheck = noDelegation.get(0);
            } else if (mCurrentUser.getIdentifier() != ActivityManager.getCurrentUser()) {
                final UserHandle parentProfile = mUserManager.getProfileParent(
                        UserHandle.of(mCurrentUser.getIdentifier()));
                needToCheck = UserId.of(parentProfile);
            }

            if (needToCheck != null && CrossProfileUtils.getCrossProfileResolveInfo(mCurrentUser,
                    mContext.getPackageManager(), intent, mContext,
                    mConfigStore.isPrivateSpaceInDocsUIEnabled()) != null) {
                if (parentOrDelegatedFromParent.contains(needToCheck)) {
                    canForwardToProfileIds.addAll(parentOrDelegatedFromParent);
                } else {
                    canForwardToProfileIds.add(needToCheck);
                }
            }

            if (parentOrDelegatedFromParent.contains(mCurrentUser)) {
                canForwardToProfileIds.addAll(parentOrDelegatedFromParent);
            }

            for (UserId userId : userIds) {
                synchronized (mCanFrowardToProfileIdMap) {
                    if (userId.equals(mCurrentUser)) {
                        mCanFrowardToProfileIdMap.put(userId, true);
                        continue;
                    }
                    mCanFrowardToProfileIdMap.put(userId, canForwardToProfileIds.contains(userId));
                }
            }
        }

        @SuppressLint("NewApi")
        private boolean isCrossProfileContentSharingStrategyDelegatedFromParent(
                UserHandle userHandle) {
            if (mUserManager == null) {
                Log.e(TAG, "can not obtain user manager");
                return false;
            }
            UserProperties userProperties = mUserManager.getUserProperties(userHandle);
            if (java.util.Objects.equals(userProperties, null)) {
                Log.e(TAG, "can not obtain user properties");
                return false;
            }

            return userProperties.getCrossProfileContentSharingStrategy()
                    == UserProperties.CROSS_PROFILE_CONTENT_SHARING_DELEGATE_FROM_PARENT;
        }

        private void getCanForwardToProfileIdMapPreV(Intent intent) {
            // There only two profiles pre V
            List<UserId> userIds = getUserIds();
            for (UserId userId : userIds) {
                synchronized (mCanFrowardToProfileIdMap) {
                    if (mCurrentUser.equals(userId)) {
                        mCanFrowardToProfileIdMap.put(userId, true);
                    } else {
                        mCanFrowardToProfileIdMap.put(userId,
                                CrossProfileUtils.getCrossProfileResolveInfo(
                                        mCurrentUser, mContext.getPackageManager(), intent,
                                        mContext, mConfigStore.isPrivateSpaceInDocsUIEnabled())
                                        != null);
                    }
                }
            }
        }

        private static boolean isDeviceSupported(Context context) {
            // The feature requires Android R DocumentsContract APIs and INTERACT_ACROSS_USERS_FULL
            // permission.
            return VersionUtils.isAtLeastR()
                    && context.checkSelfPermission(Manifest.permission.INTERACT_ACROSS_USERS)
                    == PackageManager.PERMISSION_GRANTED;
        }
    }
}
