import 'veilid.dart';

import 'dart:html' as html;
import 'dart:js' as js;
import 'dart:js_util' as js_util;
import 'dart:async';
import 'dart:convert';
import 'dart:typed_data';

import 'veilid_encoding.dart';

//////////////////////////////////////////////////////////

Veilid getVeilid() => VeilidJS();

Object wasm = js_util.getProperty(html.window, "veilid_wasm");

Future<T> _wrapApiPromise<T>(Object p) {
  return js_util.promiseToFuture(p).then((value) => value as T).catchError(
      (error) => Future<T>.error(
          VeilidAPIException.fromJson(jsonDecode(error as String))));
}

class _Ctx {
  final int id;
  final VeilidJS js;
  _Ctx(this.id, this.js);
}

// JS implementation of VeilidRoutingContext
class VeilidRoutingContextJS implements VeilidRoutingContext {
  final _Ctx _ctx;
  static final Finalizer<_Ctx> _finalizer = Finalizer(
      (ctx) => js_util.callMethod(wasm, "release_routing_context", [ctx.id]));

  VeilidRoutingContextJS._(this._ctx) {
    _finalizer.attach(this, _ctx, detach: this);
  }

  @override
  VeilidRoutingContextJS withPrivacy() {
    int newId =
        js_util.callMethod(wasm, "routing_context_with_privacy", [_ctx.id]);
    return VeilidRoutingContextJS._(_Ctx(newId, _ctx.js));
  }

  @override
  VeilidRoutingContextJS withCustomPrivacy(SafetySelection safetySelection) {
    final newId = js_util.callMethod(
        wasm,
        "routing_context_with_custom_privacy",
        [_ctx.id, jsonEncode(safetySelection)]);

    return VeilidRoutingContextJS._(_Ctx(newId, _ctx.js));
  }

  @override
  VeilidRoutingContextJS withSequencing(Sequencing sequencing) {
    final newId = js_util.callMethod(wasm, "routing_context_with_sequencing",
        [_ctx.id, jsonEncode(sequencing)]);
    return VeilidRoutingContextJS._(_Ctx(newId, _ctx.js));
  }

  @override
  Future<Uint8List> appCall(String target, Uint8List request) async {
    var encodedRequest = base64UrlNoPadEncode(request);

    return base64UrlNoPadDecode(await _wrapApiPromise(js_util.callMethod(
        wasm, "routing_context_app_call", [_ctx.id, target, encodedRequest])));
  }

  @override
  Future<void> appMessage(String target, Uint8List message) {
    var encodedMessage = base64UrlNoPadEncode(message);

    return _wrapApiPromise(js_util.callMethod(wasm,
        "routing_context_app_message", [_ctx.id, target, encodedMessage]));
  }

  @override
  Future<DHTRecordDescriptor> createDHTRecord(DHTSchema schema,
      {CryptoKind kind = 0}) async {
    return DHTRecordDescriptor.fromJson(jsonDecode(await _wrapApiPromise(js_util
        .callMethod(wasm, "routing_context_create_dht_record",
            [_ctx.id, jsonEncode(schema), kind]))));
  }

  @override
  Future<DHTRecordDescriptor> openDHTRecord(
      TypedKey key, KeyPair? writer) async {
    return DHTRecordDescriptor.fromJson(jsonDecode(await _wrapApiPromise(js_util
        .callMethod(wasm, "routing_context_open_dht_record", [
      _ctx.id,
      jsonEncode(key),
      writer != null ? jsonEncode(writer) : null
    ]))));
  }

  @override
  Future<void> closeDHTRecord(TypedKey key) {
    return _wrapApiPromise(js_util.callMethod(
        wasm, "routing_context_close_dht_record", [_ctx.id, jsonEncode(key)]));
  }

  @override
  Future<void> deleteDHTRecord(TypedKey key) {
    return _wrapApiPromise(js_util.callMethod(
        wasm, "routing_context_delete_dht_record", [_ctx.id, jsonEncode(key)]));
  }

  @override
  Future<ValueData?> getDHTValue(
      TypedKey key, int subkey, bool forceRefresh) async {
    final opt = await _wrapApiPromise(js_util.callMethod(
        wasm,
        "routing_context_get_dht_value",
        [_ctx.id, jsonEncode(key), subkey, forceRefresh]));
    return opt == null ? null : ValueData.fromJson(jsonDecode(opt));
  }

