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

/// Résultat d'une tentative de redressement de cible.
class RectifyResult {
  /// Chemin du fichier image redressé (ou original si échec).
  final String outputPath;

  /// true si une cible a été détectée et redressée, false si on a renvoyé
  /// l'image d'origine sans transformation.
  final bool rectified;

  /// Angle d'inclinaison estimé de la cible AVANT redressement, en degrés.
  /// (0 = déjà de face). Utile pour informer l'utilisateur.
  final double estimatedTiltDegrees;

  /// Message de diagnostic (utile pour debug / affichage).
  final String message;

  const RectifyResult({
    required this.outputPath,
    required this.rectified,
    required this.estimatedTiltDegrees,
    required this.message,
  });
}

/// Service qui redresse une cible RONDE (cercles concentriques) photographiée
/// de biais, en la ramenant parfaitement de face.
///
/// Principe :
///   1. Détecter le plus grand contour ~circulaire de l'image.
///   2. Ajuster une ELLIPSE sur ce contour (fitEllipse).
///   3. Un cercle vu en perspective devient une ellipse : on calcule la
///      transformation de perspective qui remappe cette ellipse vers un
///      CERCLE parfait, et on l'applique à toute l'image.
///   4. La cible apparaît alors de face.
///
/// L'image résultat est carrée et centrée sur la cible.
class TargetRectifyService {
  /// Taille (en pixels) du côté de l'image carrée de sortie.
  final int outputSize;

  /// Marge autour de la cible dans l'image de sortie (1.0 = cible pile au bord,
  /// 1.3 = 30 % de marge autour). Garde un peu de contexte.
  final double marginFactor;

  /// En dessous de cet écart d'axes (ratio petit/grand axe proche de 1),
  /// la cible est considérée déjà de face → pas de warp inutile.
  final double minTiltRatioToRectify;

  TargetRectifyService({
    this.outputSize = 1024,
    this.marginFactor = 1.25,
    this.minTiltRatioToRectify = 0.985,
  });

