// Copyright (c) 2021, 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.

import 'dart:async';
import 'dart:typed_data';

/// All serialization must be done in a serialization Zone, as well as a remote
/// instance cache zone. This outer serialization zone informs the code of which
/// protocol we are using for serialization.
T withSerializationMode<T>(
  SerializationMode mode,
  T Function() fn, {
  Serializer Function()? serializerFactory,
  Deserializer Function(Object? data)? deserializerFactory,
}) =>
    runZoned(fn, zoneValues: {
      #serializationMode: mode,
    });

/// Serializable interface
abstract class Serializable {
  /// Serializes this object using [serializer].
  void serialize(Serializer serializer);
}

/// A push based object serialization interface.
abstract class Serializer {
  /// Serializes a [String].
  void addString(String value);

  /// Serializes a nullable [String].
  void addNullableString(String? value) =>
      value == null ? addNull() : addString(value);

  /// Serializes a [double].
  void addDouble(double value);

  /// Serializes a nullable [double].
  void addNullableDouble(double? value) =>
      value == null ? addNull() : addDouble(value);

  /// Serializes an [int].
  void addInt(int value);

  /// Serializes a nullable [int].
  void addNullableInt(int? value) => value == null ? addNull() : addInt(value);

  /// Serializes a [bool].
  void addBool(bool value);

  /// Serializes a nullable [bool].
  void addNullableBool(bool? value) =>
      value == null ? addNull() : addBool(value);

  /// Serializes a `null` literal.
  void addNull();

  /// Used to signal the start of an arbitrary length list of items.
  void startList();

  /// Used to signal the end of an arbitrary length list of items.
  void endList();

  /// Returns the resulting serialized object.
  Object get result;
}

/// A pull based object deserialization interface.
///
/// You must call [moveNext] before reading any items, and in order to advance
/// to the next item.
abstract class Deserializer {
  /// Checks if the current value is a null, returns `true` if so and `false`
  /// otherwise.
  bool checkNull();

  /// Reads the current value as a non-nullable [String].
  bool expectBool();

  /// Reads the current value as a nullable [bool].
  bool? expectNullableBool() => checkNull() ? null : expectBool();

  /// Reads the current value as a non-nullable [double].
  double expectDouble();

  /// Reads the current value as a nullable [double].
  double? expectNullableDouble() => checkNull() ? null : expectDouble();

  /// Reads the current value as a non-nullable [int].
  int expectInt();

  /// Reads the current value as a nullable [int].
  int? expectNullableInt() => checkNull() ? null : expectInt();

  /// Reads the current value as a non-nullable [String].
  String expectString();

  /// Reads the current value as a nullable [String].
  String? expectNullableString() => checkNull() ? null : expectString();

  /// Asserts that the current item is the start of a list.
  ///
  /// An example for how to read from a list is as follows:
  ///
  /// var json = JsonReader.fromString(source);
  /// I know it's a list of strings.
  ///
  /// ```
  ///   var result = <String>[];
  ///   deserializer.moveNext();
  ///   deserializer.expectList();
  ///   while (json.moveNext()) {
  ///     result.add(json.expectString());
  ///   }
  ///   // Can now read later items, but need to call `moveNext` again to move
  ///   // past the list.
  ///   deserializer.moveNext();
  ///   deserializer.expectBool();
  /// ```
  void expectList();

  /// Moves to the next item, returns `false` if there are no more items to
  /// read.
  ///
  /// If inside of a list, this returns `false` when the end of the list is
  /// reached, and moves back to the parent, but does not advance it, so another
  /// call to `moveNext` is needed. See example in the [expectList] docs.
  bool moveNext();
}

class JsonSerializer implements Serializer {
  /// The full result.
  final _result = <Object?>[];

  /// A path to the current list we are modifying.
  late final List<List<Object?>> _path = [_result];

  /// Returns the result as an unmodifiable [Iterable].
  ///
  /// Asserts that all [List] entries have not been closed with [endList].
  @override
  Iterable<Object?> get result {
    assert(_path.length == 1);
    return _result;
  }

  @override
  void addBool(bool value) => _path.last.add(value);
  @override
  void addNullableBool(bool? value) => _path.last.add(value);

