import 'dart:math' as math;
import 'package:opencv_dart/opencv_dart.dart' as cv;

class TargetDetectionResult {
  final double centerX;
  final double centerY;
  final double radius;
  final bool success;

  TargetDetectionResult({
    required this.centerX,
    required this.centerY,
    required this.radius,
    this.success = true,
  });

  factory TargetDetectionResult.failure() {
    return TargetDetectionResult(
      centerX: 0.5,
      centerY: 0.5,
      radius: 0.4,
      success: false,
    );
  }
}

class OpenCVTargetService {
  /// Detect the main target (center and radius) from an image file
  Future<TargetDetectionResult> detectTarget(String imagePath) async {
    try {
      // Read image
      final img = cv.imread(imagePath, flags: cv.IMREAD_COLOR);
      if (img.isEmpty) {
        return TargetDetectionResult.failure();
      }

      // Convert to grayscale
      final gray = cv.cvtColor(img, cv.COLOR_BGR2GRAY);

      // Apply Gaussian blur to reduce noise
      final blurred = cv.gaussianBlur(gray, (9, 9), 2, sigmaY: 2);

      // Detect circles using Hough Transform
      // Parameters need to be tuned for the specific target type
      final circles = cv.HoughCircles(
        blurred,
        cv.HOUGH_GRADIENT,
        1, // dp
        (img.rows / 16)
            .toDouble(), // minDist decreased to allow more rings in same general area
        param1: 100, // Canny edge detection
        param2:
            60, // Accumulator threshold (higher = fewer false circles, more accurate)
        minRadius: img.cols ~/ 20,
        maxRadius: img.cols ~/ 2,
      );

      // HoughCircles returns a Mat of shape (1, N, 3) where N is number of circles.
      // In opencv_dart, we cannot iterate easily.
      // However, we can access data via pointer if needed, or check if Vec3f is supported.
      // Given the user report, `at<Vec3f>` likely failed compilation or runtime.
      // Let's use a safer approach: assume standard memory layout (x, y, r, x, y, r...).
      // Or use `at<double>` carefully.

      // Better yet: try to use `circles.data` if available, but it returns a Pointer.
      // Let's stick to `at` but use `double` and manual offset if Vec3f fails.
      // actually, let's try to trust `at<double>` for flattened access OR `at<Vec3f>`.
      // NOTE: `at<Vec3f>` was reported as "method at not defined for VecPoint2f" earlier, NOT for Mat.
      // The user error was for `VecPoint2f`. `Mat` definitely has `at`.
      // BUT `VecPoint2f` is a List-like structure in Dart wrapper.
      // usage of `at` on `VecPoint2f` was the error.
      // Here `circles` IS A MAT. So `at` IS defined.
      // However, to be safe and robust, and to implement clustering...

      if (circles.isEmpty) {
        // Try with different parameters if first attempt fails (more lenient)
        final looseCircles = cv.HoughCircles(
          blurred,
          cv.HOUGH_GRADIENT,
          1,
          (img.rows / 8).toDouble(),
          param1: 100,
          param2: 40,
          minRadius: img.cols ~/ 20,
          maxRadius: img.cols ~/ 2,
        );

        if (looseCircles.isEmpty) {
          return TargetDetectionResult.failure();
        }
        return _findBestConcentricCircles(looseCircles, img.cols, img.rows);
      }

      return _findBestConcentricCircles(circles, img.cols, img.rows);
    } catch (e) {
      // print('Error detecting target with OpenCV: $e');
      return TargetDetectionResult.failure();
    }
  }

