library;

import 'dart:convert';
import 'dart:typed_data';

import 'package:wolf_3d_dart/src/engine/save/game_session_snapshot.dart';
import 'package:wolf_3d_dart/wolf_3d_data_types.dart';
import 'package:wolf_3d_dart/wolf_3d_entities.dart';

class SaveGameFile {
  const SaveGameFile({
    required this.slot,
    required this.gameVersion,
    required this.dataVersionName,
    required this.description,
    required this.createdAtMs,
    required this.snapshot,
    required this.checksum,
  });

  final int slot;
  final GameVersion gameVersion;
  final String dataVersionName;
  final String description;
  final int createdAtMs;
  final GameSessionSnapshot snapshot;
  final int checksum;
}

class SaveGameCodec {
  static const int _formatVersion = 1;
  static const List<int> _magic = <int>[0x57, 0x33, 0x44, 0x53]; // W3DS
  static const int _descriptionBytes = 32;

  Uint8List encode(SaveGameFile file) {
    final _BinaryWriter writer = _BinaryWriter();

    final Uint8List payload = _encodeSnapshot(file.snapshot);
    final int checksum = doChecksum(payload);

    writer.writeBytes(_magic);
    writer.writeUint16(_formatVersion);
    writer.writeUint8(file.gameVersion.index);
    writer.writeUint8(file.slot & 0xFF);
    writer.writeInt64(file.createdAtMs);
    writer.writeFixedUtf8(file.description, _descriptionBytes);
    writer.writeUtf8(file.dataVersionName);
    writer.writeUint32(payload.length);
    writer.writeBytes(payload);
    writer.writeUint32(checksum);

    return writer.toBytes();
  }

  SaveGameFile decode(Uint8List bytes) {
    final _BinaryReader reader = _BinaryReader(bytes);

    final Uint8List magic = reader.readBytes(_magic.length);
    if (!_listEquals(magic, _magic)) {
      throw const FormatException('Invalid save magic header.');
    }

    final int version = reader.readUint16();
    if (version != _formatVersion) {
      throw FormatException('Unsupported save format version: $version');
    }

    final int gameVersionIndex = reader.readUint8();
    if (gameVersionIndex < 0 || gameVersionIndex >= GameVersion.values.length) {
      throw const FormatException('Invalid game version in save header.');
    }
    final GameVersion gameVersion = GameVersion.values[gameVersionIndex];

    final int slot = reader.readUint8();
    final int createdAtMs = reader.readInt64();
    final String description = reader.readFixedUtf8(_descriptionBytes);
    final String dataVersionName = reader.readUtf8();

    final int payloadLength = reader.readUint32();
    final Uint8List payload = reader.readBytes(payloadLength);
    final int checksum = reader.readUint32();

    final int computed = doChecksum(payload);
    if (computed != checksum) {
      throw FormatException(
        'Save checksum mismatch. expected=$checksum actual=$computed',
      );
    }

    final GameSessionSnapshot snapshot = _decodeSnapshot(payload);
    return SaveGameFile(
      slot: slot,
      gameVersion: gameVersion,
      dataVersionName: dataVersionName,
      description: description,
      createdAtMs: createdAtMs,
      snapshot: snapshot,
      checksum: checksum,
    );
  }

  int doChecksum(Uint8List source, {int checksum = 0}) {
    if (source.length <= 1) {
      return checksum & 0xFFFFFFFF;
    }

    int running = checksum;
    for (int i = 0; i < source.length - 1; i++) {
      running = (running + (source[i] ^ source[i + 1])) & 0xFFFFFFFF;
    }
    return running;
  }

  Uint8List encodeSnapshotPayload(GameSessionSnapshot snapshot) {
    return _encodeSnapshot(snapshot);
  }

  GameSessionSnapshot decodeSnapshotPayload(Uint8List payload) {
    return _decodeSnapshot(payload);
  }

  Uint8List _encodeSnapshot(GameSessionSnapshot snapshot) {
    final _BinaryWriter writer = _BinaryWriter();

    writer.writeInt32(snapshot.currentGameIndex);
    writer.writeInt32(snapshot.currentEpisodeIndex);
    writer.writeInt32(snapshot.currentLevelIndex);
    writer.writeBool(snapshot.returnLevelIndex != null);
    writer.writeInt32(snapshot.returnLevelIndex ?? -1);
    writer.writeInt32(snapshot.difficulty.index);
    writer.writeInt64(snapshot.timeAliveMs);
    writer.writeInt64(snapshot.lastAcousticAlertTime);
    writer.writeBool(snapshot.isMapOverlayVisible);
    writer.writeBool(snapshot.isMenuOverlayVisible);

    _writePlayerState(writer, snapshot.player);

    _writeIntGrid(writer, snapshot.currentLevel);
    _writeIntGrid(writer, snapshot.areaGrid);

    writer.writeUint32(snapshot.areasByPlayer.length);
    for (final bool value in snapshot.areasByPlayer) {
      writer.writeBool(value);
    }

    writer.writeUint32(snapshot.entities.length);
    for (final EntitySaveState entity in snapshot.entities) {
      writer.writeUtf8(entity.kind);
      writer.writeFloat64(entity.x);
      writer.writeFloat64(entity.y);
      writer.writeInt32(entity.spriteIndex);
      writer.writeFloat64(entity.angle);
      writer.writeInt32(entity.state.index);
      writer.writeInt32(entity.mapId);
      writer.writeInt32(entity.lastActionTime);
      writer.writeUtf8(jsonEncode(entity.extraData));
    }

    writer.writeUint32(snapshot.doors.length);
    for (final DoorSaveState door in snapshot.doors) {
      writer.writeInt32(door.x);
      writer.writeInt32(door.y);
      writer.writeInt32(door.mapId);
      writer.writeInt32(door.state.index);
      writer.writeFloat64(door.offset);
      writer.writeInt32(door.openTime);
    }

    writer.writeUint32(snapshot.pushwalls.length);
    for (final PushwallSaveState pushwall in snapshot.pushwalls) {
      writer.writeInt32(pushwall.x);
      writer.writeInt32(pushwall.y);
      writer.writeInt32(pushwall.mapId);
      writer.writeInt32(pushwall.dirX);
      writer.writeInt32(pushwall.dirY);
      writer.writeFloat64(pushwall.offset);
      writer.writeInt32(pushwall.tilesMoved);
      writer.writeBool(pushwall.isActive);
    }

    return writer.toBytes();
  }