  @override
  void addDouble(double value) => _path.last.add(value);
  @override
  void addNullableDouble(double? value) => _path.last.add(value);

  @override
  void addInt(int value) => _path.last.add(value);
  @override
  void addNullableInt(int? value) => _path.last.add(value);

  @override
  void addString(String value) => _path.last.add(value);
  @override
  void addNullableString(String? value) => _path.last.add(value);

  @override
  void addNull() => _path.last.add(null);

  @override
  void startList() {
    List<Object?> sublist = [];
    _path.last.add(sublist);
    _path.add(sublist);
  }

  @override
  void endList() {
    _path.removeLast();
  }
}

class JsonDeserializer implements Deserializer {
  /// The root source list to read from.
  final Iterable<Object?> _source;

  /// The path to the current iterator we are reading from.
  late final List<Iterator<Object?>> _path = [];

  /// Whether we have received our first [moveNext] call.
  bool _initialized = false;

  /// Initialize this deserializer from `_source`.
  JsonDeserializer(this._source);

  @override
  bool checkNull() => _expectValue<Object?>() == null;

  @override
  void expectList() => _path.add(_expectValue<Iterable<Object?>>().iterator);

  @override
  bool expectBool() => _expectValue();
  @override
  bool? expectNullableBool() => _expectValue();

  @override
  double expectDouble() => _expectValue();
  @override
  double? expectNullableDouble() => _expectValue();

  @override
  int expectInt() => _expectValue();
  @override
  int? expectNullableInt() => _expectValue();

  @override
  String expectString() => _expectValue();
  @override
  String? expectNullableString() => _expectValue();

  /// Reads the current value and casts it to [T].
  T _expectValue<T>() {
    if (!_initialized) {
      throw StateError('You must call `moveNext()` before reading any values.');
    }
    Object? current = _path.last.current;
    if (current is! T) {
      throw StateError('Expected $T, got: ${_path.last.current}');
    }
    return current;
  }

  @override
  bool moveNext() {
    if (!_initialized) {
      _path.add(_source.iterator);
      _initialized = true;
    }

    // Move the current iterable, if it's at the end of its items remove it from
    // the current path and return false.
    if (!_path.last.moveNext()) {
      _path.removeLast();
      return false;
    }

    return true;
  }
}

class ByteDataSerializer extends Serializer {
  final BytesBuilder _builder = BytesBuilder();

  // Re-usable 8 byte list and view for encoding doubles.
  final Uint8List _eightByteList = Uint8List(8);
  late final ByteData _eightByteListData = ByteData.sublistView(_eightByteList);

  @override
  void addBool(bool value) => _builder
      .addByte(value ? DataKind.boolTrue.index : DataKind.boolFalse.index);

  @override
  void addDouble(double value) {
    _eightByteListData.setFloat64(0, value);
    _builder
      ..addByte(DataKind.float64.index)
      ..add(_eightByteList);
  }

  @override
  void addNull() => _builder.addByte(DataKind.nil.index);

  @override
  void addInt(int value) {
    if (value >= 0x0) {
      assert(DataKind.values.length < 0xff);
      if (value <= 0xff - DataKind.values.length) {
        _builder.addByte(value + DataKind.values.length);
      } else if (value <= 0xff) {
        _builder
          ..addByte(DataKind.uint8.index)
          ..addByte(value);
      } else if (value <= 0xffff) {
        _builder
          ..addByte(DataKind.uint16.index)
          ..addByte(value >> 8)
          ..addByte(value);
      } else if (value <= 0xffffffff) {
        _builder
          ..addByte(DataKind.uint32.index)
          ..addByte(value >> 24)
          ..addByte(value >> 16)
          ..addByte(value >> 8)
          ..addByte(value);
      } else {
        _builder
          ..addByte(DataKind.uint64.index)
          ..addByte(value >> 56)
          ..addByte(value >> 48)
          ..addByte(value >> 40)
          ..addByte(value >> 32)
          ..addByte(value >> 24)
          ..addByte(value >> 16)
          ..addByte(value >> 8)
          ..addByte(value);
      }
    } else {
      if (value >= -0x80) {
        _builder
          ..addByte(DataKind.int8.index)
          ..addByte(value);
      } else if (value >= -0x8000) {
        _builder
          ..addByte(DataKind.int16.index)
          ..addByte(value >> 8)
          ..addByte(value);
      } else if (value >= -0x8000000) {
        _builder
          ..addByte(DataKind.int32.index)
          ..addByte(value >> 24)
          ..addByte(value >> 16)
          ..addByte(value >> 8)
          ..addByte(value);
      } else {
        _builder
          ..addByte(DataKind.int64.index)
          ..addByte(value >> 56)
          ..addByte(value >> 48)
          ..addByte(value >> 40)
          ..addByte(value >> 32)
          ..addByte(value >> 24)
          ..addByte(value >> 16)
          ..addByte(value >> 8)
          ..addByte(value);
      }
    }
  }

