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

import static com.android.settingslib.drawer.SwitchesProvider.METHOD_GET_DYNAMIC_SUMMARY;
import static com.android.settingslib.drawer.SwitchesProvider.METHOD_GET_DYNAMIC_TITLE;
import static com.android.settingslib.drawer.SwitchesProvider.METHOD_GET_PROVIDER_ICON;
import static com.android.settingslib.drawer.TileUtils.META_DATA_PREFERENCE_ICON_URI;
import static com.android.settingslib.drawer.TileUtils.META_DATA_PREFERENCE_SUMMARY;
import static com.android.settingslib.drawer.TileUtils.META_DATA_PREFERENCE_SUMMARY_URI;
import static com.android.settingslib.drawer.TileUtils.META_DATA_PREFERENCE_TITLE;
import static com.android.settingslib.drawer.TileUtils.META_DATA_PREFERENCE_TITLE_URI;

import android.car.drivingstate.CarUxRestrictions;
import android.content.ContentResolver;
import android.content.Context;
import android.content.IContentProvider;
import android.content.Intent;
import android.database.ContentObserver;
import android.graphics.drawable.Drawable;
import android.net.Uri;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.text.TextUtils;
import android.util.ArrayMap;
import android.util.Pair;

import androidx.annotation.VisibleForTesting;
import androidx.preference.Preference;
import androidx.preference.PreferenceGroup;

import com.android.car.settings.R;
import com.android.settingslib.drawer.TileUtils;
import com.android.settingslib.utils.ThreadUtils;

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

/**
 * Injects preferences from other system applications at a placeholder location. The placeholder
 * should be a {@link PreferenceGroup} which sets the controller attribute to the fully qualified
 * name of this class. The preference should contain an intent which will be passed to
 * {@link ExtraSettingsLoader#loadPreferences(Intent)}.
 *
 * {@link com.android.settingslib.drawer.TileUtils#EXTRA_SETTINGS_ACTION} is automatically added
 * for backwards compatibility. Please make sure to use
 * {@link com.android.settingslib.drawer.TileUtils#IA_SETTINGS_ACTION} instead.
 *
 * <p>For example:
 * <pre>{@code
 * <PreferenceCategory
 *     android:key="@string/pk_system_extra_settings"
 *     android:title="@string/system_extra_settings_title"
 *     settings:controller="com.android.settings.common.ExtraSettingsPreferenceController">
 *     <intent android:action="com.android.settings.action.IA_SETTINGS">
 *         <extra android:name="com.android.settings.category"
 *                android:value="com.android.settings.category.system"/>
 *     </intent>
 * </PreferenceCategory>
 * }</pre>
 *
 * @see ExtraSettingsLoader
 */
// TODO: investigate using SettingsLib Tiles.
public class ExtraSettingsPreferenceController extends PreferenceController<PreferenceGroup> {
    private static final Logger LOG = new Logger(ExtraSettingsPreferenceController.class);

    @VisibleForTesting
    static final String META_DATA_DISTRACTION_OPTIMIZED = "distractionOptimized";

    private Context mContext;
    private ContentResolver mContentResolver;
    private ExtraSettingsLoader mExtraSettingsLoader;
    private boolean mSettingsLoaded;
    @VisibleForTesting
    List<DynamicDataObserver> mObservers = new ArrayList<>();

    public ExtraSettingsPreferenceController(Context context, String preferenceKey,
            FragmentController fragmentController, CarUxRestrictions restrictionInfo) {
        super(context, preferenceKey, fragmentController, restrictionInfo);
        mContext = context;
        mContentResolver = context.getContentResolver();
        mExtraSettingsLoader = new ExtraSettingsLoader(context);
    }

    @VisibleForTesting(otherwise = VisibleForTesting.NONE)
    public void setExtraSettingsLoader(ExtraSettingsLoader extraSettingsLoader) {
        mExtraSettingsLoader = extraSettingsLoader;
    }

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

