/*
 * Copyright (C) 2018 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.settings.accounts;

import static android.os.UserManager.DISALLOW_MODIFY_ACCOUNTS;

import static com.android.car.settings.enterprise.EnterpriseUtils.hasUserRestrictionByUm;

import android.accounts.Account;
import android.accounts.AccountManager;
import android.car.drivingstate.CarUxRestrictions;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.pm.UserInfo;
import android.graphics.drawable.Drawable;
import android.os.UserHandle;

import androidx.collection.ArrayMap;
import androidx.preference.Preference;
import androidx.preference.PreferenceCategory;

import com.android.car.settings.R;
import com.android.car.settings.common.FragmentController;
import com.android.car.settings.common.PreferenceController;
import com.android.car.settings.profiles.ProfileHelper;
import com.android.car.settings.profiles.ProfileUtils;
import com.android.car.ui.preference.CarUiPreference;
import com.android.internal.annotations.VisibleForTesting;
import com.android.settingslib.accounts.AuthenticatorHelper;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

/**
 * Controller for listing accounts.
 *
 * <p>Largely derived from {@link com.android.settings.accounts.AccountPreferenceController}
 */
public class AccountListPreferenceController extends
        PreferenceController<PreferenceCategory> implements
        AuthenticatorHelper.OnAccountsUpdateListener {
    private static final String NO_ACCOUNT_PREF_KEY = "no_accounts_added";

    private final ArrayMap<String, Preference> mPreferences = new ArrayMap<>();

    private UserInfo mUserInfo;
    private AuthenticatorHelper mAuthenticatorHelper;
    private String[] mAuthorities;
    private boolean mListenerRegistered = false;
    private boolean mNeedToRefreshUserInfo = false;

    private final BroadcastReceiver mUserUpdateReceiver = new BroadcastReceiver() {
        @Override
        public void onReceive(Context context, Intent intent) {
            onUsersUpdate();
        }
    };

    public AccountListPreferenceController(Context context, String preferenceKey,
            FragmentController fragmentController, CarUxRestrictions uxRestrictions) {
        super(context, preferenceKey, fragmentController, uxRestrictions);
        mUserInfo = ProfileHelper.getInstance(context).getCurrentProcessUserInfo();
        mAuthenticatorHelper = createAuthenticatorHelper();
    }

    /** Sets the account authorities that are available. */
    public void setAuthorities(String[] authorities) {
        mAuthorities = authorities;
    }

    @Override
    protected Class<PreferenceCategory> getPreferenceType() {
        return PreferenceCategory.class;
    }

    @Override
    protected void updateState(PreferenceCategory preference) {
        forceUpdateAccountsCategory();
    }

    @Override
    protected void onCreateInternal() {
        super.onCreateInternal();
        setClickableWhileDisabled(getPreference(), /* clickable= */ true, p -> getProfileHelper()
                .runClickableWhileDisabled(getContext(), getFragmentController()));
    }

    @Override
    protected int getDefaultAvailabilityStatus() {
        ProfileHelper profileHelper = getProfileHelper();
        boolean canModifyAccounts = profileHelper.canCurrentProcessModifyAccounts();

        if (canModifyAccounts) {
            return AVAILABLE;
        }

        if (profileHelper.isDemoOrGuest()
                || hasUserRestrictionByUm(getContext(), DISALLOW_MODIFY_ACCOUNTS)) {
            return DISABLED_FOR_PROFILE;
        }

        return AVAILABLE_FOR_VIEWING;
    }

    /**
     * Registers the account update and user update callbacks.
     */
    @Override
    protected void onStartInternal() {
        mAuthenticatorHelper.listenToAccountUpdates();
        registerForUserEvents();
        mListenerRegistered = true;

        /* refresh UserInfo only when restarting */
        if (mNeedToRefreshUserInfo) {
            onUsersUpdate();
        }
    }

    /**
     * Unregisters the account update and user update callbacks.
     */
    @Override
    protected void onStopInternal() {
        mAuthenticatorHelper.stopListeningToAccountUpdates();
        unregisterForUserEvents();
        mListenerRegistered = false;
        mNeedToRefreshUserInfo = true;
    }

    @Override
    public void onAccountsUpdate(UserHandle userHandle) {
        if (userHandle.equals(mUserInfo.getUserHandle())) {
            forceUpdateAccountsCategory();
        }
    }

    @VisibleForTesting
    void onUsersUpdate() {
        mUserInfo = ProfileUtils.getUserInfo(getContext(), mUserInfo.id);
        forceUpdateAccountsCategory();
    }

    @VisibleForTesting
    AuthenticatorHelper createAuthenticatorHelper() {
        return new AuthenticatorHelper(getContext(), mUserInfo.getUserHandle(), this);
    }

    private boolean onAccountPreferenceClicked(AccountPreference preference) {
        // Show the account's details when an account is clicked on.
        getFragmentController().launchFragment(AccountDetailsFragment.newInstance(
                preference.getAccount(), preference.getLabel(), mUserInfo));
        return true;
    }

    /** Forces a refresh of the account preferences. */
    private void forceUpdateAccountsCategory() {
        // Set the category title and include the user's name
        getPreference().setTitle(
                getContext().getString(R.string.account_list_title, mUserInfo.name));

        // Recreate the authentication helper to refresh the list of enabled accounts
        mAuthenticatorHelper.stopListeningToAccountUpdates();
        mAuthenticatorHelper = createAuthenticatorHelper();
        if (mListenerRegistered) {
            mAuthenticatorHelper.listenToAccountUpdates();
        }

        Set<String> preferencesToRemove = new HashSet<>(mPreferences.keySet());
        List<? extends Preference> preferences = getAccountPreferences(preferencesToRemove);
        // Add all preferences that aren't already shown. Manually set the order so that existing
        // preferences are reordered correctly.
        for (int i = 0; i < preferences.size(); i++) {
            Preference pref = preferences.get(i);
            pref.setOrder(i);
            mPreferences.put(pref.getKey(), pref);
            getPreference().addPreference(pref);
        }

        for (String key : preferencesToRemove) {
            getPreference().removePreference(mPreferences.get(key));
            mPreferences.remove(key);
        }
    }

    /**
     * Returns a list of preferences corresponding to the accounts for the current user.
     *
     * <p> Derived from
     * {@link com.android.settings.accounts.AccountPreferenceController#getAccountTypePreferences}
     *
     * @param preferencesToRemove the current preferences shown; only preferences to be removed will
     *                            remain after method execution
     */
    private List<? extends Preference> getAccountPreferences(
            Set<String> preferencesToRemove) {
        String[] accountTypes = mAuthenticatorHelper.getEnabledAccountTypes();
        ArrayList<AccountPreference> accountPreferences =
                new ArrayList<>(accountTypes.length);

        for (int i = 0; i < accountTypes.length; i++) {
            String accountType = accountTypes[i];
            // Skip showing any account that does not have any of the requested authorities
            if (!accountTypeHasAnyRequestedAuthorities(accountType)) {
                continue;
            }
            CharSequence label = mAuthenticatorHelper.getLabelForType(getContext(), accountType);
            if (label == null) {
                continue;
            }

            Account[] accounts = AccountManager.get(getContext())
                    .getAccountsByTypeAsUser(accountType, mUserInfo.getUserHandle());
            Drawable icon = mAuthenticatorHelper.getDrawableForType(getContext(), accountType);

            // Add a preference row for each individual account
            for (Account account : accounts) {
                String key = AccountPreference.buildKey(account);
                AccountPreference preference = (AccountPreference) mPreferences.getOrDefault(key,
                        new AccountPreference(getContext(), account, label, icon));
                preference.setOnPreferenceClickListener(
                        (Preference pref) -> onAccountPreferenceClicked((AccountPreference) pref));

                accountPreferences.add(preference);
                preferencesToRemove.remove(key);
            }
            mAuthenticatorHelper.preloadDrawableForType(getContext(), accountType);
        }

        // If there are no accounts, return the "no account added" preference.
        if (accountPreferences.isEmpty()) {
            preferencesToRemove.remove(NO_ACCOUNT_PREF_KEY);
            return Arrays.asList(mPreferences.getOrDefault(NO_ACCOUNT_PREF_KEY,
                    createNoAccountsAddedPreference()));
        }

        Collections.sort(accountPreferences, Comparator.comparing(
                (AccountPreference a) -> a.getSummary().toString())
                .thenComparing((AccountPreference a) -> a.getTitle().toString()));

        return accountPreferences;
    }

    private Preference createNoAccountsAddedPreference() {
        CarUiPreference emptyPreference = new CarUiPreference(getContext());
        emptyPreference.setTitle(R.string.no_accounts_added);
        emptyPreference.setKey(NO_ACCOUNT_PREF_KEY);
        emptyPreference.setSelectable(false);

        return emptyPreference;
    }

    private void registerForUserEvents() {
        IntentFilter filter = new IntentFilter(Intent.ACTION_USER_INFO_CHANGED);
        getContext().registerReceiver(mUserUpdateReceiver, filter);
    }

    private void unregisterForUserEvents() {
        getContext().unregisterReceiver(mUserUpdateReceiver);
    }


    /**
     * Returns whether the account type has any of the authorities requested by the caller.
     *
     * <p> Derived from {@link AccountPreferenceController#accountTypeHasAnyRequestedAuthorities}
     */
    private boolean accountTypeHasAnyRequestedAuthorities(String accountType) {
        if (mAuthorities == null || mAuthorities.length == 0) {
            // No authorities required
            return true;
        }
        ArrayList<String> authoritiesForType =
                mAuthenticatorHelper.getAuthoritiesForAccountType(accountType);
        if (authoritiesForType == null) {
            return false;
        }
        for (int j = 0; j < mAuthorities.length; j++) {
            if (authoritiesForType.contains(mAuthorities[j])) {
                return true;
            }
        }
        return false;
    }

    @VisibleForTesting
    ProfileHelper getProfileHelper() {
        return ProfileHelper.getInstance(getContext());
    }

    private static class AccountPreference extends CarUiPreference {
        /** Account that this Preference represents. */
        private final Account mAccount;
        private final CharSequence mLabel;

        private AccountPreference(Context context, Account account, CharSequence label,
                Drawable icon) {
            super(context);
            mAccount = account;
            mLabel = label;

            setKey(buildKey(account));
            setTitle(account.name);
            setSummary(label);
            setIcon(icon);
            setShowChevron(false);
        }

        /**
         * Build a unique preference key based on the account.
         */
        public static String buildKey(Account account) {
            return String.valueOf(account.hashCode());
        }

        public Account getAccount() {
            return mAccount;
        }

        public CharSequence getLabel() {
            return mLabel;
        }
    }
}