  @override
  void addString(String value) {
    for (int i = 0; i < value.length; i++) {
      if (value.codeUnitAt(i) > 0xff) {
        _addTwoByteString(value);
        return;
      }
    }
    _addOneByteString(value);
  }

  void _addOneByteString(String value) {
    _builder.addByte(DataKind.oneByteString.index);
    addInt(value.length);
    for (int i = 0; i < value.length; i++) {
      _builder.addByte(value.codeUnitAt(i));
    }
  }

  void _addTwoByteString(String value) {
    _builder.addByte(DataKind.twoByteString.index);
    addInt(value.length);
    for (int i = 0; i < value.length; i++) {
      int codeUnit = value.codeUnitAt(i);
      switch (Endian.host) {
        case Endian.little:
          _builder
            ..addByte(codeUnit)
            ..addByte(codeUnit >> 8);
          break;
        case Endian.big:
          _builder
            ..addByte(codeUnit >> 8)
            ..addByte(codeUnit);
          break;
      }
    }
  }

  @override
  void startList() => _builder.addByte(DataKind.startList.index);

  @override
  void endList() => _builder.addByte(DataKind.endList.index);

  /// Used to signal the start of an arbitrary length list of map entries.
  void startMap() => _builder.addByte(DataKind.startMap.index);

  /// Used to signal the end of an arbitrary length list of map entries.
  void endMap() => _builder.addByte(DataKind.endMap.index);

  /// Serializes a [Uint8List].
  void addUint8List(Uint8List value) {
    _builder.addByte(DataKind.uint8List.index);
    addInt(value.length);
    _builder.add(value);
  }

  /// Serializes an object with arbitrary structure. It supports `bool`,
  /// `int`, `String`, `null`, `Uint8List`, `List`, `Map`.
  void addAny(Object? value) {
    if (value == null) {
      addNull();
    } else if (value is bool) {
      addBool(value);
    } else if (value is int) {
      addInt(value);
    } else if (value is String) {
      addString(value);
    } else if (value is Uint8List) {
      addUint8List(value);
    } else if (value is List) {
      startList();
      value.forEach(addAny);
      endList();
    } else if (value is Map) {
      startMap();
      for (MapEntry<Object?, Object?> entry in value.entries) {
        addAny(entry.key);
        addAny(entry.value);
      }
      endMap();
    } else {
      throw ArgumentError('(${value.runtimeType}) $value');
    }
  }

  @override
  Uint8List get result => _builder.takeBytes();
}

class ByteDataDeserializer extends Deserializer {
  final ByteData _bytes;
  int _byteOffset = 0;
  int? _byteOffsetIncrement = 0;

  ByteDataDeserializer(this._bytes);

  /// Reads the next [DataKind] and advances [_byteOffset].
  DataKind _readKind([int offset = 0]) {
    int value = _bytes.getUint8(_byteOffset + offset);
    if (value < DataKind.values.length) {
      return DataKind.values[value];
    } else {
      return DataKind.directEncodedUint8;
    }
  }

  @override
  bool checkNull() {
    _byteOffsetIncrement = 1;
    return _readKind() == DataKind.nil;
  }

  @override
  bool expectBool() {
    DataKind kind = _readKind();
    _byteOffsetIncrement = 1;
    if (kind == DataKind.boolTrue) {
      return true;
    } else if (kind == DataKind.boolFalse) {
      return false;
    } else {
      throw StateError('Expected a bool but found a $kind');
    }
  }

