import 'dart:math' as math;

import 'package:flutter/material.dart';
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';
import 'package:wolf_dart/features/renderer/color_palette.dart';

class RaycasterPainter extends CustomPainter {
  final Level map;
  final List<Sprite> textures;
  final Player player;
  final double fov;
  final Map<String, double> doorOffsets;
  final Pushwall? activePushwall;
  final List<Sprite> sprites;
  final List<Entity> entities;

  RaycasterPainter({
    required this.map,
    required this.textures,
    required this.player,
    required this.fov,
    required this.doorOffsets,
    this.activePushwall,
    required this.sprites,
    required this.entities,
  });

  @override
  void paint(Canvas canvas, Size size) {
    final Paint bgPaint = Paint()..isAntiAlias = false;

    // 1. Draw Ceiling & Floor
    canvas.drawRect(
      Rect.fromLTWH(0, 0, size.width, size.height / 2),
      bgPaint..color = Colors.blueGrey[900]!,
    );
    canvas.drawRect(
      Rect.fromLTWH(0, size.height / 2, size.width, size.height / 2),
      bgPaint..color = Colors.brown[900]!,
    );

    const int renderWidth = 320;
    double columnWidth = size.width / renderWidth;

    final Paint columnPaint = Paint()
      ..isAntiAlias = false
      ..strokeWidth = columnWidth + 0.5;

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

    Coordinate2D dir = Coordinate2D(
      math.cos(player.angle),
      math.sin(player.angle),
    );

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

    // --- 1. CAST WALLS ---
    for (int x = 0; x < renderWidth; x++) {
      double cameraX = 2 * x / renderWidth - 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; // Replaces doorOffset to handle both
      bool customDistCalculated = false; // Flag to skip standard distance
      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 passed through the open door gap
              }
            }
            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);

            // Did we hit the face that is being pushed deeper?
            if (side == 0 && pDirX != 0) {
              if (pDirX == stepX) {
                double intersect = perpWallDist + pOffset * deltaDistX;
                if (intersect < sideDistY) {
                  perpWallDist = intersect; // Hit the recessed front face
                } else {
                  side =
                      1; // Missed the front face, hit the newly exposed side!
                  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 {
              // We hit the side of the sliding block. Did the ray slip behind it?
              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; // Stick the texture to the block
                }
              } else {
                if (pDirX == 1 && wallFraction < pOffset) hit = false;
                if (pDirX == -1 && wallFraction > (1.0 - pOffset)) hit = false;
                if (hit) {
                  textureOffset =
                      pOffset * pDirX; // Stick the texture to the block
                }
              }
            }
            if (!hit) continue; // The ray slipped past! Keep looping.
            customDistCalculated = true; // Lock in our custom distance math
          }
          // --- STANDARD WALL ---
          else {
            hit = true;
            hitWallId = map[mapY][mapX];
          }
        }
      }

      if (hitOutOfBounds) continue;

      // Apply standard math ONLY if we didn't calculate a sub-tile pushwall distance
      if (!customDistCalculated) {
        if (side == 0) {
          perpWallDist = (sideDistX - deltaDistX);
        } else {
          perpWallDist = (sideDistY - deltaDistY);
        }
      }

      zBuffer[x] = perpWallDist;

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

      double drawX = x * columnWidth;

      _drawTexturedColumn(
        canvas,
        drawX,
        perpWallDist,
        wallX,
        side,
        size,
        hitWallId,
        textures,
        textureOffset,
        columnPaint,
      );
    }

    // --- 2. DRAW SPRITES ---
    // (Keep your existing sprite rendering logic exactly the same)
    List<Entity> activeSprites = List.from(entities);

    activeSprites.sort((a, b) {
      double distA = player.position.distanceTo(a.position);
      double distB = player.position.distanceTo(b.position);
      return distB.compareTo(distA);
    });

    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 = ((renderWidth / 2) * (1 + transformX / transformY))
            .toInt();
        int spriteHeight = (size.height / transformY).abs().toInt();
        int spriteColumnWidth = (spriteHeight / columnWidth).toInt();

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

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

        for (int stripe = clipStartX; stripe < clipEndX; stripe++) {
          if (transformY < zBuffer[stripe]) {
            double texXDouble = (stripe - drawStartX) * 64 / spriteColumnWidth;
            int texX = texXDouble.toInt().clamp(0, 63);

            double startY = (size.height / 2) - (spriteHeight / 2);
            double stepY = spriteHeight / 64.0;
            double drawX = stripe * columnWidth;

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

            for (int ty = 0; ty < 64; ty++) {
              int colorByte = spritePixels[texX][ty];

              if (colorByte != 255) {
                double endY = startY + stepY + 0.5;

                if (endY > 0 && startY < size.height) {
                  columnPaint.color = ColorPalette.vga[colorByte];
                  canvas.drawLine(
                    Offset(drawX, startY),
                    Offset(drawX, endY),
                    columnPaint,
                  );
                }
              }
              startY += stepY;
            }
          }
        }
      }
    }
  }

  void _drawTexturedColumn(
    Canvas canvas,
    double drawX,
    double distance,
    double wallX,
    int side,
    Size size,
    int hitWallId,
    List<Sprite> textures,
    double textureOffset,
    Paint paint,
  ) {
    if (distance <= 0.01) distance = 0.01;

    double wallHeight = size.height / distance;
    int drawStart = ((size.height / 2) - (wallHeight / 2)).toInt();

    int texNum;
    int texX;

    if (hitWallId >= 90) {
      // DOORS
      texNum = 98.clamp(0, textures.length - 1);
      texX = ((wallX - textureOffset) * 64).toInt().clamp(0, 63);
    } else {
      // WALLS & PUSHWALLS
      texNum = ((hitWallId - 1) * 2).clamp(0, textures.length - 2);
      if (side == 1) texNum += 1;
      // We apply the modulo % 1.0 to handle negative texture offsets smoothly!
      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;

    double startY = drawStart.toDouble();
    double stepY = wallHeight / 64.0;

    for (int ty = 0; ty < 64; ty++) {
      int colorByte = textures[texNum][texX][ty];

      paint.color = ColorPalette.vga[colorByte];

      double endY = startY + stepY + 0.5;

      if (endY > 0 && startY < size.height) {
        canvas.drawLine(
          Offset(drawX, startY),
          Offset(drawX, endY),
          paint,
        );
      }
      startY += stepY;
    }
  }

  @override
  bool shouldRepaint(RaycasterPainter oldDelegate) => true;
}
