import 'dart:io';
import 'package:flutter/foundation.dart';
import 'dart:math' as math;
import 'package:image/image.dart' as img;

class DetectedCircle {
  final double centerX;
  final double centerY;
  final double radius;

  DetectedCircle({
    required this.centerX,
    required this.centerY,
    required this.radius,
  });
}

class DetectedImpact {
  final double x;
  final double y;
  final double radius;

  DetectedImpact({
    required this.x,
    required this.y,
    required this.radius,
  });
}

/// Image processing settings for impact detection
class ImpactDetectionSettings {
  /// Threshold for dark spot detection (0-255, lower = darker)
  final int darkThreshold;

  /// Minimum impact size in pixels
  final int minImpactSize;

  /// Maximum impact size in pixels
  final int maxImpactSize;

  /// Blur radius for noise reduction
  final int blurRadius;

  /// Contrast enhancement factor
  final double contrastFactor;

  /// Minimum circularity (0-1, 1 = perfect circle)
  /// Used to filter out non-circular shapes like numbers
  final double minCircularity;

  /// Maximum aspect ratio (width/height or height/width)
  /// Used to filter out elongated shapes like numbers
  final double maxAspectRatio;

  /// Minimum fill ratio (0-1, ~0.7 for filled circle, lower for rings/hollow shapes)
  /// A bullet hole is FILLED, numbers on target are hollow rings
  final double minFillRatio;

  const ImpactDetectionSettings({
    this.darkThreshold = 80,
    this.minImpactSize = 20,
    this.maxImpactSize = 500,
    this.blurRadius = 2,
    this.contrastFactor = 1.2,
    this.minCircularity = 0.6,
    this.maxAspectRatio = 2.0,
    this.minFillRatio = 0.5, // Filled circles should have ratio > 0.5
  });
}

/// Reference impact for calibrated detection
class ReferenceImpact {
  final double x; // Normalized 0-1
  final double y; // Normalized 0-1

  const ReferenceImpact({required this.x, required this.y});
}

/// Characteristics learned from reference impacts
class ImpactCharacteristics {
  final double avgLuminance;
  final double luminanceStdDev;
  final double avgSize;
  final double sizeStdDev;
  final double avgCircularity;
  final double avgFillRatio; // How filled is the blob vs its bounding circle
  final double avgDarkThreshold; // The threshold used to detect the blob

  const ImpactCharacteristics({
    required this.avgLuminance,
    required this.luminanceStdDev,
    required this.avgSize,
    required this.sizeStdDev,
    required this.avgCircularity,
    required this.avgFillRatio,
    required this.avgDarkThreshold,
  });

  @override
  String toString() {
    return 'ImpactCharacteristics(lum: ${avgLuminance.toStringAsFixed(1)} ± ${luminanceStdDev.toStringAsFixed(1)}, '
        'size: ${avgSize.toStringAsFixed(1)} ± ${sizeStdDev.toStringAsFixed(1)}, '
        'circ: ${avgCircularity.toStringAsFixed(2)}, fill: ${avgFillRatio.toStringAsFixed(2)})';
  }
}

/// Service for image processing and impact detection
class ImageProcessingService {
  /// Detect the main target circle from an image
  DetectedCircle? detectMainTarget(String imagePath) {
    // Return center of image as target (basic implementation)
    // Could be enhanced with circle detection algorithm
    return DetectedCircle(
      centerX: 0.5,
      centerY: 0.5,
      radius: 0.4,
    );
  }

  /// Detect impacts (bullet holes) from an image file
  List<DetectedImpact> detectImpacts(String imagePath) {
    return detectImpactsWithSettings(
      imagePath,
      const ImpactDetectionSettings(),
    );
  }