  GameSessionSnapshot _decodeSnapshot(Uint8List payload) {
    final _BinaryReader reader = _BinaryReader(payload);

    final int currentGameIndex = reader.readInt32();
    final int currentEpisodeIndex = reader.readInt32();
    final int currentLevelIndex = reader.readInt32();
    final bool hasReturnLevel = reader.readBool();
    final int returnLevelRaw = reader.readInt32();
    final int difficultyIndex = reader.readInt32();
    if (difficultyIndex < 0 || difficultyIndex >= Difficulty.values.length) {
      throw const FormatException('Invalid difficulty value in save payload.');
    }
    final Difficulty difficulty = Difficulty.values[difficultyIndex];

    final int timeAliveMs = reader.readInt64();
    final int lastAcousticAlertTime = reader.readInt64();
    final bool isMapOverlayVisible = reader.readBool();
    final bool isMenuOverlayVisible = reader.readBool();

    final PlayerSaveState player = _readPlayerState(reader);

    final List<List<int>> currentLevel = _readIntGrid(reader);
    final List<List<int>> areaGrid = _readIntGrid(reader);

    final int areasByPlayerLength = reader.readUint32();
    final List<bool> areasByPlayer = List<bool>.generate(
      areasByPlayerLength,
      (_) => reader.readBool(),
      growable: false,
    );

    final int entityCount = reader.readUint32();
    final List<EntitySaveState> entities = List<EntitySaveState>.generate(
      entityCount,
      (_) {
        final String kind = reader.readUtf8();
        final double x = reader.readFloat64();
        final double y = reader.readFloat64();
        final int spriteIndex = reader.readInt32();
        final double angle = reader.readFloat64();
        final int stateIndex = reader.readInt32();
        final int mapId = reader.readInt32();
        final int lastActionTime = reader.readInt32();
        final String extraDataRaw = reader.readUtf8();
        final Object? decoded = jsonDecode(extraDataRaw);
        final Map<String, Object?> extraData = decoded is Map<String, Object?>
            ? decoded
            : <String, Object?>{};

        if (stateIndex < 0 || stateIndex >= EntityState.values.length) {
          throw const FormatException('Invalid entity state index in save.');
        }

        return EntitySaveState(
          kind: kind,
          x: x,
          y: y,
          spriteIndex: spriteIndex,
          angle: angle,
          state: EntityState.values[stateIndex],
          mapId: mapId,
          lastActionTime: lastActionTime,
          extraData: extraData,
        );
      },
      growable: false,
    );

    final int doorCount = reader.readUint32();
    final List<DoorSaveState> doors = List<DoorSaveState>.generate(
      doorCount,
      (_) {
        final int x = reader.readInt32();
        final int y = reader.readInt32();
        final int mapId = reader.readInt32();
        final int stateIndex = reader.readInt32();
        final double offset = reader.readFloat64();
        final int openTime = reader.readInt32();
        if (stateIndex < 0 || stateIndex >= DoorState.values.length) {
          throw const FormatException('Invalid door state index in save.');
        }
        return DoorSaveState(
          x: x,
          y: y,
          mapId: mapId,
          state: DoorState.values[stateIndex],
          offset: offset,
          openTime: openTime,
        );
      },
      growable: false,
    );

    final int pushwallCount = reader.readUint32();
    final List<PushwallSaveState> pushwalls = List<PushwallSaveState>.generate(
      pushwallCount,
      (_) => PushwallSaveState(
        x: reader.readInt32(),
        y: reader.readInt32(),
        mapId: reader.readInt32(),
        dirX: reader.readInt32(),
        dirY: reader.readInt32(),
        offset: reader.readFloat64(),
        tilesMoved: reader.readInt32(),
        isActive: reader.readBool(),
      ),
      growable: false,
    );

    return GameSessionSnapshot(
      currentGameIndex: currentGameIndex,
      currentEpisodeIndex: currentEpisodeIndex,
      currentLevelIndex: currentLevelIndex,
      returnLevelIndex: hasReturnLevel ? returnLevelRaw : null,
      difficulty: difficulty,
      timeAliveMs: timeAliveMs,
      lastAcousticAlertTime: lastAcousticAlertTime,
      isMapOverlayVisible: isMapOverlayVisible,
      isMenuOverlayVisible: isMenuOverlayVisible,
      player: player,
      currentLevel: currentLevel,
      areaGrid: areaGrid,
      areasByPlayer: areasByPlayer,
      entities: entities,
      doors: doors,
      pushwalls: pushwalls,
    );
  }

  void _writePlayerState(_BinaryWriter writer, PlayerSaveState player) {
    writer.writeFloat64(player.x);
    writer.writeFloat64(player.y);
    writer.writeFloat64(player.angle);
    writer.writeInt32(player.health);
    writer.writeInt32(player.ammo);
    writer.writeInt32(player.score);
    writer.writeInt32(player.lives);
    writer.writeFloat64(player.damageFlash);
    writer.writeFloat64(player.bonusFlash);
    writer.writeInt32(player.chaingunPickupFaceMsRemaining);
    writer.writeBool(player.mutantDeathFaceActive);
    writer.writeBool(player.godModeFaceEnabled);
    writer.writeInt32(player.faceSeed);
    writer.writeInt32(player.faceFrame);
    writer.writeFloat64(player.faceCountTics);
    writer.writeInt32(player.nextFaceChangeThreshold);
    writer.writeBool(player.hasGoldKey);
    writer.writeBool(player.hasSilverKey);
    writer.writeBool(player.hasMachineGun);
    writer.writeBool(player.hasChainGun);
    writer.writeInt32(player.currentWeaponType.index);
    writer.writeInt32(player.switchStateIndex);
    writer.writeBool(player.pendingWeaponType != null);
    writer.writeInt32(player.pendingWeaponType?.index ?? -1);
    writer.writeFloat64(player.weaponAnimOffset);

    writer.writeUint32(player.weaponStates.length);
    for (final MapEntry<WeaponType, WeaponSaveState> entry
        in player.weaponStates.entries) {
      final WeaponSaveState state = entry.value;
      writer.writeInt32(entry.key.index);
      writer.writeInt32(state.type.index);
      writer.writeInt32(state.state.index);
      writer.writeInt32(state.frameIndex);
      writer.writeInt32(state.lastFrameTime);
      writer.writeBool(state.triggerReleased);
    }
  }

  PlayerSaveState _readPlayerState(_BinaryReader reader) {
    final double x = reader.readFloat64();
    final double y = reader.readFloat64();
    final double angle = reader.readFloat64();
    final int health = reader.readInt32();
    final int ammo = reader.readInt32();
    final int score = reader.readInt32();
    final int lives = reader.readInt32();
    final double damageFlash = reader.readFloat64();
    final double bonusFlash = reader.readFloat64();
    final int chaingunPickupFaceMsRemaining = reader.readInt32();
    final bool mutantDeathFaceActive = reader.readBool();
    final bool godModeFaceEnabled = reader.readBool();
    final int faceSeed = reader.readInt32();
    final int faceFrame = reader.readInt32();
    final double faceCountTics = reader.readFloat64();
    final int nextFaceChangeThreshold = reader.readInt32();
    final bool hasGoldKey = reader.readBool();
    final bool hasSilverKey = reader.readBool();
    final bool hasMachineGun = reader.readBool();
    final bool hasChainGun = reader.readBool();

    final int currentWeaponIndex = reader.readInt32();
    final int switchStateIndex = reader.readInt32();
    final bool hasPendingWeapon = reader.readBool();
    final int pendingWeaponIndex = reader.readInt32();
    final double weaponAnimOffset = reader.readFloat64();

    if (currentWeaponIndex < 0 ||
        currentWeaponIndex >= WeaponType.values.length) {
      throw const FormatException('Invalid current weapon index in save.');
    }

    final int weaponStateCount = reader.readUint32();
    final Map<WeaponType, WeaponSaveState> weaponStates =
        <WeaponType, WeaponSaveState>{};
    for (int i = 0; i < weaponStateCount; i++) {
      final int keyIndex = reader.readInt32();
      final int typeIndex = reader.readInt32();
      final int stateIndex = reader.readInt32();
      final int frameIndex = reader.readInt32();
      final int lastFrameTime = reader.readInt32();
      final bool triggerReleased = reader.readBool();

      if (keyIndex < 0 || keyIndex >= WeaponType.values.length) {
        throw const FormatException('Invalid weapon key index in save.');
      }
      if (typeIndex < 0 || typeIndex >= WeaponType.values.length) {
        throw const FormatException('Invalid weapon type index in save.');
      }
      if (stateIndex < 0 || stateIndex >= WeaponState.values.length) {
        throw const FormatException('Invalid weapon state index in save.');
      }

      weaponStates[WeaponType.values[keyIndex]] = WeaponSaveState(
        type: WeaponType.values[typeIndex],
        state: WeaponState.values[stateIndex],
        frameIndex: frameIndex,
        lastFrameTime: lastFrameTime,
        triggerReleased: triggerReleased,
      );
    }

    return PlayerSaveState(
      x: x,
      y: y,
      angle: angle,
      health: health,
      ammo: ammo,
      score: score,
      lives: lives,
      damageFlash: damageFlash,
      bonusFlash: bonusFlash,
      chaingunPickupFaceMsRemaining: chaingunPickupFaceMsRemaining,
      mutantDeathFaceActive: mutantDeathFaceActive,
      godModeFaceEnabled: godModeFaceEnabled,
      faceSeed: faceSeed,
      faceFrame: faceFrame,
      faceCountTics: faceCountTics,
      nextFaceChangeThreshold: nextFaceChangeThreshold,
      hasGoldKey: hasGoldKey,
      hasSilverKey: hasSilverKey,
      hasMachineGun: hasMachineGun,
      hasChainGun: hasChainGun,
      currentWeaponType: WeaponType.values[currentWeaponIndex],
      weaponStates: weaponStates,
      switchStateIndex: switchStateIndex,
      pendingWeaponType:
          hasPendingWeapon &&
              pendingWeaponIndex >= 0 &&
              pendingWeaponIndex < WeaponType.values.length
          ? WeaponType.values[pendingWeaponIndex]
          : null,
      weaponAnimOffset: weaponAnimOffset,
    );
  }

