import 'dart:developer' show log;
import 'dart:math' as math;

import 'package:arcane_helper_utils/arcane_helper_utils.dart';
import 'package:wolf_3d_dart/src/menu/menu_manager.dart';
import 'package:wolf_3d_dart/wolf_3d_data_types.dart';
import 'package:wolf_3d_dart/wolf_3d_engine.dart';
import 'package:wolf_3d_dart/wolf_3d_menu.dart';

import 'cli_renderer_backend.dart';
import 'menu_font.dart';

class AsciiTheme {
  final String name;

  /// The character ramp, ordered from most dense (index 0) to least dense (last index).
  final String ramp;

  const AsciiTheme(this.name, this.ramp);

  /// Always returns the densest character (e.g., for walls, UI, floors)
  String get solid => ramp[0];

  /// Always returns the completely empty character (e.g., for pitch black darkness)
  String get empty => ramp[ramp.length - 1];

  /// Returns a character based on a 0.0 to 1.0 brightness scale.
  /// 1.0 returns the [solid] character, 0.0 returns the [empty] character.
  String getByBrightness(double brightness) {
    double b = brightness.clamp(0.0, 1.0);
    int index = ((1.0 - b) * (ramp.length - 1)).round();
    return ramp[index];
  }
}

/// A collection of pre-defined character sets
abstract class AsciiThemes {
  static const AsciiTheme blocks = AsciiTheme('Blocks', "█▓▒░ ");
  static const AsciiTheme quadrant = AsciiTheme('Quadrant', "▛▜▟▙▚▞▖ ");

  static const List<AsciiTheme> values = [blocks, quadrant];

  static AsciiTheme nextOf(AsciiTheme current) {
    final int currentIndex = values.indexOf(current);
    final int nextIndex = currentIndex == -1
        ? 0
        : (currentIndex + 1) % values.length;
    return values[nextIndex];
  }
}

class ColoredChar {
  final String char;
  final int rawColor; // Stores the AABBGGRR integer from the palette
  final int? rawBackgroundColor;

  ColoredChar(this.char, this.rawColor, [this.rawBackgroundColor]);

  // Safely extract the exact RGB channels regardless of framework
  int get r => rawColor & 0xFF;
  int get g => (rawColor >> 8) & 0xFF;
  int get b => (rawColor >> 16) & 0xFF;

  // Outputs standard AARRGGBB for Flutter's Color(int) constructor
  int get argb => (0xFF000000) | (r << 16) | (g << 8) | b;

  // Same AABBGGRR → AARRGGBB conversion for the background channel.
  int? get backgroundArgb {
    final bg = rawBackgroundColor;
    if (bg == null) return null;
    final int bgR = bg & 0xFF;
    final int bgG = (bg >> 8) & 0xFF;
    final int bgB = (bg >> 16) & 0xFF;
    return 0xFF000000 | (bgR << 16) | (bgG << 8) | bgB;
  }
}

enum AsciiRendererMode {
  terminalAnsi,
  terminalGrid,
}

enum _AsciiMenuRowFont {
  bitmap,
  compactText,
}

class _AsciiMenuTypography {
  const _AsciiMenuTypography({
    required this.headingScale,
    required this.rowFont,
  });

  final int headingScale;
  final _AsciiMenuRowFont rowFont;

  bool get usesCompactRows => rowFont == _AsciiMenuRowFont.compactText;
}

/// Text-mode renderer that can render to ANSI escape output or a Flutter
/// grid model of colored characters.
class AsciiRenderer extends CliRendererBackend<dynamic> {
  static const double _targetAspectRatio = 4 / 3;
  static const int _terminalBackdropPaletteIndex = 153;
  static const int _minimumTerminalColumns = 80;
  static const int _minimumTerminalRows = 24;
  static const int _simpleHudMinWidth = 84;
  static const int _simpleHudMinRows = 7;

  static const int _menuHintKeyPaletteIndex = 12;
  static const int _menuHintLabelPaletteIndex = 4;
  static const int _menuHintBackgroundPaletteIndex = 0;
  static const int _headerHeadingY = 24;

  AsciiRenderer({
    this.activeTheme = AsciiThemes.blocks,
    this.mode = AsciiRendererMode.terminalGrid,
    this.useTerminalLayout = true,
    this.aspectMultiplier = 1.0,
    this.verticalStretch = 1.0,
  });

  AsciiTheme activeTheme = AsciiThemes.blocks;
  final AsciiRendererMode mode;
  final bool useTerminalLayout;

  bool get _usesTerminalLayout => useTerminalLayout;

  bool get _emitAnsi => mode == AsciiRendererMode.terminalAnsi;

  late List<List<ColoredChar>> _screen;
  late List<List<int>> _scenePixels;
  List<int>? _mainMenuBandFirstColumn;
  String? _lastLoggedThemeName;

  static const List<String> _quadrantByMask = <String>[
    ' ',
    '▘',
    '▝',
    '▀',
    '▖',
    '▌',
    '▞',
    '▛',
    '▗',
    '▚',
    '▐',
    '▜',
    '▄',
    '▙',
    '▟',
    '█',
  ];

  @override
  final double aspectMultiplier;
  @override
  final double verticalStretch;

  @override
  int get projectionWidth => _usesTerminalLayout
      ? math.max(
          1,
          math.min(width, (_terminalPixelHeight * _targetAspectRatio).floor()),
        )
      : width;

  @override
  int get projectionOffsetX =>
      _usesTerminalLayout ? (width - projectionWidth) ~/ 2 : 0;

  @override
  int get projectionViewHeight =>
      _usesTerminalLayout ? viewHeight * 2 : viewHeight;

  @override
  bool isTerminalSizeSupported(int columns, int rows) {
    if (!_emitAnsi) {
      return true;
    }
    return columns >= _minimumTerminalColumns && rows >= _minimumTerminalRows;
  }

  @override
  String get terminalSizeRequirement =>
      'ASCII renderer requires a minimum resolution of '
      '${_minimumTerminalColumns}x$_minimumTerminalRows.';

  int get _terminalPixelHeight => _usesTerminalLayout ? height * 2 : height;

  int get _terminalSceneWidth => width;

  bool get _usesQuadrantCompose => activeTheme == AsciiThemes.quadrant;

  int get _viewportRightX => projectionOffsetX + projectionWidth;

  int get _terminalBackdropColor =>
      ColorPalette.vga32Bit[_terminalBackdropPaletteIndex];

  // Intercept the base render call to initialize our text grid
  @override
  /// Initializes the character grid before running the shared render pipeline.
  dynamic render(WolfEngine engine) {
    _logThemeIfChanged();
    _screen = List.generate(
      engine.frameBuffer.height,
      (_) => List.filled(
        engine.frameBuffer.width,
        ColoredChar(' ', ColorPalette.vga32Bit[0]),
      ),
    );
    return super.render(engine);
  }

  @override
  void prepareFrame(WolfEngine engine) {
    // Just grab the raw ints!
    final int ceilingColor = ColorPalette.vga32Bit[25];
    final int floorColor = ColorPalette.vga32Bit[29];
    final int backdropColor = _usesTerminalLayout
        ? _terminalBackdropColor
        : ColorPalette.vga32Bit[0];

    _scenePixels = List.generate(
      _terminalPixelHeight,
      (_) => List.filled(width, backdropColor),
    );

    for (int y = 0; y < projectionViewHeight; y++) {
      final int color = y < projectionViewHeight / 2
          ? ceilingColor
          : floorColor;
      for (int x = projectionOffsetX; x < _viewportRightX; x++) {
        _scenePixels[y][x] = color;
      }
    }

    if (!_usesTerminalLayout) {
      for (int y = 0; y < height; y++) {
        for (int x = 0; x < width; x++) {
          if (y < viewHeight / 2) {
            _screen[y][x] = ColoredChar(activeTheme.solid, ceilingColor);
          } else if (y < viewHeight) {
            _screen[y][x] = ColoredChar(activeTheme.solid, floorColor);
          }
        }
      }
    }
  }

  @override
  void drawWallColumn(
    int x,
    int drawStart,
    int drawEnd,
    int columnHeight,
    Sprite texture,
    int texX,
    double perpWallDist,
    int side,
  ) {
    double brightness = calculateDepthBrightness(perpWallDist);

    for (int y = drawStart; y < drawEnd; y++) {
      int texY = wallTexY(y, columnHeight);

      int colorByte = texture.pixels[texX * 64 + texY];
      int pixelColor = ColorPalette.vga32Bit[colorByte]; // Raw int

      // Faux directional lighting using your new base class method
      if (side == 1) {
        pixelColor = shadeColor(pixelColor);
      }

      if (_usesTerminalLayout) {
        _scenePixels[y][x] = _scaleColor(pixelColor, brightness);
      } else {
        final String wallChar = activeTheme.getByBrightness(brightness);
        _screen[y][x] = ColoredChar(wallChar, pixelColor);
      }
    }
  }