  /// Detect impacts with custom settings
  List<DetectedImpact> detectImpactsWithSettings(
    String imagePath,
    ImpactDetectionSettings settings,
  ) {
    try {
      // Load the image
      final file = File(imagePath);
      final bytes = file.readAsBytesSync();
      final originalImage = img.decodeImage(bytes);

      if (originalImage == null) {
        return [];
      }

      // Resize for faster processing if image is too large
      img.Image image;
      final maxDimension = 1000;
      if (originalImage.width > maxDimension || originalImage.height > maxDimension) {
        final scale = maxDimension / math.max(originalImage.width, originalImage.height);
        image = img.copyResize(
          originalImage,
          width: (originalImage.width * scale).round(),
          height: (originalImage.height * scale).round(),
        );
      } else {
        image = originalImage;
      }

      // Convert to grayscale
      final grayscale = img.grayscale(image);

      // Apply gaussian blur to reduce noise
      final blurred = img.gaussianBlur(grayscale, radius: settings.blurRadius);

      // Enhance contrast
      final enhanced = img.adjustColor(
        blurred,
        contrast: settings.contrastFactor,
      );

      // Detect dark spots (potential impacts)
      // Filter by circularity and fill ratio to avoid detecting numbers (hollow rings)
      final impacts = _detectDarkSpots(
        enhanced,
        settings.darkThreshold,
        settings.minImpactSize,
        settings.maxImpactSize,
        minCircularity: settings.minCircularity,
        maxAspectRatio: settings.maxAspectRatio,
        minFillRatio: settings.minFillRatio,
      );

      // Convert to relative coordinates
      final width = originalImage.width.toDouble();
      final height = originalImage.height.toDouble();

      return impacts.map((impact) {
        return DetectedImpact(
          x: impact.x / width,
          y: impact.y / height,
          radius: impact.radius,
        );
      }).toList();
    } catch (e) {
      debugPrint('Error detecting impacts: $e');
      return [];
    }
  }

  /// Analyze reference impacts to learn their characteristics
  /// This actually finds the blob at each reference point and extracts its real properties
  /// AMÉLIORÉ : Recherche plus large et analyse plus robuste
  ImpactCharacteristics? analyzeReferenceImpacts(
    String imagePath,
    List<ReferenceImpact> references, {
    int searchRadius = 50, // Augmenté de 30 à 50
  }) {
    if (references.length < 2) return null;

    try {
      final file = File(imagePath);
      final bytes = file.readAsBytesSync();
      final originalImage = img.decodeImage(bytes);
      if (originalImage == null) return null;

      // Resize for faster processing - taille augmentée
      img.Image image;
      double scale = 1.0;
      final maxDimension = 1200; // Augmenté pour plus de précision
      if (originalImage.width > maxDimension || originalImage.height > maxDimension) {
        scale = maxDimension / math.max(originalImage.width, originalImage.height);
        image = img.copyResize(
          originalImage,
          width: (originalImage.width * scale).round(),
          height: (originalImage.height * scale).round(),
        );
      } else {
        image = originalImage;
      }

      final grayscale = img.grayscale(image);
      final blurred = img.gaussianBlur(grayscale, radius: 2);
      final width = image.width;
      final height = image.height;

      final luminances = <double>[];
      final sizes = <double>[];
      final circularities = <double>[];
      final fillRatios = <double>[];
      final thresholds = <double>[];

      debugPrint('Analyzing ${references.length} reference impacts...');

      for (int refIndex = 0; refIndex < references.length; refIndex++) {
        final ref = references[refIndex];
        final centerX = (ref.x * width).round().clamp(0, width - 1);
        final centerY = (ref.y * height).round().clamp(0, height - 1);

        debugPrint('Reference $refIndex at ($centerX, $centerY)');

        // AMÉLIORATION : Recherche du point le plus sombre dans une zone plus large
        int darkestX = centerX;
        int darkestY = centerY;
        double darkestLum = 255;

        // Recherche en spirale du point le plus sombre
        for (int r = 0; r <= searchRadius; r++) {
          for (int dy = -r; dy <= r; dy++) {
            for (int dx = -r; dx <= r; dx++) {
              // Seulement le périmètre du carré pour éviter les doublons
              if (r > 0 && math.max(dx.abs(), dy.abs()) < r) continue;

              final px = centerX + dx;
              final py = centerY + dy;
              if (px < 0 || px >= width || py < 0 || py >= height) continue;

              final pixel = blurred.getPixel(px, py);
              final lum = img.getLuminance(pixel).toDouble();
              if (lum < darkestLum) {
                darkestLum = lum;
                darkestX = px;
                darkestY = py;
              }
            }
          }

          // Si on a trouvé un point très sombre, on peut s'arrêter
          if (darkestLum < 50 && r > 5) break;
        }

        debugPrint('  Darkest point at ($darkestX, $darkestY), lum=$darkestLum');

        // Now find the blob at the darkest point using adaptive threshold
        final blobResult = _findBlobAtPoint(blurred, darkestX, darkestY, width, height);

        if (blobResult != null && blobResult.size >= 10) { // Au moins 10 pixels
          luminances.add(blobResult.avgLuminance);
          sizes.add(blobResult.size.toDouble());
          circularities.add(blobResult.circularity);
          fillRatios.add(blobResult.fillRatio);
          thresholds.add(blobResult.threshold);
          debugPrint('  Found blob: size=${blobResult.size}, circ=${blobResult.circularity.toStringAsFixed(2)}, '
                'fill=${blobResult.fillRatio.toStringAsFixed(2)}, threshold=${blobResult.threshold.toStringAsFixed(0)}');
        } else {
          debugPrint('  No valid blob found at this reference');
        }
      }

      if (luminances.isEmpty) {
        debugPrint('ERROR: No valid blobs found from any reference!');
        return null;
      }

      // Calculate statistics
      final avgLum = luminances.reduce((a, b) => a + b) / luminances.length;
      final avgSize = sizes.reduce((a, b) => a + b) / sizes.length;
      final avgCirc = circularities.reduce((a, b) => a + b) / circularities.length;
      final avgFill = fillRatios.reduce((a, b) => a + b) / fillRatios.length;
      final avgThreshold = thresholds.reduce((a, b) => a + b) / thresholds.length;

      // Calculate standard deviations
      double lumVariance = 0;
      double sizeVariance = 0;
      for (int i = 0; i < luminances.length; i++) {
        lumVariance += math.pow(luminances[i] - avgLum, 2);
        sizeVariance += math.pow(sizes[i] - avgSize, 2);
      }
      final lumStdDev = math.sqrt(lumVariance / luminances.length);
      // AMÉLIORATION : Écart-type minimum pour éviter des plages trop étroites
      final sizeStdDev = math.max(
        math.sqrt(sizeVariance / sizes.length),
        avgSize * 0.3, // Au moins 30% de variance
      );

      final result = ImpactCharacteristics(
        avgLuminance: avgLum,
        luminanceStdDev: math.max(lumStdDev, 10), // Minimum 10 de variance
        avgSize: avgSize,
        sizeStdDev: sizeStdDev,
        avgCircularity: avgCirc,
        avgFillRatio: avgFill,
        avgDarkThreshold: avgThreshold,
      );

      debugPrint('Learned characteristics: $result');

      return result;
    } catch (e) {
      debugPrint('Error analyzing reference impacts: $e');
      return null;
    }
  }

