import 'dart:math' as math;

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

class SoftwareRasterizer implements Rasterizer {
  // Pre-calculated VGA colors for ceiling and floor
  final int ceilingColor = ColorPalette.vga32Bit[25];
  final int floorColor = ColorPalette.vga32Bit[29];

  @override
  void render(WolfEngine engine, FrameBuffer buffer) {
    _clearScreen(buffer);

    List<double> zBuffer = List.filled(buffer.width, 0.0);

    _castWalls(engine, buffer, zBuffer);
    _castSprites(engine, buffer, zBuffer);

    // NEW: Draw the weapon on top of the 3D world
    _drawWeapon(engine, buffer);

    // NEW: Apply the full-screen damage tint last
    _drawDamageFlash(engine, buffer);

    _drawHud(engine, buffer);
  }

  void _clearScreen(FrameBuffer buffer) {
    const int viewHeight = 160;
    int halfScreen = (buffer.width * viewHeight) ~/ 2;
    // Only clear the top 160 rows!
    buffer.pixels.fillRange(0, halfScreen, ceilingColor);
    buffer.pixels.fillRange(halfScreen, buffer.width * viewHeight, floorColor);
  }

  void _castWalls(WolfEngine engine, FrameBuffer buffer, List<double> zBuffer) {
    final double fov = math.pi / 3;
    final Player player = engine.player;
    final SpriteMap map = engine.currentLevel;

    // Fetch dynamic states from the managers
    final Map<String, double> doorOffsets = engine.doorManager
        .getOffsetsForRenderer();
    final Pushwall? activePushwall = engine.pushwallManager.activePushwall;

    Coordinate2D dir = Coordinate2D(
      math.cos(player.angle),
      math.sin(player.angle),
    );
    Coordinate2D plane = Coordinate2D(-dir.y, dir.x) * math.tan(fov / 2);

    for (int x = 0; x < buffer.width; x++) {
      double cameraX = 2 * x / buffer.width - 1.0;
      Coordinate2D rayDir = dir + (plane * cameraX);

      int mapX = player.x.toInt();
      int mapY = player.y.toInt();

      double sideDistX;
      double sideDistY;
      double deltaDistX = (rayDir.x == 0) ? 1e30 : (1.0 / rayDir.x).abs();
      double deltaDistY = (rayDir.y == 0) ? 1e30 : (1.0 / rayDir.y).abs();
      double perpWallDist = 0.0;

      int stepX;
      int stepY;
      bool hit = false;
      bool hitOutOfBounds = false;
      int side = 0;
      int hitWallId = 0;

      double textureOffset = 0.0;
      bool customDistCalculated = false;
      Set<String> ignoredDoors = {};

      if (rayDir.x < 0) {
        stepX = -1;
        sideDistX = (player.x - mapX) * deltaDistX;
      } else {
        stepX = 1;
        sideDistX = (mapX + 1.0 - player.x) * deltaDistX;
      }
      if (rayDir.y < 0) {
        stepY = -1;
        sideDistY = (player.y - mapY) * deltaDistY;
      } else {
        stepY = 1;
        sideDistY = (mapY + 1.0 - player.y) * deltaDistY;
      }

      // --- DDA LOOP ---
      while (!hit) {
        if (sideDistX < sideDistY) {
          sideDistX += deltaDistX;
          mapX += stepX;
          side = 0;
        } else {
          sideDistY += deltaDistY;
          mapY += stepY;
          side = 1;
        }

        if (mapY < 0 ||
            mapY >= map.length ||
            mapX < 0 ||
            mapX >= map[0].length) {
          hit = true;
          hitOutOfBounds = true;
        } else if (map[mapY][mapX] > 0) {
          String mapKey = '$mapX,$mapY';

          // DOOR LOGIC
          if (map[mapY][mapX] >= 90 && !ignoredDoors.contains(mapKey)) {
            double currentOffset = doorOffsets[mapKey] ?? 0.0;
            if (currentOffset > 0.0) {
              double perpWallDistTemp = (side == 0)
                  ? (sideDistX - deltaDistX)
                  : (sideDistY - deltaDistY);
              double wallXTemp = (side == 0)
                  ? player.y + perpWallDistTemp * rayDir.y
                  : player.x + perpWallDistTemp * rayDir.x;
              wallXTemp -= wallXTemp.floor();
              if (wallXTemp < currentOffset) {
                ignoredDoors.add(mapKey);
                continue;
              }
            }
            hit = true;
            hitWallId = map[mapY][mapX];
            textureOffset = currentOffset;
          }
          // PUSHWALL LOGIC
          else if (activePushwall != null &&
              mapX == activePushwall.x &&
              mapY == activePushwall.y) {
            hit = true;
            hitWallId = map[mapY][mapX];

            double pOffset = activePushwall.offset;
            int pDirX = activePushwall.dirX;
            int pDirY = activePushwall.dirY;

            perpWallDist = (side == 0)
                ? (sideDistX - deltaDistX)
                : (sideDistY - deltaDistY);

            if (side == 0 && pDirX != 0) {
              if (pDirX == stepX) {
                double intersect = perpWallDist + pOffset * deltaDistX;
                if (intersect < sideDistY) {
                  perpWallDist = intersect;
                } else {
                  side = 1;
                  perpWallDist = sideDistY - deltaDistY;
                }
              } else {
                perpWallDist -= (1.0 - pOffset) * deltaDistX;
              }
            } else if (side == 1 && pDirY != 0) {
              if (pDirY == stepY) {
                double intersect = perpWallDist + pOffset * deltaDistY;
                if (intersect < sideDistX) {
                  perpWallDist = intersect;
                } else {
                  side = 0;
                  perpWallDist = sideDistX - deltaDistX;
                }
              } else {
                perpWallDist -= (1.0 - pOffset) * deltaDistY;
              }
            } else {
              double wallFraction = (side == 0)
                  ? player.y + perpWallDist * rayDir.y
                  : player.x + perpWallDist * rayDir.x;
              wallFraction -= wallFraction.floor();

              if (side == 0) {
                if (pDirY == 1 && wallFraction < pOffset) hit = false;
                if (pDirY == -1 && wallFraction > (1.0 - pOffset)) hit = false;
                if (hit) textureOffset = pOffset * pDirY;
              } else {
                if (pDirX == 1 && wallFraction < pOffset) hit = false;
                if (pDirX == -1 && wallFraction > (1.0 - pOffset)) hit = false;
                if (hit) textureOffset = pOffset * pDirX;
              }
            }
            if (!hit) continue;
            customDistCalculated = true;
          }
          // STANDARD WALL
          else {
            hit = true;
            hitWallId = map[mapY][mapX];
          }
        }
      }

      if (hitOutOfBounds) continue;

      if (!customDistCalculated) {
        if (side == 0) {
          perpWallDist = (sideDistX - deltaDistX);
        } else {
          perpWallDist = (sideDistY - deltaDistY);
        }
      }

      // Log the distance so sprites know if they are occluded
      zBuffer[x] = perpWallDist;

      double wallX = (side == 0)
          ? player.y + perpWallDist * rayDir.y
          : player.x + perpWallDist * rayDir.x;
      wallX -= wallX.floor();

      _drawTexturedColumn(
        x,
        perpWallDist,
        wallX,
        side,
        hitWallId,
        engine.data.walls, // Wall textures
        textureOffset,
        buffer,
        player.angle,
      );
    }
  }

