// Copyright 2013 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import 'dart:async';

import 'package:flutter/foundation.dart' show visibleForTesting;
import 'package:shared_preferences_platform_interface/shared_preferences_platform_interface.dart';
import 'package:shared_preferences_platform_interface/types.dart';

/// Wraps NSUserDefaults (on iOS) and SharedPreferences (on Android), providing
/// a persistent store for simple data.
///
/// Data is persisted to disk asynchronously.
///
/// This is a legacy API. For new code, consider [SharedPreferencesAsync] or [SharedPreferencesWithCache].
class SharedPreferences {
  SharedPreferences._(this._preferenceCache);

  static String _prefix = 'flutter.';

  static bool _prefixHasBeenChanged = false;

  static Set<String>? _allowList;

  static Completer<SharedPreferences>? _completer;

  static SharedPreferencesStorePlatform get _store =>
      SharedPreferencesStorePlatform.instance;

  /// Sets the prefix that is attached to all keys for all shared preferences
  /// read or written via this class.
  ///
  /// This changes the inputs when adding data to preferences as well as
  /// setting the filter that determines what data will be returned
  /// from the `getInstance` method.
  ///
  /// By default, the prefix is 'flutter.', which is compatible with the
  /// previous behavior of this plugin. To use preferences with no prefix,
  /// set [prefix] to ''.
  ///
  /// If [prefix] is set to '', you may encounter preferences that are
  /// incompatible with shared_preferences. The optional parameter
  /// [allowList] will cause the plugin to only return preferences that
  /// are both contained in the list AND match the provided prefix.
  ///
  /// If [prefix] is changed, and an [allowList] is used, the prefix must be included
  /// on the keys added to the [allowList].
  ///
  /// No migration of existing preferences is performed by this method.
  /// If you set a different prefix, and have previously stored preferences,
  /// you will need to handle any migration yourself.
  ///
  /// This cannot be called after `getInstance`.
  static void setPrefix(String prefix, {Set<String>? allowList}) {
    if (_completer != null) {
      throw StateError('setPrefix cannot be called after getInstance');
    }
    _prefix = prefix;
    _prefixHasBeenChanged = true;
    _allowList = allowList;
  }

  /// Resets class's static values to allow for testing of setPrefix flow.
  @visibleForTesting
  static void resetStatic() {
    _completer = null;
    _prefix = 'flutter.';
    _prefixHasBeenChanged = false;
    _allowList = null;
  }

  /// Loads and parses the [SharedPreferences] for this app from disk.
  ///
  /// Because this is reading from disk, it shouldn't be awaited in
  /// performance-sensitive blocks.
  static Future<SharedPreferences> getInstance() async {
    if (_completer == null) {
      final Completer<SharedPreferences> completer =
          Completer<SharedPreferences>();
      _completer = completer;
      try {
        final Map<String, Object> preferencesMap =
            await _getSharedPreferencesMap();
        completer.complete(SharedPreferences._(preferencesMap));
      } catch (e) {
        // If there's an error, explicitly return the future with an error.
        // then set the completer to null so we can retry.
        completer.completeError(e);
        final Future<SharedPreferences> sharedPrefsFuture = completer.future;
        _completer = null;
        return sharedPrefsFuture;
      }
    }
    return _completer!.future;
  }

  /// The cache that holds all preferences.
  ///
  /// It is instantiated to the current state of the SharedPreferences or
  /// NSUserDefaults object and then kept in sync via setter methods in this
  /// class.
  ///
  /// It is NOT guaranteed that this cache and the device prefs will remain
  /// in sync since the setter method might fail for any reason.
  final Map<String, Object> _preferenceCache;

  /// Returns all keys in the persistent storage.
  Set<String> getKeys() => Set<String>.from(_preferenceCache.keys);

  /// Reads a value of any type from persistent storage.
  Object? get(String key) => _preferenceCache[key];

  /// Reads a value from persistent storage, throwing an exception if it's not a
  /// bool.
  bool? getBool(String key) => _preferenceCache[key] as bool?;

  /// Reads a value from persistent storage, throwing an exception if it's not
  /// an int.
  int? getInt(String key) => _preferenceCache[key] as int?;

  /// Reads a value from persistent storage, throwing an exception if it's not a
  /// double.
  double? getDouble(String key) => _preferenceCache[key] as double?;

  /// Reads a value from persistent storage, throwing an exception if it's not a
  /// String.
  String? getString(String key) => _preferenceCache[key] as String?;

  /// Returns true if the persistent storage contains the given [key].
  bool containsKey(String key) => _preferenceCache.containsKey(key);

  /// Reads a set of string values from persistent storage, throwing an
  /// exception if it's not a string list.
  List<String>? getStringList(String key) {
    List<dynamic>? list = _preferenceCache[key] as List<dynamic>?;
    list = list?.cast<String>();
    // Make a copy of the list so that later mutations won't propagate
    return list?.toList() as List<String>?;
  }

  /// Saves a boolean [value] to persistent storage in the background.
  Future<bool> setBool(String key, bool value) => _setValue('Bool', key, value);