    @Override
    protected void onApplyUxRestrictions(CarUxRestrictions uxRestrictions) {
        // If preference intents into an activity that's not distraction optimized, disable the
        // preference. This will override the UXRE flags config_ignore_ux_restrictions and
        // config_always_ignore_ux_restrictions because navigating to these non distraction
        // optimized activities will cause the blocking activity to come up, which dead ends the
        // user.
        for (int i = 0; i < getPreference().getPreferenceCount(); i++) {
            boolean restricted = false;
            Preference preference = getPreference().getPreference(i);
            if (uxRestrictions.isRequiresDistractionOptimization()
                    && !preference.getExtras().getBoolean(META_DATA_DISTRACTION_OPTIMIZED)
                    && getAvailabilityStatus() != AVAILABLE_FOR_VIEWING) {
                restricted = true;
            }
            preference.setEnabled(getAvailabilityStatus() != AVAILABLE_FOR_VIEWING);
            restrictPreference(preference, restricted);
        }
    }

    @Override
    protected void updateState(PreferenceGroup preference) {
        Map<Preference, Bundle> preferenceBundleMap = mExtraSettingsLoader.loadPreferences(
                preference.getIntent());
        if (!mSettingsLoaded) {
            addExtraSettings(preferenceBundleMap);
            mSettingsLoaded = true;
        }
        preference.setVisible(preference.getPreferenceCount() > 0);
    }

    @Override
    protected void onStartInternal() {
        mObservers.forEach(observer -> {
            observer.register(mContentResolver, /* register= */ true);
        });
    }

    @Override
    protected void onStopInternal() {
        mObservers.forEach(observer -> {
            observer.register(mContentResolver, /* register= */ false);
        });
    }

    /**
     * Adds the extra settings from the system based on the intent that is passed in the preference
     * group. All the preferences that resolve these intents will be added in the preference group.
     *
     * @param preferenceBundleMap a map of {@link Preference} and {@link Bundle} representing
     * settings injected from system apps and their metadata.
     */
    protected void addExtraSettings(Map<Preference, Bundle> preferenceBundleMap) {
        for (Preference setting : preferenceBundleMap.keySet()) {
            Bundle metaData = preferenceBundleMap.get(setting);
            boolean distractionOptimized = false;
            if (metaData.containsKey(META_DATA_DISTRACTION_OPTIMIZED)) {
                distractionOptimized =
                        metaData.getBoolean(META_DATA_DISTRACTION_OPTIMIZED);
            }
            setting.getExtras().putBoolean(META_DATA_DISTRACTION_OPTIMIZED, distractionOptimized);
            getDynamicData(setting, metaData);
            getPreference().addPreference(setting);
        }
    }

    /**
     * Retrieve dynamic injected preference data and create observers for updates.
     */
    protected void getDynamicData(Preference preference, Bundle metaData) {
        if (metaData.containsKey(META_DATA_PREFERENCE_TITLE_URI)) {
            // Set a placeholder title before starting to fetch real title to prevent vertical
            // preference shift.
            preference.setTitle(R.string.empty_placeholder);
            Uri uri = ExtraSettingsUtil.getCompleteUri(metaData, META_DATA_PREFERENCE_TITLE_URI,
                    METHOD_GET_DYNAMIC_TITLE);
            refreshTitle(uri, preference);
            mObservers.add(
                    new DynamicDataObserver(METHOD_GET_DYNAMIC_TITLE, uri, metaData, preference));
        }
        if (metaData.containsKey(META_DATA_PREFERENCE_SUMMARY_URI)) {
            // Set a placeholder summary before starting to fetch real summary to prevent vertical
            // preference shift.
            preference.setSummary(R.string.empty_placeholder);
            Uri uri = ExtraSettingsUtil.getCompleteUri(metaData, META_DATA_PREFERENCE_SUMMARY_URI,
                    METHOD_GET_DYNAMIC_SUMMARY);
            refreshSummary(uri, preference);
            mObservers.add(
                    new DynamicDataObserver(METHOD_GET_DYNAMIC_SUMMARY, uri, metaData, preference));
        }
        if (metaData.containsKey(META_DATA_PREFERENCE_ICON_URI)) {
            // Set a placeholder icon before starting to fetch real icon to prevent horizontal
            // preference shift.
            preference.setIcon(R.drawable.ic_placeholder);
            Uri uri = ExtraSettingsUtil.getCompleteUri(metaData, META_DATA_PREFERENCE_ICON_URI,
                    METHOD_GET_PROVIDER_ICON);
            refreshIcon(uri, metaData, preference);
            mObservers.add(
                    new DynamicDataObserver(METHOD_GET_PROVIDER_ICON, uri, metaData, preference));
        }
    }

