import 'dart:convert';

import 'package:collection/collection.dart';
import 'package:drift/drift.dart';
import 'package:meta/meta.dart';

/// Common interface for objects which can be inserted or updated into a
/// database.
/// [D] is the associated data class.
@optionalTypeArgs
abstract class Insertable<D> {
  /// Constant constructor so the classes that extend this can be const.
  const Insertable();

  /// Converts this object into a map of column names to expressions to insert
  /// or update.
  ///
  /// Note that the keys in the map are the raw column names, they're not
  /// escaped.
  ///
  /// The [nullToAbsent] can be used on [DataClass]es to control whether null
  /// fields should be set to a null constant in sql or absent from the map.
  /// Other implementations ignore that [nullToAbsent], it mainly exists for
  /// legacy reasons.
  Map<String, Expression> toColumns(bool nullToAbsent);
}

/// A common supertype for all data classes generated by drift. Data classes are
/// immutable structures that represent a single row in a database table.
abstract class DataClass {
  /// Constant constructor so that generated data classes can be constant.
  const DataClass();

  /// Converts this object into a representation that can be encoded with
  /// [json]. The [serializer] can be used to configure how individual values
  /// will be encoded. By default, [DriftRuntimeOptions.defaultSerializer] will
  /// be used. See [ValueSerializer.defaults] for details.
  Map<String, dynamic> toJson({ValueSerializer? serializer});

  /// Converts this object into a json representation. The [serializer] can be
  /// used to configure how individual values will be encoded. By default,
  /// [DriftRuntimeOptions.defaultSerializer] will be used. See
  /// [ValueSerializer.defaults] for details.
  String toJsonString({ValueSerializer? serializer}) {
    return json.encode(toJson(serializer: serializer));
  }

  /// Used internally be generated code
  @protected
  static dynamic parseJson(String jsonString) {
    return json.decode(jsonString);
  }
}

/// An update companion for a [DataClass] which is used to write data into a
/// database using [InsertStatement.insert] or [UpdateStatement.write].
///
/// [D] is the associated data class for this companion.
///
/// See also:
/// - the explanation in the changelog for 1.5
/// - https://github.com/simolus3/drift/issues/25
abstract class UpdateCompanion<D> implements Insertable<D> {
  /// Constant constructor so that generated companion classes can be constant.
  const UpdateCompanion();

  static const _mapEquality = MapEquality<dynamic, dynamic>();

  @override
  int get hashCode {
    return _mapEquality.hash(toColumns(false));
  }

  @override
  bool operator ==(Object other) {
    if (identical(this, other)) return true;
    if (other is! UpdateCompanion<D>) return false;

    return _mapEquality.equals(other.toColumns(false), toColumns(false));
  }
}

/// An [Insertable] implementation based on raw column expressions.
///
/// Mostly used in generated code.
class RawValuesInsertable<D> implements Insertable<D> {
  /// A map from column names to a value that should be inserted or updated.
  ///
  /// See also:
  ///  - [toColumns], which returns [data] in a [RawValuesInsertable]
  final Map<String, Expression> data;

  /// Creates a [RawValuesInsertable] based on the [data] to insert or update.
  const RawValuesInsertable(this.data);

  @override
  Map<String, Expression> toColumns(bool nullToAbsent) => data;

  @override
  String toString() {
    return 'RawValuesInsertable($data)';
  }
}

/// A wrapper around arbitrary data [T] to indicate presence or absence
/// explicitly.
///
/// [Value]s are commonly used in companions to distringuish between `null` and
/// absent values.
/// For instance, consider a table with a nullable column with a non-nullable
/// default value:
///
/// ```sql
/// CREATE TABLE orders (
///   priority INT DEFAULT 1 -- may be null if there's no assigned priority
/// );
///
/// For inserts in Dart, there are three different scenarios for the `priority`
/// column:
///
/// - It may be set to `null`, overriding the default value
/// - It may be absent, meaning that the default value should be used
/// - It may be set to an `int` to override the default value
/// ```
///
/// As you can see, a simple `int?` does not provide enough information to
/// distinguish between the three cases. A `null` value could mean that the
/// column is absent, or that it should explicitly be set to `null`.
/// For this reason, drift introduces the [Value] wrapper to make the
/// distinction explicit.
class Value<T> {
  /// Whether this [Value] wrapper contains a present [value] that should be
  /// inserted or updated.
  final bool present;