  @override
  Future<ValueData?> setDHTValue(
      TypedKey key, int subkey, Uint8List data) async {
    final opt = await _wrapApiPromise(js_util.callMethod(
        wasm,
        "routing_context_set_dht_value",
        [_ctx.id, jsonEncode(key), subkey, base64UrlNoPadEncode(data)]));
    return opt == null ? null : ValueData.fromJson(jsonDecode(opt));
  }

  @override
  Future<Timestamp> watchDHTValues(TypedKey key, List<ValueSubkeyRange> subkeys,
      Timestamp expiration, int count) async {
    final ts = await _wrapApiPromise(js_util.callMethod(
        wasm, "routing_context_watch_dht_values", [
      _ctx.id,
      jsonEncode(key),
      jsonEncode(subkeys),
      expiration.toString(),
      count
    ]));
    return Timestamp.fromString(ts);
  }

  @override
  Future<bool> cancelDHTWatch(TypedKey key, List<ValueSubkeyRange> subkeys) {
    return _wrapApiPromise(js_util.callMethod(
        wasm,
        "routing_context_cancel_dht_watch",
        [_ctx.id, jsonEncode(key), jsonEncode(subkeys)]));
  }
}

// JS implementation of VeilidCryptoSystem
class VeilidCryptoSystemJS implements VeilidCryptoSystem {
  final CryptoKind _kind;
  final VeilidJS _js;

  VeilidCryptoSystemJS._(this._js, this._kind) {
    // Keep the reference
    _js;
  }

  @override
  CryptoKind kind() {
    return _kind;
  }

  @override
  Future<SharedSecret> cachedDH(PublicKey key, SecretKey secret) async {
    return SharedSecret.fromJson(jsonDecode(await _wrapApiPromise(js_util
        .callMethod(wasm, "crypto_cached_dh",
            [_kind, jsonEncode(key), jsonEncode(secret)]))));
  }

  @override
  Future<SharedSecret> computeDH(PublicKey key, SecretKey secret) async {
    return SharedSecret.fromJson(jsonDecode(await _wrapApiPromise(js_util
        .callMethod(wasm, "crypto_compute_dh",
            [_kind, jsonEncode(key), jsonEncode(secret)]))));
  }

  @override
  Future<Uint8List> randomBytes(int len) async {
    return base64UrlNoPadDecode(await _wrapApiPromise(
        js_util.callMethod(wasm, "crypto_random_bytes", [_kind, len])));
  }

  @override
  Future<int> defaultSaltLength() {
    return _wrapApiPromise(
        js_util.callMethod(wasm, "crypto_default_salt_length", [_kind]));
  }

  @override
  Future<String> hashPassword(Uint8List password, Uint8List salt) {
    return _wrapApiPromise(js_util.callMethod(wasm, "crypto_hash_password",
        [_kind, base64UrlNoPadEncode(password), base64UrlNoPadEncode(salt)]));
  }

  @override
  Future<bool> verifyPassword(Uint8List password, String passwordHash) {
    return _wrapApiPromise(js_util.callMethod(wasm, "crypto_verify_password",
        [_kind, base64UrlNoPadEncode(password), passwordHash]));
  }

  @override
  Future<SharedSecret> deriveSharedSecret(
      Uint8List password, Uint8List salt) async {
    return SharedSecret.fromJson(jsonDecode(await _wrapApiPromise(js_util
        .callMethod(wasm, "crypto_derive_shared_secret", [
      _kind,
      base64UrlNoPadEncode(password),
      base64UrlNoPadEncode(salt)
    ]))));
  }

  @override
  Future<Nonce> randomNonce() async {
    return Nonce.fromJson(jsonDecode(await _wrapApiPromise(
        js_util.callMethod(wasm, "crypto_random_nonce", [_kind]))));
  }

  @override
  Future<SharedSecret> randomSharedSecret() async {
    return SharedSecret.fromJson(jsonDecode(await _wrapApiPromise(
        js_util.callMethod(wasm, "crypto_random_shared_secret", [_kind]))));
  }

  @override
  Future<KeyPair> generateKeyPair() async {
    return KeyPair.fromJson(jsonDecode(await _wrapApiPromise(
        js_util.callMethod(wasm, "crypto_generate_key_pair", [_kind]))));
  }

  @override
  Future<HashDigest> generateHash(Uint8List data) async {
    return HashDigest.fromJson(jsonDecode(await _wrapApiPromise(js_util
        .callMethod(wasm, "crypto_generate_hash",
            [_kind, base64UrlNoPadEncode(data)]))));
  }