  /// Find a blob at a specific point and extract its characteristics
  /// AMÉLIORÉ : Utilise plusieurs méthodes de détection et retourne le meilleur résultat
  _BlobAnalysis? _findBlobAtPoint(img.Image image, int startX, int startY, int width, int height) {
    // Get the luminance at the center point
    final centerPixel = image.getPixel(startX, startY);
    final centerLum = img.getLuminance(centerPixel).toDouble();

    // MÉTHODE 1 : Expansion radiale pour trouver le bord
    double sumLum = centerLum;
    int pixelCount = 1;
    double maxRadius = 0;

    // Collecter les luminances à différents rayons pour une analyse plus robuste
    final radialLuminances = <double>[];

    // Sample at different radii to find the edge - LIMITE RAISONNABLE pour impacts de balle
    final maxSearchRadius = 60; // Un impact de balle ne fait pas plus de 60 pixels de rayon
    for (int r = 1; r <= maxSearchRadius; r++) {
      double ringSum = 0;
      int ringCount = 0;

      // Sample points on a ring
      final numSamples = math.max(12, r ~/ 2);
      for (int i = 0; i < numSamples; i++) {
        final angle = (i / numSamples) * 2 * math.pi;
        final px = startX + (r * math.cos(angle)).round();
        final py = startY + (r * math.sin(angle)).round();
        if (px < 0 || px >= width || py < 0 || py >= height) continue;

        final pixel = image.getPixel(px, py);
        final lum = img.getLuminance(pixel).toDouble();
        ringSum += lum;
        ringCount++;
      }

      if (ringCount > 0) {
        final avgRingLum = ringSum / ringCount;
        radialLuminances.add(avgRingLum);

        // Détection du bord : gradient de luminosité significatif
        // Seuil adaptatif basé sur la différence avec le centre
        final luminanceDiff = avgRingLum - centerLum;

        // Le bord est trouvé quand on a une augmentation significative de luminosité
        if (luminanceDiff > 30 && maxRadius == 0) {
          maxRadius = r.toDouble();
          break; // Arrêter dès qu'on trouve le bord
        }

        if (maxRadius == 0) {
          sumLum += ringSum;
          pixelCount += ringCount;
        }
      }
    }

    // Si aucun bord trouvé, chercher le gradient maximum
    if (maxRadius < 2 && radialLuminances.length > 3) {
      double maxGradient = 0;
      int maxGradientIndex = 0;
      for (int i = 1; i < radialLuminances.length; i++) {
        final gradient = radialLuminances[i] - radialLuminances[i - 1];
        if (gradient > maxGradient) {
          maxGradient = gradient;
          maxGradientIndex = i;
        }
      }
      if (maxGradient > 10) {
        maxRadius = (maxGradientIndex + 1).toDouble();
      }
    }

    // Rayon minimum de 3 pixels, maximum de 50 pour un impact de balle
    if (maxRadius < 3) maxRadius = 3;
    if (maxRadius > 50) maxRadius = 50;

    // Calculate threshold as weighted average between center and edge luminance
    final edgeRadius = math.min((maxRadius * 1.2).round(), maxSearchRadius - 1);
    double edgeLum = 0;
    int edgeCount = 0;
    for (int i = 0; i < 16; i++) {
      final angle = (i / 16) * 2 * math.pi;
      final px = startX + (edgeRadius * math.cos(angle)).round();
      final py = startY + (edgeRadius * math.sin(angle)).round();
      if (px < 0 || px >= width || py < 0 || py >= height) continue;
      final pixel = image.getPixel(px, py);
      edgeLum += img.getLuminance(pixel).toDouble();
      edgeCount++;
    }
    if (edgeCount > 0) {
      edgeLum /= edgeCount;
    } else {
      edgeLum = centerLum + 50;
    }

    // Calculer le seuil optimal
    final threshold = ((centerLum + edgeLum) / 2).round().clamp(20, 200);

    // Utiliser une zone de recherche locale limitée autour du point
    final analysis = _tryFindBlobWithThresholdLocal(
      image, startX, startY, width, height, threshold, sumLum / pixelCount,
      maxRadius.round() + 10, // Zone de recherche légèrement plus grande que le rayon détecté
    );

    return analysis;
  }