  void _writeIntGrid(_BinaryWriter writer, List<List<int>> grid) {
    writer.writeUint32(grid.length);
    final int width = grid.isEmpty ? 0 : grid.first.length;
    writer.writeUint32(width);
    for (final List<int> row in grid) {
      if (row.length != width) {
        throw const FormatException('Grid rows must all have equal width.');
      }
      for (final int value in row) {
        writer.writeInt32(value);
      }
    }
  }

  List<List<int>> _readIntGrid(_BinaryReader reader) {
    final int height = reader.readUint32();
    final int width = reader.readUint32();

    return List<List<int>>.generate(
      height,
      (_) =>
          List<int>.generate(width, (_) => reader.readInt32(), growable: false),
      growable: false,
    );
  }

  bool _listEquals(List<int> a, List<int> b) {
    if (a.length != b.length) {
      return false;
    }
    for (int i = 0; i < a.length; i++) {
      if (a[i] != b[i]) {
        return false;
      }
    }
    return true;
  }
}

/// Step toward source-layout compatibility: classic save-style envelope.
///
/// This writes a fixed 32-byte description prefix and stores a checksum over
/// the payload body using canonical DoChecksum semantics.
class OriginalLayoutEnvelopeSaveGameCodec extends SaveGameCodec {
  static const int _envelopeVersion = 1;
  static const int _descriptionBytes = 32;
  static const List<int> _signature = <int>[0x57, 0x4C, 0x46, 0x53]; // WLFS

  @override
  Uint8List encode(SaveGameFile file) {
    final _BinaryWriter writer = _BinaryWriter();
    final Uint8List body = encodeSnapshotPayload(file.snapshot);
    final int checksum = doChecksum(body);

    writer.writeFixedUtf8(file.description, _descriptionBytes);
    writer.writeBytes(_signature);
    writer.writeUint16(_envelopeVersion);
    writer.writeUint8(file.gameVersion.index);
    writer.writeUint8(file.slot & 0xFF);
    writer.writeInt64(file.createdAtMs);
    writer.writeUtf8(file.dataVersionName);
    writer.writeUint32(body.length);
    writer.writeBytes(body);
    writer.writeUint32(checksum);

    return writer.toBytes();
  }

  @override
  SaveGameFile decode(Uint8List bytes) {
    final _BinaryReader reader = _BinaryReader(bytes);

    final String description = reader.readFixedUtf8(_descriptionBytes);
    final Uint8List signature = reader.readBytes(_signature.length);
    if (!_listEquals(signature, _signature)) {
      throw const FormatException('Invalid original-layout save signature.');
    }

    final int envelopeVersion = reader.readUint16();
    if (envelopeVersion != _envelopeVersion) {
      throw FormatException(
        'Unsupported original-layout envelope version: $envelopeVersion',
      );
    }

    final int gameVersionIndex = reader.readUint8();
    if (gameVersionIndex < 0 || gameVersionIndex >= GameVersion.values.length) {
      throw const FormatException('Invalid game version in save envelope.');
    }
    final GameVersion gameVersion = GameVersion.values[gameVersionIndex];

    final int slot = reader.readUint8();
    final int createdAtMs = reader.readInt64();
    final String dataVersionName = reader.readUtf8();
    final int bodyLength = reader.readUint32();
    final Uint8List body = reader.readBytes(bodyLength);
    final int checksum = reader.readUint32();

    final int computed = doChecksum(body);
    if (computed != checksum) {
      throw FormatException(
        'Save checksum mismatch. expected=$checksum actual=$computed',
      );
    }

    final snapshot = decodeSnapshotPayload(body);
    return SaveGameFile(
      slot: slot,
      gameVersion: gameVersion,
      dataVersionName: dataVersionName,
      description: description,
      createdAtMs: createdAtMs,
      snapshot: snapshot,
      checksum: checksum,
    );
  }

  @override
  bool _listEquals(List<int> a, List<int> b) {
    if (a.length != b.length) {
      return false;
    }
    for (int i = 0; i < a.length; i++) {
      if (a[i] != b[i]) {
        return false;
      }
    }
    return true;
  }
}

/// Writes classic-envelope saves while remaining able to read legacy W3DS
/// saves created by earlier engine builds.
class CompatibleSaveGameCodec extends OriginalLayoutEnvelopeSaveGameCodec {
  static const int _descriptionBytes = 32;
  static const int _mapSize = 64;
  static const int _numAreas = 37;
  static const int _maxStats = 400;
  static const int _maxDoors = 64;
  static const int _levelRatioCount = 8;

  static const int _dosGamestateSize = 66;
  static const int _dosLevelRatioSize = 10;
  static const int _dosActorRecordSize = 60;
  static const int _dosStatRecordSize = 8;
  static const int _dosDoorRecordSize = 10;

  static const int _dosAcBadObject = -1;
  static const int _dosAcNo = 0;
  static const int _dosAcYes = 1;

  static const int _dosClassNothing = 0;
  static const int _dosClassPlayer = 1;
  static const int _dosClassGuard = 3;
  static const int _dosClassOfficer = 4;
  static const int _dosClassSs = 5;
  static const int _dosClassDog = 6;
  static const int _dosClassBoss = 7;
  static const int _dosClassMutant = 11;

  static const int _doorDrOpen = 0;
  static const int _doorDrClosed = 1;
  static const int _doorDrOpening = 2;
  static const int _doorDrClosing = 3;

  static const int _dirEast = 0;
  static const int _dirNorthEast = 1;
  static const int _dirNorth = 2;
  static const int _dirNorthWest = 3;
  static const int _dirWest = 4;
  static const int _dirSouthWest = 5;
  static const int _dirSouth = 6;
  static const int _dirSouthEast = 7;
  static const int _dirNoDir = 8;

  static const int _weapKnife = 0;
  static const int _weapPistol = 1;
  static const int _weapMachineGun = 2;
  static const int _weapChainGun = 3;
  static const List<int> _extensionMagic = <int>[
    0x57,
    0x33,
    0x45,
    0x58,
  ]; // W3EX

  final SaveGameCodec _legacyCodec = SaveGameCodec();

