/*
 * Copyright (C) 2022 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.settingslib.devicestate;

import static android.provider.Settings.Secure.DEVICE_STATE_ROTATION_LOCK_IGNORED;
import static android.provider.Settings.Secure.DEVICE_STATE_ROTATION_LOCK_LOCKED;
import static android.provider.Settings.Secure.DEVICE_STATE_ROTATION_LOCK_UNLOCKED;

import android.content.ContentResolver;
import android.content.Context;
import android.content.res.Resources;
import android.database.ContentObserver;
import android.os.Handler;
import android.os.Looper;
import android.os.UserHandle;
import android.provider.Settings;
import android.text.TextUtils;
import android.util.IndentingPrintWriter;
import android.util.Log;
import android.util.SparseIntArray;

import com.android.internal.R;
import com.android.internal.annotations.VisibleForTesting;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.Objects;
import java.util.Set;

/**
 * Manages device-state based rotation lock settings. Handles reading, writing, and listening for
 * changes.
 */
public final class DeviceStateRotationLockSettingsManager {

    private static final String TAG = "DSRotLockSettingsMngr";
    private static final String SEPARATOR_REGEX = ":";

    private static DeviceStateRotationLockSettingsManager sSingleton;

    private final Handler mMainHandler = new Handler(Looper.getMainLooper());
    private final Set<DeviceStateRotationLockSettingsListener> mListeners = new HashSet<>();
    private final SecureSettings mSecureSettings;
    private final PosturesHelper mPosturesHelper;
    private String[] mPostureRotationLockDefaults;
    private SparseIntArray mPostureRotationLockSettings;
    private SparseIntArray mPostureDefaultRotationLockSettings;
    private SparseIntArray mPostureRotationLockFallbackSettings;
    private List<SettableDeviceState> mSettableDeviceStates;

    @VisibleForTesting
    DeviceStateRotationLockSettingsManager(Context context, SecureSettings secureSettings) {
        mSecureSettings = secureSettings;
        mPosturesHelper = new PosturesHelper(context);
        mPostureRotationLockDefaults =
                context.getResources()
                        .getStringArray(R.array.config_perDeviceStateRotationLockDefaults);
        loadDefaults();
        initializeInMemoryMap();
        listenForSettingsChange();
    }

    /** Returns a singleton instance of this class */
    public static synchronized DeviceStateRotationLockSettingsManager getInstance(Context context) {
        if (sSingleton == null) {
            Context applicationContext = context.getApplicationContext();
            ContentResolver contentResolver = applicationContext.getContentResolver();
            SecureSettings secureSettings = new AndroidSecureSettings(contentResolver);
            sSingleton =
                    new DeviceStateRotationLockSettingsManager(applicationContext, secureSettings);
        }
        return sSingleton;
    }

    /** Resets the singleton instance of this class. Only used for testing. */
    @VisibleForTesting
    public static synchronized void resetInstance() {
        sSingleton = null;
    }

    /** Returns true if device-state based rotation lock settings are enabled. */
    public static boolean isDeviceStateRotationLockEnabled(Context context) {
        return context.getResources()
                .getStringArray(R.array.config_perDeviceStateRotationLockDefaults).length > 0;
    }

    private void listenForSettingsChange() {
        mSecureSettings
                .registerContentObserver(
                        Settings.Secure.DEVICE_STATE_ROTATION_LOCK,
                        /* notifyForDescendants= */ false,
                        new ContentObserver(mMainHandler) {
                            @Override
                            public void onChange(boolean selfChange) {
                                onPersistedSettingsChanged();
                            }
                        },
                        UserHandle.USER_CURRENT);
    }

    /**
     * Registers a {@link DeviceStateRotationLockSettingsListener} to be notified when the settings
     * change. Can be called multiple times with different listeners.
     */
    public void registerListener(DeviceStateRotationLockSettingsListener runnable) {
        mListeners.add(runnable);
    }