  @override
  void drawSpriteStripe(
    int stripeX,
    int drawStartY,
    int drawEndY,
    int spriteHeight,
    Sprite texture,
    int texX,
    double transformY,
  ) {
    double brightness = calculateDepthBrightness(transformY);

    for (
      int y = math.max(0, drawStartY);
      y < math.min(projectionViewHeight, drawEndY);
      y++
    ) {
      int texY = spriteTexY(y, drawStartY, spriteHeight);

      int colorByte = texture.pixels[texX * 64 + texY];
      if (colorByte != 255) {
        int rawColor = ColorPalette.vga32Bit[colorByte];

        // Shade the sprite's actual RGB color based on distance
        int r = (rawColor & 0xFF);
        int g = ((rawColor >> 8) & 0xFF);
        int b = ((rawColor >> 16) & 0xFF);

        r = (r * brightness).toInt();
        g = (g * brightness).toInt();
        b = (b * brightness).toInt();

        int shadedColor = (0xFF000000) | (b << 16) | (g << 8) | r;

        if (_usesTerminalLayout) {
          _scenePixels[y][stripeX] = shadedColor;
        } else {
          _screen[y][stripeX] = ColoredChar(activeTheme.solid, shadedColor);
        }
      }
    }
  }

  @override
  void drawWeapon(WolfEngine engine) {
    int spriteIndex = engine.player.currentWeapon.getCurrentSpriteIndex(
      engine.data.sprites.length,
    );
    Sprite weaponSprite = engine.data.sprites[spriteIndex];

    final bounds = weaponScreenBounds(engine);
    final int weaponWidth = bounds.weaponWidth;
    final int weaponHeight = (projectionViewHeight * 0.8).toInt();
    final int startX = bounds.startX;
    final int startY =
        projectionViewHeight -
        weaponHeight +
        (engine.player.weaponAnimOffset * (_usesTerminalLayout ? 2 : 1) ~/ 4);

    for (int dy = 0; dy < weaponHeight; dy++) {
      for (int dx = 0; dx < weaponWidth; dx++) {
        int texX = (dx * 64 ~/ weaponWidth).clamp(0, 63);
        int texY = (dy * 64 ~/ weaponHeight).clamp(0, 63);

        int colorByte = weaponSprite.pixels[texX * 64 + texY];
        if (colorByte != 255) {
          int sceneX = startX + dx;
          int drawY = startY + dy;
          if (sceneX >= projectionOffsetX &&
              sceneX < _viewportRightX &&
              drawY >= 0) {
            if (_usesTerminalLayout && drawY < projectionViewHeight) {
              _scenePixels[drawY][sceneX] = ColorPalette.vga32Bit[colorByte];
            } else if (!_usesTerminalLayout && drawY < viewHeight) {
              _screen[drawY][sceneX] = ColoredChar(
                activeTheme.solid,
                ColorPalette.vga32Bit[colorByte],
              );
            }
          }
        }
      }
    }
  }

  // --- PRIVATE HUD DRAWING HELPER ---

  /// Injects a pure text string directly into the renderer grid
  void _writeString(
    int startX,
    int y,
    String text,
    int color, [
    int? backgroundColor,
  ]) {
    for (int i = 0; i < text.length; i++) {
      int x = startX + i;
      if (x >= 0 && x < width && y >= 0 && y < height) {
        _screen[y][x] = ColoredChar(text[i], color, backgroundColor);
      }
    }
  }

  @override
  void drawHud(WolfEngine engine) {
    // If the terminal is at least 160 columns wide and 50 rows tall,
    // there are enough "pixels" to downscale the VGA image clearly.
    int hudWidth = _usesTerminalLayout ? projectionWidth : width;
    if (hudWidth >= 160 && height >= 50) {
      _drawFullVgaHud(engine);
    } else {
      _drawSimpleHud(engine);
    }
  }

  @override
  void drawFpsOverlay(WolfEngine engine) {
    const int textColor = 0xFFFFFFFF;
    const int bgColor = 0xFF000000;
    _writeString(1, 0, ' ${fpsLabel(engine)} ', textColor, bgColor);
  }