  @override
  Uint8List encode(SaveGameFile file) {
    final _BinaryWriter writer = _BinaryWriter();
    writer.writeFixedUtf8(file.description, _descriptionBytes);

    int checksum = 0;

    final Uint8List gamestate = _encodeDosGamestate(file.snapshot);
    writer.writeBytes(gamestate);
    checksum = doChecksum(gamestate, checksum: checksum);

    final Uint8List levelRatios = Uint8List(
      _levelRatioCount * _dosLevelRatioSize,
    );
    writer.writeBytes(levelRatios);
    checksum = doChecksum(levelRatios, checksum: checksum);

    final Uint8List tilemap = _encodeDosTilemap(file.snapshot.currentLevel);
    writer.writeBytes(tilemap);
    checksum = doChecksum(tilemap, checksum: checksum);

    final Uint8List actorat = _encodeDosActorAt(file.snapshot);
    writer.writeBytes(actorat);
    checksum = doChecksum(actorat, checksum: checksum);

    final Uint8List areaconnect = Uint8List(_numAreas * _numAreas);
    writer.writeBytes(areaconnect);

    final Uint8List areabyplayer = _encodeDosAreasByPlayer(
      file.snapshot.areasByPlayer,
    );
    writer.writeBytes(areabyplayer);

    final Uint8List actorStream = _encodeDosActorStream(file.snapshot);
    writer.writeBytes(actorStream);

    final Uint8List laststatobj = Uint8List(2);
    writer.writeBytes(laststatobj);
    checksum = doChecksum(laststatobj, checksum: checksum);

    final Uint8List statobjlist = Uint8List(_maxStats * _dosStatRecordSize);
    writer.writeBytes(statobjlist);
    checksum = doChecksum(statobjlist, checksum: checksum);

    final Uint8List doorposition = _encodeDosDoorPosition(file.snapshot.doors);
    writer.writeBytes(doorposition);
    checksum = doChecksum(doorposition, checksum: checksum);

    final Uint8List doorobjlist = _encodeDosDoorObjList(file.snapshot.doors);
    writer.writeBytes(doorobjlist);
    checksum = doChecksum(doorobjlist, checksum: checksum);

    final Uint8List pushwallState = _encodeDosPushwallState(file.snapshot);
    writer.writeBytes(pushwallState);
    checksum = doChecksum(pushwallState, checksum: checksum);

    writer.writeUint32(checksum);

    final Uint8List extendedPayload = super.encodeSnapshotPayload(
      file.snapshot,
    );
    final int extendedChecksum = doChecksum(extendedPayload);
    writer.writeBytes(_extensionMagic);
    writer.writeUint8(file.gameVersion.index);
    writer.writeUint8(file.slot & 0xFF);
    writer.writeInt64(file.createdAtMs);
    writer.writeUtf8(file.dataVersionName);
    writer.writeUint32(extendedPayload.length);
    writer.writeBytes(extendedPayload);
    writer.writeUint32(extendedChecksum);

    return writer.toBytes();
  }

  @override
  SaveGameFile decode(Uint8List bytes) {
    try {
      return _decodeDos(bytes);
    } on FormatException {
      try {
        return super.decode(bytes);
      } on FormatException {
        return _legacyCodec.decode(bytes);
      }
    }
  }

  SaveGameFile _decodeDos(Uint8List bytes) {
    final _BinaryReader reader = _BinaryReader(bytes);
    final String description = reader.readFixedUtf8(_descriptionBytes);

    int checksum = 0;

    final Uint8List gamestateBytes = reader.readBytes(_dosGamestateSize);
    checksum = doChecksum(gamestateBytes, checksum: checksum);
    final _DosGamestate gamestate = _decodeDosGamestate(gamestateBytes);

    final Uint8List levelRatios = reader.readBytes(
      _levelRatioCount * _dosLevelRatioSize,
    );
    checksum = doChecksum(levelRatios, checksum: checksum);

    final Uint8List tilemapBytes = reader.readBytes(_mapSize * _mapSize);
    checksum = doChecksum(tilemapBytes, checksum: checksum);
    final List<List<int>> tilemap = _decodeDosTilemap(tilemapBytes);

    final Uint8List actorat = reader.readBytes(_mapSize * _mapSize * 2);
    checksum = doChecksum(actorat, checksum: checksum);

    reader.readBytes(_numAreas * _numAreas);
    final Uint8List areasByPlayerBytes = reader.readBytes(_numAreas * 2);
    final List<bool> areasByPlayer = _decodeDosAreasByPlayer(
      areasByPlayerBytes,
    );

    final List<_DosActorRecord> actors = <_DosActorRecord>[];
    while (true) {
      final Uint8List record = reader.readBytes(_dosActorRecordSize);
      final _DosActorRecord actor = _decodeDosActorRecord(record);
      actors.add(actor);
      if (actor.active == _dosAcBadObject) {
        break;
      }
    }

    final Uint8List laststatobj = reader.readBytes(2);
    checksum = doChecksum(laststatobj, checksum: checksum);

    final Uint8List statobjlist = reader.readBytes(
      _maxStats * _dosStatRecordSize,
    );
    checksum = doChecksum(statobjlist, checksum: checksum);

    final Uint8List doorposition = reader.readBytes(_maxDoors * 2);
    checksum = doChecksum(doorposition, checksum: checksum);

    final Uint8List doorobjlist = reader.readBytes(
      _maxDoors * _dosDoorRecordSize,
    );
    checksum = doChecksum(doorobjlist, checksum: checksum);

    final Uint8List pushwallState = reader.readBytes(10);
    checksum = doChecksum(pushwallState, checksum: checksum);

    final int storedChecksum = reader.readUint32();
    if (storedChecksum != checksum) {
      throw FormatException(
        'Save checksum mismatch. expected=$storedChecksum actual=$checksum',
      );
    }

    final PlayerSaveState dosPlayer = _decodeDosPlayer(gamestate, actors);
    final List<EntitySaveState> dosEntities = _decodeDosEntities(actors);
    final List<DoorSaveState> dosDoors = _decodeDosDoors(
      doorposition,
      doorobjlist,
    );
    final List<PushwallSaveState> dosPushwalls = _decodeDosPushwalls(
      pushwallState,
    );

    int slot = 0;
    GameVersion gameVersion = GameVersion.retail;
    String dataVersionName = 'dos-compat';
    int createdAtMs = 0;

    GameSessionSnapshot snapshot = GameSessionSnapshot(
      currentGameIndex: 0,
      currentEpisodeIndex: gamestate.episode.clamp(0, 5),
      currentLevelIndex: gamestate.mapon.clamp(0, 9),
      returnLevelIndex: null,
      difficulty: _decodeDifficulty(gamestate.difficulty),
      timeAliveMs: (gamestate.timeCount * 1000) ~/ 70,
      lastAcousticAlertTime: 0,
      isMapOverlayVisible: false,
      isMenuOverlayVisible: false,
      player: dosPlayer,
      currentLevel: tilemap,
      areaGrid: _emptyAreaGrid(),
      areasByPlayer: areasByPlayer,
      entities: dosEntities,
      doors: dosDoors,
      pushwalls: dosPushwalls,
    );

    if (reader.remaining > 0) {
      try {
        final Uint8List extMagic = reader.readBytes(_extensionMagic.length);
        if (_listEquals(extMagic, _extensionMagic)) {
          final int gameVersionIndex = reader.readUint8();
          if (gameVersionIndex >= 0 &&
              gameVersionIndex < GameVersion.values.length) {
            gameVersion = GameVersion.values[gameVersionIndex];
          }
          slot = reader.readUint8();
          createdAtMs = reader.readInt64();
          dataVersionName = reader.readUtf8();
          final int extendedLength = reader.readUint32();
          final Uint8List extendedPayload = reader.readBytes(extendedLength);
          final int extendedChecksum = reader.readUint32();
          final int computed = doChecksum(extendedPayload);
          if (computed == extendedChecksum) {
            snapshot = super.decodeSnapshotPayload(extendedPayload);
          }
        }
      } catch (_) {}
    }

    return SaveGameFile(
      slot: slot,
      gameVersion: gameVersion,
      dataVersionName: dataVersionName,
      description: description,
      createdAtMs: createdAtMs,
      snapshot: snapshot,
      checksum: storedChecksum,
    );
  }