    /**
     * Unregisters a {@link DeviceStateRotationLockSettingsListener}. No-op if the given instance
     * was never registered.
     */
    public void unregisterListener(
            DeviceStateRotationLockSettingsListener deviceStateRotationLockSettingsListener) {
        if (!mListeners.remove(deviceStateRotationLockSettingsListener)) {
            Log.w(TAG, "Attempting to unregister a listener hadn't been registered");
        }
    }

    /** Updates the rotation lock setting for a specified device state. */
    public void updateSetting(int deviceState, boolean rotationLocked) {
        int posture = mPosturesHelper.deviceStateToPosture(deviceState);
        if (mPostureRotationLockFallbackSettings.indexOfKey(posture) >= 0) {
            // The setting for this device posture is IGNORED, and has a fallback posture.
            // The setting for that fallback posture should be the changed in this case.
            posture = mPostureRotationLockFallbackSettings.get(posture);
        }
        mPostureRotationLockSettings.put(
                posture,
                rotationLocked
                        ? DEVICE_STATE_ROTATION_LOCK_LOCKED
                        : DEVICE_STATE_ROTATION_LOCK_UNLOCKED);
        persistSettings();
    }

    /**
     * Returns the {@link Settings.Secure.DeviceStateRotationLockSetting} for the given device
     * state.
     *
     * <p>If the setting for this device state is {@link DEVICE_STATE_ROTATION_LOCK_IGNORED}, it
     * will return the setting for the fallback device state.
     *
     * <p>If no fallback is specified for this device state, it will return {@link
     * DEVICE_STATE_ROTATION_LOCK_IGNORED}.
     */
    @Settings.Secure.DeviceStateRotationLockSetting
    public int getRotationLockSetting(int deviceState) {
        int devicePosture = mPosturesHelper.deviceStateToPosture(deviceState);
        int rotationLockSetting = mPostureRotationLockSettings.get(
                devicePosture, /* valueIfKeyNotFound= */ DEVICE_STATE_ROTATION_LOCK_IGNORED);
        if (rotationLockSetting == DEVICE_STATE_ROTATION_LOCK_IGNORED) {
            rotationLockSetting = getFallbackRotationLockSetting(devicePosture);
        }
        return rotationLockSetting;
    }

    private int getFallbackRotationLockSetting(int devicePosture) {
        int indexOfFallback = mPostureRotationLockFallbackSettings.indexOfKey(devicePosture);
        if (indexOfFallback < 0) {
            Log.w(TAG, "Setting is ignored, but no fallback was specified.");
            return DEVICE_STATE_ROTATION_LOCK_IGNORED;
        }
        int fallbackPosture = mPostureRotationLockFallbackSettings.valueAt(indexOfFallback);
        return mPostureRotationLockSettings.get(fallbackPosture,
                /* valueIfKeyNotFound= */ DEVICE_STATE_ROTATION_LOCK_IGNORED);
    }


    /** Returns true if the rotation is locked for the current device state */
    public boolean isRotationLocked(int deviceState) {
        return getRotationLockSetting(deviceState) == DEVICE_STATE_ROTATION_LOCK_LOCKED;
    }

    /**
     * Returns true if there is no device state for which the current setting is {@link
     * DEVICE_STATE_ROTATION_LOCK_UNLOCKED}.
     */
    public boolean isRotationLockedForAllStates() {
        for (int i = 0; i < mPostureRotationLockSettings.size(); i++) {
            if (mPostureRotationLockSettings.valueAt(i)
                    == DEVICE_STATE_ROTATION_LOCK_UNLOCKED) {
                return false;
            }
        }
        return true;
    }

    /** Returns a list of device states and their respective auto-rotation setting availability. */
    public List<SettableDeviceState> getSettableDeviceStates() {
        // Returning a copy to make sure that nothing outside can mutate our internal list.
        return new ArrayList<>(mSettableDeviceStates);
    }