    @VisibleForTesting
    void executeBackgroundTask(Runnable r) {
        ThreadUtils.postOnBackgroundThread(r);
    }

    @VisibleForTesting
    void executeUiTask(Runnable r) {
        ThreadUtils.postOnMainThread(r);
    }

    private void refreshTitle(Uri uri, Preference preference) {
        executeBackgroundTask(() -> {
            Map<String, IContentProvider> providerMap = new ArrayMap<>();
            String titleFromUri = TileUtils.getTextFromUri(
                    mContext, uri, providerMap, META_DATA_PREFERENCE_TITLE);
            if (!TextUtils.equals(titleFromUri, preference.getTitle())) {
                executeUiTask(() -> preference.setTitle(titleFromUri));
            }
        });
    }

    private void refreshSummary(Uri uri, Preference preference) {
        executeBackgroundTask(() -> {
            Map<String, IContentProvider> providerMap = new ArrayMap<>();
            String summaryFromUri = TileUtils.getTextFromUri(
                    mContext, uri, providerMap, META_DATA_PREFERENCE_SUMMARY);
            if (!TextUtils.equals(summaryFromUri, preference.getSummary())) {
                executeUiTask(() -> preference.setSummary(summaryFromUri));
            }
        });
    }

    private void refreshIcon(Uri uri, Bundle metaData, Preference preference) {
        executeBackgroundTask(() -> {
            Intent intent = preference.getIntent();
            String packageName = null;
            if (!TextUtils.isEmpty(intent.getPackage())) {
                packageName = intent.getPackage();
            } else if (intent.getComponent() != null) {
                packageName = intent.getComponent().getPackageName();
            }
            Map<String, IContentProvider> providerMap = new ArrayMap<>();
            Pair<String, Integer> iconInfo = TileUtils.getIconFromUri(
                    mContext, packageName, uri, providerMap);
            Drawable icon;
            if (iconInfo != null) {
                icon = ExtraSettingsUtil.createIcon(mContext, metaData, iconInfo.first,
                        iconInfo.second);
            } else {
                LOG.w("Failed to get icon from uri " + uri);
                icon = ExtraSettingsUtil.createIcon(mContext, metaData, packageName, 0);
            }
            if (icon != null) {
                executeUiTask(() -> {
                    preference.setIcon(icon);
                });
            }
        });
    }

    /**
     * Observer for updating injected dynamic data.
     */
    private class DynamicDataObserver extends ContentObserver {
        private final String mMethod;
        private final Uri mUri;
        private final Bundle mMetaData;
        private final Preference mPreference;

        DynamicDataObserver(String method, Uri uri, Bundle metaData, Preference preference) {
            super(new Handler(Looper.getMainLooper()));
            mMethod = method;
            mUri = uri;
            mMetaData = metaData;
            mPreference = preference;
        }

        /** Registers or unregisters this observer to the given content resolver. */
        void register(ContentResolver cr, boolean register) {
            if (register) {
                cr.registerContentObserver(mUri, /* notifyForDescendants= */ false,
                        /* observer= */ this);
            } else {
                cr.unregisterContentObserver(this);
            }
        }

        @Override
        public void onChange(boolean selfChange) {
            switch (mMethod) {
                case METHOD_GET_DYNAMIC_TITLE:
                    refreshTitle(mUri, mPreference);
                    break;
                case METHOD_GET_DYNAMIC_SUMMARY:
                    refreshSummary(mUri, mPreference);
                    break;
                case METHOD_GET_PROVIDER_ICON:
                    refreshIcon(mUri, mMetaData, mPreference);
                    break;
            }
        }
    }
}