content: import 'dart:convert';
  
  import 'package:codable_dart/core.dart';
  import 'package:codable_dart/src/extended/lazy.dart';
  
  class Person with PersonRaw implements SelfEncodable {
    Person(this.name, this.age, this.height, this.isDeveloper, this.parent, this.hobbies, this.friends);
  
    static const LazyCodable<Person> codable = PersonCodable();
  
    final String name;
    final int age;
    final double height;
    final bool isDeveloper;
    final Person? parent;
    final List<String> hobbies;
    final List<Person> friends;
  
    @override
    bool operator ==(Object other) =>
        identical(this, other) ||
        other is Person &&
            runtimeType == other.runtimeType &&
            name == other.name &&
            age == other.age &&
            height == other.height &&
            isDeveloper == other.isDeveloper &&
            parent == other.parent &&
            hobbies.indexed.every((e) => other.hobbies[e.$1] == e.$2) &&
            friends.indexed.every((e) => other.friends[e.$1] == e.$2);
  
    @override
    int get hashCode => Object.hash(name, age, height, isDeveloper, parent, hobbies, friends);
  
    @override
    String toString() {
      return 'Person(name: $name, age: $age, height: $height, isDeveloper: $isDeveloper, parent: $parent, hobbies: $hobbies, friends: $friends)';
    }
  
    // ====== Codable Code ======
    // Keep in mind that the below code could also easily be generated by macros or a code generator.
  
    @override
    void encode(Encoder encoder) {
      encoder.encodeKeyed()
        ..encodeString('name', name)
        ..encodeInt('age', age)
        ..encodeDouble('height', height)
        ..encodeBool('isDeveloper', isDeveloper)
        ..encodeObjectOrNull('parent', parent)
        ..encodeIterable('hobbies', hobbies)
        ..encodeIterable('friends', friends)
        ..end();
    }
  }
  
  /// Codable implementation for [Person].
  ///
  /// This extends the [SelfCodable] class for a default implementation of [encode] and
  /// implements the [decode] method.
  class PersonCodable extends SelfCodable<Person> implements LazyCodable<Person> {
    const PersonCodable();
  
    @override
    Person decode(Decoder decoder) {
      return switch (decoder.whatsNext()) {
        // If the format prefers mapped decoding, use mapped decoding.
        DecodingType.mapped || DecodingType.map => decodeMapped(decoder.decodeMapped()),
        // If the format prefers keyed decoding or is non-self describing, use keyed decoding.
        DecodingType.keyed || DecodingType.unknown => decodeKeyed(decoder.decodeKeyed()),
        _ => decoder.expect('mapped or keyed'),
      };
    }
  
    Person decodeKeyed(KeyedDecoder keyed) {
      late String name;
      late int age;
      late double height;
      late bool isDeveloper;
      Person? parent;
      late List<String> hobbies;
      late List<Person> friends;
  
      for (Object? key; (key = keyed.nextKey()) != null;) {
        switch (key) {
          case 'name':
            name = keyed.decodeString();
          case 'age':
            age = keyed.decodeInt();
          case 'height':
            height = keyed.decodeDouble();
          case 'isDeveloper':
            isDeveloper = keyed.decodeBool();
          case 'parent':
            parent = keyed.decodeObjectOrNull(using: Person.codable);
          case 'hobbies':
            hobbies = keyed.decodeList();
          case 'friends':
            friends = keyed.decodeList(using: Person.codable);
          default:
            keyed.skipCurrentValue();
        }
      }
  
      return Person(name, age, height, isDeveloper, parent, hobbies, friends);
    }
  
    Person decodeMapped(MappedDecoder mapped) {
      return Person(
        mapped.decodeString('name'),
        mapped.decodeInt('age'),
        mapped.decodeDouble('height'),
        mapped.decodeBool('isDeveloper'),
        mapped.decodeObjectOrNull('parent', using: Person.codable),
        mapped.decodeList('hobbies'),
        mapped.decodeList('friends', using: Person.codable),
      );
    }
  
    @override
    void decodeLazy(LazyDecoder decoder, void Function(Person) resolve) {
      late String name;
      late int age;
      late double height;
      late bool isDeveloper;
      Person? parent;
      late List<String> hobbies;
      late List<Person> friends;
  
      void setName(Decoder d) => name = d.decodeString();
      void setAge(Decoder d) => age = d.decodeInt();
      void setHeight(Decoder d) => height = d.decodeDouble();
      void setIsDeveloper(Decoder d) => isDeveloper = d.decodeBool();
      void setParent(Person? value) => parent = value;
      void setHobbies(List<String> value) => hobbies = value;
      void setFriends(List<Person> value) => friends = value;
  
      decoder.decodeKeyed((key, decoder) {
        switch (key) {
          case 'name':
            decoder.decodeEager(setName);
          case 'age':
            decoder.decodeEager(setAge);
          case 'height':
            decoder.decodeEager(setHeight);
          case 'isDeveloper':
            decoder.decodeEager(setIsDeveloper);
          case 'parent':
            decoder.decodeObjectOrNull(setParent, using: Person.codable);
          case 'hobbies':
            decoder.decodeList<String>(setHobbies);
          case 'friends':
            decoder.decodeList<Person>(setFriends, using: Person.codable);
          default:
            decoder.skipCurrentValue();
        }
      }, done: () {
        final person = Person(name, age, height, isDeveloper, parent, hobbies, friends);
        resolve(person);
      });
    }
  }
  
  /// Baseline implementations for encoding and decoding a [Person] instance.
  ///
  /// This is how we usually encode and decode models in Dart (e.g. code generated by json_serializable).
  /// Its used as a baseline against checking performance and correctness of the codable implementation.
  mixin PersonRaw {
    static Person fromMapRaw(Map<String, dynamic> map) {
      return Person(
        map['name'] as String,
        (map['age'] as num).toInt(),
        (map['height'] as num).toDouble(),
        map['isDeveloper'] as bool,
        map['parent'] == null ? null : PersonRaw.fromMapRaw(map['parent'] as Map<String, dynamic>),
        (map['hobbies'] as List).cast<String>(),
        (map['friends'] as List).map((e) => PersonRaw.fromMapRaw(e as Map<String, dynamic>)).toList(),
      );
    }
  
    static Person fromJsonRaw(String json) {
      return fromMapRaw(jsonDecode(json) as Map<String, dynamic>);
    }
  
    static Person fromJsonBytesRaw(List<int> json) {
      return fromMapRaw(jsonBytes.decode(json) as Map<String, dynamic>);
    }
  
    Map<String, dynamic> toMapRaw() {
      final value = this as Person;
      return {
        'name': value.name,
        'age': value.age,
        'height': value.height,
        'isDeveloper': value.isDeveloper,
        'parent': value.parent?.toMapRaw(),
        'hobbies': value.hobbies,
        'friends': value.friends.map((e) => e.toMapRaw()).toList(),
      };
    }
  
    String toJsonRaw() {
      return jsonEncode(toMapRaw());
    }
  
    List<int> toJsonBytesRaw() {
      return jsonBytes.encode(toMapRaw());
    }
  }
  
  final jsonBytes = json.fuse(utf8);
  