    private void initializeInMemoryMap() {
        String serializedSetting = getPersistedSettingValue();
        if (TextUtils.isEmpty(serializedSetting)) {
            // No settings saved, we should load the defaults and persist them.
            fallbackOnDefaults();
            return;
        }
        String[] values = serializedSetting.split(SEPARATOR_REGEX);
        if (values.length % 2 != 0) {
            // Each entry should be a key/value pair, so this is corrupt.
            Log.wtf(TAG, "Can't deserialize saved settings, falling back on defaults");
            fallbackOnDefaults();
            return;
        }
        mPostureRotationLockSettings = new SparseIntArray(values.length / 2);
        int key;
        int value;

        for (int i = 0; i < values.length - 1; ) {
            try {
                key = Integer.parseInt(values[i++]);
                value = Integer.parseInt(values[i++]);
                boolean isPersistedValueIgnored = value == DEVICE_STATE_ROTATION_LOCK_IGNORED;
                boolean isDefaultValueIgnored = mPostureDefaultRotationLockSettings.get(key)
                        == DEVICE_STATE_ROTATION_LOCK_IGNORED;
                if (isPersistedValueIgnored != isDefaultValueIgnored) {
                    Log.w(TAG, "Conflict for ignored device state " + key
                            + ". Falling back on defaults");
                    fallbackOnDefaults();
                    return;
                }
                mPostureRotationLockSettings.put(key, value);
            } catch (NumberFormatException e) {
                Log.wtf(TAG, "Error deserializing one of the saved settings", e);
                fallbackOnDefaults();
                return;
            }
        }
    }

    /**
     * Resets the state of the class and saved settings back to the default values provided by the
     * resources config.
     */
    @VisibleForTesting
    public void resetStateForTesting(Resources resources) {
        mPostureRotationLockDefaults =
                resources.getStringArray(R.array.config_perDeviceStateRotationLockDefaults);
        fallbackOnDefaults();
    }

    private void fallbackOnDefaults() {
        loadDefaults();
        persistSettings();
    }

    private void persistSettings() {
        if (mPostureRotationLockSettings.size() == 0) {
            persistSettingIfChanged(/* newSettingValue= */ "");
            return;
        }

        StringBuilder stringBuilder = new StringBuilder();
        stringBuilder
                .append(mPostureRotationLockSettings.keyAt(0))
                .append(SEPARATOR_REGEX)
                .append(mPostureRotationLockSettings.valueAt(0));

        for (int i = 1; i < mPostureRotationLockSettings.size(); i++) {
            stringBuilder
                    .append(SEPARATOR_REGEX)
                    .append(mPostureRotationLockSettings.keyAt(i))
                    .append(SEPARATOR_REGEX)
                    .append(mPostureRotationLockSettings.valueAt(i));
        }
        persistSettingIfChanged(stringBuilder.toString());
    }

    private void persistSettingIfChanged(String newSettingValue) {
        String lastSettingValue = getPersistedSettingValue();
        Log.v(TAG, "persistSettingIfChanged: "
                + "last=" + lastSettingValue + ", "
                + "new=" + newSettingValue);
        if (TextUtils.equals(lastSettingValue, newSettingValue)) {
            return;
        }
        mSecureSettings.putStringForUser(
                Settings.Secure.DEVICE_STATE_ROTATION_LOCK,
                /* value= */ newSettingValue,
                UserHandle.USER_CURRENT);
    }

    private String getPersistedSettingValue() {
        return mSecureSettings.getStringForUser(
                Settings.Secure.DEVICE_STATE_ROTATION_LOCK,
                UserHandle.USER_CURRENT);
    }