  /// Trouve un blob avec un seuil dans une zone locale limitée
  _BlobAnalysis? _tryFindBlobWithThresholdLocal(
    img.Image image,
    int startX,
    int startY,
    int width,
    int height,
    int threshold,
    double avgLuminance,
    int maxSearchRadius,
  ) {
    // Limiter la zone de recherche
    final minX = math.max(0, startX - maxSearchRadius);
    final maxX = math.min(width - 1, startX + maxSearchRadius);
    final minY = math.max(0, startY - maxSearchRadius);
    final maxY = math.min(height - 1, startY + maxSearchRadius);

    final localWidth = maxX - minX + 1;
    final localHeight = maxY - minY + 1;

    // Create binary mask ONLY for the local region
    final mask = List.generate(localHeight, (_) => List.filled(localWidth, false));
    for (int y = 0; y < localHeight; y++) {
      for (int x = 0; x < localWidth; x++) {
        final globalX = minX + x;
        final globalY = minY + y;
        final pixel = image.getPixel(globalX, globalY);
        final lum = img.getLuminance(pixel);
        mask[y][x] = lum < threshold;
      }
    }

    final visited = List.generate(localHeight, (_) => List.filled(localWidth, false));

    // Find the blob containing the start point (in local coordinates)
    final localStartX = startX - minX;
    final localStartY = startY - minY;

    int searchX = localStartX;
    int searchY = localStartY;

    if (!mask[localStartY][localStartX]) {
      // Start point might not be in mask, find nearest point that is
      bool found = false;
      for (int r = 1; r <= 15 && !found; r++) {
        for (int dy = -r; dy <= r && !found; dy++) {
          for (int dx = -r; dx <= r && !found; dx++) {
            final px = localStartX + dx;
            final py = localStartY + dy;
            if (px >= 0 && px < localWidth && py >= 0 && py < localHeight && mask[py][px]) {
              searchX = px;
              searchY = py;
              found = true;
            }
          }
        }
      }
      if (!found) return null;
    }

    final blob = _floodFillLocal(mask, visited, searchX, searchY, localWidth, localHeight);

    // Vérifier que le blob est valide - taille raisonnable pour un impact
    if (blob.size < 10 || blob.size > 5000) return null; // Entre 10 et 5000 pixels

    // Calculate fill ratio: actual pixels / bounding circle area
    final boundingRadius = math.max(blob.radius, 1);
    final boundingCircleArea = math.pi * boundingRadius * boundingRadius;
    final fillRatio = (blob.size / boundingCircleArea).clamp(0.0, 1.0);

    return _BlobAnalysis(
      avgLuminance: avgLuminance,
      size: blob.size,
      circularity: blob.circularity,
      fillRatio: fillRatio,
      threshold: threshold.toDouble(),
    );
  }

