/*
 * Copyright 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 android.car.drivingstate.CarUxRestrictions;
import android.car.drivingstate.CarUxRestrictionsManager.OnUxRestrictionsChangedListener;
import android.content.Context;
import android.os.SystemClock;
import android.widget.Toast;

import androidx.annotation.IntDef;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.lifecycle.DefaultLifecycleObserver;
import androidx.lifecycle.LifecycleOwner;
import androidx.preference.Preference;
import androidx.preference.PreferenceGroup;

import com.android.car.settings.R;
import com.android.car.ui.preference.ClickableWhileDisabledPreference;
import com.android.car.ui.preference.UxRestrictablePreference;

import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Set;
import java.util.function.Consumer;

/**
 * Controller which encapsulates the business logic associated with a {@link Preference}. All car
 * settings controllers should extend this class.
 *
 * <p>Controllers are responsible for populating and modifying the presentation of an associated
 * preference while responding to changes in system state. This is enabled via {@link
 * SettingsFragment} which registers controllers as observers on its lifecycle and dispatches
 * {@link CarUxRestrictions} change events to the controllers via the {@link
 * OnUxRestrictionsChangedListener} interface.
 *
 * <p>Controllers should be instantiated from XML. To do so, define a preference and include the
 * {@code controller} attribute in the preference tag and assign the fully qualified class name.
 *
 * <p>For example:
 * <pre>{@code
 * <Preference
 *     android:key="my_preference_key"
 *     android:title="@string/my_preference_title"
 *     android:icon="@drawable/ic_settings"
 *     android:fragment="com.android.settings.foo.MyFragment"
 *     settings:controller="com.android.settings.foo.MyPreferenceController"/>
 * }</pre>
 *
 * <p>Subclasses must implement {@link #getPreferenceType()} to define the upper bound type on the
 * {@link Preference} that the controller is associated with. For example, a bound of {@link
 * androidx.preference.PreferenceGroup} indicates that the controller will utilize preference group
 * methods in its operation. {@link #setPreference(Preference)} will throw an {@link
 * IllegalArgumentException} if not passed a subclass of the upper bound type.
 *
 * <p>Subclasses may implement any or all of the following methods (see method Javadocs for more
 * information):
 *
 * <ul>
 * <li>{@link #checkInitialized()}
 * <li>{@link #onCreateInternal()}
 * <li>{@link #getAvailabilityStatus()}
 * <li>{@link #onStartInternal()}
 * <li>{@link #onResumeInternal()}
 * <li>{@link #onPauseInternal()}
 * <li>{@link #onStopInternal()}
 * <li>{@link #onDestroyInternal()}
 * <li>{@link #updateState(Preference)}
 * <li>{@link #onApplyUxRestrictions(CarUxRestrictions)}
 * <li>{@link #handlePreferenceChanged(Preference, Object)}
 * <li>{@link #handlePreferenceClicked(Preference)}
 * </ul>
 *
 * @param <V> the upper bound on the type of {@link Preference} on which the controller
 *            expects to operate.
 */