  @override
  double expectDouble() {
    DataKind kind = _readKind();
    if (kind != DataKind.float64) {
      throw StateError('Expected a double but found a $kind');
    }
    _byteOffsetIncrement = 9;
    return _bytes.getFloat64(_byteOffset + 1);
  }

  @override
  int expectInt() => _expectInt(0);

  int _expectInt(int offset) {
    DataKind kind = _readKind(offset);
    if (kind == DataKind.directEncodedUint8) {
      _byteOffsetIncrement = offset + 1;
      return _bytes.getUint8(_byteOffset + offset) - DataKind.values.length;
    }
    offset += 1;
    int result;
    switch (kind) {
      case DataKind.int8:
        result = _bytes.getInt8(_byteOffset + offset);
        _byteOffsetIncrement = 1 + offset;
        break;
      case DataKind.int16:
        result = _bytes.getInt16(_byteOffset + offset);
        _byteOffsetIncrement = 2 + offset;
        break;
      case DataKind.int32:
        result = _bytes.getInt32(_byteOffset + offset);
        _byteOffsetIncrement = 4 + offset;
        break;
      case DataKind.int64:
        result = _bytes.getInt64(_byteOffset + offset);
        _byteOffsetIncrement = 8 + offset;
        break;
      case DataKind.uint8:
        result = _bytes.getUint8(_byteOffset + offset);
        _byteOffsetIncrement = 1 + offset;
        break;
      case DataKind.uint16:
        result = _bytes.getUint16(_byteOffset + offset);
        _byteOffsetIncrement = 2 + offset;
        break;
      case DataKind.uint32:
        result = _bytes.getUint32(_byteOffset + offset);
        _byteOffsetIncrement = 4 + offset;
        break;
      case DataKind.uint64:
        result = _bytes.getUint64(_byteOffset + offset);
        _byteOffsetIncrement = 8 + offset;
        break;
      default:
        throw StateError('Expected an int but found a $kind');
    }
    return result;
  }

  @override
  void expectList() {
    DataKind kind = _readKind();
    if (kind != DataKind.startList) {
      throw StateError('Expected the start to a list but found a $kind');
    }
    _byteOffsetIncrement = 1;
  }

  /// Asserts that the current item is the start of a map.
  ///
  /// An example for how to read from a map is as follows:
  ///
  /// I know it's a map of ints to strings.
  ///
  /// ```
  ///   var result = <int, String>[];
  ///   deserializer.expectMap();
  ///   while (deserializer.moveNext()) {
  ///     var key = deserializer.expectInt();
  ///     deserializer.next();
  ///     var value = deserializer.expectString();
  ///     result[key] = value;
  ///   }
  ///   // We have already called `moveNext` to move past the map.
  ///   deserializer.expectBool();
  /// ```
  void expectMap() {
    DataKind kind = _readKind();
    if (kind != DataKind.startMap) {
      throw StateError('Expected the start to a map but found a $kind');
    }
    _byteOffsetIncrement = 1;
  }

  @override
  String expectString() {
    DataKind kind = _readKind();
    int length = _expectInt(1);
    int offset = _byteOffsetIncrement! + _byteOffset;
    if (kind == DataKind.oneByteString) {
      _byteOffsetIncrement = _byteOffsetIncrement! + length;
      return String.fromCharCodes(_bytes.buffer.asUint8List(offset, length));
    } else if (kind == DataKind.twoByteString) {
      length = length * 2;
      _byteOffsetIncrement = _byteOffsetIncrement! + length;
      Uint8List bytes =
          Uint8List.fromList(_bytes.buffer.asUint8List(offset, length));
      return String.fromCharCodes(bytes.buffer.asUint16List());
    } else {
      throw StateError('Expected a string but found a $kind');
    }
  }

  /// Reads the current value as [Uint8List].
  Uint8List expectUint8List() {
    _byteOffsetIncrement = 1;
    moveNext();
    int length = expectInt();
    int offset = _byteOffset + _byteOffsetIncrement!;
    _byteOffsetIncrement = _byteOffsetIncrement! + length;
    return _bytes.buffer.asUint8List(offset, length);
  }