  void _drawTexturedColumn(
    int x,
    double distance,
    double wallX,
    int side,
    int hitWallId,
    List<Sprite> textures,
    double textureOffset,
    FrameBuffer buffer,
    double playerAngle,
  ) {
    const int viewHeight = 160;
    if (distance <= 0.01) distance = 0.01;

    int lineHeight = (viewHeight / distance).toInt();

    int drawStart = -lineHeight ~/ 2 + viewHeight ~/ 2;
    if (drawStart < 0) drawStart = 0;

    int drawEnd = lineHeight ~/ 2 + viewHeight ~/ 2;
    if (drawEnd >= viewHeight) drawEnd = viewHeight - 1;

    int texNum;
    if (hitWallId >= 90) {
      texNum = 98.clamp(0, textures.length - 1);
    } else {
      texNum = ((hitWallId - 1) * 2).clamp(0, textures.length - 2);
      if (side == 1) texNum += 1;
    }

    int texX = (((wallX - textureOffset) % 1.0) * 64).toInt().clamp(0, 63);
    if (side == 0 && math.cos(playerAngle) > 0) texX = 63 - texX;
    if (side == 1 && math.sin(playerAngle) < 0) texX = 63 - texX;

    double step = 64.0 / lineHeight;
    double texPos = (drawStart - viewHeight / 2 + lineHeight / 2) * step;

    Sprite texture = textures[texNum];

    for (int y = drawStart; y < drawEnd; y++) {
      int texY = texPos.toInt() & 63;
      texPos += step;

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

      buffer.pixels[y * buffer.width + x] = color32;
    }
  }