  @override
  void drawMenu(WolfEngine engine) {
    final int bgColor = _rgbToPaletteColor(
      engine.menuManager.menuBackgroundRgb,
    );
    final int panelColor = _rgbToPaletteColor(engine.menuPanelRgb);
    final int headingColor = WolfMenuPalette.headerTextColor;
    final int selectedTextColor = WolfMenuPalette.selectedTextColor;
    final int unselectedTextColor = WolfMenuPalette.unselectedTextColor;
    final int disabledTextColor = WolfMenuPalette.disabledTextColor;
    final _AsciiMenuTypography menuTypography = _resolveMenuTypography();

    if (_usesTerminalLayout) {
      _fillTerminalRect(0, 0, width, _terminalPixelHeight, bgColor);
    } else {
      _fillRect(0, 0, width, height, activeTheme.solid, bgColor);
    }

    final art = WolfClassicMenuArt(engine.data);
    final optionsLabel = art.optionsLabel;
    if (optionsLabel != null) {
      _mainMenuBandFirstColumn = _cacheFirstColumn(optionsLabel);
    }

    if (engine.menuManager.activeMenu == WolfMenuScreen.introSplash) {
      _drawIntroSplash(engine, art, menuTypography);
      return;
    }

    if (engine.menuManager.activeMenu == WolfMenuScreen.mainMenu) {
      _fillRect320(68, 52, 178, 136, panelColor);

      final optionsLabel = art.optionsLabel;
      if (optionsLabel != null) {
        final int optionsX = ((320 - optionsLabel.width) ~/ 2).clamp(0, 319);
        _drawMainMenuOptionsSideBars(optionsLabel, optionsX);
        _blitVgaImageAscii(optionsLabel, optionsX, 0);
      } else {
        _drawHeaderBarStack(
          headingY200: _headerHeadingY,
          backgroundColor: bgColor,
          barColor: ColorPalette.vga32Bit[0],
        );
        _drawMenuTextCentered(
          'OPTIONS',
          _headerHeadingY,
          headingColor,
          scale: menuTypography.headingScale,
        );
      }

      final cursor = art.mappedPic(
        engine.menuManager.isCursorAltFrame(engine.timeAliveMs) ? 9 : 8,
      );
      const int rowYStart = 55;
      const int rowStep = 13;
      final entries = engine.menuManager.mainMenuEntries;

      _drawSelectableMenuRows(
        typography: menuTypography,
        rows: entries.map((entry) => entry.label).toList(growable: false),
        selectedIndex: engine.menuManager.selectedMainIndex,
        rowYStart200: rowYStart,
        rowStep200: rowStep,
        textX320: 100,
        panelX320: 68,
        panelW320: 178,
        colorForRow: (int index, bool isSelected) {
          final entry = entries[index];
          if (!entry.isEnabled) {
            return disabledTextColor;
          }
          return isSelected ? selectedTextColor : unselectedTextColor;
        },
      );

      if (cursor != null) {
        _blitVgaImageAscii(
          cursor,
          72,
          (rowYStart + (engine.menuManager.selectedMainIndex * rowStep)) - 2,
        );
      }

      _drawCenteredMenuFooter();
      _applyMenuFade(engine.menuManager.transitionAlpha, bgColor);
      return;
    }

    if (engine.menuManager.activeMenu == WolfMenuScreen.gameSelect) {
      _drawHeaderBarStack(
        headingY200: _headerHeadingY,
        backgroundColor: bgColor,
        barColor: ColorPalette.vga32Bit[0],
      );
      _fillRect320(28, 58, 264, 104, panelColor);

      final cursor = art.mappedPic(
        engine.menuManager.isCursorAltFrame(engine.timeAliveMs) ? 9 : 8,
      );
      const int rowYStart = 84;
      const int rowStep = 18;
      final List<String> rows = engine.availableGames
          .map((game) => _gameTitle(game.version))
          .toList(growable: false);
      _drawMenuTextCentered(
        'SELECT GAME',
        _headerHeadingY,
        headingColor,
        scale: menuTypography.headingScale,
      );

      _drawSelectableMenuRows(
        typography: menuTypography,
        rows: rows,
        selectedIndex: engine.menuManager.selectedGameIndex,
        rowYStart200: rowYStart,
        rowStep200: rowStep,
        textX320: 70,
        panelX320: 28,
        panelW320: 264,
        colorForRow: (int _, bool isSelected) {
          return isSelected ? selectedTextColor : unselectedTextColor;
        },
      );

      if (cursor != null) {
        _blitVgaImageAscii(
          cursor,
          38,
          (rowYStart + (engine.menuManager.selectedGameIndex * rowStep)) - 2,
        );
      }

      _drawCenteredMenuFooter();
      _applyMenuFade(engine.menuManager.transitionAlpha, bgColor);
      return;
    }

    if (engine.menuManager.activeMenu == WolfMenuScreen.episodeSelect) {
      _drawHeaderBarStack(
        headingY200: _headerHeadingY,
        backgroundColor: bgColor,
        barColor: ColorPalette.vga32Bit[0],
      );
      _fillRect320(12, 18, 296, 168, panelColor);

      final cursor = art.mappedPic(
        engine.menuManager.isCursorAltFrame(engine.timeAliveMs) ? 9 : 8,
      );
      const int rowYStart = 24;
      const int rowStep = 26;
      final List<String> rows = engine.data.episodes
          .map((episode) => episode.name.replaceAll('\n', ' '))
          .toList(growable: false);

      if (menuTypography.usesCompactRows) {
        _drawMenuTextCentered(
          'WHICH EPISODE TO PLAY?',
          _headerHeadingY,
          headingColor,
          scale: menuTypography.headingScale,
        );
        _drawMinimalMenuRows(
          rows: rows,
          selectedIndex: engine.menuManager.selectedEpisodeIndex,
          rowYStart200: rowYStart,
          rowStep200: rowStep,
          textX320: 98,
          panelX320: 12,
          panelW320: 296,
          panelColor: panelColor,
          colorForRow: (int _, bool isSelected) {
            return isSelected ? selectedTextColor : unselectedTextColor;
          },
        );

        // Keep episode icons visible in compact ASCII layouts so this screen
        // still communicates the same visual affordances as full-size menus.
        for (int i = 0; i < engine.data.episodes.length; i++) {
          final image = art.episodeOption(i);
          if (image != null) {
            _blitVgaImageAscii(image, 40, rowYStart + (i * rowStep));
          }
        }

        if (cursor != null) {
          _blitVgaImageAscii(
            cursor,
            16,
            rowYStart + (engine.menuManager.selectedEpisodeIndex * rowStep) + 2,
          );
        }
        _drawCenteredMenuFooter();
        _applyMenuFade(engine.menuManager.transitionAlpha, bgColor);
        return;
      }

      _drawMenuTextCentered(
        'WHICH EPISODE TO PLAY?',
        _headerHeadingY,
        headingColor,
        scale: menuTypography.headingScale,
      );

      for (int i = 0; i < engine.data.episodes.length; i++) {
        final int y = rowYStart + (i * rowStep);
        final bool isSelected = i == engine.menuManager.selectedEpisodeIndex;
        if (isSelected && cursor != null) {
          _blitVgaImageAscii(cursor, 16, y + 2);
        }
        final image = art.episodeOption(i);
        if (image != null) {
          _blitVgaImageAscii(image, 40, y);
        }
        final parts = engine.data.episodes[i].name.split('\n');
        if (parts.isNotEmpty) {
          _drawMenuText(
            parts.first,
            98,
            y + 1,
            isSelected ? selectedTextColor : unselectedTextColor,
          );
        }
        if (parts.length > 1) {
          _drawMenuText(
            parts.sublist(1).join(' '),
            98,
            y + 13,
            isSelected ? selectedTextColor : unselectedTextColor,
          );
        }
      }

      _drawCenteredMenuFooter();
      _applyMenuFade(engine.menuManager.transitionAlpha, bgColor);
      return;
    }

    if (engine.menuManager.activeMenu == WolfMenuScreen.changeView) {
      _drawCustomizeMenuHeader(art, headingColor, bgColor);
      final cursor = art.mappedPic(
        engine.menuManager.isCursorAltFrame(engine.timeAliveMs) ? 9 : 8,
      );
      final selectedMarker = art.selectedMarker;
      final unselectedMarker = art.unselectedMarker;
      const int rowYStart = 66;
      const int rowStep = 18;
      const int cursorX = 62;
      const int markerX = 92;
      const int textX = 122;
      final entries = engine.menuManager.changeViewEntries;
      final optionEntries = engine.menuManager.rendererOptionEntries;
      final int modeCount = entries.length;
      final int optionCount = optionEntries.length;

      const int modesPanelY = 52;
      final int modesContentHeight = modeCount <= 0
          ? 0
          : ((modeCount - 1) * rowStep) + 12;
      final int modesPanelHeight = math.max(56, modesContentHeight + 14);
      final int sectionHeaderY = modesPanelY + modesPanelHeight + 6;
      final int optionsPanelY = sectionHeaderY + 14;
      final int optionsContentHeight = optionCount <= 0
          ? 0
          : ((optionCount - 1) * 15) + 12;
      final int optionsPanelHeight = math.max(30, optionsContentHeight + 10);

      _fillRect320(46, modesPanelY, 228, modesPanelHeight, panelColor);
      _fillRect320(46, optionsPanelY, 228, optionsPanelHeight, panelColor);

      for (int i = 0; i < entries.length; i++) {
        final int y = rowYStart + (i * rowStep);
        final bool isSelected = i == engine.menuManager.selectedChangeViewIndex;
        if (isSelected && cursor != null) {
          _blitVgaImageAscii(cursor, cursorX, y - 2);
        }

        final entry = entries[i];
        final marker = entry.isChecked ? selectedMarker : unselectedMarker;
        if (marker != null) {
          _blitVgaImageAscii(marker, markerX, y);
        }
        final int textColor = entry.isEnabled
            ? (isSelected ? selectedTextColor : unselectedTextColor)
            : disabledTextColor;
        _drawMenuLabelAdaptive(
          typography: menuTypography,
          text: entry.label,
          textX320: textX,
          y200: y + 1,
          color: textColor,
          panelX320: 46,
          panelW320: 228,
          panelColor: panelColor,
        );
      }

      _drawMenuSectionHeader(
        text: engine.menuManager.rendererOptionsTitle,
        y200: sectionHeaderY,
      );

      const int optionsRowStep = 15;
      final int optionsRowsHeight = optionCount <= 0
          ? 0
          : ((optionCount - 1) * optionsRowStep) + 10;
      final int optionsRowStart =
          optionsPanelY +
          ((optionsPanelHeight - optionsRowsHeight) ~/ 2).clamp(0, 200);
      for (int i = 0; i < optionEntries.length; i++) {
        final int optionIndex = modeCount + i;
        final bool isSelected =
            optionIndex == engine.menuManager.selectedChangeViewIndex;
        final int y = optionsRowStart + (i * optionsRowStep);
        if (isSelected && cursor != null) {
          _blitVgaImageAscii(cursor, cursorX, y - 2);
        }
        final entry = optionEntries[i];
        final marker = entry.isChecked ? selectedMarker : unselectedMarker;
        if (marker != null) {
          _blitVgaImageAscii(marker, markerX, y);
        }
        final int textColor = entry.isEnabled
            ? (isSelected ? selectedTextColor : unselectedTextColor)
            : disabledTextColor;
        _drawMenuLabelAdaptive(
          typography: menuTypography,
          text: entry.label,
          textX320: textX,
          y200: y + 1,
          color: textColor,
          panelX320: 46,
          panelW320: 228,
          panelColor: panelColor,
        );
      }

      _drawCenteredMenuFooter();
      _applyMenuFade(engine.menuManager.transitionAlpha, bgColor);
      return;
    }

    if (engine.menuManager.activeMenu == WolfMenuScreen.rendererOptions) {
      _drawCustomizeMenuHeader(art, headingColor, bgColor);
      _fillRect320(56, 52, 208, 120, panelColor);
      _drawMenuTextCentered(
        engine.menuManager.rendererOptionsTitle,
        46,
        headingColor,
        scale: 1,
      );

      final cursor = art.mappedPic(
        engine.menuManager.isCursorAltFrame(engine.timeAliveMs) ? 9 : 8,
      );
      final selectedMarker = art.selectedMarker;
      final unselectedMarker = art.unselectedMarker;
      const int rowYStart = 68;
      const int rowStep = 20;
      const int cursorX = 62;
      const int markerX = 92;
      const int textX = 122;
      final entries = engine.menuManager.rendererOptionEntries;
      for (int i = 0; i < entries.length; i++) {
        final int y = rowYStart + (i * rowStep);
        final bool isSelected =
            i == engine.menuManager.selectedRendererOptionIndex;
        final entry = entries[i];
        if (isSelected && cursor != null) {
          _blitVgaImageAscii(cursor, cursorX, y - 2);
        }
        final marker = entry.isChecked ? selectedMarker : unselectedMarker;
        if (marker != null) {
          _blitVgaImageAscii(marker, markerX, y);
        }
        final int textColor = entry.isEnabled
            ? (isSelected ? selectedTextColor : unselectedTextColor)
            : disabledTextColor;
        _drawMenuLabelAdaptive(
          typography: menuTypography,
          text: entry.label,
          textX320: textX,
          y200: y + 1,
          color: textColor,
          panelX320: 56,
          panelW320: 208,
          panelColor: panelColor,
        );
      }

      _drawCenteredMenuFooter();
      _applyMenuFade(engine.menuManager.transitionAlpha, bgColor);
      return;
    }

    final int selectedDifficultyIndex =
        engine.menuManager.selectedDifficultyIndex;

    _drawHeaderBarStack(
      headingY200: _headerHeadingY,
      backgroundColor: bgColor,
      barColor: ColorPalette.vga32Bit[0],
    );

    _fillRect320(28, 70, 264, 82, panelColor);

    final face = art.difficultyOption(
      Difficulty.values[selectedDifficultyIndex],
    );
    if (face != null) {
      _blitVgaImageAscii(face, 28 + 264 - face.width - 18, 92);
    }

    final cursor = art.mappedPic(
      engine.menuManager.isCursorAltFrame(engine.timeAliveMs) ? 9 : 8,
    );
    const rowYStart = 86;
    const rowStep = 15;

    if (menuTypography.usesCompactRows) {
      _drawMenuTextCentered(
        Difficulty.menuText,
        _headerHeadingY,
        headingColor,
        scale: menuTypography.headingScale,
      );
      _drawSelectableMenuRows(
        typography: menuTypography,
        rows: Difficulty.values.map((d) => d.title).toList(growable: false),
        selectedIndex: selectedDifficultyIndex,
        rowYStart200: rowYStart,
        rowStep200: rowStep,
        textX320: 70,
        panelX320: 28,
        panelW320: 264,
        colorForRow: (int _, bool isSelected) {
          return isSelected ? selectedTextColor : unselectedTextColor;
        },
      );
      if (cursor != null) {
        _blitVgaImageAscii(
          cursor,
          38,
          (rowYStart + (selectedDifficultyIndex * rowStep)) - 2,
        );
      }
      _drawCenteredMenuFooter();
      _applyMenuFade(engine.menuManager.transitionAlpha, bgColor);
      return;
    }

    _drawMenuTextCentered(
      Difficulty.menuText,
      _headerHeadingY,
      headingColor,
      scale: menuTypography.headingScale,
    );

    for (int i = 0; i < Difficulty.values.length; i++) {
      final y = rowYStart + (i * rowStep);
      final isSelected = i == selectedDifficultyIndex;
      if (isSelected && cursor != null) {
        _blitVgaImageAscii(cursor, 38, y - 2);
      }

      _drawMenuText(
        Difficulty.values[i].title,
        70,
        y,
        isSelected ? selectedTextColor : unselectedTextColor,
      );
    }

    _drawCenteredMenuFooter();
    _applyMenuFade(engine.menuManager.transitionAlpha, bgColor);
  }