  /// Flood fill pour une zone locale
  _Blob _floodFillLocal(
    List<List<bool>> mask,
    List<List<bool>> visited,
    int startX,
    int startY,
    int width,
    int height,
  ) {
    final stack = <_Point>[_Point(startX, startY)];
    final points = <_Point>[];

    int minX = startX, maxX = startX;
    int minY = startY, maxY = startY;
    int perimeterCount = 0;

    while (stack.isNotEmpty) {
      final point = stack.removeLast();
      final x = point.x;
      final y = point.y;

      if (x < 0 || x >= width || y < 0 || y >= height) continue;
      if (visited[y][x] || !mask[y][x]) continue;

      visited[y][x] = true;
      points.add(point);

      minX = math.min(minX, x);
      maxX = math.max(maxX, x);
      minY = math.min(minY, y);
      maxY = math.max(maxY, y);

      // Check if this is a perimeter pixel
      bool isPerimeter = false;
      for (final delta in [[-1, 0], [1, 0], [0, -1], [0, 1]]) {
        final nx = x + delta[0];
        final ny = y + delta[1];
        if (nx < 0 || nx >= width || ny < 0 || ny >= height || !mask[ny][nx]) {
          isPerimeter = true;
          break;
        }
      }
      if (isPerimeter) perimeterCount++;

      // Add neighbors (4-connectivity)
      stack.add(_Point(x + 1, y));
      stack.add(_Point(x - 1, y));
      stack.add(_Point(x, y + 1));
      stack.add(_Point(x, y - 1));
    }

    // Calculate centroid
    double sumX = 0, sumY = 0;
    for (final p in points) {
      sumX += p.x;
      sumY += p.y;
    }

    final centerX = points.isNotEmpty ? sumX / points.length : startX.toDouble();
    final centerY = points.isNotEmpty ? sumY / points.length : startY.toDouble();

    // Calculate bounding box dimensions
    final blobWidth = (maxX - minX + 1).toDouble();
    final blobHeight = (maxY - minY + 1).toDouble();

    // Calculate approximate radius based on bounding box
    final radius = math.max(blobWidth, blobHeight) / 2.0;

    // Calculate circularity
    final area = points.length.toDouble();
    final perimeter = perimeterCount.toDouble();
    final circularity = perimeter > 0
        ? (4 * math.pi * area) / (perimeter * perimeter)
        : 0.0;

    // Calculate aspect ratio
    final aspectRatio = blobWidth > blobHeight
        ? blobWidth / blobHeight
        : blobHeight / blobWidth;

    // Calculate fill ratio
    final boundingCircleArea = math.pi * radius * radius;
    final fillRatio = boundingCircleArea > 0 ? (area / boundingCircleArea).clamp(0.0, 1.0) : 0.0;

    return _Blob(
      x: centerX,
      y: centerY,
      radius: radius,
      size: points.length,
      circularity: circularity.clamp(0.0, 1.0),
      aspectRatio: aspectRatio,
      fillRatio: fillRatio,
    );
  }


