import 'dart:math' as math;

import 'package:wolf_3d_dart/src/menu/menu_manager.dart';
import 'package:wolf_3d_dart/src/rendering/menu_font.dart';
import 'package:wolf_3d_dart/src/rendering/renderer_backend.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';

/// Pixel-accurate software renderer that writes directly into [FrameBuffer].
///
/// This is the canonical "modern framebuffer" implementation and serves as a
/// visual reference for terminal renderers.
class SoftwareRenderer extends RendererBackend<FrameBuffer> {
  static const int _menuFooterBottomMargin = 1;
  static const int _menuFooterY = 184;
  static const int _menuFooterHeight = 12;
  static const int _headerHeadingY = 24;

  late FrameBuffer _buffer;
  List<int>? _mainMenuBandFirstColumn;

  @override
  void prepareFrame(WolfEngine engine) {
    _buffer = engine.frameBuffer;
    // Top half is ceiling color (25), bottom half is floor color (29)
    int ceilingColor = ColorPalette.vga32Bit[25];
    int floorColor = ColorPalette.vga32Bit[29];

    for (int y = 0; y < viewHeight; y++) {
      int color = (y < viewHeight / 2) ? ceilingColor : floorColor;
      for (int x = 0; x < width; x++) {
        _buffer.pixels[y * width + x] = color;
      }
    }
  }

  @override
  void drawWallColumn(
    int x,
    int drawStart,
    int drawEnd,
    int columnHeight,
    Sprite texture,
    int texX,
    double perpWallDist,
    int side,
  ) {
    for (int y = drawStart; y < drawEnd; y++) {
      int texY = wallTexY(y, columnHeight);

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

      // Darken Y-side walls for faux directional lighting
      if (side == 1) {
        pixelColor = shadeColor(pixelColor);
      }

      _buffer.pixels[y * width + x] = pixelColor;
    }
  }

  @override
  void drawSpriteStripe(
    int stripeX,
    int drawStartY,
    int drawEndY,
    int spriteHeight,
    Sprite texture,
    int texX,
    double transformY,
  ) {
    for (
      int y = math.max(0, drawStartY);
      y < math.min(viewHeight, drawEndY);
      y++
    ) {
      int texY = spriteTexY(y, drawStartY, spriteHeight);

      int colorByte = texture.pixels[texX * 64 + texY];

      // 255 is the "transparent" color index in VGA Wolfenstein
      if (colorByte != 255) {
        _buffer.pixels[y * width + stripeX] = ColorPalette.vga32Bit[colorByte];
      }
    }
  }