  String _gameTitle(GameVersion version) {
    switch (version) {
      case GameVersion.shareware:
        return 'SHAREWARE';
      case GameVersion.retail:
        return 'RETAIL';
      case GameVersion.spearOfDestiny:
        return 'SPEAR OF DESTINY';
      case GameVersion.spearOfDestinyDemo:
        return 'SOD DEMO';
    }
  }

  void _drawIntroSplash(
    WolfEngine engine,
    WolfClassicMenuArt art,
    _AsciiMenuTypography menuTypography,
  ) {
    final image = switch (engine.menuManager.currentIntroSlide) {
      WolfIntroSlide.retailWarning => null,
      WolfIntroSlide.pg13 => art.mappedPic(WolfMenuPic.pg13),
      WolfIntroSlide.title => art.mappedPic(WolfMenuPic.title),
    };

    int splashBg = _rgbToPaletteColor(engine.menuManager.menuBackgroundRgb);
    if (engine.menuManager.isIntroPg13Slide &&
        image != null &&
        image.pixels.isNotEmpty) {
      splashBg = ColorPalette.vga32Bit[_dominantPaletteIndex(image)];
    }

    if (_usesTerminalLayout) {
      _fillTerminalRect(0, 0, width, _terminalPixelHeight, splashBg);
    } else {
      _fillRect(0, 0, width, height, activeTheme.solid, splashBg);
    }

    if (engine.menuManager.isIntroRetailWarningSlide) {
      _drawRetailWarningIntro(splashBg, menuTypography);
    }

    if (image != null) {
      final int x = engine.menuManager.isIntroPg13Slide
          ? (320 - image.width).clamp(0, 319)
          : ((320 - image.width) ~/ 2).clamp(0, 319);
      final int y = engine.menuManager.isIntroPg13Slide
          ? (200 - image.height).clamp(0, 199)
          : ((200 - image.height) ~/ 2).clamp(0, 199);
      _blitVgaImageAscii(image, x, y);
    }

    _applyMenuFade(
      engine.menuManager.introOverlayAlpha,
      _rgbToPaletteColor(0x000000),
    );
  }

  void _drawRetailWarningIntro(
    int backgroundColor,
    _AsciiMenuTypography menuTypography,
  ) {
    final int black = ColorPalette.vga32Bit[0];
    final int yellow = ColorPalette.vga32Bit[14];
    final int white = ColorPalette.vga32Bit[15];

    _drawHeaderBarStack(
      headingY200: _headerHeadingY,
      backgroundColor: backgroundColor,
      barColor: black,
    );
    _drawMenuTextCentered(
      'Attention',
      _headerHeadingY,
      yellow,
      scale: menuTypography.headingScale,
    );

    if (menuTypography.usesCompactRows) {
      final int textLeft = _menuX320ToColumn(40);
      final int textRight = _menuX320ToColumn(280);
      final int textWidth = math.max(1, textRight - textLeft);

      void writeCenteredLine(int y200, String text) {
        final String clipped = _clipWithEllipsis(text, textWidth);
        final int centeredLeft =
            textLeft + math.max(0, ((textWidth - clipped.length) ~/ 2));
        _writeLeftClipped(
          _menuY200ToRow(y200),
          clipped,
          white,
          backgroundColor,
          clipped.length,
          centeredLeft,
        );
      }

      writeCenteredLine(62, 'THIS GAME IS NOT SHAREWARE.');
      writeCenteredLine(74, 'PLEASE DO NOT DISTRIBUTE IT.');
      writeCenteredLine(86, 'THANKS.');

      writeCenteredLine(112, 'ID SOFTWARE');
    } else {
      _drawMenuText('This game is NOT shareware.', 40, 56, white, scale: 1);
      _drawMenuText('Please do not distribute it.', 40, 68, white, scale: 1);
      _drawMenuText('Thanks.', 40, 80, white, scale: 1);
      _drawMenuTextCentered('Id Software', 106, white, scale: 1);
    }

    _fillRect320(0, 196, 320, 4, backgroundColor);
  }

  int _dominantPaletteIndex(VgaImage image) {
    final List<int> histogram = List<int>.filled(256, 0);
    for (final int colorIndex in image.pixels) {
      if (colorIndex >= 0 && colorIndex < histogram.length) {
        histogram[colorIndex]++;
      }
    }

    int bestIndex = 0;
    int bestCount = -1;
    for (int i = 0; i < histogram.length; i++) {
      if (histogram[i] > bestCount) {
        bestCount = histogram[i];
        bestIndex = i;
      }
    }
    return bestIndex;
  }

  void _drawHeaderBarStack({
    required int headingY200,
    required int backgroundColor,
    required int barColor,
  }) {
    final List<int>? cachedColumn = _mainMenuBandFirstColumn;
    if (cachedColumn != null && cachedColumn.isNotEmpty) {
      _drawScaledColumnBand(cachedColumn);
      return;
    }

    final int mainBarTop = (headingY200 - 4).clamp(0, 199);

    _fillRect320(0, mainBarTop, 320, 18, barColor);

    int stripeY = mainBarTop + 18;
    for (int i = 0; i < 4; i++) {
      _fillRect320(
        0,
        (stripeY + i).clamp(0, 199),
        320,
        1,
        i.isEven ? barColor : backgroundColor,
      );
    }
  }

  void _drawMainMenuOptionsSideBars(VgaImage optionsLabel, int optionsX320) {
    _mainMenuBandFirstColumn = _cacheFirstColumn(optionsLabel);
    _drawScaledColumnBand(_mainMenuBandFirstColumn!);
  }

  List<int> _cacheFirstColumn(VgaImage image) {
    final List<int> column = List<int>.filled(image.height, 0);
    for (int y = 0; y < image.height; y++) {
      column[y] = image.decodePixel(0, y);
    }
    return column;
  }

  void _drawScaledColumnBand(List<int> column) {
    if (column.isEmpty) {
      return;
    }

    final int maxDrawHeight = _usesTerminalLayout
        ? _terminalPixelHeight
        : height;
    final double scaleY = maxDrawHeight / 200.0;
    final int destHeight = math.max(1, (column.length * scaleY).toInt());

    for (int dy = 0; dy < destHeight; dy++) {
      final int srcY = (dy / scaleY).toInt().clamp(0, column.length - 1);
      final int fillColor = ColorPalette.vga32Bit[column[srcY]];

      if (_usesTerminalLayout) {
        if (dy < 0 || dy >= _terminalPixelHeight) {
          continue;
        }
        for (int drawX = 0; drawX < _terminalSceneWidth; drawX++) {
          _scenePixels[dy][drawX] = fillColor;
        }
      } else {
        if (dy < 0 || dy >= height) {
          continue;
        }
        final List<ColoredChar> row = _screen[dy];
        for (int drawX = 0; drawX < width; drawX++) {
          row[drawX] = ColoredChar(activeTheme.solid, fillColor);
        }
      }
    }
  }

