import 'package:flutter/foundation.dart';
import 'package:hive_ce/hive.dart';

import '../const/app.dart';
import '../utils/app_data_dir/app_data_dir.dart';
import '../utils/same_types.dart';
import 'theme_service.dart';
import 'theme_service_hive_adapters.dart';

// Set the bool flag to true to show debug prints. Even if it is forgotten
// to set it to false, debug prints will not show in release builds.
// The handy part is that if it gets in the way in debugging, it is an easy
// toggle to turn it off here too. Often I just leave them true if it is one
// I want to see in dev mode, unless it is too chatty.
// ignore: no_literal_bool_comparisons, used for debug config.
const bool _debug = !kReleaseMode && true;

/// A [ThemeService] implementation that stores and retrieves theme settings
/// locally using the package Hive: https://pub.dev/packages/hive
class ThemeServiceHive implements ThemeService {
  ThemeServiceHive(this.boxName);

  /// The name of the Hive storage box.
  ///
  /// This is the filename without any extension or path, to use for
  /// the Hive storage box, for example: 'my_app_settings'
  final String boxName;

  // Holds an instance to Hive box, must be initialized
  // by the init call before accessing the storage box.
  late final Box<dynamic> _hiveBox;

  /// ThemeServiceHive's init implementation. Must call be before accessing
  /// the storage box.
  ///
  /// - Registers Hive data type adapters for our enum values
  /// - Gets a usable platform appropriate folder where data can be stored.
  /// - Open the box in the folder with name given via class constructor.
  /// - Assign box to local Hive box instance.
  @override
  Future<void> init() async {
    // First register all Hive data type adapters. Used for our enum values.
    registerHiveAdapters();
    // Get platform compatible storage folder for the Hive box,
    // this setup should work on all Flutter platforms. Hive does not do this
    // right, the folder we got with it did not work on Windows. This
    // implementation works and it uses the same folder that SharedPreferences
    // does.
    final String appDataDir = await getAppDataDir();
    // To make it easier to find the files on your device, this should help.
    // Usually you find the "shared_preferences.json" file in the same folder
    // that the ThemeServicePref creates with SharedPreferences. You cannot
    // set the name on that file so all examples would have shared the same
    // settings on local builds if SharedPreferences would have been used for
    // all examples. Wanted to avoid that, which we can do with Hive. Sure we
    // could have used only Hive too, but SharedPreferences is a very popular
    // choice for this type of feature. I wanted to show how it can be
    // used as well. We always show this path info in none release builds.
    if (_debug) {
      debugPrint(
          'Hive using storage path: $appDataDir and file name: $boxName');
    }
    // Init the Hive box box giving it the platform usable folder.
    Hive.init(appDataDir);
    // Open the Hive box with passed in name, we just keep it open all the
    // time in this demo app.
    await Hive.openBox<dynamic>(boxName);
    // Assign the box to our instance.
    _hiveBox = Hive.box<dynamic>(boxName);
  }

  // Register all custom Hive data adapters.
  void registerHiveAdapters() {
    Hive.registerAdapter(ThemeModeAdapter());
    Hive.registerAdapter(ColorAdapter());
    Hive.registerAdapter(FlexSchemeAdapter());
    Hive.registerAdapter(FlexSurfaceModeAdapter());
    Hive.registerAdapter(FlexInputBorderTypeAdapter());
    Hive.registerAdapter(FlexTabBarStyleAdapter());
    Hive.registerAdapter(FlexAppBarStyleAdapter());
    Hive.registerAdapter(FlexSystemNavBarStyleAdapter());
    Hive.registerAdapter(FlexSchemeColorAdapter());
    Hive.registerAdapter(NavigationDestinationLabelBehaviorAdapter());
    Hive.registerAdapter(NavigationRailLabelTypeAdapter());
    Hive.registerAdapter(FlexSliderIndicatorTypeAdapter());
    Hive.registerAdapter(ShowValueIndicatorAdapter());
    Hive.registerAdapter(TabBarIndicatorSizeAdapter());
    Hive.registerAdapter(AdaptiveThemeAdapter());
    Hive.registerAdapter(FlexPaletteTypeAdapter());
    Hive.registerAdapter(VisualDensityEnumAdapter());
    Hive.registerAdapter(SplashTypeEnumAdapter());
    Hive.registerAdapter(TabAlignmentAdapter());
    Hive.registerAdapter(FlexFixedColorStyleAdapter());
    Hive.registerAdapter(ListTileStyleAdapter());
    Hive.registerAdapter(ListTileTitleAlignmentAdapter());
    Hive.registerAdapter(ListTileControlAffinityAdapter());
    Hive.registerAdapter(MaterialTapTargetSizeAdapter());
    Hive.registerAdapter(FlexScaffoldBaseColorAdapter());
    Hive.registerAdapter(ClipAdapter());
    Hive.registerAdapter(TabIndicatorAnimationAdapter());
    Hive.registerAdapter(StrokeCapAdapter());
  }