  @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 = bounds.weaponHeight;
    final int startX = bounds.startX;
    final int startY = bounds.startY;

    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 drawX = startX + dx;
          int drawY = startY + dy;
          if (drawX >= 0 && drawX < width && drawY >= 0 && drawY < viewHeight) {
            _buffer.pixels[drawY * width + drawX] =
                ColorPalette.vga32Bit[colorByte];
          }
        }
      }
    }
  }

  @override
  /// Delegates the canonical Wolf3D VGA HUD draw sequence to the base class.
  void drawHud(WolfEngine engine) {
    drawStandardVgaHud(engine);
  }

  @override
  void drawFpsOverlay(WolfEngine engine) {
    const int panelX = 2;
    const int panelY = 2;
    const int panelW = 72;
    const int panelH = 10;
    final int panelColor = ColorPalette.vga32Bit[0];
    final int textColor = ColorPalette.vga32Bit[15];

    _fillMenuPanel(panelX, panelY, panelW, panelH, panelColor);
    _drawMenuText(fpsLabel(engine), panelX + 3, panelY + 1, textColor);
  }

  @override
  /// Blits a VGA image into the software framebuffer HUD space (320x200).
  void blitHudVgaImage(VgaImage image, int startX320, int startY200) {
    _blitVgaImage(image, startX320, startY200);
  }

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

    for (int i = 0; i < _buffer.pixels.length; i++) {
      _buffer.pixels[i] = bgColor;
    }

    final art = WolfClassicMenuArt(engine.data);
    final optionsLabel = art.optionsLabel;
    if (optionsLabel != null) {
      _mainMenuBandFirstColumn = _cacheFirstColumn(optionsLabel);
    }
    // Draw footer first so menu panels can clip overlap in the center.
    _drawCenteredMenuFooter(art);

    switch (engine.menuManager.activeMenu) {
      case WolfMenuScreen.introSplash:
        _drawIntroSplash(engine, art);
        break;
      case WolfMenuScreen.mainMenu:
        _drawMainMenu(
          engine,
          art,
          panelColor,
          headingColor,
          selectedTextColor,
          unselectedTextColor,
          disabledTextColor,
        );
        break;
      case WolfMenuScreen.gameSelect:
        _drawGameSelectMenu(
          engine,
          art,
          panelColor,
          headingColor,
          selectedTextColor,
          unselectedTextColor,
        );
        break;
      case WolfMenuScreen.episodeSelect:
        _drawEpisodeSelectMenu(
          engine,
          art,
          panelColor,
          headingColor,
          selectedTextColor,
          unselectedTextColor,
        );
        break;
      case WolfMenuScreen.difficultySelect:
        _drawDifficultyMenu(
          engine,
          art,
          panelColor,
          headingColor,
          selectedTextColor,
          unselectedTextColor,
        );
        break;
      case WolfMenuScreen.changeView:
        _drawChangeViewMenu(
          engine,
          art,
          panelColor,
          headingColor,
          selectedTextColor,
          unselectedTextColor,
          disabledTextColor,
        );
        break;
      case WolfMenuScreen.rendererOptions:
        _drawRendererOptionsMenu(
          engine,
          art,
          panelColor,
          headingColor,
          selectedTextColor,
          unselectedTextColor,
          disabledTextColor,
        );
        break;
    }

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

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

    int splashBgColor = _rgbToFrameColor(engine.menuManager.menuBackgroundRgb);
    int? matteIndex;
    if (engine.menuManager.isIntroPg13Slide &&
        image != null &&
        image.pixels.isNotEmpty) {
      matteIndex = _dominantPaletteIndex(image);
      splashBgColor =
          ColorPalette.vga32Bit[_pg13BackgroundPaletteIndex(image, matteIndex)];
    }

    for (int i = 0; i < _buffer.pixels.length; i++) {
      _buffer.pixels[i] = splashBgColor;
    }

    if (engine.menuManager.isIntroRetailWarningSlide) {
      _drawRetailWarningIntro(splashBgColor);
    }

    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);
      if (engine.menuManager.isIntroPg13Slide && matteIndex != null) {
        _blitVgaImage(image, x, y, transparentIndex: matteIndex);
      } else {
        _blitVgaImage(image, x, y);
      }
    }

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

  void _drawRetailWarningIntro(int backgroundColor) {
    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,
    );
    _drawCanonicalMenuTextCentered(
      'Attention',
      _headerHeadingY,
      yellow,
      scale: 2,
    );

    _drawCanonicalMenuText(
      'This game is NOT shareware.',
      40,
      56,
      white,
      scale: 1,
    );
    _drawCanonicalMenuText(
      'Please do not distribute it.',
      40,
      68,
      white,
      scale: 1,
    );
    _drawCanonicalMenuText('Thanks.', 40, 80, white, scale: 1);
    _drawCanonicalMenuTextCentered('Id Software', 106, white, scale: 1);

    _fillCanonicalRect(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;
  }

  int _pg13BackgroundPaletteIndex(VgaImage image, int matteIndex) {
    final List<(int x, int y)> probes = <(int x, int y)>[
      (0, 0),
      (image.width - 1, 0),
      (0, image.height - 1),
      (image.width - 1, image.height - 1),
      (image.width - 8, image.height - 8),
      (image.width - 16, image.height - 16),
    ];
    final Map<int, int> counts = <int, int>{};
    for (final probe in probes) {
      final int x = probe.$1.clamp(0, image.width - 1);
      final int y = probe.$2.clamp(0, image.height - 1);
      final int idx = image.decodePixel(x, y);
      if (idx == matteIndex) {
        continue;
      }
      counts[idx] = (counts[idx] ?? 0) + 1;
    }

    if (counts.isEmpty) {
      return matteIndex;
    }

    int bestIndex = counts.keys.first;
    int bestCount = counts[bestIndex] ?? 0;
    for (final MapEntry<int, int> entry in counts.entries) {
      if (entry.value > bestCount) {
        bestCount = entry.value;
        bestIndex = entry.key;
      }
    }
    return bestIndex;
  }

  void _drawMainMenu(
    WolfEngine engine,
    WolfClassicMenuArt art,
    int panelColor,
    int headingColor,
    int selectedTextColor,
    int unselectedTextColor,
    int disabledTextColor,
  ) {
    const int panelX = 68;
    const int panelY = 52;
    const int panelW = 178;
    const int panelH = 136;
    _fillCanonicalRect(panelX, panelY, panelW, panelH, panelColor);

    final optionsLabel = art.optionsLabel;
    if (optionsLabel != null) {
      final int optionsX = ((320 - optionsLabel.width) ~/ 2).clamp(0, 319);
      _drawMainMenuOptionsSideBars(optionsLabel, optionsX);
      _blitVgaImage(optionsLabel, optionsX, 0);
    } else {
      _drawHeaderBarStack(
        headingY200: _headerHeadingY,
        backgroundColor: _rgbToFrameColor(engine.menuManager.menuBackgroundRgb),
        barColor: ColorPalette.vga32Bit[0],
      );
      _drawCanonicalMenuTextCentered(
        'OPTIONS',
        _headerHeadingY,
        headingColor,
        scale: 2,
      );
    }

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

    for (int i = 0; i < entries.length; i++) {
      final bool isSelected = i == selectedIndex;
      final int y = rowYStart + (i * rowStep);
      if (isSelected && cursor != null) {
        _blitVgaImage(cursor, panelX + 4, y - 2);
      }
      _drawCanonicalMenuText(
        entries[i].label,
        textX,
        y,
        entries[i].isEnabled
            ? (isSelected ? selectedTextColor : unselectedTextColor)
            : disabledTextColor,
      );
    }
  }

  void _drawChangeViewMenu(
    WolfEngine engine,
    WolfClassicMenuArt art,
    int panelColor,
    int headingColor,
    int selectedTextColor,
    int unselectedTextColor,
    int disabledTextColor,
  ) {
    _drawHeaderBarStack(
      headingY200: _headerHeadingY,
      backgroundColor: _rgbToFrameColor(engine.menuManager.menuBackgroundRgb),
      barColor: ColorPalette.vga32Bit[0],
    );

    const int modesPanelX = 46;
    const int modesPanelY = 52;
    const int modesPanelW = 228;
    const int modesPanelH = 74;
    _fillCanonicalRect(
      modesPanelX,
      modesPanelY,
      modesPanelW,
      modesPanelH,
      panelColor,
    );

    const int optionsPanelX = 46;
    const int optionsPanelY = 146;
    const int optionsPanelW = 228;
    const int optionsPanelH = 42;
    _fillCanonicalRect(
      optionsPanelX,
      optionsPanelY,
      optionsPanelW,
      optionsPanelH,
      panelColor,
    );

    final VgaImage? heading = art.customizeLabel ?? art.optionsLabel;
    if (heading != null) {
      final int headingX = ((320 - heading.width) ~/ 2).clamp(0, 319);
      _blitVgaImage(heading, headingX, 0);
    } else {
      _drawCanonicalMenuTextCentered(
        'CUSTOMIZE',
        _headerHeadingY,
        headingColor,
        scale: 2,
      );
    }

    final VgaImage? selectedMarker = art.selectedMarker;
    final VgaImage? unselectedMarker = art.unselectedMarker;
    final VgaImage? cursor = art.mappedPic(
      engine.menuManager.isCursorAltFrame(engine.timeAliveMs) ? 9 : 8,
    );

    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 selectedIndex = engine.menuManager.selectedChangeViewIndex;

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

      final entry = entries[i];
      final VgaImage? marker = entry.isChecked
          ? selectedMarker
          : unselectedMarker;
      if (marker != null) {
        _blitVgaImage(marker, markerX, y);
      }

      _drawCanonicalMenuText(
        entry.label,
        textX,
        y + 1,
        entry.isEnabled
            ? (isSelected ? selectedTextColor : unselectedTextColor)
            : disabledTextColor,
      );
    }

    _drawMenuSectionHeader(
      text: engine.menuManager.rendererOptionsTitle,
      y200: 132,
      textColor: ColorPalette.vga32Bit[8],
    );

    const int optionsRowStart = 159;
    const int optionsRowStep = 15;
    for (int i = 0; i < optionEntries.length; i++) {
      final int optionIndex = modeCount + i;
      final bool isSelected = optionIndex == selectedIndex;
      final int y = optionsRowStart + (i * optionsRowStep);
      if (isSelected && cursor != null) {
        _blitVgaImage(cursor, cursorX, y - 2);
      }

      final entry = optionEntries[i];
      final VgaImage? marker = entry.isChecked
          ? selectedMarker
          : unselectedMarker;
      if (marker != null) {
        _blitVgaImage(marker, markerX, y);
      }
      _drawCanonicalMenuText(
        entry.label,
        textX,
        y + 1,
        entry.isEnabled
            ? (isSelected ? selectedTextColor : unselectedTextColor)
            : disabledTextColor,
      );
    }
  }

  void _drawRendererOptionsMenu(
    WolfEngine engine,
    WolfClassicMenuArt art,
    int panelColor,
    int headingColor,
    int selectedTextColor,
    int unselectedTextColor,
    int disabledTextColor,
  ) {
    _drawHeaderBarStack(
      headingY200: _headerHeadingY,
      backgroundColor: _rgbToFrameColor(engine.menuManager.menuBackgroundRgb),
      barColor: ColorPalette.vga32Bit[0],
    );

    const int panelX = 56;
    const int panelY = 52;
    const int panelW = 208;
    const int panelH = 120;
    _fillCanonicalRect(panelX, panelY, panelW, panelH, panelColor);

    final VgaImage? heading = art.customizeLabel ?? art.optionsLabel;
    if (heading != null) {
      final int headingX = ((320 - heading.width) ~/ 2).clamp(0, 319);
      _blitVgaImage(heading, headingX, 0);
    } else {
      _drawCanonicalMenuTextCentered(
        'CUSTOMIZE',
        _headerHeadingY,
        headingColor,
        scale: 2,
      );
    }

    final VgaImage? selectedMarker = art.selectedMarker;
    final VgaImage? unselectedMarker = art.unselectedMarker;
    final VgaImage? cursor = art.mappedPic(
      engine.menuManager.isCursorAltFrame(engine.timeAliveMs) ? 9 : 8,
    );

    _drawCanonicalMenuTextCentered(
      engine.menuManager.rendererOptionsTitle,
      46,
      headingColor,
      scale: 1,
    );

    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;
    final int selectedIndex = engine.menuManager.selectedRendererOptionIndex;

    for (int i = 0; i < entries.length; i++) {
      final entry = entries[i];
      final bool isSelected = i == selectedIndex;
      final int y = rowYStart + (i * rowStep);
      if (isSelected && cursor != null) {
        _blitVgaImage(cursor, cursorX, y - 2);
      }

      final VgaImage? marker = entry.isChecked
          ? selectedMarker
          : unselectedMarker;
      if (marker != null) {
        _blitVgaImage(marker, markerX, y);
      }

      _drawCanonicalMenuText(
        entry.label,
        textX,
        y + 1,
        entry.isEnabled
            ? (isSelected ? selectedTextColor : unselectedTextColor)
            : disabledTextColor,
      );
    }
  }

  void _drawGameSelectMenu(
    WolfEngine engine,
    WolfClassicMenuArt art,
    int panelColor,
    int headingColor,
    int selectedTextColor,
    int unselectedTextColor,
  ) {
    _drawHeaderBarStack(
      headingY200: _headerHeadingY,
      backgroundColor: _rgbToFrameColor(engine.menuManager.menuBackgroundRgb),
      barColor: ColorPalette.vga32Bit[0],
    );

    const int panelX = 28;
    const int panelY = 58;
    const int panelW = 264;
    const int panelH = 104;
    _fillCanonicalRect(panelX, panelY, panelW, panelH, panelColor);

    _drawCanonicalMenuTextCentered(
      'SELECT GAME',
      _headerHeadingY,
      headingColor,
      scale: 2,
    );

    final cursor = art.mappedPic(
      engine.menuManager.isCursorAltFrame(engine.timeAliveMs) ? 9 : 8,
    );

    const int rowYStart = 78;
    const int rowStep = 20;
    const int textX = 70;
    final int selectedIndex = engine.menuManager.selectedGameIndex;

    for (int i = 0; i < engine.availableGames.length; i++) {
      final bool isSelected = i == selectedIndex;
      final int y = rowYStart + (i * rowStep);
      if (isSelected && cursor != null) {
        _blitVgaImage(cursor, panelX + 10, y - 2);
      }
      _drawCanonicalMenuText(
        _gameTitle(engine.availableGames[i].version),
        textX,
        y,
        isSelected ? selectedTextColor : unselectedTextColor,
      );
    }
  }

  void _drawMenuSectionHeader({
    required String text,
    required int y200,
    required int textColor,
  }) {
    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 y = y200.clamp(0, 199);
    final int outer = _rgbToFrameColor(0x727272);
    final int inner = _rgbToFrameColor(0x8E8E8E);
    final int fill = _rgbToFrameColor(0xC9C9C9);
    final int menuTextColor = _rgbToFrameColor(0x5A5A5A);

    _fillCanonicalRect(x, y, boxW, 12, outer);
    _fillCanonicalRect(x + 1, y + 1, boxW - 2, 10, inner);
    _fillCanonicalRect(x + 2, y + 2, boxW - 4, 8, fill);
    _drawCanonicalMenuTextCentered(text, y + 2, menuTextColor, scale: 1);
  }

  void _drawEpisodeSelectMenu(
    WolfEngine engine,
    WolfClassicMenuArt art,
    int panelColor,
    int headingColor,
    int selectedTextColor,
    int unselectedTextColor,
  ) {
    _drawHeaderBarStack(
      headingY200: _headerHeadingY,
      backgroundColor: _rgbToFrameColor(engine.menuManager.menuBackgroundRgb),
      barColor: ColorPalette.vga32Bit[0],
    );

    const int panelX = 12;
    const int panelY = 20;
    const int panelW = 296;
    const int panelH = 158;
    _fillCanonicalRect(panelX, panelY, panelW, panelH, panelColor);

    _drawCanonicalMenuTextCentered(
      'WHICH EPISODE TO PLAY?',
      _headerHeadingY,
      headingColor,
      scale: 2,
    );

    final cursor = art.mappedPic(
      engine.menuManager.isCursorAltFrame(engine.timeAliveMs) ? 9 : 8,
    );
    const int rowYStart = 30;
    const int rowStep = 24;
    const int imageX = 40;
    const int textX = 98;

    final int selectedIndex = engine.menuManager.selectedEpisodeIndex;
    for (int i = 0; i < engine.data.episodes.length; i++) {
      final int y = rowYStart + (i * rowStep);
      final bool isSelected = i == selectedIndex;

      if (isSelected && cursor != null) {
        _blitVgaImage(cursor, 16, y + 2);
      }

      final image = art.episodeOption(i);
      if (image != null) {
        _blitVgaImage(image, imageX, y);
      }

      final parts = engine.data.episodes[i].name.split('\n');
      if (parts.isNotEmpty) {
        _drawCanonicalMenuText(
          parts.first,
          textX,
          y + 1,
          isSelected ? selectedTextColor : unselectedTextColor,
        );
      }
      if (parts.length > 1) {
        _drawCanonicalMenuText(
          parts.sublist(1).join(' '),
          textX,
          y + 12,
          isSelected ? selectedTextColor : unselectedTextColor,
        );
      }
    }
  }

  void _drawCenteredMenuFooter(WolfClassicMenuArt art) {
    final bottom = art.mappedPic(15);
    if (bottom != null) {
      final int x = ((320 - bottom.width) ~/ 2).clamp(0, 319);
      final int y = (200 - bottom.height - _menuFooterBottomMargin).clamp(
        0,
        199,
      );
      _blitVgaImage(bottom, x, y);
      return;
    }

    final int hintKeyColor = ColorPalette.vga32Bit[12];
    final int hintLabelColor = ColorPalette.vga32Bit[4];
    final int hintBackground = ColorPalette.vga32Bit[0];

    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 panelX = ((320 - panelWidth) ~/ 2).clamp(0, 319);
    _fillCanonicalRect(
      panelX,
      _menuFooterY,
      panelWidth,
      _menuFooterHeight,
      hintBackground,
    );

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

  void _drawDifficultyMenu(
    WolfEngine engine,
    WolfClassicMenuArt art,
    int panelColor,
    int headingColor,
    int selectedTextColor,
    int unselectedTextColor,
  ) {
    _drawHeaderBarStack(
      headingY200: _headerHeadingY,
      backgroundColor: _rgbToFrameColor(engine.menuManager.menuBackgroundRgb),
      barColor: ColorPalette.vga32Bit[0],
    );

    final int selectedDifficultyIndex =
        engine.menuManager.selectedDifficultyIndex;
    const int panelX = 28;
    const int panelY = 70;
    const int panelW = 264;
    const int panelH = 82;
    _fillCanonicalRect(panelX, panelY, panelW, panelH, panelColor);

    _drawCanonicalMenuTextCentered(
      Difficulty.menuText,
      _headerHeadingY,
      headingColor,
      scale: 2,
    );

    final face = art.difficultyOption(
      Difficulty.values[selectedDifficultyIndex],
    );
    if (face != null) {
      _blitVgaImage(face, panelX + panelW - face.width - 10, panelY + 22);
    }

    final cursor = art.mappedPic(
      engine.menuManager.isCursorAltFrame(engine.timeAliveMs) ? 9 : 8,
    );
    const int rowYStart = panelY + 16;
    const int rowStep = 15;
    const int textX = panelX + 42;
    for (int i = 0; i < Difficulty.values.length; i++) {
      final y = rowYStart + (i * rowStep);
      final isSelected = i == selectedDifficultyIndex;

      if (isSelected && cursor != null) {
        _blitVgaImage(cursor, panelX + 10, y - 2);
      }

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

  void _fillMenuPanel(
    int panelX,
    int panelY,
    int panelW,
    int panelH,
    int color,
  ) {
    for (int y = panelY; y < panelY + panelH; y++) {
      if (y < 0 || y >= height) continue;
      final rowStart = y * width;
      for (int x = panelX; x < panelX + panelW; x++) {
        if (x >= 0 && x < width) {
          _buffer.pixels[rowStart + x] = color;
        }
      }
    }
  }

  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 _applyMenuFade(double alpha, int fadeColor) {
    if (alpha <= 0.0) {
      return;
    }

    final int fadeR = fadeColor & 0xFF;
    final int fadeG = (fadeColor >> 8) & 0xFF;
    final int fadeB = (fadeColor >> 16) & 0xFF;

    for (int i = 0; i < _buffer.pixels.length; i++) {
      final int c = _buffer.pixels[i];
      final int r = c & 0xFF;
      final int g = (c >> 8) & 0xFF;
      final int b = (c >> 16) & 0xFF;

      final int outR = (r + ((fadeR - r) * alpha)).round().clamp(0, 255);
      final int outG = (g + ((fadeG - g) * alpha)).round().clamp(0, 255);
      final int outB = (b + ((fadeB - b) * alpha)).round().clamp(0, 255);
      _buffer.pixels[i] = (0xFF << 24) | (outB << 16) | (outG << 8) | outR;
    }
  }

  /// Converts an `RRGGBB` menu color into the framebuffer's packed channel
  /// order (`0xAABBGGRR`) used throughout this renderer.
  int _rgbToFrameColor(int rgb) {
    final int r = (rgb >> 16) & 0xFF;
    final int g = (rgb >> 8) & 0xFF;
    final int b = rgb & 0xFF;
    // Framebuffer expects bytes in RGBA order; this packed int produces that
    // layout on little-endian platforms.
    return (0xFF000000) | (b << 16) | (g << 8) | r;
  }

  void _drawHeaderBarStack({
    required int headingY200,
    required int backgroundColor,
    required int barColor,
  }) {
    final List<int>? cachedColumn = _mainMenuBandFirstColumn;
    if (cachedColumn != null && cachedColumn.isNotEmpty) {
      final int bandHeight = cachedColumn.length.clamp(0, 200);
      for (int y = 0; y < bandHeight; y++) {
        final int paletteIndex = cachedColumn[y];
        final int fillIndex = paletteIndex == 0 ? 0 : paletteIndex;
        _fillCanonicalRect(0, y, 320, 1, ColorPalette.vga32Bit[fillIndex]);
      }
      return;
    }

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

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

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

  double get _uiScaleX => width / 320.0;

  double get _uiScaleY => height / 200.0;

  void _fillCanonicalRect(
    int startX320,
    int startY200,
    int width320,
    int height200,
    int color,
  ) {
    final int startX = (startX320 * _uiScaleX).floor();
    final int endX = ((startX320 + width320) * _uiScaleX).ceil();
    final int startY = (startY200 * _uiScaleY).floor();
    final int endY = ((startY200 + height200) * _uiScaleY).ceil();

    for (int y = startY; y < endY; y++) {
      if (y < 0 || y >= height) continue;
      final int rowStart = y * width;
      for (int x = startX; x < endX; x++) {
        if (x >= 0 && x < width) {
          _buffer.pixels[rowStart + x] = color;
        }
      }
    }
  }

  void _drawMainMenuOptionsSideBars(VgaImage optionsLabel, int optionsX320) {
    _mainMenuBandFirstColumn = _cacheFirstColumn(optionsLabel);
    final List<int> firstColumn = _mainMenuBandFirstColumn!;
    for (int y = 0; y < optionsLabel.height; y++) {
      final int paletteIndex = firstColumn[y];
      final int fillIndex = paletteIndex == 0 ? 0 : paletteIndex;
      _fillCanonicalRect(0, y, 320, 1, ColorPalette.vga32Bit[fillIndex]);
    }
  }

  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 _drawCanonicalMenuText(
    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;
          _fillCanonicalRect(
            x320 + (col * scale),
            startY200 + (row * scale),
            scale,
            scale,
            color,
          );
        }
      }

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

  void _drawCanonicalMenuTextCentered(
    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);
    _drawCanonicalMenuText(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);
  }

  /// Draws bitmap menu text directly into the framebuffer.
  void _drawMenuText(
    String text,
    int startX,
    int startY,
    int color, {
    int scale = 1,
  }) {
    int x = startX;
    for (final rune in text.runes) {
      final char = String.fromCharCode(rune).toUpperCase();
      final pattern = WolfMenuFont.glyphFor(char);

      for (int row = 0; row < pattern.length; row++) {
        final 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++) {
              final drawX = x + (col * scale) + sx;
              final drawY = startY + (row * scale) + sy;
              if (drawX >= 0 && drawX < width && drawY >= 0 && drawY < height) {
                _buffer.pixels[drawY * width + drawX] = color;
              }
            }
          }
        }
      }

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

  @override
  FrameBuffer finalizeFrame() {
    // If the player took damage, overlay a red tint across the 3D view
    if (engine.difficulty != null && engine.player.damageFlash > 0) {
      _applyDamageFlash();
    }
    return _buffer; // Return the fully painted pixel array
  }

  // ===========================================================================
  // PRIVATE HELPER METHODS
  // ===========================================================================

  /// Maps planar VGA image data directly to 32-bit framebuffer pixels.
  ///
  /// UI coordinates are expressed in canonical 320x200 space and scaled to the
  /// current framebuffer so higher-resolution render targets preserve layout.
  void _blitVgaImage(
    VgaImage image,
    int startX,
    int startY, {
    int? transparentIndex,
  }) {
    final int destStartX = (startX * _uiScaleX).floor();
    final int destStartY = (startY * _uiScaleY).floor();
    final int destWidth = math.max(1, (image.width * _uiScaleX).ceil());
    final int destHeight = math.max(1, (image.height * _uiScaleY).ceil());

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

        if (drawX >= 0 && drawX < width && drawY >= 0 && drawY < height) {
          final int srcX = (dx / _uiScaleX).toInt().clamp(0, image.width - 1);
          final int srcY = (dy / _uiScaleY).toInt().clamp(0, image.height - 1);
          final int colorByte = image.decodePixel(srcX, srcY);
          if (colorByte != 255 && colorByte != transparentIndex) {
            _buffer.pixels[drawY * width + drawX] =
                ColorPalette.vga32Bit[colorByte];
          }
        }
      }
    }
  }

  /// Tints the 3D view red based on `player.damageFlash` intensity.
  void _applyDamageFlash() {
    // Grab the intensity (0.0 to 1.0)
    double intensity = engine.player.damageFlash;

    // Calculate how much to boost red and drop green/blue
    int redBoost = (150 * intensity).toInt();
    double colorDrop = 1.0 - (0.5 * intensity);

    for (int y = 0; y < viewHeight; y++) {
      for (int x = 0; x < width; x++) {
        int index = y * width + x;
        int color = _buffer.pixels[index];

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

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

        _buffer.pixels[index] = (0xFF000000) | (b << 16) | (g << 8) | r;
      }
    }
  }
}