  Uint8List _encodeDosGamestate(GameSessionSnapshot snapshot) {
    final _BinaryWriter writer = _BinaryWriter();
    final PlayerSaveState player = snapshot.player;

    writer.writeInt16(snapshot.difficulty.index.clamp(0, 3));
    writer.writeInt16(snapshot.currentLevelIndex.clamp(0, 9));
    writer.writeInt32(player.score);
    writer.writeInt32(player.score);
    writer.writeInt32(40000);
    writer.writeInt16(player.lives);
    writer.writeInt16(player.health);
    writer.writeInt16(player.ammo);
    int keys = 0;
    if (player.hasGoldKey) keys |= 1;
    if (player.hasSilverKey) keys |= 2;
    writer.writeInt16(keys);

    final int bestWeapon = _encodeDosWeapon(
      player.hasChainGun
          ? WeaponType.chainGun
          : (player.hasMachineGun ? WeaponType.machineGun : WeaponType.pistol),
    );
    writer.writeInt16(bestWeapon);
    writer.writeInt16(_encodeDosWeapon(player.currentWeaponType));
    writer.writeInt16(_encodeDosWeapon(player.currentWeaponType));

    writer.writeInt16(player.faceFrame);
    writer.writeInt16(0);
    writer.writeInt16(0);
    writer.writeInt16(0);

    writer.writeInt16(snapshot.currentEpisodeIndex.clamp(0, 5));
    writer.writeInt16(0);
    writer.writeInt16(0);
    writer.writeInt16(0);
    writer.writeInt16(0);
    writer.writeInt16(0);
    writer.writeInt16(0);

    writer.writeInt32((snapshot.timeAliveMs * 70) ~/ 1000);
    writer.writeInt32((player.x * 65536).round());
    writer.writeInt32((player.y * 65536).round());
    writer.writeInt16(0);

    final Uint8List bytes = writer.toBytes();
    if (bytes.length != _dosGamestateSize) {
      throw FormatException(
        'Unexpected DOS gamestate size ${bytes.length} (expected $_dosGamestateSize).',
      );
    }
    return bytes;
  }

  _DosGamestate _decodeDosGamestate(Uint8List bytes) {
    final _BinaryReader reader = _BinaryReader(bytes);
    return _DosGamestate(
      difficulty: reader.readInt16(),
      mapon: reader.readInt16(),
      oldscore: reader.readInt32(),
      score: reader.readInt32(),
      nextextra: reader.readInt32(),
      lives: reader.readInt16(),
      health: reader.readInt16(),
      ammo: reader.readInt16(),
      keys: reader.readInt16(),
      bestweapon: reader.readInt16(),
      weapon: reader.readInt16(),
      chosenweapon: reader.readInt16(),
      faceframe: reader.readInt16(),
      attackframe: reader.readInt16(),
      attackcount: reader.readInt16(),
      weaponframe: reader.readInt16(),
      episode: reader.readInt16(),
      secretcount: reader.readInt16(),
      treasurecount: reader.readInt16(),
      killcount: reader.readInt16(),
      secrettotal: reader.readInt16(),
      treasuretotal: reader.readInt16(),
      killtotal: reader.readInt16(),
      timeCount: reader.readInt32(),
      killx: reader.readInt32(),
      killy: reader.readInt32(),
      victoryflag: reader.readInt16(),
    );
  }

  Uint8List _encodeDosTilemap(List<List<int>> grid) {
    final Uint8List bytes = Uint8List(_mapSize * _mapSize);
    for (int y = 0; y < _mapSize; y++) {
      final List<int> row = y < grid.length ? grid[y] : const <int>[];
      for (int x = 0; x < _mapSize; x++) {
        final int value = x < row.length ? row[x] : 0;
        bytes[(x * _mapSize) + y] = value & 0xFF;
      }
    }
    return bytes;
  }

  List<List<int>> _decodeDosTilemap(Uint8List bytes) {
    final List<List<int>> grid = List<List<int>>.generate(
      _mapSize,
      (_) => List<int>.filled(_mapSize, 0, growable: false),
      growable: false,
    );
    for (int x = 0; x < _mapSize; x++) {
      for (int y = 0; y < _mapSize; y++) {
        grid[y][x] = bytes[(x * _mapSize) + y];
      }
    }
    return grid;
  }

  Uint8List _encodeDosActorAt(GameSessionSnapshot snapshot) {
    final Uint8List bytes = Uint8List(_mapSize * _mapSize * 2);
    final ByteData view = ByteData.sublistView(bytes);

    for (int y = 0; y < _mapSize; y++) {
      final List<int> row = y < snapshot.currentLevel.length
          ? snapshot.currentLevel[y]
          : const <int>[];
      for (int x = 0; x < _mapSize; x++) {
        final int tile = x < row.length ? row[x] : 0;
        int value = 0;
        if (tile > 0) {
          value = tile & 0xFF;
        }

        final int offset = ((x * _mapSize) + y) * 2;
        view.setUint16(offset, value, Endian.little);
      }
    }

    for (int i = 0; i < snapshot.doors.length && i < _maxDoors; i++) {
      final DoorSaveState door = snapshot.doors[i];
      if (door.x < 0 ||
          door.x >= _mapSize ||
          door.y < 0 ||
          door.y >= _mapSize) {
        continue;
      }
      final int offset = ((door.x * _mapSize) + door.y) * 2;
      view.setUint16(offset, (i & 0x3F) | 0x80, Endian.little);
    }

    for (int i = 0; i < snapshot.entities.length; i++) {
      final EntitySaveState entity = snapshot.entities[i];
      final int x = entity.x.floor();
      final int y = entity.y.floor();
      if (x < 0 || x >= _mapSize || y < 0 || y >= _mapSize) {
        continue;
      }
      final int offset = ((x * _mapSize) + y) * 2;
      view.setUint16(offset, 0x100 + (i * 2), Endian.little);
    }

    final int px = snapshot.player.x.floor();
    final int py = snapshot.player.y.floor();
    if (px >= 0 && px < _mapSize && py >= 0 && py < _mapSize) {
      final int offset = ((px * _mapSize) + py) * 2;
      view.setUint16(offset, 0x200, Endian.little);
    }

    return bytes;
  }

  Uint8List _encodeDosAreasByPlayer(List<bool> areasByPlayer) {
    final _BinaryWriter writer = _BinaryWriter();
    for (int i = 0; i < _numAreas; i++) {
      final bool value = i < areasByPlayer.length ? areasByPlayer[i] : false;
      writer.writeInt16(value ? 1 : 0);
    }
    return writer.toBytes();
  }

  List<bool> _decodeDosAreasByPlayer(Uint8List bytes) {
    final _BinaryReader reader = _BinaryReader(bytes);
    return List<bool>.generate(
      _numAreas,
      (_) => reader.readInt16() != 0,
      growable: false,
    );
  }