  TargetDetectionResult _findBestConcentricCircles(
    cv.Mat circles,
    int width,
    int height,
  ) {
    if (circles.rows == 0 || circles.cols == 0) {
      return TargetDetectionResult.failure();
    }

    final int numCircles = circles.cols;
    final List<({double x, double y, double r})> detected = [];

    // Extract circles safely
    // We'll use `at<double>` assuming the Mat is (1, N, 3) float32 (CV_32FC3 usually)
    // Actually HoughCircles usually returns CV_32FC3.
    // So we can access `at<cv.Vec3f>(0, i)`.
    // If that fails, we can fall back. But since `Mat` has `at`, it should work unless generic is bad.
    // Let's assume it works for Mat but checking boundaries.

    // NOTE: If this throws "at not defined" (unlikely for Mat), we'd need another way.
    // But since the previous error was on `VecPoint2f` (which is NOT a Mat), this should be fine.

    for (int i = 0; i < numCircles; i++) {
      // Access using Vec3f if possible, or try to interpret memory
      // Using `at<cv.Vec3f>` is the standard way.
      final vec = circles.at<cv.Vec3f>(0, i);
      detected.add((x: vec.val1, y: vec.val2, r: vec.val3));
    }

    if (detected.isEmpty) return TargetDetectionResult.failure();

    // Cluster circles by center position
    // We consider circles "concentric" if their centers are within 5% of image min dimension
    final double tolerance = math.min(width, height) * 0.05;
    final List<List<({double x, double y, double r})>> clusters = [];

    for (final circle in detected) {
      bool added = false;
      for (final cluster in clusters) {
        // Calculate the actual center of the cluster based on the smallest circle (the likely bullseye)
        double clusterCenterX = cluster.first.x;
        double clusterCenterY = cluster.first.y;
        double minRadiusInCluster = cluster.first.r;

        for (final c in cluster) {
          if (c.r < minRadiusInCluster) {
            minRadiusInCluster = c.r;
            clusterCenterX = c.x;
            clusterCenterY = c.y;
          }
        }

        final dist = math.sqrt(
          math.pow(circle.x - clusterCenterX, 2) +
              math.pow(circle.y - clusterCenterY, 2),
        );

        if (dist < tolerance) {
          cluster.add(circle);
          added = true;
          break;
        }
      }
      if (!added) {
        clusters.add([circle]);
      }
    }

    // Find the best cluster
    // 1. Prefer clusters with more circles (concentric rings)
    // 2. Tie-break: closest to image center

    List<({double x, double y, double r})> bestCluster = clusters.first;
    double bestScore = -1.0;

    for (final cluster in clusters) {
      // Score calculation
      // Base score = number of circles squared (heavily favor concentric rings)
      double score = math.pow(cluster.length, 2).toDouble() * 10.0;

      // Small penalty for distance from center (only as tie-breaker)
      double cx = 0, cy = 0;
      for (final c in cluster) {
        cx += c.x;
        cy += c.y;
      }
      cx /= cluster.length;
      cy /= cluster.length;

      final distFromCenter = math.sqrt(
        math.pow(cx - width / 2, 2) + math.pow(cy - height / 2, 2),
      );
      final relDist = distFromCenter / math.min(width, height);

      score -=
          relDist * 2.0; // Very minor penalty so we don't snap to screen center

      // Penalize very small clusters if they are just noise
      // (Optional: check if radii are somewhat distributed?)

      if (score > bestScore) {
        bestScore = score;
        bestCluster = cluster;
      }
    }

    // Compute final result from best cluster
    // Center: Use the smallest circle (bullseye) for best precision
    // Radius: Use the largest circle (outer edge) for full coverage

    double centerX = 0;
    double centerY = 0;
    double maxR = 0;
    double minR = double.infinity;

    for (final c in bestCluster) {
      if (c.r > maxR) {
        maxR = c.r;
      }
      if (c.r < minR) {
        minR = c.r;
        centerX = c.x;
        centerY = c.y;
      }
    }

    // Fallback if something went wrong (shouldn't happen with non-empty cluster)
    if (minR == double.infinity) {
      centerX = bestCluster.first.x;
      centerY = bestCluster.first.y;
    }

    return TargetDetectionResult(
      centerX: centerX / width,
      centerY: centerY / height,
      radius: maxR / math.min(width, height),
      success: true,
    );
  }
}
