import 'dart:math' as math;

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_entities.dart';

abstract class Rasterizer {
  late List<double> zBuffer;
  late int width;
  late int height;
  late int viewHeight;

  /// A multiplier to adjust the width of sprites.
  /// Pixel renderers usually keep this at 1.0.
  /// ASCII renderers can override this (e.g., 0.6) to account for tall characters.
  double get aspectMultiplier => 1.0;

  /// A multiplier to counteract tall pixel formats (like 1:2 terminal fonts).
  /// Defaults to 1.0 (no squish) for standard pixel rendering.
  double get verticalStretch => 1.0;

  /// The main entry point called by the game loop.
  /// Orchestrates the mathematical rendering pipeline.
  dynamic render(WolfEngine engine, FrameBuffer buffer) {
    width = buffer.width;
    height = buffer.height;
    // The 3D view typically takes up the top 80% of the screen
    viewHeight = (height * 0.8).toInt();
    zBuffer = List.filled(width, 0.0);

    // 1. Setup the frame (clear screen, draw floor/ceiling)
    prepareFrame(engine);

    // 2. Do the heavy math for Raycasting Walls
    _castWalls(engine);

    // 3. Do the heavy math for Projecting Sprites
    _castSprites(engine);

    // 4. Draw 2D Overlays
    drawWeapon(engine);
    drawHud(engine);

    // 5. Finalize and return the frame data (Buffer or String/List)
    return finalizeFrame();
  }

  // ===========================================================================
  // ABSTRACT METHODS (Implemented by the child renderers)
  // ===========================================================================

  /// Initialize buffers, clear the screen, and draw the floor/ceiling.
  void prepareFrame(WolfEngine engine);

  /// Draw a single vertical column of a wall.
  void drawWallColumn(
    int x,
    int drawStart,
    int drawEnd,
    int columnHeight,
    Sprite texture,
    int texX,
    double perpWallDist,
    int side,
  );

  /// Draw a single vertical stripe of a sprite (enemy/item).
  void drawSpriteStripe(
    int stripeX,
    int drawStartY,
    int drawEndY,
    int spriteHeight,
    Sprite texture,
    int texX,
    double transformY,
  );

  /// Draw the player's weapon overlay at the bottom of the 3D view.
  void drawWeapon(WolfEngine engine);

  /// Draw the 2D status bar at the bottom 20% of the screen.
  void drawHud(WolfEngine engine);

  /// Return the finished frame (e.g., the FrameBuffer itself, or an ASCII list).
  dynamic finalizeFrame();

  // ===========================================================================
  // SHARED LIGHTING MATH
  // ===========================================================================

  /// Calculates depth-based lighting falloff (0.0 to 1.0).
  /// While the original Wolf3D didn't use depth fog, this provides a great
  /// atmospheric effect for custom renderers (like ASCII dithering).
  double calculateDepthBrightness(double distance) {
    return (10.0 / (distance + 2.0)).clamp(0.0, 1.0);
  }

  // ===========================================================================
  // CORE ENGINE MATH (Shared across all renderers)
  // ===========================================================================

  void _castWalls(WolfEngine engine) {
    final Player player = engine.player;
    final SpriteMap map = engine.currentLevel;
    final List<Sprite> wallTextures = engine.data.walls;

    final Map<String, double> doorOffsets = engine.doorManager
        .getOffsetsForRenderer();
    final Pushwall? activePushwall = engine.pushwallManager.activePushwall;

    final double fov = math.pi / 3;
    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 < width; x++) {
      double cameraX = 2 * x / width - 1.0;
      Coordinate2D rayDir = dir + (plane * cameraX);

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

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

      double sideDistX, sideDistY, perpWallDist = 0.0;
      int stepX, stepY, side = 0, hitWallId = 0;
      bool hit = false, hitOutOfBounds = false, customDistCalculated = false;
      double textureOffset = 0.0;
      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; // Ray passes through the open part of the door
              }
            }
            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;
          } else {
            hit = true;
            hitWallId = map[mapY][mapX];
          }
        }
      }

      if (hitOutOfBounds) continue;

      if (!customDistCalculated) {
        perpWallDist = (side == 0)
            ? (sideDistX - deltaDistX)
            : (sideDistY - deltaDistY);
      }
      if (perpWallDist < 0.1) perpWallDist = 0.1;

      // Save for sprite depth checks
      zBuffer[x] = perpWallDist;

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

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

      // Texture flipping for specific orientations
      int texX = (((wallX - textureOffset) % 1.0) * 64).toInt().clamp(0, 63);
      if (side == 0 && math.cos(player.angle) > 0) texX = 63 - texX;
      if (side == 1 && math.sin(player.angle) < 0) texX = 63 - texX;

      // Calculate drawing dimensions
      int columnHeight = ((viewHeight / perpWallDist) * verticalStretch)
          .toInt();
      int drawStart = (-columnHeight ~/ 2 + viewHeight ~/ 2).clamp(
        0,
        viewHeight,
      );
      int drawEnd = (columnHeight ~/ 2 + viewHeight ~/ 2).clamp(0, viewHeight);

      // Tell the implementation to draw this column
      drawWallColumn(
        x,
        drawStart,
        drawEnd,
        columnHeight,
        texture,
        texX,
        perpWallDist,
        side,
      );
    }
  }

  void _castSprites(WolfEngine engine) {
    final Player player = engine.player;
    final List<Entity> activeSprites = List.from(engine.entities);

    // Sort 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);

      // Only process if the sprite is in front of the camera
      if (transformY > 0) {
        int spriteScreenX = ((width / 2) * (1 + transformX / transformY))
            .toInt();
        int spriteHeight = ((viewHeight / transformY).abs() * verticalStretch)
            .toInt();

        // Scale width based on the aspectMultiplier (useful for ASCII)
        int spriteWidth = (spriteHeight * aspectMultiplier / verticalStretch)
            .toInt();

        int drawStartY = -spriteHeight ~/ 2 + viewHeight ~/ 2;
        int drawEndY = spriteHeight ~/ 2 + viewHeight ~/ 2;
        int drawStartX = -spriteWidth ~/ 2 + spriteScreenX;
        int drawEndX = spriteWidth ~/ 2 + spriteScreenX;

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

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

        // Loop through the visible vertical stripes
        for (int stripe = clipStartX; stripe < clipEndX; stripe++) {
          // Check the Z-Buffer to see if a wall is in front of this stripe
          if (transformY < zBuffer[stripe]) {
            int texX = ((stripe - drawStartX) * 64 ~/ spriteWidth).clamp(0, 63);

            // Tell the implementation to draw this stripe
            drawSpriteStripe(
              stripe,
              drawStartY,
              drawEndY,
              spriteHeight,
              texture,
              texX,
              transformY,
            );
          }
        }
      }
    }
  }

  /// Darkens a 32-bit 0xAABBGGRR color by roughly 30% without touching Alpha
  int shadeColor(int color) {
    int r = (color & 0xFF) * 7 ~/ 10;
    int g = ((color >> 8) & 0xFF) * 7 ~/ 10;
    int b = ((color >> 16) & 0xFF) * 7 ~/ 10;
    return (0xFF000000) | (b << 16) | (g << 8) | r;
  }
}