  void _applyMenuFade(double alpha, int fadeColor) {
    if (alpha <= 0.0) {
      return;
    }

    final int threshold = (alpha * 3).round().clamp(1, 3);

    if (_usesTerminalLayout) {
      for (int y = 0; y < _terminalPixelHeight; y++) {
        for (int x = 0; x < _terminalSceneWidth; x++) {
          if (((x + y) % 3) < threshold) {
            _scenePixels[y][x] = fadeColor;
          }
        }
      }
      return;
    }

    for (int y = 0; y < _screen.length; y++) {
      final row = _screen[y];
      for (int x = 0; x < row.length; x++) {
        if (((x + y) % 3) < threshold) {
          row[x] = ColoredChar(' ', fadeColor, fadeColor);
        }
      }
    }
  }

  void _drawMenuText(
    String text,
    int startX320,
    int startY200,
    int color, {
    int scale = 1,
  }) {
    int x320 = startX320;
    for (final rune in text.runes) {
      final String char = String.fromCharCode(rune).toUpperCase();
      final List<String> pattern = WolfMenuFont.glyphFor(char);

      for (int row = 0; row < pattern.length; row++) {
        final String bits = pattern[row];
        for (int col = 0; col < bits.length; col++) {
          if (bits[col] != '1') {
            continue;
          }
          for (int sy = 0; sy < scale; sy++) {
            for (int sx = 0; sx < scale; sx++) {
              _plotMenuPixel320(
                x320 + (col * scale) + sx,
                startY200 + (row * scale) + sy,
                color,
              );
            }
          }
        }
      }

      x320 += WolfMenuFont.glyphAdvance(char, scale);
    }
  }

  void _drawMenuTextCentered(
    String text,
    int y200,
    int color, {
    int scale = 1,
  }) {
    if (y200 == _headerHeadingY) {
      y200 = _centerHeaderTitleInBlackBand(
        defaultY: y200,
        scale: scale,
      );
    }
    final int textWidth = WolfMenuFont.measureTextWidth(text, scale);
    final int x320 = ((320 - textWidth) ~/ 2).clamp(0, 319);
    _drawMenuText(text, x320, y200, color, scale: scale);
  }

  int _centerHeaderTitleInBlackBand({
    required int defaultY,
    required int scale,
  }) {
    final List<int>? column = _mainMenuBandFirstColumn;
    if (column == null || column.isEmpty) {
      return defaultY;
    }

    int bestStart = -1;
    int bestLength = 0;
    int runStart = -1;

    for (int i = 0; i <= column.length; i++) {
      final bool isBlack = i < column.length && column[i] == 0;
      if (isBlack) {
        runStart = runStart == -1 ? i : runStart;
        continue;
      }
      if (runStart != -1) {
        final int runLength = i - runStart;
        if (runLength > bestLength) {
          bestLength = runLength;
          bestStart = runStart;
        }
        runStart = -1;
      }
    }

    if (bestStart == -1 || bestLength == 0) {
      return defaultY;
    }

    final int textHeight = 7 * scale;
    final int centered = bestStart + ((bestLength - textHeight) ~/ 2);
    return centered.clamp(0, 199 - textHeight);
  }

  _AsciiMenuTypography _resolveMenuTypography() {
    final bool usesCompactRows = _menuGlyphHeightInRows(scale: 1) <= 4;
    return _AsciiMenuTypography(
      headingScale: usesCompactRows ? 2 : _defaultMenuHeadingScale,
      rowFont: usesCompactRows
          ? _AsciiMenuRowFont.compactText
          : _AsciiMenuRowFont.bitmap,
    );
  }

  int get _defaultMenuHeadingScale {
    if (!_usesTerminalLayout) {
      return 2;
    }
    return projectionWidth < 140 ? 1 : 2;
  }

  int _menuGlyphHeightInRows({required int scale}) {
    final double scaleY =
        (_usesTerminalLayout ? _terminalPixelHeight : height) / 200.0;
    // Use pre-compose pixel scale so terminal mode can graduate back to
    // bitmap menu text at practical window sizes.
    final double rasterHeight = 7.0 * scale * scaleY;
    return math.max(1, rasterHeight.ceil());
  }

  int _menuY200ToRow(int y200) {
    final double scaleY =
        (_usesTerminalLayout ? _terminalPixelHeight : height) / 200.0;
    final int pixelY = (y200 * scaleY).toInt();
    final int row = _usesTerminalLayout ? (pixelY / 2).round() : pixelY;
    return row.clamp(0, height - 1);
  }

  int _menuX320ToColumn(int x320) {
    final double scaleX =
        (_usesTerminalLayout ? projectionWidth : width) / 320.0;
    final int offsetX = _usesTerminalLayout ? projectionOffsetX : 0;
    final int pixelX = offsetX + (x320 * scaleX).toInt();
    return pixelX.clamp(0, width - 1);
  }

  void _drawSelectableMenuRows({
    required _AsciiMenuTypography typography,
    required List<String> rows,
    required int selectedIndex,
    required int rowYStart200,
    required int rowStep200,
    required int textX320,
    required int panelX320,
    required int panelW320,
    required int Function(int index, bool isSelected) colorForRow,
  }) {
    if (typography.usesCompactRows) {
      _drawMinimalMenuRows(
        rows: rows,
        selectedIndex: selectedIndex,
        rowYStart200: rowYStart200,
        rowStep200: rowStep200,
        textX320: textX320,
        panelX320: panelX320,
        panelW320: panelW320,
        panelColor: _rgbToPaletteColor(engine.menuPanelRgb),
        colorForRow: colorForRow,
      );
      return;
    }

    for (int i = 0; i < rows.length; i++) {
      final bool isSelected = i == selectedIndex;
      _drawMenuText(
        rows[i],
        textX320,
        rowYStart200 + (i * rowStep200),
        colorForRow(i, isSelected),
      );
    }
  }

  void _drawMinimalMenuRows({
    required List<String> rows,
    required int selectedIndex,
    required int rowYStart200,
    required int rowStep200,
    required int textX320,
    required int panelX320,
    required int panelW320,
    required int panelColor,
    required int Function(int index, bool isSelected) colorForRow,
  }) {
    final int panelX =
        projectionOffsetX + ((panelX320 / 320.0) * projectionWidth).toInt();
    final int panelW = math.max(
      1,
      ((panelW320 / 320.0) * projectionWidth).toInt(),
    );
    final int panelRight = panelX + panelW - 1;
    final int textLeft = _menuX320ToColumn(textX320);
    final int textWidth = math.max(1, panelRight - textLeft);

    for (int i = 0; i < rows.length; i++) {
      final bool isSelected = i == selectedIndex;
      final int rowY = _menuY200ToRow(rowYStart200 + (i * rowStep200));
      _writeLeftClipped(
        rowY,
        rows[i],
        colorForRow(i, isSelected),
        panelColor,
        textWidth,
        textLeft,
      );
    }
  }

  void _drawCustomizeMenuHeader(
    WolfClassicMenuArt art,
    int headingColor,
    int backgroundColor,
  ) {
    _drawHeaderBarStack(
      headingY200: _headerHeadingY,
      backgroundColor: backgroundColor,
      barColor: ColorPalette.vga32Bit[0],
    );

    final VgaImage? heading = art.customizeLabel ?? art.optionsLabel;
    if (heading != null) {
      final int headingX = ((320 - heading.width) ~/ 2).clamp(0, 319);
      _blitVgaImageAscii(heading, headingX, 0);
      return;
    }

    _drawMenuTextCentered('CUSTOMIZE', _headerHeadingY, headingColor, scale: 2);
  }

  void _drawMenuSectionHeader({
    required String text,
    required int y200,
  }) {
    final int textW = WolfMenuFont.measureTextWidth(text, 1);
    final int boxW = (textW + 28).clamp(110, 250);
    final int x = ((320 - boxW) ~/ 2).clamp(0, 319);
    final int outer = _rgbToPaletteColor(0x727272);
    final int inner = _rgbToPaletteColor(0x8E8E8E);
    final int fill = _rgbToPaletteColor(0xC9C9C9);
    final int textColor = _rgbToPaletteColor(0x5A5A5A);

    _fillRect320(x, y200, boxW, 12, outer);
    _fillRect320(x + 1, y200 + 1, boxW - 2, 10, inner);
    _fillRect320(x + 2, y200 + 2, boxW - 4, 8, fill);
    _drawMenuLabelAdaptive(
      typography: _resolveMenuTypography(),
      text: text,
      textX320: x + 8,
      y200: y200 + 2,
      color: textColor,
      panelX320: x + 2,
      panelW320: boxW - 4,
      panelColor: fill,
      centerWhenCompact: true,
    );
  }

