import 'dart:convert';
import 'dart:io';

import 'package:comunic/constants.dart';
import 'package:comunic/utils/flutter_utils.dart';
import 'package:comunic/utils/log_utils.dart';
import 'package:path/path.dart' as path;
import 'package:path_provider/path_provider.dart';

/// Base serialization helper
///
/// @author Pierre Hubert

abstract class SerializableElement<T> extends Comparable<T> {
  Map<String, dynamic> toJson();
}

abstract class BaseSerializationHelper<T extends SerializableElement> {
  /// List cache
  List<T> _cache;

  /// The name of the type of data to serialise
  String get type;

  /// Parse an json entry into a [T] object
  T parse(Map<String, dynamic> m);

  /// Get the file where data should be stored
  Future<File> _getFilePath() async {
    final dir = await getApplicationDocumentsDirectory();
    final targetDir =
        Directory(path.join(dir.absolute.path, SERIALIZATION_DIRECTORY));

    targetDir.create(recursive: true);

    return File(path.join(targetDir.absolute.path, type));
  }

  /// Load the cache
  Future<void> _loadCache() async {
    if (_cache != null) return;

    if (isWeb) {
      _cache = [];
      return;
    }

    try {
      final file = await _getFilePath();

      if (!await file.exists()) return _cache = [];

      final List<dynamic> json = jsonDecode(await file.readAsString());
      _cache = json.cast<Map<String, dynamic>>().map(parse).toList();

      _cache.sort();
    } catch (e, s) {
      logError(e, s);
      print("Failed to read serialized data!");
      _cache = [];
    }
  }

  /// Save the cache to the persistent memory
  Future<void> _saveCache() async {
    if (isWeb) return;

    try {
      final file = await _getFilePath();
      await file.writeAsString(jsonEncode(
          _cache.map((e) => e.toJson()).toList().cast<Map<String, dynamic>>()));
    } catch (e, s) {
      print("Failed to write file!");
      logError(e, s);
    }
  }

  /// Get the current list of elements
  Future<List<T>> getList() async {
    await _loadCache();
    return List.from(_cache);
  }

  /// Set a new list of conversations
  Future<void> setList(List<T> list) async {
    _cache = List.from(list);
    await _saveCache();
  }

  /// Insert new element
  Future<void> insert(T el) async {
    await _loadCache();
    _cache.add(el);
    _cache.sort();
    await _saveCache();
  }

  /// Insert new element
  Future<void> insertMany(List<T> els) async {
    await _loadCache();
    _cache.addAll(els);
    _cache.sort();
    await _saveCache();
  }

  /// Check if any entry in the last match the predicate
  Future<bool> any(bool isContained(T t)) async {
    await _loadCache();
    return _cache.any((element) => isContained(element));
  }

  Future<bool> has(T el) => any((t) => t == el);

  /// Check if any entry in the last match the predicate
  Future<T> first(bool filter(T t)) async {
    await _loadCache();
    return _cache.firstWhere((element) => filter(element));
  }

  /// Replace an element with another one
  Future<void> insertOrReplaceElement(bool isToReplace(T t), T newEl) async {
    await _loadCache();

    // Insert or replace the element
    _cache = _cache.where((element) => !isToReplace(element)).toList();
    _cache.add(newEl);

    _cache.sort();
    await _saveCache();
  }

  /// Insert or replace many elements
  Future<void> insertOrReplaceElements(List<T> list) async {
    await _loadCache();

    _cache.removeWhere((element) => list.any((newEl) => element == newEl));
    _cache.addAll(list);

    await _saveCache();
  }

  /// Remove elements
  Future<void> removeElement(bool isToRemove(T t)) async {
    await _loadCache();
    _cache.removeWhere((element) => isToRemove(element));
    await _saveCache();
  }

  /// Remove all elements
  Future<void> removeAll() async {
    _cache = [];
    await _saveCache();
  }
}