  @override
  Future<bool> validateKeyPair(PublicKey key, SecretKey secret) {
    return _wrapApiPromise(js_util.callMethod(wasm, "crypto_validate_key_pair",
        [_kind, jsonEncode(key), jsonEncode(secret)]));
  }

  @override
  Future<bool> validateHash(Uint8List data, HashDigest hash) {
    return _wrapApiPromise(js_util.callMethod(wasm, "crypto_validate_hash",
        [_kind, base64UrlNoPadEncode(data), jsonEncode(hash)]));
  }

  @override
  Future<CryptoKeyDistance> distance(CryptoKey key1, CryptoKey key2) async {
    return CryptoKeyDistance.fromJson(jsonDecode(await _wrapApiPromise(js_util
        .callMethod(wasm, "crypto_distance",
            [_kind, jsonEncode(key1), jsonEncode(key2)]))));
  }

  @override
  Future<Signature> sign(
      PublicKey key, SecretKey secret, Uint8List data) async {
    return Signature.fromJson(jsonDecode(await _wrapApiPromise(js_util
        .callMethod(wasm, "crypto_sign", [
      _kind,
      jsonEncode(key),
      jsonEncode(secret),
      base64UrlNoPadEncode(data)
    ]))));
  }

  @override
  Future<void> verify(PublicKey key, Uint8List data, Signature signature) {
    return _wrapApiPromise(js_util.callMethod(wasm, "crypto_verify", [
      _kind,
      jsonEncode(key),
      base64UrlNoPadEncode(data),
      jsonEncode(signature),
    ]));
  }

  @override
  Future<int> aeadOverhead() {
    return _wrapApiPromise(
        js_util.callMethod(wasm, "crypto_aead_overhead", [_kind]));
  }

  @override
  Future<Uint8List> decryptAead(Uint8List body, Nonce nonce,
      SharedSecret sharedSecret, Uint8List? associatedData) async {
    return base64UrlNoPadDecode(
        await _wrapApiPromise(js_util.callMethod(wasm, "crypto_decrypt_aead", [
      _kind,
      base64UrlNoPadEncode(body),
      jsonEncode(nonce),
      jsonEncode(sharedSecret),
      associatedData != null ? base64UrlNoPadEncode(associatedData) : null
    ])));
  }

  @override
  Future<Uint8List> encryptAead(Uint8List body, Nonce nonce,
      SharedSecret sharedSecret, Uint8List? associatedData) async {
    return base64UrlNoPadDecode(
        await _wrapApiPromise(js_util.callMethod(wasm, "crypto_encrypt_aead", [
      _kind,
      base64UrlNoPadEncode(body),
      jsonEncode(nonce),
      jsonEncode(sharedSecret),
      associatedData != null ? base64UrlNoPadEncode(associatedData) : null
    ])));
  }

  @override
  Future<Uint8List> cryptNoAuth(
      Uint8List body, Nonce nonce, SharedSecret sharedSecret) async {
    return base64UrlNoPadDecode(await _wrapApiPromise(js_util.callMethod(
        wasm, "crypto_crypt_no_auth", [
      _kind,
      base64UrlNoPadEncode(body),
      jsonEncode(nonce),
      jsonEncode(sharedSecret)
    ])));
  }
}

class _TDBT {
  final int id;
  VeilidTableDBJS tdbjs;
  VeilidJS js;

  _TDBT(this.id, this.tdbjs, this.js);
}

// JS implementation of VeilidTableDBTransaction
class VeilidTableDBTransactionJS extends VeilidTableDBTransaction {
  final _TDBT _tdbt;
  static final Finalizer<_TDBT> _finalizer = Finalizer((tdbt) =>
      js_util.callMethod(wasm, "release_table_db_transaction", [tdbt.id]));

  VeilidTableDBTransactionJS._(this._tdbt) {
    _finalizer.attach(this, _tdbt, detach: this);
  }

  @override
  Future<void> commit() {
    return _wrapApiPromise(
        js_util.callMethod(wasm, "table_db_transaction_commit", [_tdbt.id]));
  }

  @override
  Future<void> rollback() {
    return _wrapApiPromise(
        js_util.callMethod(wasm, "table_db_transaction_rollback", [_tdbt.id]));
  }

  @override
  Future<void> store(int col, Uint8List key, Uint8List value) {
    final encodedKey = base64UrlNoPadEncode(key);
    final encodedValue = base64UrlNoPadEncode(value);

    return _wrapApiPromise(js_util.callMethod(
        wasm,
        "table_db_transaction_store",
        [_tdbt.id, col, encodedKey, encodedValue]));
  }