  // ----------

  /// Loads a setting from the Theme service, using a key to access it from
  /// the Hive storage box.
  ///
  /// If type `T` is not an atomic Dart type, there must be a
  /// Hive type adapter that converts `T` into one.
  @override
  Future<T> load<T>(String key, T defaultValue) async {
    try {
      final dynamic value = await _hiveBox.get(key, defaultValue: defaultValue);
      if (_debug) {
        debugPrint('Hive LOAD _______________');
        debugPrint(' Store key     : $key');
        debugPrint(' Type expected : $T');
        debugPrint(' Stored value  : $value');
        debugPrint(' Stored type   : ${value.runtimeType}');
        debugPrint(' Default value : $defaultValue');
        debugPrint(' Default type  : ${defaultValue.runtimeType}');
      }
      // Add workaround for Hive WASM returning double instead of int, when
      // values saved to IndexedDb were int.
      // See issue: https://github.com/IO-Design-Team/hive_ce/issues/46
      if (App.isRunningWithWasm &&
          value != null &&
          (value is double) &&
          (sameTypes<T, int?>() || sameTypes<T, int>())) {
        final T loaded = value.round() as T;
        if (_debug) {
          debugPrint(' ** WASM Issue : Expected int but got double, '
              'returning as int: $loaded');
        }
        return loaded;
      } else {
        return value as T;
      }
    } catch (e) {
      debugPrint('Hive load (get) ERROR');
      debugPrint(' Error message ...... : $e');
      debugPrint(' Store key .......... : $key');
      debugPrint(' defaultValue ....... : $defaultValue');
      if (e is HiveError && e.message.contains('missing type adapter')) {
        // Skip the offending key
        debugPrint(' Missing type adapter : SKIP and return default');
      }
      // If something goes wrong we return the default value.
      return defaultValue;
    }
  }

  /// Save a setting to the Theme service with the Hive storage box,
  /// using key, as key for the value.
  ///
  /// If type `T` is not an atomic Dart type, there must be a
  /// Hive type adapter that converts `T` into one.
  @override
  Future<void> save<T>(String key, T value) async {
    try {
      await _hiveBox.put(key, value);
      if (_debug) {
        debugPrint('Hive SAVE _______________');
        debugPrint(' Type  : $key as ${value.runtimeType}');
        debugPrint(' Value : $key as $value');
      }
    } catch (e) {
      debugPrint('Hive save (put) ERROR');
      debugPrint(' Error message ...... : $e');
      debugPrint(' Store key .......... : $key');
      debugPrint(' Save value ......... : $value');
    }
  }

  /// Get all stored key-value paris from the SharedPreferences storage.
  @override
  Map<String, dynamic> getAll() {
    // Filter out entries where the value is null.
    final Map<String, dynamic> result = <String, dynamic>{};
    for (final MapEntry<dynamic, dynamic> entry in _hiveBox.toMap().entries) {
      if (entry.value != null) {
        result[entry.key.toString()] = entry.value;
      }
    }
    return result;
  }

  /// Put all key-value pairs into the Hive storage.
  @override
  Future<void> putAll(
    Map<String, dynamic> values, {
    bool clearExisting = true,
  }) async {
    if (clearExisting) {
      await _hiveBox.clear();
    }

    return _hiveBox.putAll(values);
  }

  /// This implementation supports export and import of stored values.
  @override
  bool get supportsExportImport => true;

  /// Clear all stored values.
  @override
  Future<void> clearAll() async {
    await _hiveBox.clear();
  }
}