  void _drawMenuLabelAdaptive({
    required _AsciiMenuTypography typography,
    required String text,
    required int textX320,
    required int y200,
    required int color,
    required int panelX320,
    required int panelW320,
    required int panelColor,
    bool centerWhenCompact = false,
  }) {
    if (!typography.usesCompactRows) {
      _drawMenuText(text, textX320, y200, color);
      return;
    }

    if (centerWhenCompact) {
      final int panelX = _menuX320ToColumn(panelX320);
      final int panelW = math.max(
        1,
        ((panelW320 / 320.0) * projectionWidth).toInt(),
      );
      final int textW = text.length;
      final int centeredLeft = panelX + math.max(0, ((panelW - textW) ~/ 2));
      _writeLeftClipped(
        _menuY200ToRow(y200),
        text,
        color,
        panelColor,
        math.max(1, panelW - 2),
        centeredLeft,
      );
      return;
    }

    _drawMinimalMenuRows(
      rows: <String>[text],
      selectedIndex: -1,
      rowYStart200: y200,
      rowStep200: 1,
      textX320: textX320,
      panelX320: panelX320,
      panelW320: panelW320,
      panelColor: panelColor,
      colorForRow: (_, _) => color,
    );
  }

  void _drawCenteredMenuFooter() {
    if (_usesTerminalLayout && !_emitAnsi) {
      _drawFlutterGridMenuFooter();
      return;
    }

    final int hintKeyColor = ColorPalette.vga32Bit[_menuHintKeyPaletteIndex];
    final int hintLabelColor =
        ColorPalette.vga32Bit[_menuHintLabelPaletteIndex];
    final int hintBackground =
        ColorPalette.vga32Bit[_menuHintBackgroundPaletteIndex];

    final List<(String text, int color)> segments = <(String, int)>[
      ('^/v', hintKeyColor),
      (' MOVE  ', hintLabelColor),
      ('RET', hintKeyColor),
      (' SELECT  ', hintLabelColor),
      ('ESC', hintKeyColor),
      (' BACK', hintLabelColor),
    ];
    int textWidth = 0;
    for (final (text, _) in segments) {
      textWidth += text.length;
    }

    final int boxWidth = math.min(width, textWidth + 2);
    final int boxHeight = 3;
    final int boxX = ((width - boxWidth) ~/ 2).clamp(0, width - boxWidth);
    final int boxY = math.max(0, height - boxHeight);

    if (_usesTerminalLayout) {
      _fillTerminalRect(
        boxX,
        boxY * 2,
        boxWidth,
        boxHeight * 2,
        hintBackground,
      );
    } else {
      _fillRect(boxX, boxY, boxWidth, boxHeight, ' ', hintBackground);
    }

    final int contentWidth = math.max(0, boxWidth - 2);
    final int textY = boxY + 1;
    int textX = boxX + 1;
    int remaining = contentWidth;
    for (final (text, color) in segments) {
      if (remaining <= 0) {
        break;
      }
      final String clipped = text.length <= remaining
          ? text
          : text.substring(0, remaining);
      _writeString(textX, textY, clipped, color, hintBackground);
      textX += clipped.length;
      remaining -= clipped.length;
    }
  }

  void _drawFlutterGridMenuFooter() {
    final int hintKeyColor = ColorPalette.vga32Bit[_menuHintKeyPaletteIndex];
    final int hintLabelColor =
        ColorPalette.vga32Bit[_menuHintLabelPaletteIndex];
    final int hintBackground =
        ColorPalette.vga32Bit[_menuHintBackgroundPaletteIndex];

    final List<(String text, int color)> segments = <(String, int)>[
      ('UP/DN', hintKeyColor),
      (' MOVE  ', hintLabelColor),
      ('RET', hintKeyColor),
      (' SELECT  ', hintLabelColor),
      ('ESC', hintKeyColor),
      (' BACK', hintLabelColor),
    ];

    int textWidth = 0;
    for (final (text, _) in segments) {
      textWidth += WolfMenuFont.measureTextWidth(text, 1);
    }

    final int panelWidth = (textWidth + 12).clamp(1, 320);
    final int panelHeight = 12;
    final int panelX = ((320 - panelWidth) ~/ 2).clamp(0, 319);
    const int panelY = 188;
    _fillRect320(panelX, panelY, panelWidth, panelHeight, hintBackground);

    int cursorX = panelX + 6;
    const int textY = panelY + 2;
    for (final (text, color) in segments) {
      _drawMenuText(text, cursorX, textY, color, scale: 1);
      cursorX += WolfMenuFont.measureTextWidth(text, 1);
    }
  }

  void _writeLeftClipped(
    int y,
    String text,
    int color,
    int backgroundColor,
    int maxWidth,
    int left,
  ) {
    if (y < 0 || y >= height || maxWidth <= 0) {
      return;
    }
    final String clipped = _clipWithEllipsis(text, maxWidth);
    _writeString(left, y, clipped, color, backgroundColor);
  }

  String _clipWithEllipsis(String text, int maxWidth) {
    if (text.length <= maxWidth) {
      return text;
    }
    if (maxWidth <= 3) {
      return text.substring(0, maxWidth);
    }
    return '${text.substring(0, maxWidth - 3)}...';
  }

  void _plotMenuPixel320(int x320, int y200, int color) {
    final double scaleX =
        (_usesTerminalLayout ? projectionWidth : width) / 320.0;
    final double scaleY =
        (_usesTerminalLayout ? _terminalPixelHeight : height) / 200.0;

    final int offsetX = _usesTerminalLayout ? projectionOffsetX : 0;
    final int startX = offsetX + (x320 * scaleX).toInt();
    final int startY = (y200 * scaleY).toInt();
    final int pixelW = math.max(1, scaleX.ceil());
    final int pixelH = math.max(1, scaleY.ceil());
    if (_usesTerminalLayout) {
      for (int dy = 0; dy < pixelH; dy++) {
        final int y = startY + dy;
        if (y < 0 || y >= _terminalPixelHeight) {
          continue;
        }
        for (int dx = 0; dx < pixelW; dx++) {
          final int x = startX + dx;
          if (x < 0 || x >= width) {
            continue;
          }
          _scenePixels[y][x] = color;
        }
      }
      return;
    }

    for (int dy = 0; dy < pixelH; dy++) {
      final int y = startY + dy;
      if (y < 0 || y >= height) {
        continue;
      }
      for (int dx = 0; dx < pixelW; dx++) {
        final int x = startX + dx;
        if (x < 0 || x >= width) {
          continue;
        }
        _screen[y][x] = ColoredChar(activeTheme.solid, color);
      }
    }
  }