public abstract class PreferenceController<V extends Preference> implements
        DefaultLifecycleObserver,
        OnUxRestrictionsChangedListener {
    private static final Logger LOG = new Logger(PreferenceController.class);

    /**
     * Denotes the availability of a setting.
     *
     * @see #getAvailabilityStatus()
     */
    @Retention(RetentionPolicy.SOURCE)
    @IntDef({AVAILABLE, CONDITIONALLY_UNAVAILABLE, UNSUPPORTED_ON_DEVICE, DISABLED_FOR_PROFILE,
            AVAILABLE_FOR_VIEWING})
    public @interface AvailabilityStatus {
    }

    /**
     * The setting is available.
     */
    public static final int AVAILABLE = 0;

    /**
     * The setting is currently unavailable but may become available in the future. Use
     * {@link #DISABLED_FOR_PROFILE} if it describes the condition more accurately.
     */
    public static final int CONDITIONALLY_UNAVAILABLE = 1;

    /**
     * The setting is not and will not be supported by this device.
     */
    public static final int UNSUPPORTED_ON_DEVICE = 2;

    /**
     * The setting cannot be changed by the current profile.
     */
    public static final int DISABLED_FOR_PROFILE = 3;

    /**
     * The setting cannot be changed.
     */
    public static final int AVAILABLE_FOR_VIEWING = 4;

    /**
     * Indicates whether all Preferences are configured to ignore UX Restrictions Event.
     */
    private final boolean mAlwaysIgnoreUxRestrictions;

    /**
     * Set of the keys of Preferences that ignore UX Restrictions. When mAlwaysIgnoreUxRestrictions
     * is configured to be false, then only the Preferences whose keys are contained in this Set
     * ignore UX Restrictions.
     */
    private final Set<String> mPreferencesIgnoringUxRestrictions;

    private final Context mContext;
    private final String mPreferenceKey;
    private final FragmentController mFragmentController;
    private final String mRestrictedWhileDrivingMessage;
    private final int mDebounceIntervalMs;

    private CarUxRestrictions mUxRestrictions;
    private V mPreference;
    private boolean mIsCreated;
    private boolean mIsStarted;
    private long mDebounceStartTimeMs;

    /**
     * Controllers should be instantiated from XML. To pass additional arguments see
     * {@link SettingsFragment#use(Class, int)}.
     */
    public PreferenceController(Context context, String preferenceKey,
            FragmentController fragmentController, CarUxRestrictions uxRestrictions) {
        mContext = context;
        mPreferenceKey = preferenceKey;
        mFragmentController = fragmentController;
        mUxRestrictions = uxRestrictions;
        mPreferencesIgnoringUxRestrictions = new HashSet<String>(Arrays.asList(
                mContext.getResources().getStringArray(R.array.config_ignore_ux_restrictions)));
        mAlwaysIgnoreUxRestrictions =
                mContext.getResources().getBoolean(R.bool.config_always_ignore_ux_restrictions);
        mRestrictedWhileDrivingMessage =
                mContext.getResources().getString(R.string.car_ui_restricted_while_driving);
        mDebounceIntervalMs =
                mContext.getResources().getInteger(R.integer.config_preference_onclick_debounce_ms);
    }

    /**
     * Returns the context used to construct the controller.
     */
    protected final Context getContext() {
        return mContext;
    }

    /**
     * Returns the key for the preference managed by this controller set at construction.
     */
    protected final String getPreferenceKey() {
        return mPreferenceKey;
    }

    /**
     * Returns the {@link FragmentController} used to launch fragments and go back to previous
     * fragments. This is set at construction.
     */
    protected final FragmentController getFragmentController() {
        return mFragmentController;
    }

    /**
     * Returns the current {@link CarUxRestrictions} applied to the controller. Subclasses may use
     * this to limit which content is displayed in the associated preference. May be called anytime.
     */
    protected final CarUxRestrictions getUxRestrictions() {
        return mUxRestrictions;
    }

    /**
     * Returns the preference associated with this controller. This may be used in any of the
     * lifecycle methods, as the preference is set before they are called..
     */
    protected final V getPreference() {
        return mPreference;
    }

    /**
     * Called by {@link SettingsFragment} to associate the controller with its preference after the
     * screen is created. This is guaranteed to be called before {@link #onCreateInternal()}.
     *
     * @throws IllegalArgumentException if the given preference does not match the type
     *                                  returned by {@link #getPreferenceType()}
     * @throws IllegalStateException    if subclass defined initialization is not
     *                                  complete.
     */
    final void setPreference(Preference preference) {
        PreferenceUtil.requirePreferenceType(preference, getPreferenceType());
        mPreference = getPreferenceType().cast(preference);
        mPreference.setOnPreferenceChangeListener(
                (changedPref, newValue) -> handlePreferenceChanged(
                        getPreferenceType().cast(changedPref), newValue));
        mPreference.setOnPreferenceClickListener(
                clickedPref -> {
                    // Debounce onClick() calls
                    long curTime = SystemClock.elapsedRealtime();
                    if (mDebounceStartTimeMs != 0
                            && curTime < (mDebounceStartTimeMs + mDebounceIntervalMs)) {
                        LOG.i("OnClick event dropped due to debouncing");
                        return true;
                    }
                    mDebounceStartTimeMs = curTime;
                    return handlePreferenceClicked(getPreferenceType().cast(clickedPref));
                });
        checkInitialized();
    }

    /**
     * Called by {@link SettingsFragment} to notify that the applied ux restrictions have changed.
     * The controller will refresh its UI accordingly unless it is not yet created. In that case,
     * the UI will refresh once created.
     */
    @Override
    public final void onUxRestrictionsChanged(CarUxRestrictions uxRestrictions) {
        mUxRestrictions = uxRestrictions;
        refreshUi();
    }

    /**
     * Updates the preference presentation based on its {@link #getAvailabilityStatus()} status. If
     * the controller is available, the associated preference is shown and a call to {@link
     * #updateState(Preference)} and {@link #onApplyUxRestrictions(CarUxRestrictions)} are
     * dispatched to allow the controller to modify the presentation for the current state. If the
     * controller is not available, the associated preference is hidden from the screen. This is a
     * no-op if the controller is not yet created.
     */
    public final void refreshUi() {
        if (!mIsCreated) {
            return;
        }

        if (isAvailable()) {
            mPreference.setVisible(true);
            mPreference.setEnabled(getAvailabilityStatus() != AVAILABLE_FOR_VIEWING);
            updateState(mPreference);
            onApplyUxRestrictions(mUxRestrictions);
        } else {
            mPreference.setVisible(false);
        }
    }

    private boolean isAvailable() {
        int availabilityStatus = getAvailabilityStatus();
        return availabilityStatus == AVAILABLE || availabilityStatus == AVAILABLE_FOR_VIEWING;
    }

    // Controller lifecycle ========================================================================

    /**
     * Dispatches a call to {@link #onCreateInternal()} and {@link #refreshUi()} to enable
     * controllers to setup initial state before a preference is visible. If the controller is
     * {@link #UNSUPPORTED_ON_DEVICE}, the preference is hidden and no further action is taken.
     */
    @Override
    public final void onCreate(@NonNull LifecycleOwner owner) {
        if (getAvailabilityStatus() == UNSUPPORTED_ON_DEVICE) {
            mPreference.setVisible(false);
            return;
        }
        onCreateInternal();
        mIsCreated = true;
        refreshUi();
    }

    /**
     * Dispatches a call to {@link #onStartInternal()} and {@link #refreshUi()} to account for any
     * state changes that may have occurred while the controller was stopped. Returns immediately
     * if the controller is {@link #UNSUPPORTED_ON_DEVICE}.
     */
    @Override
    public final void onStart(@NonNull LifecycleOwner owner) {
        if (getAvailabilityStatus() == UNSUPPORTED_ON_DEVICE) {
            return;
        }
        onStartInternal();
        mIsStarted = true;
        refreshUi();
    }

    /**
     * Notifies that the controller is resumed by dispatching a call to {@link #onResumeInternal()}.
     * Returns immediately if the controller is {@link #UNSUPPORTED_ON_DEVICE}.
     */
    @Override
    public final void onResume(@NonNull LifecycleOwner owner) {
        if (getAvailabilityStatus() == UNSUPPORTED_ON_DEVICE) {
            return;
        }
        onResumeInternal();
    }

    /**
     * Notifies that the controller is paused by dispatching a call to {@link #onPauseInternal()}.
     * Returns immediately if the controller is {@link #UNSUPPORTED_ON_DEVICE}.
     */
    @Override
    public final void onPause(@NonNull LifecycleOwner owner) {
        if (getAvailabilityStatus() == UNSUPPORTED_ON_DEVICE) {
            return;
        }
        onPauseInternal();
    }

    /**
     * Notifies that the controller is stopped by dispatching a call to {@link #onStopInternal()}.
     * Returns immediately if the controller is {@link #UNSUPPORTED_ON_DEVICE}.
     */
    @Override
    public final void onStop(@NonNull LifecycleOwner owner) {
        if (getAvailabilityStatus() == UNSUPPORTED_ON_DEVICE) {
            return;
        }
        mIsStarted = false;
        onStopInternal();
    }

    /**
     * Notifies that the controller is destroyed by dispatching a call to {@link
     * #onDestroyInternal()}. Returns immediately if the controller is
     * {@link #UNSUPPORTED_ON_DEVICE}.
     */
    @Override
    public final void onDestroy(@NonNull LifecycleOwner owner) {
        if (getAvailabilityStatus() == UNSUPPORTED_ON_DEVICE) {
            return;
        }
        mIsCreated = false;
        onDestroyInternal();
    }

    // Methods for override ========================================================================

    /**
     * Returns the upper bound type of the preference on which this controller will operate.
     */
    protected abstract Class<V> getPreferenceType();

    /**
     * Subclasses may override this method to throw {@link IllegalStateException} if any expected
     * post-instantiation setup is not completed using {@link SettingsFragment#use(Class, int)}
     * prior to associating the controller with its preference. This will be called before the
     * controller lifecycle begins.
     */
    protected void checkInitialized() {
    }

    /**
     * Returns the {@link AvailabilityStatus} for the setting. This status is used to determine
     * if the setting should be shown, hidden, or disabled. Defaults to {@link #AVAILABLE}. This
     * will be called before the controller lifecycle begins and on refresh events.
     */
    @AvailabilityStatus
    protected int getAvailabilityStatus() {
        return AVAILABLE;
    }

    /**
     * Subclasses may override this method to complete any operations needed at creation time e.g.
     * loading static configuration.
     *
     * <p>Note: this will not be called on {@link #UNSUPPORTED_ON_DEVICE} controllers.
     */
    protected void onCreateInternal() {
    }

    /**
     * Subclasses may override this method to complete any operations needed each time the
     * controller is started e.g. registering broadcast receivers.
     *
     * <p>Note: this will not be called on {@link #UNSUPPORTED_ON_DEVICE} controllers.
     */
    protected void onStartInternal() {
    }

    /**
     * Subclasses may override this method to complete any operations needed each time the
     * controller is resumed. Prefer to use {@link #onStartInternal()} unless absolutely necessary
     * as controllers may not be resumed in a multi-display scenario.
     *
     * <p>Note: this will not be called on {@link #UNSUPPORTED_ON_DEVICE} controllers.
     */
    protected void onResumeInternal() {
    }

    /**
     * Subclasses may override this method to complete any operations needed each time the
     * controller is paused. Prefer to use {@link #onStartInternal()} unless absolutely necessary
     * as controllers may not be resumed in a multi-display scenario.
     *
     * <p>Note: this will not be called on {@link #UNSUPPORTED_ON_DEVICE} controllers.
     */
    protected void onPauseInternal() {
    }

    /**
     * Subclasses may override this method to complete any operations needed each time the
     * controller is stopped e.g. unregistering broadcast receivers.
     *
     * <p>Note: this will not be called on {@link #UNSUPPORTED_ON_DEVICE} controllers.
     */
    protected void onStopInternal() {
    }

    /**
     * Subclasses may override this method to complete any operations needed when the controller is
     * destroyed e.g. freeing up held resources.
     *
     * <p>Note: this will not be called on {@link #UNSUPPORTED_ON_DEVICE} controllers.
     */
    protected void onDestroyInternal() {
    }

    /**
     * Subclasses may override this method to update the presentation of the preference for the
     * current system state (summary, switch state, etc). If the preference has dynamic content
     * (such as preferences added to a group), it may be updated here as well.
     *
     * <p>Important: Operations should be idempotent as this may be called multiple times.
     *
     * <p>Note: this will only be called when the following are true:
     * <ul>
     * <li>{@link #getAvailabilityStatus()} returns {@link #AVAILABLE}
     * <li>{@link #onCreateInternal()} has completed.
     * </ul>
     */
    protected void updateState(V preference) {
    }

    /**
     * Updates the preference enabled status given the {@code restrictionInfo}. This will be called
     * before the controller lifecycle begins and on refresh events. The preference is disabled by
     * default when {@link CarUxRestrictions#UX_RESTRICTIONS_NO_SETUP} is set in {@code
     * uxRestrictions}. Subclasses may override this method to modify enabled state based on
     * additional driving restrictions.
     */
    protected void onApplyUxRestrictions(CarUxRestrictions uxRestrictions) {
        boolean restrict = shouldApplyUxRestrictions(uxRestrictions);

        restrictPreference(mPreference, restrict);
    }

    /**
     * Decides whether or not this {@link PreferenceController} should apply {@code uxRestrictions}
     * based on the type of restrictions currently present, and the value of the {@code
     * config_always_ignore_ux_restrictions} and
     * {@code config_ignore_ux_restrictions} config flags.
     * <p>
     * It is not expected that subclasses will override this functionality. If they do, it is
     * important to respect the config flags being consulted here.
     *
     * @return true if {@code uxRestrictions} should be applied and false otherwise.
     */
    protected boolean shouldApplyUxRestrictions(CarUxRestrictions uxRestrictions) {
        return !isUxRestrictionsIgnored(mAlwaysIgnoreUxRestrictions,
                mPreferencesIgnoringUxRestrictions)
                && CarUxRestrictionsHelper.isNoSetup(uxRestrictions)
                && getAvailabilityStatus() != AVAILABLE_FOR_VIEWING;
    }

    /**
     * Updates the UxRestricted state and action for a preference. This will also update all child
     * preferences with the same state and action when {@param preference} is a PreferenceGroup.
     *
     * @param preference the preference to update
     * @param restrict whether or not the preference should be restricted
     */
    protected void restrictPreference(Preference preference, boolean restrict) {
        if (preference instanceof UxRestrictablePreference) {
            UxRestrictablePreference restrictablePreference = (UxRestrictablePreference) preference;
            restrictablePreference.setUxRestricted(restrict);
            restrictablePreference.setOnClickWhileRestrictedListener(p ->
                    Toast.makeText(mContext, mRestrictedWhileDrivingMessage,
                            Toast.LENGTH_LONG).show());
        }
        if (preference instanceof PreferenceGroup) {
            PreferenceGroup preferenceGroup = (PreferenceGroup) preference;
            for (int i = 0; i < preferenceGroup.getPreferenceCount(); i++) {
                restrictPreference(preferenceGroup.getPreference(i), restrict);
            }
        }
    }

    /**
     * Updates the clickable while disabled state and action for a preference. This will also
     * update all child preferences with the same state and action when {@param preference}
     * is a PreferenceGroup.
     *
     * @param preference the preference to update
     * @param clickable whether or not the preference should be clickable when disabled
     * @param disabledClickAction the action that should be taken when clicked while disabled
     */
    protected void setClickableWhileDisabled(Preference preference, boolean clickable,
            @Nullable Consumer<Preference> disabledClickAction) {
        if (preference instanceof ClickableWhileDisabledPreference) {
            ClickableWhileDisabledPreference pref =
                    (ClickableWhileDisabledPreference) preference;
            pref.setClickableWhileDisabled(clickable);
            pref.setDisabledClickListener(disabledClickAction);
        }
        if (preference instanceof PreferenceGroup) {
            PreferenceGroup preferenceGroup = (PreferenceGroup) preference;
            for (int i = 0; i < preferenceGroup.getPreferenceCount(); i++) {
                setClickableWhileDisabled(preferenceGroup.getPreference(i), clickable,
                        disabledClickAction);
            }
        }
    }

    /**
     * Called when the associated preference is changed by the user. This is called before the state
     * of the preference is updated and before the state is persisted.
     *
     * @param preference the changed preference.
     * @param newValue   the new value of the preference.
     * @return {@code true} to update the state of the preference with the new value. Defaults to
     * {@code true}.
     */
    protected boolean handlePreferenceChanged(V preference, Object newValue) {
        return true;
    }

    /**
     * Called when the preference associated with this controller is clicked. Subclasses may
     * choose to handle the click event.
     *
     * @param preference the clicked preference.
     * @return {@code true} if click is handled and further propagation should cease. Defaults to
     * {@code false}.
     */
    protected boolean handlePreferenceClicked(V preference) {
        return false;
    }

    protected boolean isUxRestrictionsIgnored(boolean allIgnores, Set prefsThatIgnore) {
        return allIgnores || prefsThatIgnore.contains(mPreferenceKey);
    }

    protected final boolean isStarted() {
        return mIsStarted;
    }
}