  Uint8List _encodeDosActorStream(GameSessionSnapshot snapshot) {
    final _BinaryWriter writer = _BinaryWriter();

    writer.writeBytes(
      _encodeDosActorRecord(
        _DosActorRecord(
          active: _dosAcYes,
          ticcount: 0,
          obclass: _dosClassPlayer,
          statePtr: 0,
          flags: 0,
          distance: 0,
          dir: _dirNoDir,
          x: (snapshot.player.x * 65536).round(),
          y: (snapshot.player.y * 65536).round(),
          tilex: snapshot.player.x.floor().clamp(0, _mapSize - 1),
          tiley: snapshot.player.y.floor().clamp(0, _mapSize - 1),
          areanumber: 0,
          viewx: 0,
          viewheight: 0,
          transx: 0,
          transy: 0,
          angle: ((snapshot.player.angle * 180.0) / 3.141592653589793).round(),
          hitpoints: snapshot.player.health,
          speed: 0,
          temp1: 0,
          temp2: 0,
          temp3: 0,
          nextPtr: 0,
          prevPtr: 0,
        ),
      ),
    );

    for (final EntitySaveState entity in snapshot.entities) {
      writer.writeBytes(
        _encodeDosActorRecord(
          _DosActorRecord(
            active: entity.state == EntityState.dead ? _dosAcNo : _dosAcYes,
            ticcount: 0,
            obclass: _encodeDosClass(entity.kind),
            statePtr: 0,
            flags: entity.state == EntityState.dead ? 0 : 1,
            distance: 0,
            dir: _angleToDosDir(entity.angle),
            x: (entity.x * 65536).round(),
            y: (entity.y * 65536).round(),
            tilex: entity.x.floor().clamp(0, _mapSize - 1),
            tiley: entity.y.floor().clamp(0, _mapSize - 1),
            areanumber: entity.mapId.clamp(0, 255),
            viewx: 0,
            viewheight: 0,
            transx: 0,
            transy: 0,
            angle: ((entity.angle * 180.0) / 3.141592653589793).round(),
            hitpoints: (entity.extraData['health'] as num?)?.toInt() ?? 100,
            speed: (entity.extraData['speed'] as num?)?.toInt() ?? 0,
            temp1: entity.mapId,
            temp2: 0,
            temp3: 0,
            nextPtr: 0,
            prevPtr: 0,
          ),
        ),
      );
    }

    writer.writeBytes(
      _encodeDosActorRecord(
        const _DosActorRecord(
          active: _dosAcBadObject,
          ticcount: 0,
          obclass: _dosClassNothing,
          statePtr: 0,
          flags: 0,
          distance: 0,
          dir: _dirNoDir,
          x: 0,
          y: 0,
          tilex: 0,
          tiley: 0,
          areanumber: 0,
          viewx: 0,
          viewheight: 0,
          transx: 0,
          transy: 0,
          angle: 0,
          hitpoints: 0,
          speed: 0,
          temp1: 0,
          temp2: 0,
          temp3: 0,
          nextPtr: 0,
          prevPtr: 0,
        ),
      ),
    );

    return writer.toBytes();
  }

  Uint8List _encodeDosActorRecord(_DosActorRecord actor) {
    final _BinaryWriter writer = _BinaryWriter();
    writer.writeInt16(actor.active);
    writer.writeInt16(actor.ticcount);
    writer.writeInt16(actor.obclass);
    writer.writeUint16(actor.statePtr & 0xFFFF);
    writer.writeUint8(actor.flags & 0xFF);
    writer.writeUint8(0);
    writer.writeInt32(actor.distance);
    writer.writeInt16(actor.dir);
    writer.writeInt32(actor.x);
    writer.writeInt32(actor.y);
    writer.writeUint16(actor.tilex & 0xFFFF);
    writer.writeUint16(actor.tiley & 0xFFFF);
    writer.writeUint8(actor.areanumber & 0xFF);
    writer.writeUint8(0);
    writer.writeInt16(actor.viewx);
    writer.writeUint16(actor.viewheight & 0xFFFF);
    writer.writeInt32(actor.transx);
    writer.writeInt32(actor.transy);
    writer.writeInt16(actor.angle);
    writer.writeInt16(actor.hitpoints);
    writer.writeInt32(actor.speed);
    writer.writeInt16(actor.temp1);
    writer.writeInt16(actor.temp2);
    writer.writeInt16(actor.temp3);
    writer.writeUint16(actor.nextPtr & 0xFFFF);
    writer.writeUint16(actor.prevPtr & 0xFFFF);

    final Uint8List bytes = writer.toBytes();
    if (bytes.length != _dosActorRecordSize) {
      throw FormatException(
        'Unexpected DOS actor size ${bytes.length} (expected $_dosActorRecordSize).',
      );
    }
    return bytes;
  }

  _DosActorRecord _decodeDosActorRecord(Uint8List bytes) {
    final _BinaryReader reader = _BinaryReader(bytes);
    return _DosActorRecord(
      active: reader.readInt16(),
      ticcount: reader.readInt16(),
      obclass: reader.readInt16(),
      statePtr: reader.readUint16(),
      flags: reader.readUint8(),
      distance: (() {
        reader.readUint8();
        return reader.readInt32();
      })(),
      dir: reader.readInt16(),
      x: reader.readInt32(),
      y: reader.readInt32(),
      tilex: reader.readUint16(),
      tiley: reader.readUint16(),
      areanumber: (() {
        final int area = reader.readUint8();
        reader.readUint8();
        return area;
      })(),
      viewx: reader.readInt16(),
      viewheight: reader.readUint16(),
      transx: reader.readInt32(),
      transy: reader.readInt32(),
      angle: reader.readInt16(),
      hitpoints: reader.readInt16(),
      speed: reader.readInt32(),
      temp1: reader.readInt16(),
      temp2: reader.readInt16(),
      temp3: reader.readInt16(),
      nextPtr: reader.readUint16(),
      prevPtr: reader.readUint16(),
    );
  }

  Uint8List _encodeDosDoorPosition(List<DoorSaveState> doors) {
    final _BinaryWriter writer = _BinaryWriter();
    for (int i = 0; i < _maxDoors; i++) {
      final DoorSaveState? door = i < doors.length ? doors[i] : null;
      final int value = door == null
          ? 0
          : (door.offset.clamp(0.0, 1.0) * 65535).round();
      writer.writeUint16(value);
    }
    return writer.toBytes();
  }

  Uint8List _encodeDosDoorObjList(List<DoorSaveState> doors) {
    final _BinaryWriter writer = _BinaryWriter();
    for (int i = 0; i < _maxDoors; i++) {
      final DoorSaveState? door = i < doors.length ? doors[i] : null;
      if (door == null) {
        writer.writeUint8(0);
        writer.writeUint8(0);
        writer.writeInt16(0);
        writer.writeUint8(0);
        writer.writeUint8(0);
        writer.writeInt16(_doorDrClosed);
        writer.writeInt16(0);
        continue;
      }

      writer.writeUint8(door.x & 0xFF);
      writer.writeUint8(door.y & 0xFF);
      writer.writeInt16(door.mapId.isOdd ? 1 : 0);
      writer.writeUint8(0);
      writer.writeUint8(0);
      writer.writeInt16(_encodeDoorAction(door.state));
      writer.writeInt16(door.openTime);
    }
    return writer.toBytes();
  }

  List<DoorSaveState> _decodeDosDoors(Uint8List positions, Uint8List doorsRaw) {
    final _BinaryReader posReader = _BinaryReader(positions);
    final _BinaryReader doorReader = _BinaryReader(doorsRaw);
    final List<DoorSaveState> doors = <DoorSaveState>[];

    for (int i = 0; i < _maxDoors; i++) {
      final int position = posReader.readUint16();
      final int tilex = doorReader.readUint8();
      final int tiley = doorReader.readUint8();
      final bool vertical = doorReader.readInt16() != 0;
      final int lock = doorReader.readUint8();
      doorReader.readUint8();
      final int action = doorReader.readInt16();
      final int ticcount = doorReader.readInt16();

      if (tilex == 0 &&
          tiley == 0 &&
          action == _doorDrClosed &&
          position == 0) {
        continue;
      }

      doors.add(
        DoorSaveState(
          x: tilex,
          y: tiley,
          mapId: (vertical ? 1 : 0) | (lock << 1),
          state: _decodeDoorState(action),
          offset: position / 65535.0,
          openTime: ticcount,
        ),
      );
    }

    return doors;
  }

  Uint8List _encodeDosPushwallState(GameSessionSnapshot snapshot) {
    final _BinaryWriter writer = _BinaryWriter();
    final PushwallSaveState? pushwall = snapshot.pushwalls.isNotEmpty
        ? snapshot.pushwalls.first
        : null;
    if (pushwall == null) {
      writer.writeUint16(0);
      writer.writeUint16(0);
      writer.writeUint16(0);
      writer.writeInt16(0);
      writer.writeUint16(0);
      return writer.toBytes();
    }

    final int state =
        ((pushwall.tilesMoved * 128) + (pushwall.offset * 128).round()).clamp(
          1,
          65535,
        );
    writer.writeUint16(pushwall.isActive ? state : 0);
    writer.writeUint16(pushwall.x.clamp(0, _mapSize - 1));
    writer.writeUint16(pushwall.y.clamp(0, _mapSize - 1));
    writer.writeInt16(_encodePushwallDir(pushwall.dirX, pushwall.dirY));
    writer.writeUint16((pushwall.offset.clamp(0.0, 1.0) * 63).round());
    return writer.toBytes();
  }