  void _drawSimpleHud(WolfEngine engine) {
    final int hudWidth = _usesTerminalLayout ? projectionWidth : width;
    final int hudRows = height - viewHeight;
    if (hudWidth < _simpleHudMinWidth || hudRows < _simpleHudMinRows) {
      _drawMinimalHud(engine);
      return;
    }

    // 1. Pull Retro Colors
    final int hudBackground = ColorPalette.vga32Bit[0];
    final int vgaPanelDark = ColorPalette.vga32Bit[0];
    final int white = ColorPalette.vga32Bit[15];
    final int yellow = ColorPalette.vga32Bit[11];
    final int red = ColorPalette.vga32Bit[4];

    // Compact full simple HUD layout.
    const int floorW = 10;
    const int scoreW = 14;
    const int livesW = 9;
    const int faceW = 10;
    const int healthW = 12;
    const int ammoW = 10;
    const int weaponW = 13;
    const int gap = 1;
    const int hudContentWidth =
        floorW +
        scoreW +
        livesW +
        faceW +
        healthW +
        ammoW +
        weaponW +
        (gap * 6);

    final int offsetX =
        projectionOffsetX +
        ((projectionWidth - hudContentWidth) ~/ 2).clamp(0, projectionWidth);
    final int baseY = viewHeight + 1;

    // 3. Clear HUD Base
    if (_usesTerminalLayout) {
      _fillTerminalRect(
        projectionOffsetX,
        viewHeight * 2,
        projectionWidth,
        hudRows * 2,
        hudBackground,
      );
      _fillTerminalRect(
        projectionOffsetX,
        viewHeight * 2,
        projectionWidth,
        1,
        white,
      );
    } else {
      _fillRect(
        0,
        viewHeight,
        width,
        height - viewHeight,
        ' ',
        hudBackground,
      );
      _writeString(0, viewHeight, "═" * width, white);
    }

    // 4. Panel Drawing Helper
    void drawBorderedPanel(int startX, int startY, int w, int h) {
      if (_usesTerminalLayout) {
        _fillTerminalRect(startX, startY * 2, w, h * 2, vgaPanelDark);
        _fillTerminalRect(startX, startY * 2, w, 1, white);
        _fillTerminalRect(startX, (startY + h) * 2 - 1, w, 1, white);
        _fillTerminalRect(startX, startY * 2, 1, h * 2, white);
        _fillTerminalRect(startX + w - 1, startY * 2, 1, h * 2, white);
      } else {
        _fillRect(startX, startY, w, h, ' ', vgaPanelDark);
        // Horizontal lines
        _writeString(startX, startY, "┌${"─" * (w - 2)}┐", white);
        _writeString(startX, startY + h - 1, "└${"─" * (w - 2)}┘", white);
        // Vertical sides
        for (int i = 1; i < h - 1; i++) {
          _writeString(startX, startY + i, "│", white);
          _writeString(startX + w - 1, startY + i, "│", white);
        }
      }
    }

    // 5. Draw compact panels.
    int cursorX = offsetX;

    drawBorderedPanel(cursorX, baseY + 1, floorW, 4);
    _writeString(cursorX + 2, baseY + 2, "FLR", white, vgaPanelDark);
    String floorLabel = engine.activeLevel.name.split(' ').last;
    if (floorLabel.length > 4) {
      floorLabel = floorLabel.substring(floorLabel.length - 4);
    }
    _writeString(cursorX + 2, baseY + 3, floorLabel, white, vgaPanelDark);
    cursorX += floorW + gap;

    drawBorderedPanel(cursorX, baseY + 1, scoreW, 4);
    _writeString(cursorX + 4, baseY + 2, "SCORE", white, vgaPanelDark);
    _writeString(
      cursorX + 4,
      baseY + 3,
      engine.player.score.toString().padLeft(6, '0'),
      white,
      vgaPanelDark,
    );
    cursorX += scoreW + gap;

    drawBorderedPanel(cursorX, baseY + 1, livesW, 4);
    _writeString(cursorX + 2, baseY + 2, "LIV", white, vgaPanelDark);
    _writeString(cursorX + 3, baseY + 3, "3", white, vgaPanelDark);
    cursorX += livesW + gap;

    drawBorderedPanel(cursorX, baseY, faceW, 5);
    String face = "ಠ⌣ಠ";
    if (engine.player.health <= 0) {
      face = "x⸑x";
    } else if (engine.player.damageFlash > 0.1) {
      face = "ಠoಠ";
    } else if (engine.player.health <= 25) {
      face = "ಥ_ಥ";
    } else if (engine.player.health <= 60) {
      face = "ಠ~ಠ";
    }
    _writeString(cursorX + 3, baseY + 2, face, yellow, vgaPanelDark);
    cursorX += faceW + gap;

    int healthColor = engine.player.health > 25 ? white : red;
    drawBorderedPanel(cursorX, baseY + 1, healthW, 4);
    _writeString(cursorX + 2, baseY + 2, "HEALTH", white, vgaPanelDark);
    _writeString(
      cursorX + 3,
      baseY + 3,
      "${engine.player.health}%",
      healthColor,
      vgaPanelDark,
    );
    cursorX += healthW + gap;

    drawBorderedPanel(cursorX, baseY + 1, ammoW, 4);
    _writeString(cursorX + 2, baseY + 2, "AMMO", white, vgaPanelDark);
    _writeString(
      cursorX + 2,
      baseY + 3,
      "${engine.player.ammo}",
      white,
      vgaPanelDark,
    );
    cursorX += ammoW + gap;

    drawBorderedPanel(cursorX, baseY + 1, weaponW, 4);
    String weapon = engine.player.currentWeapon.type.name.spacePascalCase!
        .toUpperCase();
    if (weapon.length > weaponW - 2) {
      weapon = weapon.substring(0, weaponW - 2);
    }
    _writeString(cursorX + 1, baseY + 3, weapon, white, vgaPanelDark);
  }

  void _drawMinimalHud(WolfEngine engine) {
    final int hudBackground = ColorPalette.vga32Bit[0];
    final int white = ColorPalette.vga32Bit[15];
    final int red = ColorPalette.vga32Bit[4];

    final int hudRows = height - viewHeight;
    if (_usesTerminalLayout) {
      _fillTerminalRect(
        projectionOffsetX,
        viewHeight * 2,
        projectionWidth,
        hudRows * 2,
        hudBackground,
      );
      _fillTerminalRect(
        projectionOffsetX,
        viewHeight * 2,
        projectionWidth,
        1,
        white,
      );
    } else {
      _fillRect(0, viewHeight, width, hudRows, ' ', hudBackground);
      _writeString(0, viewHeight, "═" * width, white);
    }

    final int healthColor = engine.player.health > 25 ? white : red;
    String weapon = engine.player.currentWeapon.type.name.spacePascalCase!
        .toUpperCase();
    if (weapon.length > 8) {
      weapon = weapon.substring(0, 8);
    }
    final String hudText =
        'H:${engine.player.health}% A:${engine.player.ammo} S:${engine.player.score} W:$weapon';

    final int lineY = viewHeight + 1;
    if (lineY >= height) return;

    final int drawStartX = _usesTerminalLayout ? projectionOffsetX : 0;
    final int drawWidth = _usesTerminalLayout ? projectionWidth : width;
    final int maxTextLen = math.max(0, drawWidth - 2);
    String clipped = hudText;
    if (clipped.length > maxTextLen) {
      clipped = clipped.substring(0, maxTextLen);
    }

    final int startX = drawStartX + ((drawWidth - clipped.length) ~/ 2);
    _writeString(startX, lineY, clipped, healthColor, hudBackground);
  }

  void _drawFullVgaHud(WolfEngine engine) {
    drawStandardVgaHud(engine);
  }

  @override
  /// Blits a VGA image into ASCII HUD space (mapped/scaled from 320x200).
  void blitHudVgaImage(VgaImage image, int startX320, int startY200) {
    _blitVgaImageAscii(image, startX320, startY200);
  }

  /// Helper to fill a rectangular area with a specific char and background color
  void _fillRect(int startX, int startY, int w, int h, String char, int color) {
    for (int dy = 0; dy < h; dy++) {
      for (int dx = 0; dx < w; dx++) {
        int x = startX + dx;
        int y = startY + dy;
        if (x >= 0 && x < width && y >= 0 && y < height) {
          _screen[y][x] = ColoredChar(char, color);
        }
      }
    }
  }

  @override
  dynamic finalizeFrame() {
    if (engine.difficulty != null && engine.player.damageFlash > 0.0) {
      if (_usesTerminalLayout) {
        _applyDamageFlashToScene();
      } else {
        _applyDamageFlash();
      }
    }
    if (_usesTerminalLayout) {
      _composeTerminalScene();
    }
    return _emitAnsi ? toAnsiString() : _screen;
  }

  // --- PRIVATE HUD DRAWING HELPERS ---

  void _blitVgaImageAscii(VgaImage image, int startX_320, int startY_200) {
    int maxDrawHeight = _usesTerminalLayout ? _terminalPixelHeight : height;
    int maxDrawWidth = _usesTerminalLayout ? _viewportRightX : width;

    double scaleX = (_usesTerminalLayout ? projectionWidth : width) / 320.0;
    double scaleY =
        (_usesTerminalLayout ? _terminalPixelHeight : height) / 200.0;

    int destStartX =
        (_usesTerminalLayout ? projectionOffsetX : 0) +
        (startX_320 * scaleX).toInt();
    int destStartY = (startY_200 * scaleY).toInt();
    int destWidth = math.max(1, (image.width * scaleX).toInt());
    int destHeight = math.max(1, (image.height * scaleY).toInt());

    for (int dy = 0; dy < destHeight; dy++) {
      for (int dx = 0; dx < destWidth; dx++) {
        int drawX = destStartX + dx;
        int drawY = destStartY + dy;

        if (drawX >= 0 &&
            drawX < maxDrawWidth &&
            drawY >= 0 &&
            drawY < maxDrawHeight) {
          int srcX = (dx / scaleX).toInt().clamp(0, image.width - 1);
          int srcY = (dy / scaleY).toInt().clamp(0, image.height - 1);

          int colorByte = image.decodePixel(srcX, srcY);
          if (colorByte != 255) {
            if (_usesTerminalLayout) {
              _scenePixels[drawY][drawX] = ColorPalette.vga32Bit[colorByte];
            } else {
              _screen[drawY][drawX] = ColoredChar(
                activeTheme.solid,
                ColorPalette.vga32Bit[colorByte],
              );
            }
          }
        }
      }
    }
  }

  void _fillTerminalRect(int startX, int startY, int w, int h, int color) {
    for (int dy = 0; dy < h; dy++) {
      for (int dx = 0; dx < w; dx++) {
        int x = startX + dx;
        int y = startY + dy;
        if (x >= 0 &&
            x < _terminalSceneWidth &&
            y >= 0 &&
            y < _terminalPixelHeight) {
          _scenePixels[y][x] = color;
        }
      }
    }
  }