  /// Detect impacts based on reference characteristics with tolerance
  ///
  /// Utilise une approche multi-seuils adaptative pour une meilleure détection
  List<DetectedImpact> detectImpactsFromReferences(
    String imagePath,
    ImpactCharacteristics characteristics, {
    double tolerance = 2.0, // Number of standard deviations
    double minCircularity = 0.3,
  }) {
    try {
      final file = File(imagePath);
      final bytes = file.readAsBytesSync();
      final originalImage = img.decodeImage(bytes);
      if (originalImage == null) return [];

      // Resize for faster processing
      img.Image image;
      double scale = 1.0;
      final maxDimension = 1200; // Augmenté pour plus de précision
      if (originalImage.width > maxDimension || originalImage.height > maxDimension) {
        scale = maxDimension / math.max(originalImage.width, originalImage.height);
        image = img.copyResize(
          originalImage,
          width: (originalImage.width * scale).round(),
          height: (originalImage.height * scale).round(),
        );
      } else {
        image = originalImage;
      }

      final grayscale = img.grayscale(image);
      final blurred = img.gaussianBlur(grayscale, radius: 2);

      // AMÉLIORATION : Utiliser plusieurs seuils autour du seuil appris
      final baseThreshold = characteristics.avgDarkThreshold.round();

      // Générer une plage de seuils plus ciblée
      final thresholds = <int>[];
      final thresholdRange = (15 * tolerance).round(); // Plage modérée
      for (int offset = -thresholdRange; offset <= thresholdRange; offset += 8) {
        final t = (baseThreshold + offset).clamp(30, 150);
        if (!thresholds.contains(t)) thresholds.add(t);
      }

      // Calculate size range based on learned characteristics
      // Utiliser la variance mais avec des limites raisonnables
      final sizeVariance = math.max(characteristics.sizeStdDev * tolerance, characteristics.avgSize * 0.4);
      final minSize = math.max(20, (characteristics.avgSize - sizeVariance).round()); // Minimum 20 pixels
      final maxSize = math.min(3000, (characteristics.avgSize + sizeVariance * 2).round()); // Maximum 3000 pixels

      // Calculate minimum circularity - équilibré
      final circularityTolerance = 0.2 * tolerance;
      final effectiveMinCircularity = math.max(
        characteristics.avgCircularity - circularityTolerance,
        minCircularity,
      ).clamp(0.35, 0.85);

      // Calculate minimum fill ratio - impacts pleins
      final minFillRatio = (characteristics.avgFillRatio - 0.2).clamp(0.35, 0.85);

      debugPrint('Detection params: thresholds=$thresholds, size=$minSize-$maxSize, '
            'circ>=$effectiveMinCircularity, fill>=$minFillRatio');

      // Détecter avec plusieurs seuils et combiner les résultats
      final allBlobs = <_Blob>[];

      for (final threshold in thresholds) {
        final blobs = _detectDarkSpots(
          blurred,
          threshold,
          minSize,
          maxSize,
          minCircularity: effectiveMinCircularity,
          maxAspectRatio: 2.5, // Plus permissif
          minFillRatio: minFillRatio,
        );
        allBlobs.addAll(blobs);
      }

      // Fusionner les blobs qui se chevauchent (même impact détecté à différents seuils)
      final mergedBlobs = _mergeOverlappingBlobs(allBlobs);

      // FILTRE POST-DÉTECTION : Garder seulement les blobs similaires aux références
      // Le filtre est plus ou moins strict selon la tolérance
      final sizeToleranceFactor = 0.3 + (tolerance - 1) * 0.3; // 0.3 à 1.5 selon tolérance
      final minSizeRatio = math.max(0.15, 1 / (1 + sizeToleranceFactor * 3));
      final maxSizeRatio = 1 + sizeToleranceFactor * 4;

      final filteredBlobs = mergedBlobs.where((blob) {
        // Vérifier la taille par rapport aux caractéristiques apprises
        final sizeRatio = blob.size / characteristics.avgSize;
        if (sizeRatio < minSizeRatio || sizeRatio > maxSizeRatio) return false;

        // Vérifier la circularité (légèrement relaxée)
        if (blob.circularity < effectiveMinCircularity * 0.85) return false;

        // Vérifier le fill ratio
        if (blob.fillRatio < minFillRatio * 0.9) return false;

        return true;
      }).toList();

      debugPrint('Found ${filteredBlobs.length} impacts after filtering (from ${mergedBlobs.length} merged)');

      // Convert to relative coordinates
      return filteredBlobs.map((blob) {
        return DetectedImpact(
          x: blob.x / image.width,
          y: blob.y / image.height,
          radius: blob.radius / scale,
        );
      }).toList();
    } catch (e) {
      debugPrint('Error detecting impacts from references: $e');
      return [];
    }
  }