  /// Redresse l'image située à [inputPath]. Écrit le résultat dans
  /// [outputPath] et renvoie un [RectifyResult].
  ///
  /// Ne bloque jamais : en cas d'échec de détection, renvoie l'image
  /// d'origine (rectified = false) pour ne pas perdre la photo du tireur.
  Future<RectifyResult> rectify({
    required String inputPath,
    required String outputPath,
  }) async {
    cv.Mat? src;
    cv.Mat? gray;
    cv.Mat? blurred;
    cv.Mat? edges;
    try {
      src = cv.imread(inputPath, flags: cv.IMREAD_COLOR);
      if (src.isEmpty) {
        return RectifyResult(
          outputPath: inputPath,
          rectified: false,
          estimatedTiltDegrees: 0,
          message: 'Image illisible',
        );
      }

      // ── 1. Prétraitement ────────────────────────────────────────────────
      gray = cv.cvtColor(src, cv.COLOR_BGR2GRAY);
      blurred = cv.gaussianBlur(gray, (5, 5), 2, sigmaY: 2);
      edges = cv.canny(blurred, 60, 160);

      // Dilatation légère pour fermer les contours brisés
      final cv.Mat kernel = cv.getStructuringElement(
        cv.MORPH_ELLIPSE,
        (3, 3),
      );
      final cv.Mat dilated = cv.dilate(edges, kernel);

      // ── 2. Recherche du meilleur contour elliptique ──────────────────────
      final (contours, _) = cv.findContours(
        dilated,
        cv.RETR_EXTERNAL,
        cv.CHAIN_APPROX_SIMPLE,
      );

      final double imgArea = (src.width * src.height).toDouble();
      cv.RotatedRect? bestEllipse;
      double bestScore = 0;

      for (int i = 0; i < contours.length; i++) {
        final c = contours[i];
        if (c.length < 5) continue; // fitEllipse exige >= 5 points

        final double area = cv.contourArea(c);
        // On ignore les contours minuscules et ceux qui couvrent presque tout
        if (area < imgArea * 0.03 || area > imgArea * 0.97) continue;

        final cv.RotatedRect e = cv.fitEllipse(c);
        final ep = e.points; // 4 sommets
        if (ep.length < 4) continue;
        final double ecx = (ep[0].x + ep[1].x + ep[2].x + ep[3].x) / 4.0;
        final double ecy = (ep[0].y + ep[1].y + ep[2].y + ep[3].y) / 4.0;
        final double mAx = (ep[0].x + ep[1].x) / 2.0;
        final double mAy = (ep[0].y + ep[1].y) / 2.0;
        final double mBx = (ep[1].x + ep[2].x) / 2.0;
        final double mBy = (ep[1].y + ep[2].y) / 2.0;
        final double w =
            2 * math.sqrt(math.pow(mAx - ecx, 2) + math.pow(mAy - ecy, 2));
        final double h =
            2 * math.sqrt(math.pow(mBx - ecx, 2) + math.pow(mBy - ecy, 2));
        if (w <= 1 || h <= 1) continue;

        // À quel point le contour ressemble-t-il vraiment à son ellipse ?
        // On compare l'aire du contour à l'aire de l'ellipse ajustée.
        final double ellipseArea = math.pi * (w / 2) * (h / 2);
        if (ellipseArea <= 0) continue;
        final double fitRatio = area / ellipseArea; // ~1 si bon ajustement
        if (fitRatio < 0.7 || fitRatio > 1.3) continue;

        // Score = taille de l'ellipse (on veut la cible la plus grande)
        final double score = ellipseArea;
        if (score > bestScore) {
          bestScore = score;
          bestEllipse = e;
        }
      }

      if (bestEllipse == null) {
        return RectifyResult(
          outputPath: inputPath,
          rectified: false,
          estimatedTiltDegrees: 0,
          message: 'Aucune cible circulaire détectée',
        );
      }

      // ── 3. Extraire les 4 sommets de l'ellipse (robuste à la version d'API) ─
      // RotatedRect.points renvoie les 4 coins de la boîte englobant l'ellipse.
      // On en dérive nous-mêmes le centre, les demi-axes et l'orientation, ce
      // qui évite de dépendre de la forme exacte de `.size` / `.center`
      // (record vs objet) qui varie selon les versions d'opencv_dart.
      final pts = bestEllipse.points; // List<Point2f> de 4 sommets

      double px(int i) => pts[i].x;
      double py(int i) => pts[i].y;

      // Centre = moyenne des 4 sommets
      final double cx = (px(0) + px(1) + px(2) + px(3)) / 4.0;
      final double cy = (py(0) + py(1) + py(2) + py(3)) / 4.0;

      // Milieux de deux côtés adjacents → extrémités des deux demi-axes
      // Côté 0-1 et côté 1-2 (ordre des sommets d'un RotatedRect)
      final double m01x = (px(0) + px(1)) / 2.0;
      final double m01y = (py(0) + py(1)) / 2.0;
      final double m12x = (px(1) + px(2)) / 2.0;
      final double m12y = (py(1) + py(2)) / 2.0;

      // Demi-axes = distance centre → milieu de chaque côté
      final double axisA =
          math.sqrt(math.pow(m01x - cx, 2) + math.pow(m01y - cy, 2));
      final double axisB =
          math.sqrt(math.pow(m12x - cx, 2) + math.pow(m12y - cy, 2));

      final double majorAxis = math.max(axisA, axisB);
      final double minorAxis = math.min(axisA, axisB);
      if (majorAxis <= 1) {
        return RectifyResult(
          outputPath: inputPath,
          rectified: false,
          estimatedTiltDegrees: 0,
          message: 'Ellipse dégénérée',
        );
      }

      final double axisRatio = minorAxis / majorAxis; // 1 = cercle parfait
      final double tiltDeg =
          math.acos(axisRatio.clamp(0.0, 1.0)) * (180.0 / math.pi);

      // Déjà quasiment de face → on ne touche pas (évite le flou inutile)
      if (axisRatio >= minTiltRatioToRectify) {
        cv.imwrite(outputPath, src);
        return RectifyResult(
          outputPath: outputPath,
          rectified: false,
          estimatedTiltDegrees: tiltDeg,
          message: 'Cible déjà de face',
        );
      }

      // ── 4. Construire la transformation de perspective ────────────────────
      // Source : extrémités des deux axes de l'ellipse (4 points).
      // Destination : extrémités des axes d'un cercle parfait centré.
      // On obtient les extrémités en prolongeant centre→milieu-de-côté.
      final srcPts = cv.VecPoint.fromList([
        cv.Point((cx + (m01x - cx)).round(), (cy + (m01y - cy)).round()),
        cv.Point((cx - (m01x - cx)).round(), (cy - (m01y - cy)).round()),
        cv.Point((cx + (m12x - cx)).round(), (cy + (m12y - cy)).round()),
        cv.Point((cx - (m12x - cx)).round(), (cy - (m12y - cy)).round()),
      ]);

      // Cible : cercle parfait centré, rayon R, dans une image carrée.
      // L'axe "A" (m01) devient l'axe horizontal, l'axe "B" (m12) le vertical.
      final double out = outputSize.toDouble();
      final double center = out / 2;
      final double radius = (out / 2) / marginFactor;
      final dstPts = cv.VecPoint.fromList([
        cv.Point((center + radius).round(), center.round()),
        cv.Point((center - radius).round(), center.round()),
        cv.Point(center.round(), (center + radius).round()),
        cv.Point(center.round(), (center - radius).round()),
      ]);

      final cv.Mat transform = cv.getPerspectiveTransform(srcPts, dstPts);

      final cv.Mat warped = cv.warpPerspective(
        src,
        transform,
        (outputSize, outputSize),
        flags: cv.INTER_LINEAR,
        borderMode: cv.BORDER_CONSTANT,
      );

      cv.imwrite(outputPath, warped);

      return RectifyResult(
        outputPath: outputPath,
        rectified: true,
        estimatedTiltDegrees: tiltDeg,
        message: 'Cible redressée (inclinaison ${tiltDeg.toStringAsFixed(1)}°)',
      );
    } catch (e) {
      debugPrint('TargetRectify erreur: $e');
      // En cas de pépin, on renvoie l'original pour ne jamais perdre la photo
      return RectifyResult(
        outputPath: inputPath,
        rectified: false,
        estimatedTiltDegrees: 0,
        message: 'Erreur de traitement: $e',
      );
    }
  }
}