  void _fillRect320(
    int startX320,
    int startY200,
    int w320,
    int h200,
    int color,
  ) {
    final double scaleX =
        (_usesTerminalLayout ? projectionWidth : width) / 320.0;
    final double scaleY =
        (_usesTerminalLayout ? _terminalPixelHeight : height) / 200.0;

    final int startX =
        (_usesTerminalLayout ? projectionOffsetX : 0) +
        (startX320 * scaleX).toInt();
    final int startY = (startY200 * scaleY).toInt();
    final int w = math.max(1, (w320 * scaleX).toInt());
    final int h = math.max(1, (h200 * scaleY).toInt());

    if (_usesTerminalLayout) {
      _fillTerminalRect(startX, startY, w, h, color);
    } else {
      _fillRect(startX, startY, w, h, activeTheme.solid, color);
    }
  }

  // --- DAMAGE FLASH ---
  void _applyDamageFlash() {
    for (int y = 0; y < viewHeight; y++) {
      for (int x = 0; x < width; x++) {
        ColoredChar cell = _screen[y][x];
        _screen[y][x] = ColoredChar(
          cell.char,
          _applyDamageFlashToColor(cell.rawColor),
          cell.rawBackgroundColor == null
              ? null
              : _applyDamageFlashToColor(cell.rawBackgroundColor!),
        );
      }
    }
  }

  void _applyDamageFlashToScene() {
    for (int y = 0; y < _terminalPixelHeight; y++) {
      for (int x = projectionOffsetX; x < _viewportRightX; x++) {
        _scenePixels[y][x] = _applyDamageFlashToColor(_scenePixels[y][x]);
      }
    }
  }

  int _applyDamageFlashToColor(int color) {
    double intensity = engine.player.damageFlash;
    int redBoost = (150 * intensity).toInt();
    double colorDrop = 1.0 - (0.5 * intensity);

    int r = color & 0xFF;
    int g = (color >> 8) & 0xFF;
    int b = (color >> 16) & 0xFF;

    r = (r + redBoost).clamp(0, 255);
    g = (g * colorDrop).toInt().clamp(0, 255);
    b = (b * colorDrop).toInt().clamp(0, 255);

    return (0xFF000000) | (b << 16) | (g << 8) | r;
  }

  int _scaleColor(int color, double brightness) {
    int r = ((color & 0xFF) * brightness).toInt().clamp(0, 255);
    int g = (((color >> 8) & 0xFF) * brightness).toInt().clamp(0, 255);
    int b = (((color >> 16) & 0xFF) * brightness).toInt().clamp(0, 255);
    return (0xFF000000) | (b << 16) | (g << 8) | r;
  }

  void _composeTerminalScene() {
    for (int y = 0; y < height; y++) {
      int topY = y * 2;
      int bottomY = math.min(topY + 1, _terminalPixelHeight - 1);
      for (int x = 0; x < width; x++) {
        final int leftX = x;
        final int rightX = _usesQuadrantCompose
            ? math.min(x + 1, _terminalSceneWidth - 1)
            : leftX;

        int topColor = _scenePixels[topY][leftX];
        int bottomColor = _scenePixels[bottomY][leftX];

        ColoredChar overlay = _screen[y][x];
        if (overlay.char != ' ') {
          // In ANSI terminal mode, inject the bottom scene pixel as background
          // so half-block rows pack two pixel rows into one cell. In Flutter
          // grid mode any injected background renders as a colored rectangle
          // behind the character, so we leave the cell as-is.
          if (_emitAnsi && overlay.rawBackgroundColor == null) {
            _screen[y][x] = ColoredChar(
              overlay.char,
              overlay.rawColor,
              _usesQuadrantCompose
                  ? _blendPackedColors(
                      _scenePixels[bottomY][leftX],
                      _scenePixels[bottomY][rightX],
                    )
                  : bottomColor,
            );
          }
          continue;
        }

        // Pack two scene rows into one cell for both terminal and Flutter grid
        // modes. Overlay characters above keep a null background in Flutter
        // mode, so this does not introduce text background artifacts.
        if (_usesQuadrantCompose) {
          final (char, fgColor, bgColor) = _composeQuadrantCell(
            _scenePixels[topY][leftX],
            _scenePixels[topY][rightX],
            _scenePixels[bottomY][leftX],
            _scenePixels[bottomY][rightX],
          );
          _screen[y][x] = bgColor == null
              ? ColoredChar(char, fgColor)
              : ColoredChar(char, fgColor, bgColor);
          continue;
        }

        if (topColor == bottomColor) {
          _screen[y][x] = ColoredChar('█', topColor);
          continue;
        }

        _screen[y][x] = ColoredChar('▀', topColor, bottomColor);
      }
    }
  }

  (String char, int fgColor, int? bgColor) _composeQuadrantCell(
    int topLeft,
    int topRight,
    int bottomLeft,
    int bottomRight,
  ) {
    final List<int> samples = <int>[topLeft, topRight, bottomLeft, bottomRight];
    int bgColor = samples.first;
    int bgCount = 0;
    for (final candidate in samples) {
      final int count = samples.where((value) => value == candidate).length;
      if (count > bgCount) {
        bgColor = candidate;
        bgCount = count;
      }
    }

    int fgColor = bgColor;
    double fgDistance = -1.0;
    for (final candidate in samples) {
      final double distance = _packedColorDistance(candidate, bgColor);
      if (distance > fgDistance) {
        fgDistance = distance;
        fgColor = candidate;
      }
    }

    if (fgColor == bgColor) {
      return ('█', fgColor, null);
    }

    int mask = 0;
    if (_packedColorDistance(topLeft, fgColor) <=
        _packedColorDistance(topLeft, bgColor)) {
      mask |= 1;
    }
    if (_packedColorDistance(topRight, fgColor) <=
        _packedColorDistance(topRight, bgColor)) {
      mask |= 2;
    }
    if (_packedColorDistance(bottomLeft, fgColor) <=
        _packedColorDistance(bottomLeft, bgColor)) {
      mask |= 4;
    }
    if (_packedColorDistance(bottomRight, fgColor) <=
        _packedColorDistance(bottomRight, bgColor)) {
      mask |= 8;
    }

    if (mask == 0) {
      return (' ', bgColor, null);
    }
    if (mask == 15) {
      return ('█', fgColor, null);
    }
    return (_quadrantByMask[mask], fgColor, bgColor);
  }

  void _logThemeIfChanged() {
    if (_lastLoggedThemeName == activeTheme.name) {
      return;
    }
    _lastLoggedThemeName = activeTheme.name;
    log('ASCII renderer theme: ${activeTheme.name}', name: 'AsciiRenderer');
  }

  int _blendPackedColors(int a, int b) {
    final int r = (((a & 0xFF) + (b & 0xFF)) / 2).round();
    final int g = ((((a >> 8) & 0xFF) + ((b >> 8) & 0xFF)) / 2).round();
    final int blue = ((((a >> 16) & 0xFF) + ((b >> 16) & 0xFF)) / 2).round();
    return 0xFF000000 | (blue << 16) | (g << 8) | r;
  }

  double _packedColorDistance(int a, int b) {
    final int dr = (a & 0xFF) - (b & 0xFF);
    final int dg = ((a >> 8) & 0xFF) - ((b >> 8) & 0xFF);
    final int db = ((a >> 16) & 0xFF) - ((b >> 16) & 0xFF);
    return (dr * dr + dg * dg + db * db).toDouble();
  }

  /// Converts the current frame to a single printable ANSI string
  StringBuffer toAnsiString() {
    StringBuffer buffer = StringBuffer();

    int? lastForeground;
    int? lastBackground;

    for (int y = 0; y < _screen.length; y++) {
      List<ColoredChar> row = _screen[y];
      for (ColoredChar cell in row) {
        if (cell.rawColor != lastForeground) {
          buffer.write('\x1b[38;2;${cell.r};${cell.g};${cell.b}m');
          lastForeground = cell.rawColor;
        }
        if (cell.rawBackgroundColor != lastBackground) {
          if (cell.rawBackgroundColor == null) {
            buffer.write('\x1b[49m');
          } else {
            int background = cell.rawBackgroundColor!;
            int bgR = background & 0xFF;
            int bgG = (background >> 8) & 0xFF;
            int bgB = (background >> 16) & 0xFF;
            buffer.write(
              '\x1b[48;2;$bgR;$bgG;$bgB'
              'm',
            );
          }
          lastBackground = cell.rawBackgroundColor;
        }
        buffer.write(cell.char);
      }

      // Only print a newline if we are NOT on the very last row.
      // This stops the terminal from scrolling down!
      if (y < _screen.length - 1) {
        buffer.write('\n');
      }
    }

    // Reset the terminal color at the very end
    buffer.write('\x1b[0m');

    return buffer;
  }

  int _rgbToPaletteColor(int rgb) {
    return ColorPalette.vga32Bit[ColorPalette.findClosestPaletteIndex(rgb)];
  }
}
