// Copyright (c) 2022, the Dart project authors.  Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.

@JS()
library;

import 'dart:async';
import 'dart:convert';
// TODO: https://github.com/dart-lang/webdev/issues/2508
// ignore: deprecated_member_use
import 'dart:js_util';

import 'package:js/js.dart';

import 'chrome_api.dart';
import 'data_serializers.dart';
import 'logger.dart';
import 'utils.dart';

enum StorageObject {
  debugInfo,
  devToolsOpener,
  devToolsUri,
  encodedUri,
  isAuthenticated,
  multipleAppsDetected;

  Persistence get persistence => switch (this) {
        StorageObject.debugInfo => Persistence.sessionOnly,
        StorageObject.devToolsOpener => Persistence.acrossSessions,
        StorageObject.devToolsUri => Persistence.sessionOnly,
        StorageObject.encodedUri => Persistence.sessionOnly,
        StorageObject.isAuthenticated => Persistence.sessionOnly,
        StorageObject.multipleAppsDetected => Persistence.sessionOnly
      };
}

enum Persistence {
  sessionOnly,
  acrossSessions;
}

Future<bool> setStorageObject<T>({
  required StorageObject type,
  required T value,
  int? tabId,
  void Function()? callback,
}) {
  final storageKey = _createStorageKey(type, tabId);
  final json =
      value is String ? value : jsonEncode(serializers.serialize(value));
  final storageObj = <String, String>{storageKey: json};
  final completer = Completer<bool>();
  final storageArea = _getStorageArea(type.persistence);
  storageArea.set(
    jsify(storageObj),
    allowInterop(() {
      if (callback != null) {
        callback();
      }
      debugLog('Set: $json', prefix: storageKey);
      completer.complete(true);
    }),
  );
  return completer.future;
}

Future<T?> fetchStorageObject<T>({required StorageObject type, int? tabId}) {
  final storageKey = _createStorageKey(type, tabId);
  final completer = Completer<T?>();
  final storageArea = _getStorageArea(type.persistence);
  storageArea.get(
    [storageKey],
    allowInterop((Object? storageObj) {
      if (storageObj == null) {
        debugWarn('Does not exist.', prefix: storageKey);
        completer.complete(null);
        return;
      }
      final json = getProperty(storageObj, storageKey) as String?;
      if (json == null) {
        debugWarn('Does not exist.', prefix: storageKey);
        completer.complete(null);
      } else {
        debugLog('Fetched: $json', prefix: storageKey);
        if (T == String) {
          completer.complete(json as T);
        } else {
          final value = serializers.deserialize(jsonDecode(json)) as T;
          completer.complete(value);
        }
      }
    }),
  );
  return completer.future;
}

Future<List<T>> fetchAllStorageObjectsOfType<T>({required StorageObject type}) {
  final completer = Completer<List<T>>();
  final storageArea = _getStorageArea(type.persistence);
  storageArea.get(
    null,
    allowInterop((Object? storageContents) {
      if (storageContents == null) {
        debugWarn('No storage objects of type exist.', prefix: type.name);
        completer.complete([]);
        return;
      }
      final allKeys = List<String>.from(objectKeys(storageContents));
      final storageKeys = allKeys.where((key) => key.contains(type.name));
      final result = <T>[];
      for (final key in storageKeys) {
        final json = getProperty(storageContents, key) as String?;
        if (json != null) {
          if (T == String) {
            result.add(json as T);
          } else {
            result.add(serializers.deserialize(jsonDecode(json)) as T);
          }
        }
      }
      completer.complete(result);
    }),
  );
  return completer.future;
}

Future<bool> removeStorageObject({required StorageObject type, int? tabId}) {
  final storageKey = _createStorageKey(type, tabId);
  final completer = Completer<bool>();
  final storageArea = _getStorageArea(type.persistence);
  storageArea.remove(
    [storageKey],
    allowInterop(() {
      debugLog('Removed object.', prefix: storageKey);
      completer.complete(true);
    }),
  );
  return completer.future;
}

void interceptStorageChange<T>({
  required Object storageObj,
  required StorageObject expectedType,
  required void Function(T? storageObj) changeHandler,
  int? tabId,
}) {
  try {
    final expectedStorageKey = _createStorageKey(expectedType, tabId);
    final isExpected = hasProperty(storageObj, expectedStorageKey);
    if (!isExpected) return;

    final objProp = getProperty(storageObj, expectedStorageKey);
    final json = getProperty(objProp, 'newValue') as String?;
    T? decodedObj;
    if (json == null || T == String) {
      decodedObj = json as T?;
    } else {
      decodedObj = serializers.deserialize(jsonDecode(json)) as T?;
    }
    debugLog('Intercepted $expectedStorageKey change: $json');
    return changeHandler(decodedObj);
  } catch (error) {
    debugError(
      'Error intercepting storage object with type $expectedType: $error',
    );
  }
}

StorageArea _getStorageArea(Persistence persistence) {
  // MV2 extensions don't have access to session storage:
  if (!isMV3) return chrome.storage.local;

  return switch (persistence) {
    Persistence.acrossSessions => chrome.storage.local,
    Persistence.sessionOnly => chrome.storage.session
  };
}

String _createStorageKey(StorageObject type, int? tabId) {
  if (tabId == null) return type.name;
  return '$tabId-${type.name}';
}