  @override
  Future<void> delete(int col, Uint8List key) {
    final encodedKey = base64UrlNoPadEncode(key);

    return _wrapApiPromise(js_util.callMethod(
        wasm, "table_db_transaction_delete", [_tdbt.id, col, encodedKey]));
  }
}

class _TDB {
  final int id;
  VeilidJS js;

  _TDB(this.id, this.js);
}

// JS implementation of VeilidTableDB
class VeilidTableDBJS extends VeilidTableDB {
  final _TDB _tdb;
  static final Finalizer<_TDB> _finalizer = Finalizer(
      (tdb) => js_util.callMethod(wasm, "release_table_db", [tdb.id]));

  VeilidTableDBJS._(this._tdb) {
    _finalizer.attach(this, _tdb, detach: this);
  }

  @override
  int getColumnCount() {
    return js_util.callMethod(wasm, "table_db_get_column_count", [_tdb.id]);
  }

  @override
  Future<List<Uint8List>> getKeys(int col) async {
    return jsonListConstructor(base64UrlNoPadDecodeDynamic)(jsonDecode(
        await js_util.callMethod(wasm, "table_db_get_keys", [_tdb.id, col])));
  }

  @override
  VeilidTableDBTransaction transact() {
    final id = js_util.callMethod(wasm, "table_db_transact", [_tdb.id]);

    return VeilidTableDBTransactionJS._(_TDBT(id, this, _tdb.js));
  }

  @override
  Future<void> store(int col, Uint8List key, Uint8List value) {
    final encodedKey = base64UrlNoPadEncode(key);
    final encodedValue = base64UrlNoPadEncode(value);

    return _wrapApiPromise(js_util.callMethod(
        wasm, "table_db_store", [_tdb.id, col, encodedKey, encodedValue]));
  }

  @override
  Future<Uint8List?> load(int col, Uint8List key) async {
    final encodedKey = base64UrlNoPadEncode(key);

    String? out = await _wrapApiPromise(
        js_util.callMethod(wasm, "table_db_load", [_tdb.id, col, encodedKey]));
    if (out == null) {
      return null;
    }
    return base64UrlNoPadDecode(out);
  }

  @override
  Future<Uint8List?> delete(int col, Uint8List key) {
    final encodedKey = base64UrlNoPadEncode(key);

    return _wrapApiPromise(js_util
        .callMethod(wasm, "table_db_delete", [_tdb.id, col, encodedKey]));
  }
}

// JS implementation of high level Veilid API

class VeilidJS implements Veilid {
  @override
  void initializeVeilidCore(Map<String, dynamic> platformConfigJson) {
    var platformConfigJsonString = jsonEncode(platformConfigJson);
    js_util
        .callMethod(wasm, "initialize_veilid_core", [platformConfigJsonString]);
  }

  @override
  void changeLogLevel(String layer, VeilidConfigLogLevel logLevel) {
    var logLevelJsonString = jsonEncode(logLevel);
    js_util.callMethod(wasm, "change_log_level", [layer, logLevelJsonString]);
  }

  @override
  Future<Stream<VeilidUpdate>> startupVeilidCore(VeilidConfig config) async {
    var streamController = StreamController<VeilidUpdate>();
    updateCallback(String update) {
      var updateJson = jsonDecode(update);
      if (updateJson["kind"] == "Shutdown") {
        streamController.close();
      } else {
        var update = VeilidUpdate.fromJson(updateJson);
        streamController.add(update);
      }
    }

    await _wrapApiPromise(js_util.callMethod(wasm, "startup_veilid_core",
        [js.allowInterop(updateCallback), jsonEncode(config)]));

    return streamController.stream;
  }

  @override
  Future<VeilidState> getVeilidState() async {
    return VeilidState.fromJson(jsonDecode(await _wrapApiPromise(
        js_util.callMethod(wasm, "get_veilid_state", []))));
  }

  @override
  Future<void> attach() {
    return _wrapApiPromise(js_util.callMethod(wasm, "attach", []));
  }

  @override
  Future<void> detach() {
    return _wrapApiPromise(js_util.callMethod(wasm, "detach", []));
  }

  @override
  Future<void> shutdownVeilidCore() {
    return _wrapApiPromise(
        js_util.callMethod(wasm, "shutdown_veilid_core", []));
  }

