/// Encryption support for drift, built with the [sqflite_sqlcipher](https://github.com/davidmartos96/sqflite_sqlcipher)
/// library.
library encrypted_drift;

// this file should be kept in sync with drift_sqflite/lib/drift_sqflite.dart

import 'dart:async';
import 'dart:io';

import 'package:drift/backends.dart';
import 'package:drift/drift.dart';
import 'package:path/path.dart';
import 'package:sqflite_sqlcipher/sqflite.dart' as s;

/// Signature of a function that runs when a database doesn't exist on file.
/// This can be useful to, for instance, load the database from an asset if it
/// doesn't exist.
typedef DatabaseCreator = FutureOr<void> Function(File file);

class _SqfliteDelegate extends DatabaseDelegate {
  late s.Database db;
  bool _isOpen = false;

  final bool inDbFolder;
  final String path;
  final String? password;

  bool singleInstance;
  final DatabaseCreator? creator;

  _SqfliteDelegate(
    this.inDbFolder,
    this.path, {
    this.singleInstance = true,
    this.creator,
    this.password,
  });

  @override
  late final DbVersionDelegate versionDelegate = _SqfliteVersionDelegate(db);

  @override
  TransactionDelegate get transactionDelegate => const NoTransactionDelegate();

  @override
  bool get isOpen => _isOpen;

  @override
  Future<void> open(QueryExecutorUser user) async {
    String resolvedPath;
    if (inDbFolder) {
      resolvedPath = join(await s.getDatabasesPath(), path);
    } else {
      resolvedPath = path;
    }

    final file = File(resolvedPath);
    if (creator != null && !await file.exists()) {
      await creator!(file);
    }

    // default value when no migration happened
    db = await s.openDatabase(
      resolvedPath,
      password: password,
      singleInstance: singleInstance,
    );
    _isOpen = true;
  }

  @override
  Future<void> close() {
    return db.close();
  }

  @override
  Future<void> runCustom(String statement, List<Object?> args) {
    return db.execute(statement, args);
  }

  @override
  Future<int> runInsert(String statement, List<Object?> args) {
    return db.rawInsert(statement, args);
  }

  @override
  Future<QueryResult> runSelect(String statement, List<Object?> args) async {
    final result = await db.rawQuery(statement, args);
    return QueryResult.fromRows(result);
  }

  @override
  Future<int> runUpdate(String statement, List<Object?> args) {
    return db.rawUpdate(statement, args);
  }
}

class _SqfliteVersionDelegate extends DynamicVersionDelegate {
  final s.Database _db;

  _SqfliteVersionDelegate(this._db);

  @override
  Future<int> get schemaVersion async {
    final result = await _db.rawQuery('PRAGMA user_version;');
    return result.single.values.first as int;
  }

  @override
  Future<void> setSchemaVersion(int version) async {
    await _db.rawUpdate('PRAGMA user_version = $version;');
  }
}

/// A query executor that uses an encrypted version of sqflite internally.
class EncryptedExecutor extends DelegatedDatabase {
  /// A query executor that will store the database in the file declared by
  /// [path]. If [logStatements] is true, statements sent to the database will
  /// be [print]ed, which can be handy for debugging. The [singleInstance]
  /// parameter sets the corresponding parameter on [s.openDatabase].
  /// The [creator] will be called when the database file doesn't exist. It can
  /// be used to, for instance, populate default data from an asset. Note that
  /// migrations might behave differently when populating the database this way.
  /// For instance, a database created by an [creator] will not receive the
  /// [MigrationStrategy.onCreate] callback because it hasn't been created by
  /// drift.
  EncryptedExecutor(
      {required String path,
      required String password,
      bool? logStatements,
      bool singleInstance = true,
      DatabaseCreator? creator})
      : super(
            _SqfliteDelegate(
              false,
              path,
              singleInstance: singleInstance,
              creator: creator,
              password: password,
            ),
            logStatements: logStatements);

  /// A query executor that will store the database in the file declared by
  /// [path], which will be resolved relative to [s.getDatabasesPath()].
  /// If [logStatements] is true, statements sent to the database will
  /// be [print]ed, which can be handy for debugging. The [singleInstance]
  /// parameter sets the corresponding parameter on [s.openDatabase].
  /// The [creator] will be called when the database file doesn't exist. It can
  /// be used to, for instance, populate default data from an asset. Note that
  /// migrations might behave differently when populating the database this way.
  /// For instance, a database created by an [creator] will not receive the
  /// [MigrationStrategy.onCreate] callback because it hasn't been created by
  /// drift.
  EncryptedExecutor.inDatabaseFolder(
      {required String path,
      required String password,
      bool? logStatements,
      bool singleInstance = true,
      DatabaseCreator? creator})
      : super(
            _SqfliteDelegate(
              true,
              path,
              singleInstance: singleInstance,
              creator: creator,
              password: password,
            ),
            logStatements: logStatements);

  /// The underlying sqflite [s.Database] object used by drift to send queries.
  ///
  /// Using the sqflite database can cause unexpected behavior in drift. For
  /// instance, stream queries won't update for updates sent to the [s.Database]
  /// directly. Further, drift assumes full control over the database for its
  /// internal connection management.
  /// For this reason, projects shouldn't use this getter unless they absolutely
  /// need to. The database is exposed to make migrating from sqflite to drift
  /// easier.
  ///
  /// Note that this returns null until the drifft database has been opened.
  /// A drift database is opened lazily when the first query runs.
  s.Database? get sqfliteDb {
    final sqfliteDelegate = delegate as _SqfliteDelegate;
    return sqfliteDelegate.isOpen ? sqfliteDelegate.db : null;
  }

  @override
  // We're not really required to be sequential since sqflite has an internal
  // lock to bring statements into a sequential order.
  // Setting isSequential here helps with cancellations in stream queries
  // though.
  bool get isSequential => true;
}