  /// Fusionne les blobs qui se chevauchent en gardant le meilleur représentant
  List<_Blob> _mergeOverlappingBlobs(List<_Blob> blobs) {
    if (blobs.isEmpty) return [];

    // Trier par score de qualité (circularité * fillRatio)
    final sortedBlobs = List<_Blob>.from(blobs);
    sortedBlobs.sort((a, b) {
      final scoreA = a.circularity * a.fillRatio * a.size;
      final scoreB = b.circularity * b.fillRatio * b.size;
      return scoreB.compareTo(scoreA);
    });

    final merged = <_Blob>[];

    for (final blob in sortedBlobs) {
      bool shouldAdd = true;

      for (final existing in merged) {
        final dx = blob.x - existing.x;
        final dy = blob.y - existing.y;
        final distance = math.sqrt(dx * dx + dy * dy);
        final minDist = math.min(blob.radius, existing.radius);

        // Si les centres sont proches, c'est le même impact
        if (distance < minDist * 1.5) {
          shouldAdd = false;
          break;
        }
      }

      if (shouldAdd) {
        merged.add(blob);
      }
    }

    return merged;
  }


  /// Detect dark spots in a grayscale image using blob detection
  List<_Blob> _detectDarkSpots(
    img.Image image,
    int threshold,
    int minSize,
    int maxSize, {
    double minCircularity = 0.6,
    double maxAspectRatio = 2.0,
    double minFillRatio = 0.5,
  }) {
    final width = image.width;
    final height = image.height;

    // Create binary mask of dark pixels
    final mask = List.generate(height, (_) => List.filled(width, false));

    for (int y = 0; y < height; y++) {
      for (int x = 0; x < width; x++) {
        final pixel = image.getPixel(x, y);
        final luminance = img.getLuminance(pixel);
        mask[y][x] = luminance < threshold;
      }
    }

    // Find connected components (blobs)
    final visited = List.generate(height, (_) => List.filled(width, false));
    final blobs = <_Blob>[];

    for (int y = 0; y < height; y++) {
      for (int x = 0; x < width; x++) {
        if (mask[y][x] && !visited[y][x]) {
          final blob = _floodFill(mask, visited, x, y, width, height);

          // Filter by size
          if (blob.size < minSize || blob.size > maxSize) continue;

          // Filter by circularity (reject non-circular shapes like numbers)
          if (blob.circularity < minCircularity) continue;

          // Filter by aspect ratio (reject elongated shapes)
          if (blob.aspectRatio > maxAspectRatio) continue;

          // Filter by fill ratio (reject hollow rings - numbers on target)
          // A filled bullet hole should have fill ratio > 0.5
          // A hollow ring (like number "0" or "8") has a much lower fill ratio
          if (blob.fillRatio < minFillRatio) continue;

          blobs.add(blob);
        }
      }
    }

    // Filter overlapping blobs (keep larger ones)
    final filteredBlobs = _filterOverlappingBlobs(blobs);

    return filteredBlobs;
  }