  @override
  List<CryptoKind> validCryptoKinds() {
    return jsonDecode(js_util.callMethod(wasm, "valid_crypto_kinds", []));
  }

  @override
  Future<VeilidCryptoSystem> getCryptoSystem(CryptoKind kind) async {
    if (!validCryptoKinds().contains(kind)) {
      throw VeilidAPIExceptionGeneric("unsupported cryptosystem");
    }
    return VeilidCryptoSystemJS._(this, kind);
  }

  @override
  Future<VeilidCryptoSystem> bestCryptoSystem() async {
    return VeilidCryptoSystemJS._(
        this, js_util.callMethod(wasm, "best_crypto_kind", []));
  }

  @override
  Future<List<TypedKey>> verifySignatures(List<TypedKey> nodeIds,
      Uint8List data, List<TypedSignature> signatures) async {
    return jsonListConstructor(TypedKey.fromJson)(jsonDecode(
        await _wrapApiPromise(js_util.callMethod(wasm, "verify_signatures", [
      jsonEncode(nodeIds),
      base64UrlNoPadEncode(data),
      jsonEncode(signatures)
    ]))));
  }

  @override
  Future<List<TypedSignature>> generateSignatures(
      Uint8List data, List<TypedKeyPair> keyPairs) async {
    return jsonListConstructor(TypedSignature.fromJson)(jsonDecode(
        await _wrapApiPromise(js_util.callMethod(wasm, "generate_signatures",
            [base64UrlNoPadEncode(data), jsonEncode(keyPairs)]))));
  }

  @override
  Future<TypedKeyPair> generateKeyPair(CryptoKind kind) async {
    return TypedKeyPair.fromJson(jsonDecode(await _wrapApiPromise(
        js_util.callMethod(wasm, "generate_key_pair", [kind]))));
  }

  @override
  Future<VeilidRoutingContext> routingContext() async {
    int id =
        await _wrapApiPromise(js_util.callMethod(wasm, "routing_context", []));
    return VeilidRoutingContextJS._(_Ctx(id, this));
  }

  @override
  Future<RouteBlob> newPrivateRoute() async {
    return RouteBlob.fromJson(jsonDecode(await _wrapApiPromise(
        js_util.callMethod(wasm, "new_private_route", []))));
  }

  @override
  Future<RouteBlob> newCustomPrivateRoute(
      Stability stability, Sequencing sequencing) async {
    var stabilityString = jsonEncode(stability);
    var sequencingString = jsonEncode(sequencing);

    return RouteBlob.fromJson(jsonDecode(await _wrapApiPromise(js_util
        .callMethod(
            wasm, "new_private_route", [stabilityString, sequencingString]))));
  }

  @override
  Future<String> importRemotePrivateRoute(Uint8List blob) {
    var encodedBlob = base64UrlNoPadEncode(blob);
    return _wrapApiPromise(
        js_util.callMethod(wasm, "import_remote_private_route", [encodedBlob]));
  }

  @override
  Future<void> releasePrivateRoute(String key) {
    return _wrapApiPromise(
        js_util.callMethod(wasm, "release_private_route", [key]));
  }

  @override
  Future<void> appCallReply(String callId, Uint8List message) {
    var encodedMessage = base64UrlNoPadEncode(message);
    return _wrapApiPromise(
        js_util.callMethod(wasm, "app_call_reply", [callId, encodedMessage]));
  }

  @override
  Future<VeilidTableDB> openTableDB(String name, int columnCount) async {
    int id = await _wrapApiPromise(
        js_util.callMethod(wasm, "open_table_db", [name, columnCount]));
    return VeilidTableDBJS._(_TDB(id, this));
  }

  @override
  Future<bool> deleteTableDB(String name) {
    return _wrapApiPromise(js_util.callMethod(wasm, "delete_table_db", [name]));
  }

  @override
  Timestamp now() {
    return Timestamp.fromString(js_util.callMethod(wasm, "now", []));
  }

  @override
  Future<String> debug(String command) async {
    return await _wrapApiPromise(js_util.callMethod(wasm, "debug", [command]));
  }

  @override
  String veilidVersionString() {
    return js_util.callMethod(wasm, "veilid_version_string", []);
  }

  @override
  VeilidVersion veilidVersion() {
    Map<String, dynamic> jsonVersion =
        jsonDecode(js_util.callMethod(wasm, "veilid_version", []));
    return VeilidVersion(
        jsonVersion["major"], jsonVersion["minor"], jsonVersion["patch"]);
  }
}