  /// Saves an integer [value] to persistent storage in the background.
  Future<bool> setInt(String key, int value) => _setValue('Int', key, value);

  /// Saves a double [value] to persistent storage in the background.
  ///
  /// Android doesn't support storing doubles, so it will be stored as a float.
  Future<bool> setDouble(String key, double value) =>
      _setValue('Double', key, value);

  /// Saves a string [value] to persistent storage in the background.
  ///
  /// Note: Due to limitations in Android's SharedPreferences,
  /// values cannot start with any one of the following:
  ///
  /// - 'VGhpcyBpcyB0aGUgcHJlZml4IGZvciBhIGxpc3Qu'
  /// - 'VGhpcyBpcyB0aGUgcHJlZml4IGZvciBCaWdJbnRlZ2Vy'
  /// - 'VGhpcyBpcyB0aGUgcHJlZml4IGZvciBEb3VibGUu'
  Future<bool> setString(String key, String value) =>
      _setValue('String', key, value);

  /// Saves a list of strings [value] to persistent storage in the background.
  Future<bool> setStringList(String key, List<String> value) =>
      _setValue('StringList', key, value);

  /// Removes an entry from persistent storage.
  Future<bool> remove(String key) {
    final String prefixedKey = '$_prefix$key';
    _preferenceCache.remove(key);
    return _store.remove(prefixedKey);
  }

  Future<bool> _setValue(String valueType, String key, Object value) {
    ArgumentError.checkNotNull(value, 'value');
    final String prefixedKey = '$_prefix$key';
    if (value is List<String>) {
      // Make a copy of the list so that later mutations won't propagate
      _preferenceCache[key] = value.toList();
    } else {
      _preferenceCache[key] = value;
    }
    return _store.setValue(valueType, prefixedKey, value);
  }

  /// Always returns true.
  /// On iOS, synchronize is marked deprecated. On Android, we commit every set.
  @Deprecated('This method is now a no-op, and should no longer be called.')
  Future<bool> commit() async => true;

  /// Completes with true once the user preferences for the app has been cleared.
  Future<bool> clear() {
    _preferenceCache.clear();
    if (_prefixHasBeenChanged) {
      try {
        return _store.clearWithParameters(
          ClearParameters(
            filter: PreferencesFilter(
              prefix: _prefix,
              allowList: _allowList,
            ),
          ),
        );
      } catch (e) {
        // Catching and clarifying UnimplementedError to provide a more robust message.
        if (e is UnimplementedError) {
          throw UnimplementedError('''
This implementation of Shared Preferences doesn't yet support the setPrefix method.
Either update the implementation to support setPrefix, or do not call setPrefix.
        ''');
        } else {
          rethrow;
        }
      }
    }
    return _store.clear();
  }

  /// Fetches the latest values from the host platform.
  ///
  /// Use this method to observe modifications that were made in native code
  /// (without using the plugin) while the app is running.
  Future<void> reload() async {
    final Map<String, Object> preferences =
        await SharedPreferences._getSharedPreferencesMap();
    _preferenceCache.clear();
    _preferenceCache.addAll(preferences);
  }

  static Future<Map<String, Object>> _getSharedPreferencesMap() async {
    final Map<String, Object> fromSystem = <String, Object>{};
    if (_prefixHasBeenChanged) {
      try {
        fromSystem.addAll(
          await _store.getAllWithParameters(
            GetAllParameters(
              filter: PreferencesFilter(
                prefix: _prefix,
                allowList: _allowList,
              ),
            ),
          ),
        );
      } catch (e) {
        // Catching and clarifying UnimplementedError to provide a more robust message.
        if (e is UnimplementedError) {
          throw UnimplementedError('''
This implementation of Shared Preferences doesn't yet support the setPrefix method.
Either update the implementation to support setPrefix, or do not call setPrefix.
        ''');
        } else {
          rethrow;
        }
      }
    } else {
      fromSystem.addAll(await _store.getAll());
    }

    if (_prefix.isEmpty) {
      return fromSystem;
    }
    // Strip the prefix from the returned preferences.
    final Map<String, Object> preferencesMap = <String, Object>{};
    for (final String key in fromSystem.keys) {
      assert(key.startsWith(_prefix));
      preferencesMap[key.substring(_prefix.length)] = fromSystem[key]!;
    }
    return preferencesMap;
  }

  /// Initializes the shared preferences with mock values for testing.
  ///
  /// If the singleton instance has been initialized already, it is nullified.
  @visibleForTesting
  static void setMockInitialValues(Map<String, Object> values) {
    final Map<String, Object> newValues =
        values.map<String, Object>((String key, Object value) {
      String newKey = key;
      if (!key.startsWith(_prefix)) {
        newKey = '$_prefix$key';
      }
      return MapEntry<String, Object>(newKey, value);
    });
    SharedPreferencesStorePlatform.instance =
        InMemorySharedPreferencesStore.withData(newValues);
    _completer = null;
  }
}