  /// Flood fill to find connected component
  _Blob _floodFill(
    List<List<bool>> mask,
    List<List<bool>> visited,
    int startX,
    int startY,
    int width,
    int height,
  ) {
    final stack = <_Point>[_Point(startX, startY)];
    final points = <_Point>[];

    int minX = startX, maxX = startX;
    int minY = startY, maxY = startY;
    int perimeterCount = 0;

    while (stack.isNotEmpty) {
      final point = stack.removeLast();
      final x = point.x;
      final y = point.y;

      if (x < 0 || x >= width || y < 0 || y >= height) continue;
      if (visited[y][x] || !mask[y][x]) continue;

      visited[y][x] = true;
      points.add(point);

      minX = math.min(minX, x);
      maxX = math.max(maxX, x);
      minY = math.min(minY, y);
      maxY = math.max(maxY, y);

      // Check if this is a perimeter pixel (has at least one non-blob neighbor)
      bool isPerimeter = false;
      for (final delta in [[-1, 0], [1, 0], [0, -1], [0, 1]]) {
        final nx = x + delta[0];
        final ny = y + delta[1];
        if (nx < 0 || nx >= width || ny < 0 || ny >= height || !mask[ny][nx]) {
          isPerimeter = true;
          break;
        }
      }
      if (isPerimeter) perimeterCount++;

      // Add neighbors (4-connectivity)
      stack.add(_Point(x + 1, y));
      stack.add(_Point(x - 1, y));
      stack.add(_Point(x, y + 1));
      stack.add(_Point(x, y - 1));
    }

    // Calculate centroid
    double sumX = 0, sumY = 0;
    for (final p in points) {
      sumX += p.x;
      sumY += p.y;
    }

    final centerX = points.isNotEmpty ? sumX / points.length : startX.toDouble();
    final centerY = points.isNotEmpty ? sumY / points.length : startY.toDouble();

    // Calculate bounding box dimensions
    final blobWidth = (maxX - minX + 1).toDouble();
    final blobHeight = (maxY - minY + 1).toDouble();

    // Calculate approximate radius based on bounding box
    final radius = math.max(blobWidth, blobHeight) / 2.0;

    // Calculate circularity: 4 * pi * area / perimeter^2
    // For a perfect circle, this equals 1
    final area = points.length.toDouble();
    final perimeter = perimeterCount.toDouble();
    final circularity = perimeter > 0
        ? (4 * math.pi * area) / (perimeter * perimeter)
        : 0.0;

    // Calculate aspect ratio (always >= 1)
    final aspectRatio = blobWidth > blobHeight
        ? blobWidth / blobHeight
        : blobHeight / blobWidth;

    // Calculate fill ratio: actual area vs bounding circle area
    // A filled circle has fill ratio ~0.78 (pi/4), a ring/hollow circle has much lower
    final boundingCircleArea = math.pi * radius * radius;
    final fillRatio = boundingCircleArea > 0 ? (area / boundingCircleArea).clamp(0.0, 1.0) : 0.0;

    return _Blob(
      x: centerX,
      y: centerY,
      radius: radius,
      size: points.length,
      circularity: circularity.clamp(0.0, 1.0),
      aspectRatio: aspectRatio,
      fillRatio: fillRatio,
    );
  }

  /// Filter overlapping blobs, keeping the larger ones
  List<_Blob> _filterOverlappingBlobs(List<_Blob> blobs) {
    if (blobs.isEmpty) return [];

    // Sort by size (largest first)
    blobs.sort((a, b) => b.size.compareTo(a.size));

    final filtered = <_Blob>[];

    for (final blob in blobs) {
      bool overlaps = false;

      for (final existing in filtered) {
        final dx = blob.x - existing.x;
        final dy = blob.y - existing.y;
        final distance = math.sqrt(dx * dx + dy * dy);

        // Check if blobs overlap
        if (distance < (blob.radius + existing.radius) * 0.8) {
          overlaps = true;
          break;
        }
      }

      if (!overlaps) {
        filtered.add(blob);
      }
    }

    return filtered;
  }
}

class _Point {
  final int x;
  final int y;

  _Point(this.x, this.y);
}

class _Blob {
  final double x;
  final double y;
  final double radius;
  final int size;
  final double circularity; // 0-1, 1 = perfect circle
  final double aspectRatio; // width/height ratio
  final double fillRatio; // How filled vs hollow the blob is

  _Blob({
    required this.x,
    required this.y,
    required this.radius,
    required this.size,
    required this.circularity,
    required this.aspectRatio,
    this.fillRatio = 1.0,
  });
}

class _BlobAnalysis {
  final double avgLuminance;
  final int size;
  final double circularity;
  final double fillRatio;
  final double threshold;

  _BlobAnalysis({
    required this.avgLuminance,
    required this.size,
    required this.circularity,
    required this.fillRatio,
    required this.threshold,
  });
}