  List<PushwallSaveState> _decodeDosPushwalls(Uint8List bytes) {
    final _BinaryReader reader = _BinaryReader(bytes);
    final int state = reader.readUint16();
    final int x = reader.readUint16();
    final int y = reader.readUint16();
    final int dir = reader.readInt16();
    final int pos = reader.readUint16();

    if (state == 0) {
      return const <PushwallSaveState>[];
    }

    final ({int dx, int dy}) mapped = _decodePushwallDir(dir);
    return <PushwallSaveState>[
      PushwallSaveState(
        x: x,
        y: y,
        mapId: 0,
        dirX: mapped.dx,
        dirY: mapped.dy,
        offset: pos / 63.0,
        tilesMoved: state ~/ 128,
        isActive: true,
      ),
    ];
  }

  PlayerSaveState _decodeDosPlayer(
    _DosGamestate gamestate,
    List<_DosActorRecord> actors,
  ) {
    final _DosActorRecord? playerActor = actors.isEmpty ? null : actors.first;
    final WeaponType weapon = _decodeDosWeapon(gamestate.weapon);
    final Map<WeaponType, WeaponSaveState> weapons =
        <WeaponType, WeaponSaveState>{
          WeaponType.knife: const WeaponSaveState(
            type: WeaponType.knife,
            state: WeaponState.idle,
            frameIndex: 0,
            lastFrameTime: 0,
            triggerReleased: true,
          ),
          WeaponType.pistol: const WeaponSaveState(
            type: WeaponType.pistol,
            state: WeaponState.idle,
            frameIndex: 0,
            lastFrameTime: 0,
            triggerReleased: true,
          ),
        };

    final int bestWeapon = gamestate.bestweapon;
    if (bestWeapon >= _weapMachineGun) {
      weapons[WeaponType.machineGun] = const WeaponSaveState(
        type: WeaponType.machineGun,
        state: WeaponState.idle,
        frameIndex: 0,
        lastFrameTime: 0,
        triggerReleased: true,
      );
    }
    if (bestWeapon >= _weapChainGun) {
      weapons[WeaponType.chainGun] = const WeaponSaveState(
        type: WeaponType.chainGun,
        state: WeaponState.idle,
        frameIndex: 0,
        lastFrameTime: 0,
        triggerReleased: true,
      );
    }

    return PlayerSaveState(
      x: playerActor != null
          ? playerActor.x / 65536.0
          : gamestate.killx / 65536.0,
      y: playerActor != null
          ? playerActor.y / 65536.0
          : gamestate.killy / 65536.0,
      angle: playerActor != null
          ? (playerActor.angle * 3.141592653589793) / 180.0
          : 0.0,
      health: gamestate.health,
      ammo: gamestate.ammo,
      score: gamestate.score,
      lives: gamestate.lives,
      damageFlash: 0,
      bonusFlash: 0,
      chaingunPickupFaceMsRemaining: 0,
      mutantDeathFaceActive: false,
      godModeFaceEnabled: false,
      faceSeed: 0,
      faceFrame: gamestate.faceframe,
      faceCountTics: 0,
      nextFaceChangeThreshold: 0,
      hasGoldKey: (gamestate.keys & 1) != 0,
      hasSilverKey: (gamestate.keys & 2) != 0,
      hasMachineGun: bestWeapon >= _weapMachineGun,
      hasChainGun: bestWeapon >= _weapChainGun,
      currentWeaponType: weapon,
      weaponStates: weapons,
      switchStateIndex: 0,
      pendingWeaponType: null,
      weaponAnimOffset: 0,
    );
  }

  List<EntitySaveState> _decodeDosEntities(List<_DosActorRecord> actors) {
    final List<EntitySaveState> entities = <EntitySaveState>[];
    for (int i = 1; i < actors.length; i++) {
      final _DosActorRecord actor = actors[i];
      if (actor.active == _dosAcBadObject || actor.obclass == _dosClassPlayer) {
        continue;
      }
      entities.add(
        EntitySaveState(
          kind: _decodeDosClass(actor.obclass),
          x: actor.x / 65536.0,
          y: actor.y / 65536.0,
          spriteIndex: 0,
          angle: (actor.angle * 3.141592653589793) / 180.0,
          state: actor.active == _dosAcNo
              ? EntityState.idle
              : EntityState.patrolling,
          mapId: actor.temp1,
          lastActionTime: 0,
          extraData: <String, Object?>{
            'health': actor.hitpoints,
            'speed': actor.speed,
          },
        ),
      );
    }
    return entities;
  }

  List<List<int>> _emptyAreaGrid() {
    return List<List<int>>.generate(
      _mapSize,
      (_) => List<int>.filled(_mapSize, -1, growable: false),
      growable: false,
    );
  }

  Difficulty _decodeDifficulty(int difficulty) {
    if (difficulty < 0 || difficulty >= Difficulty.values.length) {
      return Difficulty.medium;
    }
    return Difficulty.values[difficulty];
  }

  int _encodeDosWeapon(WeaponType weapon) {
    switch (weapon) {
      case WeaponType.knife:
        return _weapKnife;
      case WeaponType.pistol:
        return _weapPistol;
      case WeaponType.machineGun:
        return _weapMachineGun;
      case WeaponType.chainGun:
        return _weapChainGun;
    }
  }

  WeaponType _decodeDosWeapon(int weapon) {
    switch (weapon) {
      case _weapKnife:
        return WeaponType.knife;
      case _weapMachineGun:
        return WeaponType.machineGun;
      case _weapChainGun:
        return WeaponType.chainGun;
      case _weapPistol:
      default:
        return WeaponType.pistol;
    }
  }

  int _encodeDoorAction(DoorState state) {
    switch (state) {
      case DoorState.closed:
        return _doorDrClosed;
      case DoorState.opening:
        return _doorDrOpening;
      case DoorState.open:
        return _doorDrOpen;
      case DoorState.closing:
        return _doorDrClosing;
    }
  }

  DoorState _decodeDoorState(int action) {
    switch (action) {
      case _doorDrOpen:
        return DoorState.open;
      case _doorDrOpening:
        return DoorState.opening;
      case _doorDrClosing:
        return DoorState.closing;
      case _doorDrClosed:
      default:
        return DoorState.closed;
    }
  }

  int _encodePushwallDir(int dx, int dy) {
    if (dx == 1 && dy == 0) return _dirEast;
    if (dx == -1 && dy == 0) return _dirWest;
    if (dx == 0 && dy == 1) return _dirSouth;
    if (dx == 0 && dy == -1) return _dirNorth;
    return _dirEast;
  }

  ({int dx, int dy}) _decodePushwallDir(int dir) {
    switch (dir) {
      case _dirEast:
        return (dx: 1, dy: 0);
      case _dirWest:
        return (dx: -1, dy: 0);
      case _dirSouth:
        return (dx: 0, dy: 1);
      case _dirNorth:
        return (dx: 0, dy: -1);
      default:
        return (dx: 1, dy: 0);
    }
  }

  int _angleToDosDir(double angle) {
    const double fullTurn = 6.283185307179586;
    double normalized = angle % fullTurn;
    if (normalized < 0) normalized += fullTurn;
    final int octant = ((normalized / fullTurn) * 8).round() & 7;
    switch (octant) {
      case 0:
        return _dirEast;
      case 1:
        return _dirNorthEast;
      case 2:
        return _dirNorth;
      case 3:
        return _dirNorthWest;
      case 4:
        return _dirWest;
      case 5:
        return _dirSouthWest;
      case 6:
        return _dirSouth;
      default:
        return _dirSouthEast;
    }
  }