  void _castSprites(
    WolfEngine engine,
    FrameBuffer buffer,
    List<double> zBuffer,
  ) {
    const int viewHeight = 160;
    final Player player = engine.player;
    final List<Entity> activeSprites = List.from(engine.entities);

    // Sort entities from furthest to closest (Painter's Algorithm)
    activeSprites.sort((a, b) {
      double distA = player.position.distanceTo(a.position);
      double distB = player.position.distanceTo(b.position);
      return distB.compareTo(distA);
    });

    Coordinate2D dir = Coordinate2D(
      math.cos(player.angle),
      math.sin(player.angle),
    );
    Coordinate2D plane =
        Coordinate2D(-dir.y, dir.x) * math.tan((math.pi / 3) / 2);

    for (Entity entity in activeSprites) {
      Coordinate2D spritePos = entity.position - player.position;

      double invDet = 1.0 / (plane.x * dir.y - dir.x * plane.y);
      double transformX = invDet * (dir.y * spritePos.x - dir.x * spritePos.y);
      double transformY =
          invDet * (-plane.y * spritePos.x + plane.x * spritePos.y);

      if (transformY > 0) {
        int spriteScreenX = ((buffer.width / 2) * (1 + transformX / transformY))
            .toInt();
        int spriteHeight = (viewHeight / transformY).abs().toInt();

        // In 1x1 buffer pixels, the width of the sprite is equal to its height
        int spriteWidth = spriteHeight;

        int drawStartY = -spriteHeight ~/ 2 + viewHeight ~/ 2;
        if (drawStartY < 0) drawStartY = 0;

        int drawEndY = spriteHeight ~/ 2 + viewHeight ~/ 2;
        if (drawEndY >= buffer.height) drawEndY = viewHeight - 1;

        int drawStartX = -spriteWidth ~/ 2 + spriteScreenX;
        int drawEndX = spriteWidth ~/ 2 + spriteScreenX;

        int clipStartX = math.max(0, drawStartX);
        int clipEndX = math.min(buffer.width - 1, drawEndX);

        int safeIndex = entity.spriteIndex.clamp(
          0,
          engine.data.sprites.length - 1,
        );
        Sprite spritePixels = engine.data.sprites[safeIndex];

        for (int stripe = clipStartX; stripe < clipEndX; stripe++) {
          // Z-Buffer Check! Only draw the vertical stripe if it's in front of the wall
          if (transformY < zBuffer[stripe]) {
            int texX = ((stripe - drawStartX) * 64 ~/ spriteWidth).clamp(0, 63);

            double step = 64.0 / spriteHeight;
            double texPos =
                (drawStartY - viewHeight / 2 + spriteHeight / 2) * step;

            for (int y = drawStartY; y < drawEndY; y++) {
              int texY = texPos.toInt() & 63;
              texPos += step;

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

              if (colorByte != 255) {
                // 255 is transparent
                buffer.pixels[y * buffer.width + stripe] =
                    ColorPalette.vga32Bit[colorByte];
              }
            }
          }
        }
      }
    }
  }

  void _drawWeapon(WolfEngine engine, FrameBuffer buffer) {
    const int viewHeight = 160;
    int spriteIndex = engine.player.currentWeapon.getCurrentSpriteIndex(
      engine.data.sprites.length,
    );
    Sprite weaponSprite = engine.data.sprites[spriteIndex];

    // Dropped the scale from 4 to 2 (a 50% reduction in size)
    const int scale = 2;
    const int weaponWidth = 64 * scale;
    const int weaponHeight = 64 * scale;

    int startX = (buffer.width ~/ 2) - (weaponWidth ~/ 2);

    // Kept the grounding to the bottom of the screen
    int startY =
        viewHeight - weaponHeight + (engine.player.weaponAnimOffset ~/ 2);

    for (int x = 0; x < 64; x++) {
      for (int y = 0; y < 64; y++) {
        int colorByte = weaponSprite.pixels[x * 64 + y];

        if (colorByte != 255) {
          int color32 = ColorPalette.vga32Bit[colorByte];

          for (int sx = 0; sx < scale; sx++) {
            for (int sy = 0; sy < scale; sy++) {
              int drawX = startX + (x * scale) + sx;
              int drawY = startY + (y * scale) + sy;

              if (drawX >= 0 &&
                  drawX < buffer.width &&
                  drawY >= 0 &&
                  drawY < buffer.height) {
                buffer.pixels[drawY * buffer.width + drawX] = color32;
              }
            }
          }
        }
      }
    }
  }