  /// Reads the current value as an object of arbitrary structure.
  Object? expectAny() {
    const Set<DataKind> boolKinds = {
      DataKind.boolFalse,
      DataKind.boolTrue,
    };

    const Set<DataKind> intKinds = {
      DataKind.directEncodedUint8,
      DataKind.int8,
      DataKind.int16,
      DataKind.int32,
      DataKind.int64,
      DataKind.uint8,
      DataKind.uint16,
      DataKind.uint32,
      DataKind.uint64,
    };

    const Set<DataKind> stringKinds = {
      DataKind.oneByteString,
      DataKind.twoByteString,
    };

    DataKind kind = _readKind();
    if (boolKinds.contains(kind)) {
      return expectBool();
    } else if (kind == DataKind.nil) {
      checkNull();
      return null;
    } else if (intKinds.contains(kind)) {
      return expectInt();
    } else if (stringKinds.contains(kind)) {
      return expectString();
    } else if (kind == DataKind.startList) {
      List<Object?> result = [];
      expectList();
      while (moveNext()) {
        Object? element = expectAny();
        result.add(element);
      }
      return result;
    } else if (kind == DataKind.startMap) {
      Map<Object?, Object?> result = {};
      expectMap();
      while (moveNext()) {
        Object? key = expectAny();
        moveNext();
        Object? value = expectAny();
        result[key] = value;
      }
      return result;
    } else if (kind == DataKind.uint8List) {
      return expectUint8List();
    } else {
      throw StateError('Expected: $kind');
    }
  }

  @override
  bool moveNext() {
    int? increment = _byteOffsetIncrement;
    _byteOffsetIncrement = null;
    if (increment == null) {
      throw StateError("Can't move until consuming the current element");
    }
    _byteOffset += increment;
    if (_byteOffset >= _bytes.lengthInBytes) {
      return false;
    } else if (_readKind() == DataKind.endList ||
        _readKind() == DataKind.endMap) {
      // You don't explicitly consume list/map end markers.
      _byteOffsetIncrement = 1;
      return false;
    } else {
      return true;
    }
  }
}

enum DataKind {
  nil,
  boolTrue,
  boolFalse,
  directEncodedUint8, // Encoded in the kind byte.
  startList,
  endList,
  startMap,
  endMap,
  int8,
  int16,
  int32,
  int64,
  uint8,
  uint16,
  uint32,
  uint64,
  float64,
  oneByteString,
  twoByteString,
  uint8List,
}

/// Must be set using `withSerializationMode` before doing any serialization or
/// deserialization.
SerializationMode get serializationMode {
  SerializationMode? mode =
      Zone.current[#serializationMode] as SerializationMode?;
  if (mode == null) {
    throw StateError('No SerializationMode set, you must do all '
        'serialization inside a call to `withSerializationMode`.');
  }
  return mode;
}

/// Returns the current deserializer factory for the zone.
Deserializer Function(Object?) get deserializerFactory =>
    switch (serializationMode) {
      SerializationMode.byteData => (Object? message) =>
          ByteDataDeserializer(ByteData.sublistView(message as Uint8List)),
      SerializationMode.json => (Object? message) =>
          JsonDeserializer(message as Iterable<Object?>),
    };

/// Returns the current serializer factory for the zone.
Serializer Function() get serializerFactory => switch (serializationMode) {
      SerializationMode.byteData => ByteDataSerializer.new,
      SerializationMode.json => JsonSerializer.new,
    };

/// Some objects are serialized differently on the client side versus the server
/// side. This indicates the different modes, as well as the format used.
enum SerializationMode {
  byteData,
  json;

  factory SerializationMode.fromOption(String option) => switch (option) {
        'json' => SerializationMode.json,
        'bytedata' => SerializationMode.byteData,
        _ => throw ArgumentError('Unrecognized macro serialization mode '
            '$option'),
      };
}

extension SerializationModeHelpers on SerializationMode {
  /// A stable string to write in code.
  String get asCode => switch (this) {
        SerializationMode.byteData => 'SerializationMode.byteData',
        SerializationMode.json => 'SerializationMode.json',
      };
}