    private void loadDefaults() {
        mSettableDeviceStates = new ArrayList<>(mPostureRotationLockDefaults.length);
        mPostureDefaultRotationLockSettings = new SparseIntArray(
                mPostureRotationLockDefaults.length);
        mPostureRotationLockSettings = new SparseIntArray(mPostureRotationLockDefaults.length);
        mPostureRotationLockFallbackSettings = new SparseIntArray(1);
        for (String entry : mPostureRotationLockDefaults) {
            String[] values = entry.split(SEPARATOR_REGEX);
            try {
                int posture = Integer.parseInt(values[0]);
                int rotationLockSetting = Integer.parseInt(values[1]);
                if (rotationLockSetting == DEVICE_STATE_ROTATION_LOCK_IGNORED) {
                    if (values.length == 3) {
                        int fallbackPosture = Integer.parseInt(values[2]);
                        mPostureRotationLockFallbackSettings.put(posture, fallbackPosture);
                    } else {
                        Log.w(TAG,
                                "Rotation lock setting is IGNORED, but values have unexpected "
                                        + "size of "
                                        + values.length);
                    }
                }
                boolean isSettable = rotationLockSetting != DEVICE_STATE_ROTATION_LOCK_IGNORED;
                Integer deviceState = mPosturesHelper.postureToDeviceState(posture);
                if (deviceState != null) {
                    mSettableDeviceStates.add(new SettableDeviceState(deviceState, isSettable));
                } else {
                    Log.wtf(TAG, "No matching device state for posture: " + posture);
                }
                mPostureRotationLockSettings.put(posture, rotationLockSetting);
                mPostureDefaultRotationLockSettings.put(posture, rotationLockSetting);
            } catch (NumberFormatException e) {
                Log.wtf(TAG, "Error parsing settings entry. Entry was: " + entry, e);
                return;
            }
        }
    }

    /** Dumps internal state. */
    public void dump(IndentingPrintWriter pw) {
        pw.println("DeviceStateRotationLockSettingsManager");
        pw.increaseIndent();
        pw.println("mPostureRotationLockDefaults: "
                + Arrays.toString(mPostureRotationLockDefaults));
        pw.println("mPostureDefaultRotationLockSettings: " + mPostureDefaultRotationLockSettings);
        pw.println("mDeviceStateRotationLockSettings: " + mPostureRotationLockSettings);
        pw.println("mPostureRotationLockFallbackSettings: " + mPostureRotationLockFallbackSettings);
        pw.println("mSettableDeviceStates: " + mSettableDeviceStates);
        pw.decreaseIndent();
    }

    /**
     * Called when the persisted settings have changed, requiring a reinitialization of the
     * in-memory map.
     */
    @VisibleForTesting
    public void onPersistedSettingsChanged() {
        initializeInMemoryMap();
        notifyListeners();
    }

    private void notifyListeners() {
        for (DeviceStateRotationLockSettingsListener r : mListeners) {
            r.onSettingsChanged();
        }
    }

    /** Listener for changes in device-state based rotation lock settings */
    public interface DeviceStateRotationLockSettingsListener {
        /** Called whenever the settings have changed. */
        void onSettingsChanged();
    }

    /** Represents a device state and whether it has an auto-rotation setting. */
    public static class SettableDeviceState {
        private final int mDeviceState;
        private final boolean mIsSettable;

        SettableDeviceState(int deviceState, boolean isSettable) {
            mDeviceState = deviceState;
            mIsSettable = isSettable;
        }

        /** Returns the device state associated with this object. */
        public int getDeviceState() {
            return mDeviceState;
        }

        /** Returns whether there is an auto-rotation setting for this device state. */
        public boolean isSettable() {
            return mIsSettable;
        }

        @Override
        public boolean equals(Object o) {
            if (this == o) return true;
            if (!(o instanceof SettableDeviceState)) return false;
            SettableDeviceState that = (SettableDeviceState) o;
            return mDeviceState == that.mDeviceState && mIsSettable == that.mIsSettable;
        }

        @Override
        public int hashCode() {
            return Objects.hash(mDeviceState, mIsSettable);
        }

        @Override
        public String toString() {
            return "SettableDeviceState{"
                    + "mDeviceState=" + mDeviceState
                    + ", mIsSettable=" + mIsSettable
                    + '}';
        }
    }
}