  int _encodeDosClass(String kind) {
    final String normalized = kind.toLowerCase();
    if (normalized.contains('guard')) return _dosClassGuard;
    if (normalized.contains('officer')) return _dosClassOfficer;
    if (normalized.contains('ss')) return _dosClassSs;
    if (normalized.contains('dog')) return _dosClassDog;
    if (normalized.contains('boss')) return _dosClassBoss;
    if (normalized.contains('mutant')) return _dosClassMutant;
    return _dosClassNothing;
  }

  String _decodeDosClass(int cls) {
    switch (cls) {
      case _dosClassGuard:
        return 'Guard';
      case _dosClassOfficer:
        return 'Officer';
      case _dosClassSs:
        return 'SS';
      case _dosClassDog:
        return 'Dog';
      case _dosClassBoss:
        return 'Boss';
      case _dosClassMutant:
        return 'Mutant';
      default:
        return 'Entity';
    }
  }
}

class _DosGamestate {
  const _DosGamestate({
    required this.difficulty,
    required this.mapon,
    required this.oldscore,
    required this.score,
    required this.nextextra,
    required this.lives,
    required this.health,
    required this.ammo,
    required this.keys,
    required this.bestweapon,
    required this.weapon,
    required this.chosenweapon,
    required this.faceframe,
    required this.attackframe,
    required this.attackcount,
    required this.weaponframe,
    required this.episode,
    required this.secretcount,
    required this.treasurecount,
    required this.killcount,
    required this.secrettotal,
    required this.treasuretotal,
    required this.killtotal,
    required this.timeCount,
    required this.killx,
    required this.killy,
    required this.victoryflag,
  });

  final int difficulty;
  final int mapon;
  final int oldscore;
  final int score;
  final int nextextra;
  final int lives;
  final int health;
  final int ammo;
  final int keys;
  final int bestweapon;
  final int weapon;
  final int chosenweapon;
  final int faceframe;
  final int attackframe;
  final int attackcount;
  final int weaponframe;
  final int episode;
  final int secretcount;
  final int treasurecount;
  final int killcount;
  final int secrettotal;
  final int treasuretotal;
  final int killtotal;
  final int timeCount;
  final int killx;
  final int killy;
  final int victoryflag;
}

class _DosActorRecord {
  const _DosActorRecord({
    required this.active,
    required this.ticcount,
    required this.obclass,
    required this.statePtr,
    required this.flags,
    required this.distance,
    required this.dir,
    required this.x,
    required this.y,
    required this.tilex,
    required this.tiley,
    required this.areanumber,
    required this.viewx,
    required this.viewheight,
    required this.transx,
    required this.transy,
    required this.angle,
    required this.hitpoints,
    required this.speed,
    required this.temp1,
    required this.temp2,
    required this.temp3,
    required this.nextPtr,
    required this.prevPtr,
  });

  final int active;
  final int ticcount;
  final int obclass;
  final int statePtr;
  final int flags;
  final int distance;
  final int dir;
  final int x;
  final int y;
  final int tilex;
  final int tiley;
  final int areanumber;
  final int viewx;
  final int viewheight;
  final int transx;
  final int transy;
  final int angle;
  final int hitpoints;
  final int speed;
  final int temp1;
  final int temp2;
  final int temp3;
  final int nextPtr;
  final int prevPtr;
}

class _BinaryWriter {
  final BytesBuilder _builder = BytesBuilder(copy: false);

  void writeBytes(List<int> bytes) {
    _builder.add(bytes);
  }

  void writeBool(bool value) {
    writeUint8(value ? 1 : 0);
  }

  void writeUint8(int value) {
    _builder.add(<int>[value & 0xFF]);
  }

  void writeUint16(int value) {
    final ByteData data = ByteData(2);
    data.setUint16(0, value, Endian.little);
    _builder.add(data.buffer.asUint8List());
  }

  void writeInt16(int value) {
    final ByteData data = ByteData(2);
    data.setInt16(0, value, Endian.little);
    _builder.add(data.buffer.asUint8List());
  }

  void writeUint32(int value) {
    final ByteData data = ByteData(4);
    data.setUint32(0, value, Endian.little);
    _builder.add(data.buffer.asUint8List());
  }

  void writeInt32(int value) {
    final ByteData data = ByteData(4);
    data.setInt32(0, value, Endian.little);
    _builder.add(data.buffer.asUint8List());
  }

  void writeInt64(int value) {
    final ByteData data = ByteData(8);
    data.setInt64(0, value, Endian.little);
    _builder.add(data.buffer.asUint8List());
  }

  void writeFloat64(double value) {
    final ByteData data = ByteData(8);
    data.setFloat64(0, value, Endian.little);
    _builder.add(data.buffer.asUint8List());
  }

  void writeUtf8(String value) {
    final Uint8List raw = Uint8List.fromList(utf8.encode(value));
    writeUint32(raw.length);
    writeBytes(raw);
  }

  void writeFixedUtf8(String value, int width) {
    final Uint8List raw = Uint8List.fromList(utf8.encode(value));
    final Uint8List fixed = Uint8List(width);
    final int copyLength = raw.length < width ? raw.length : width;
    fixed.setRange(0, copyLength, raw);
    writeBytes(fixed);
  }

  Uint8List toBytes() => _builder.toBytes();
}

class _BinaryReader {
  _BinaryReader(this._data);

  final Uint8List _data;
  int _offset = 0;

  int get remaining => _data.length - _offset;

  Uint8List readBytes(int length) {
    _ensure(length);
    final Uint8List out = Uint8List.sublistView(
      _data,
      _offset,
      _offset + length,
    );
    _offset += length;
    return out;
  }

  bool readBool() => readUint8() != 0;

  int readUint8() {
    _ensure(1);
    final int value = _data[_offset];
    _offset += 1;
    return value;
  }

  int readUint16() {
    _ensure(2);
    final int value = ByteData.sublistView(
      _data,
      _offset,
      _offset + 2,
    ).getUint16(0, Endian.little);
    _offset += 2;
    return value;
  }

  int readInt16() {
    _ensure(2);
    final int value = ByteData.sublistView(
      _data,
      _offset,
      _offset + 2,
    ).getInt16(0, Endian.little);
    _offset += 2;
    return value;
  }

  int readUint32() {
    _ensure(4);
    final int value = ByteData.sublistView(
      _data,
      _offset,
      _offset + 4,
    ).getUint32(0, Endian.little);
    _offset += 4;
    return value;
  }

  int readInt32() {
    _ensure(4);
    final int value = ByteData.sublistView(
      _data,
      _offset,
      _offset + 4,
    ).getInt32(0, Endian.little);
    _offset += 4;
    return value;
  }

  int readInt64() {
    _ensure(8);
    final int value = ByteData.sublistView(
      _data,
      _offset,
      _offset + 8,
    ).getInt64(0, Endian.little);
    _offset += 8;
    return value;
  }

  double readFloat64() {
    _ensure(8);
    final double value = ByteData.sublistView(
      _data,
      _offset,
      _offset + 8,
    ).getFloat64(0, Endian.little);
    _offset += 8;
    return value;
  }

  String readUtf8() {
    final int length = readUint32();
    if (length == 0) {
      return '';
    }
    final Uint8List raw = readBytes(length);
    return utf8.decode(raw, allowMalformed: true);
  }

  String readFixedUtf8(int width) {
    final Uint8List raw = readBytes(width);
    int end = raw.length;
    for (int i = 0; i < raw.length; i++) {
      if (raw[i] == 0) {
        end = i;
        break;
      }
    }
    if (end <= 0) {
      return '';
    }
    return utf8.decode(raw.sublist(0, end), allowMalformed: true);
  }

  void _ensure(int needed) {
    if (remaining < needed) {
      throw FormatException(
        'Unexpected EOF while reading save payload. Needed $needed bytes, '
        'remaining $remaining.',
      );
    }
  }
}