  final T? _value;

  /// If this value is [present], contains the value to update or insert.
  T get value => _value as T;

  /// Create a (present) value by wrapping the [value] provided.
  const Value(T value)
      : _value = value,
        present = true;

  /// Create an absent value that will not be written into the database, the
  /// default value or null will be used instead.
  const Value.absent()
      : _value = null,
        present = false;

  /// Create a value that is absent if [value] is `null` and [present] if it's
  /// not.
  ///
  /// The functionality is equiavalent to the following:
  /// `x != null ? Value(x) : Value.absent()`.
  ///
  /// This constructor should only be used when [T] is not nullable. If [T] were
  /// nullable, there wouldn't be a clear interpretation for a `null` [value].
  /// See the overall documentation on [Value] for details.
  const Value.ofNullable(T? value)
      : assert(
          value != null || null is! T,
          "Value.ofNullable(null) can't be used for a nullable T, since the "
          'null value could be both absent and present.',
        ),
        _value = value,
        present = value != null;

  @override
  String toString() => present ? 'Value($value)' : 'Value.absent()';

  @override
  bool operator ==(Object other) =>
      identical(this, other) ||
      other is Value && present == other.present && _value == other._value;

  @override
  int get hashCode => present.hashCode ^ _value.hashCode;
}

/// Serializer responsible for mapping atomic types from and to json.
abstract class ValueSerializer {
  /// Constant super-constructor to allow constant child classes.
  const ValueSerializer();

  /// The builtin default serializer.
  ///
  /// This serializer won't transform numbers or strings. Date times will be
  /// encoded as a unix-timestamp.
  ///
  /// To override the default serializer drift uses, you can change the
  /// [DriftRuntimeOptions.defaultSerializer] field.
  ///
  /// The [serializeDateTimeValuesAsString] option (which defaults to `false`)
  /// describes whether [DateTime] values should be serialized to a unix
  /// timestamp ([DateTime.millisecondsSinceEpoch]) or a string
  /// ([DateTime.toIso8601String]).
  /// In either case, date time values can be _deserialized_ from both formats.
  const factory ValueSerializer.defaults(
      {bool serializeDateTimeValuesAsString}) = _DefaultValueSerializer;

  /// Converts the [value] to something that can be passed to
  /// [JsonCodec.encode].
  dynamic toJson<T>(T value);

  /// Inverse of [toJson]: Converts a value obtained from [JsonCodec.decode]
  /// into a value that can be hold by data classes.
  T fromJson<T>(dynamic json);
}

class _DefaultValueSerializer extends ValueSerializer {
  final bool serializeDateTimeValuesAsString;

  const _DefaultValueSerializer({this.serializeDateTimeValuesAsString = false});

  @override
  T fromJson<T>(dynamic json) {
    if (json == null) {
      return null as T;
    }

    final typeList = <T>[];

    if (typeList is List<DateTime?>) {
      if (json is int) {
        return DateTime.fromMillisecondsSinceEpoch(json) as T;
      } else {
        return DateTime.parse(json.toString()) as T;
      }
    }

    if (typeList is List<double?> && json is int) {
      return json.toDouble() as T;
    }

    // blobs are encoded as a regular json array, so we manually convert that to
    // a Uint8List
    if (typeList is List<Uint8List?> && json is! Uint8List) {
      final asList = (json as List).cast<int>();
      return Uint8List.fromList(asList) as T;
    }

    return json as T;
  }

  @override
  dynamic toJson<T>(T value) {
    if (value is DateTime) {
      return serializeDateTimeValuesAsString
          ? value.toIso8601String()
          : value.millisecondsSinceEpoch;
    }

    return value;
  }
}
