import 'dart:convert';

import 'package:collection/collection.dart';
import 'package:meta/meta.dart';
import 'package:moor/moor.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 extends DataClass> {
  /// 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 moor. 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, [MoorRuntimeOptions.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,
  /// [MoorRuntimeOptions.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/moor/issues/25
abstract class UpdateCompanion<D extends DataClass> 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 ==(dynamic other) {
    if (identical(this, other)) return true;
    if (other is! UpdateCompanion<D>) return false;

    return _mapEquality.equals(
      // ignore: test_types_in_equals
      (other as UpdateCompanion<D>).toColumns(false),
      toColumns(false),
    );
  }
}

/// An [Insertable] implementation based on raw column expressions.
///
/// Mostly used in generated code.
class RawValuesInsertable<D extends DataClass> 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. We can use [Value]s in companions to distinguish between null
/// and absent values.
class Value<T> {
  /// Whether this [Value] wrapper contains a present [value] that should be
  /// inserted or updated.
  final bool present;

  /// If this value is [present], contains the value to update or insert.
  final T value;

  /// Create a (present) value by wrapping the [value] provided.
  const Value(this.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;

  @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 moor uses, you can change the
  /// [MoorRuntimeOptions.defaultSerializer] field.
  const factory ValueSerializer.defaults() = _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 {
  const _DefaultValueSerializer();

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

    if (T == DateTime) {
      return DateTime.fromMillisecondsSinceEpoch(json as int) as T;
    }

    if (T == 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 (T == 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 value.millisecondsSinceEpoch;
    }

    return value;
  }
}
