import 'dart:math' as math;
import 'dart:typed_data';

import 'package:wolf_3d_dart/wolf_3d_data_types.dart';
import 'package:wolf_3d_dart/wolf_3d_engine.dart';

class SixelRasterizer extends Rasterizer {
  late Uint8List _screen;
  late WolfEngine _engine;

  @override
  dynamic render(WolfEngine engine, FrameBuffer buffer) {
    _engine = engine;
    // We only need 8-bit indices for the 256 VGA colors
    _screen = Uint8List(buffer.width * buffer.height);
    return super.render(engine, buffer);
  }

  @override
  void prepareFrame(WolfEngine engine) {
    // Top half is ceiling color index (25), bottom half is floor color index (29)
    for (int y = 0; y < viewHeight; y++) {
      int colorIndex = (y < viewHeight / 2) ? 25 : 29;
      for (int x = 0; x < width; x++) {
        _screen[y * width + x] = colorIndex;
      }
    }
  }

  @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++) {
      double relativeY =
          (y - (-columnHeight ~/ 2 + viewHeight ~/ 2)) / columnHeight;
      int texY = (relativeY * 64).toInt().clamp(0, 63);

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

      // Note: Directional shading is omitted here to preserve strict VGA palette indices.
      // Sixel uses a fixed 256-color palette, so real-time shading requires a lookup table.
      _screen[y * width + x] = colorByte;
    }
  }

  @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++
    ) {
      double relativeY = (y - drawStartY) / spriteHeight;
      int texY = (relativeY * 64).toInt().clamp(0, 63);

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

      // 255 is the "transparent" color index
      if (colorByte != 255) {
        _screen[y * width + stripeX] = colorByte;
      }
    }
  }

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

    int weaponWidth = (width * 0.5).toInt();
    int weaponHeight = (viewHeight * 0.8).toInt();

    int startX = (width ~/ 2) - (weaponWidth ~/ 2);
    int startY =
        viewHeight - weaponHeight + (engine.player.weaponAnimOffset ~/ 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 drawX = startX + dx;
          int drawY = startY + dy;
          if (drawX >= 0 && drawX < width && drawY >= 0 && drawY < viewHeight) {
            _screen[drawY * width + drawX] = colorByte;
          }
        }
      }
    }
  }

  @override
  void drawHud(WolfEngine engine) {
    int statusBarIndex = engine.data.vgaImages.indexWhere(
      (img) => img.width == 320 && img.height == 40,
    );
    if (statusBarIndex == -1) return;

    _blitVgaImage(engine.data.vgaImages[statusBarIndex], 0, 160);

    _drawNumber(1, 32, 176, engine.data.vgaImages);
    _drawNumber(engine.player.score, 96, 176, engine.data.vgaImages);
    _drawNumber(3, 120, 176, engine.data.vgaImages);
    _drawNumber(engine.player.health, 192, 176, engine.data.vgaImages);
    _drawNumber(engine.player.ammo, 232, 176, engine.data.vgaImages);

    _drawFace(engine);
    _drawWeaponIcon(engine);
  }

  @override
  dynamic finalizeFrame() {
    return toSixelString();
  }

  // ===========================================================================
  // SIXEL ENCODER
  // ===========================================================================

  /// Converts the 8-bit index buffer into a standard Sixel sequence
  String toSixelString() {
    StringBuffer sb = StringBuffer();

    // Start Sixel sequence (q = Sixel format)
    sb.write('\x1bPq');

    // 1. Define the Palette (and apply damage flash directly to the palette!)
    double damageIntensity = _engine.player.damageFlash;
    int redBoost = (150 * damageIntensity).toInt();
    double colorDrop = 1.0 - (0.5 * damageIntensity);

    for (int i = 0; i < 256; i++) {
      int color = ColorPalette.vga32Bit[i];
      int r = color & 0xFF;
      int g = (color >> 8) & 0xFF;
      int b = (color >> 16) & 0xFF;

      if (damageIntensity > 0) {
        r = (r + redBoost).clamp(0, 255);
        g = (g * colorDrop).toInt().clamp(0, 255);
        b = (b * colorDrop).toInt().clamp(0, 255);
      }

      // Sixel RGB ranges from 0 to 100
      int sixelR = (r * 100) ~/ 255;
      int sixelG = (g * 100) ~/ 255;
      int sixelB = (b * 100) ~/ 255;

      sb.write('#$i;2;$sixelR;$sixelG;$sixelB');
    }

    // 2. Encode Image in 6-pixel vertical bands
    for (int band = 0; band < height; band += 6) {
      Map<int, Uint8List> colorMap = {};

      // Map out which pixels use which color in this 6px high band
      for (int x = 0; x < width; x++) {
        for (int yOffset = 0; yOffset < 6; yOffset++) {
          int y = band + yOffset;
          if (y >= height) break;

          int colorIdx = _screen[y * width + x];
          if (!colorMap.containsKey(colorIdx)) {
            colorMap[colorIdx] = Uint8List(width);
          }
          // Set the bit corresponding to the vertical position (0-5)
          colorMap[colorIdx]![x] |= (1 << yOffset);
        }
      }

      // Write the encoded Sixel characters for each color present in the band
      bool firstColor = true;
      for (var entry in colorMap.entries) {
        if (!firstColor) {
          // Carriage return to overlay colors on the same band
          sb.write('\$');
        }
        firstColor = false;

        // Select color index
        sb.write('#${entry.key}');

        Uint8List cols = entry.value;
        int currentVal = -1;
        int runLength = 0;

        // Run-Length Encoding (RLE) loop
        for (int x = 0; x < width; x++) {
          int val = cols[x];
          if (val == currentVal) {
            runLength++;
          } else {
            if (runLength > 0) _writeSixelRle(sb, currentVal, runLength);
            currentVal = val;
            runLength = 1;
          }
        }
        if (runLength > 0) _writeSixelRle(sb, currentVal, runLength);
      }

      sb.write('-'); // Move down to the next 6-pixel band
    }

    // End Sixel sequence
    sb.write('\x1b\\');
    return sb.toString();
  }

  void _writeSixelRle(StringBuffer sb, int value, int runLength) {
    String char = String.fromCharCode(value + 63);
    // Sixel RLE format: !<count><char> (only worth it if count > 3)
    if (runLength > 3) {
      sb.write('!$runLength$char');
    } else {
      sb.write(char * runLength);
    }
  }

  // ===========================================================================
  // PRIVATE HUD HELPERS (Adapted for 8-bit index buffer)
  // ===========================================================================

  void _blitVgaImage(VgaImage image, int startX, int startY) {
    int planeWidth = image.width ~/ 4;
    int planeSize = planeWidth * image.height;

    for (int dy = 0; dy < image.height; dy++) {
      for (int dx = 0; dx < image.width; dx++) {
        int drawX = startX + dx;
        int drawY = startY + dy;

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

          int plane = srcX % 4;
          int sx = srcX ~/ 4;
          int index = (plane * planeSize) + (srcY * planeWidth) + sx;

          int colorByte = image.pixels[index];
          if (colorByte != 255) {
            _screen[drawY * width + drawX] = colorByte;
          }
        }
      }
    }
  }

  void _drawNumber(
    int value,
    int rightAlignX,
    int startY,
    List<VgaImage> vgaImages,
  ) {
    const int zeroIndex = 96;
    String numStr = value.toString();
    int currentX = rightAlignX - (numStr.length * 8);

    for (int i = 0; i < numStr.length; i++) {
      int digit = int.parse(numStr[i]);
      if (zeroIndex + digit < vgaImages.length) {
        _blitVgaImage(vgaImages[zeroIndex + digit], currentX, startY);
      }
      currentX += 8;
    }
  }

  void _drawFace(WolfEngine engine) {
    int health = engine.player.health;
    int faceIndex = (health <= 0)
        ? 127
        : 106 + (((100 - health) ~/ 16).clamp(0, 6) * 3);
    if (faceIndex < engine.data.vgaImages.length) {
      _blitVgaImage(engine.data.vgaImages[faceIndex], 136, 164);
    }
  }

  void _drawWeaponIcon(WolfEngine engine) {
    int weaponIndex = 89;
    if (engine.player.hasChainGun) {
      weaponIndex = 91;
    } else if (engine.player.hasMachineGun) {
      weaponIndex = 90;
    }

    if (weaponIndex < engine.data.vgaImages.length) {
      _blitVgaImage(engine.data.vgaImages[weaponIndex], 256, 164);
    }
  }
}