  void _drawDamageFlash(WolfEngine engine, FrameBuffer buffer) {
    if (engine.damageFlashOpacity <= 0) return;

    int alpha = (engine.damageFlashOpacity * 256).toInt().clamp(0, 256);
    int invAlpha = 256 - alpha;

    for (int i = 0; i < buffer.pixels.length; i++) {
      int color = buffer.pixels[i];

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

      // Blend with Red
      r = ((r * invAlpha) + (255 * alpha)) >> 8;
      g = (g * invAlpha) >> 8;
      b = (b * invAlpha) >> 8;

      buffer.pixels[i] = (a << 24) | (b << 16) | (g << 8) | r;
    }
  }

  void _drawNumber(
    int value,
    int rightAlignX,
    int startY,
    FrameBuffer buffer,
    List<VgaImage> vgaImages,
  ) {
    // The yellow 8x16 HUD digits start exactly at 96
    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, buffer);
      }
      currentX += 8;
    }
  }

  void _drawFace(WolfEngine engine, FrameBuffer buffer) {
    int health = engine.player.health;
    int faceIndex;

    if (health <= 0) {
      faceIndex = 127; // Dead face
    } else {
      int healthTier = ((100 - health) ~/ 16).clamp(0, 6);

      // Base face is 106 (100% health, looking forward).
      faceIndex = 106 + (healthTier * 3);
    }

    if (faceIndex < engine.data.vgaImages.length) {
      _blitVgaImage(engine.data.vgaImages[faceIndex], 142, 164, buffer);
    }
  }

  void _drawWeaponIcon(WolfEngine engine, FrameBuffer buffer) {
    int weaponIndex = 89; // Default to Pistol (Index 89)

    if (engine.player.hasChainGun) {
      weaponIndex = 91;
    } else if (engine.player.hasMachineGun) {
      weaponIndex = 90;
    }

    if (weaponIndex < engine.data.vgaImages.length) {
      // The weapon box starts at X=272. Let's center it vertically at 164.
      _blitVgaImage(engine.data.vgaImages[weaponIndex], 272, 164, buffer);
    }
  }

  void _drawHud(WolfEngine engine, FrameBuffer buffer) {
    int statusBarIndex = engine.data.vgaImages.indexWhere(
      (img) => img.width == 320 && img.height == 40,
    );

    if (statusBarIndex == -1) return;

    // 1. Draw Background
    _blitVgaImage(engine.data.vgaImages[statusBarIndex], 0, 160, buffer);

    // 2. Draw Stats (Right-aligned to fit inside the dark blue boxes)
    // (Using a hardcoded '1' for the floor since your engine hides currentLevelIndex)
    _drawNumber(1, 40, 176, buffer, engine.data.vgaImages); // Floor
    _drawNumber(
      engine.player.score,
      104,
      176,
      buffer,
      engine.data.vgaImages,
    ); // Score
    _drawNumber(3, 136, 176, buffer, engine.data.vgaImages); // Lives
    _drawNumber(
      engine.player.health,
      216,
      176,
      buffer,
      engine.data.vgaImages,
    ); // Health

    // Pulled the Ammo X coordinate back to 264 so it doesn't bleed into the weapon!
    _drawNumber(engine.player.ammo, 264, 176, buffer, engine.data.vgaImages);

    // 3. Draw BJ's Face & Current Weapon
    _drawFace(engine, buffer);
    _drawWeaponIcon(engine, buffer);
  }

  void _blitVgaImage(
    VgaImage image,
    int startX,
    int startY,
    FrameBuffer buffer,
  ) {
    // Wolfenstein 3D VGA images are stored in "Mode Y" Planar format.
    // We must de-interleave the 4 planes to draw them correctly!
    int planeWidth = image.width ~/ 4;
    int planeSize = planeWidth * image.height;

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

        if (drawX >= 0 &&
            drawX < buffer.width &&
            drawY >= 0 &&
            drawY < buffer.height) {
          // Planar to Linear coordinate conversion
          int plane = x % 4;
          int sx = x ~/ 4;
          int index = (plane * planeSize) + (y * planeWidth) + sx;

          int colorByte = image.pixels[index];

          if (colorByte != 255) {
            // 255 is transparent
            buffer.pixels[drawY * buffer.width + drawX] =
                ColorPalette.vga32Bit[colorByte];
          }
        }
      }
    }
  }
}
