/// Service de correction de distorsion d'objectif.
///
/// Utilise la calibration des cercles de la cible pour calculer et appliquer
/// une transformation qui corrige la distorsion de l'objectif. L'image est
/// transformée pour que les cercles calibrés deviennent parfaitement circulaires.
library;

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

/// Paramètres de distorsion calculés à partir de la calibration
class DistortionParameters {
  /// Ratio d'aplatissement horizontal (1.0 = pas de correction)
  final double scaleX;

  /// Ratio d'aplatissement vertical (1.0 = pas de correction)
  final double scaleY;

  /// Angle de rotation de l'axe principal (en radians)
  final double rotation;

  /// Centre de la distorsion en coordonnées normalisées (0-1)
  final double centerX;
  final double centerY;

  /// Ratio de circularité détecté (1.0 = cercle parfait)
  final double circularityRatio;

  const DistortionParameters({
    required this.scaleX,
    required this.scaleY,
    required this.rotation,
    required this.centerX,
    required this.centerY,
    required this.circularityRatio,
  });

  /// Paramètres par défaut (pas de correction)
  static const identity = DistortionParameters(
    scaleX: 1.0,
    scaleY: 1.0,
    rotation: 0.0,
    centerX: 0.5,
    centerY: 0.5,
    circularityRatio: 1.0,
  );

  /// La correction est-elle nécessaire ?
  bool get needsCorrection => circularityRatio < 0.95;

  @override
  String toString() {
    return 'DistortionParameters(scaleX: ${scaleX.toStringAsFixed(3)}, '
        'scaleY: ${scaleY.toStringAsFixed(3)}, '
        'rotation: ${(rotation * 180 / math.pi).toStringAsFixed(1)}°, '
        'circularity: ${(circularityRatio * 100).toStringAsFixed(1)}%)';
  }
}

/// Service pour détecter et corriger la distorsion d'objectif
class DistortionCorrectionService {
  /// Calcule les paramètres de distorsion à partir de la calibration des cercles
  ///
  /// [targetCenterX], [targetCenterY] : Centre de la cible calibré (0-1)
  /// [targetRadius] : Rayon de la cible calibré
  /// [imageAspectRatio] : Ratio largeur/hauteur de l'image
  ///
  /// Cette méthode analyse la forme attendue (cercle) vs la forme observée
  /// pour déterminer les paramètres de correction.
  DistortionParameters calculateDistortionFromCalibration({
    required double targetCenterX,
    required double targetCenterY,
    required double targetRadius,
    required double imageAspectRatio,
  }) {
    // En théorie, si la cible est un cercle parfait et que l'utilisateur
    // a calibré les anneaux pour qu'ils correspondent à ce qu'il voit,
    // on peut déduire la distorsion.

    // Pour l'instant, on utilise une approche simplifiée basée sur l'aspect ratio
    // et la position du centre par rapport au centre de l'image.

    // Calcul de l'excentricité basée sur la position du centre
    final offsetX = targetCenterX - 0.5;
    final offsetY = targetCenterY - 0.5;
    final offsetDistance = math.sqrt(offsetX * offsetX + offsetY * offsetY);

    // Plus le centre est éloigné du centre de l'image, plus la distorsion est probable
    // Estimation simplifiée de la distorsion radiale
    final distortionFactor = 1.0 + offsetDistance * 0.2;

    // Calculer l'angle de l'axe principal de déformation
    final angle = math.atan2(offsetY, offsetX);

    // Si l'image n'est pas carrée, tenir compte de l'aspect ratio
    double scaleX = 1.0;
    double scaleY = 1.0;

    if (imageAspectRatio > 1.0) {
      // Image plus large que haute
      scaleY = distortionFactor;
    } else if (imageAspectRatio < 1.0) {
      // Image plus haute que large
      scaleX = distortionFactor;
    }

    final circularityRatio = 1.0 / distortionFactor;

    return DistortionParameters(
      scaleX: scaleX,
      scaleY: scaleY,
      rotation: angle,
      centerX: targetCenterX,
      centerY: targetCenterY,
      circularityRatio: circularityRatio,
    );
  }

  /// Calcule les paramètres de distorsion en comparant un cercle théorique
  /// avec les points de calibration fournis par l'utilisateur
  ///
  /// [calibrationPoints] : Points sur l'ellipse visible (coordonnées 0-1)
  /// [expectedRadius] : Rayon du cercle théorique
  /// [centerX], [centerY] : Centre du cercle théorique
  DistortionParameters calculateDistortionFromPoints({
    required List<({double x, double y})> calibrationPoints,
    required double expectedRadius,
    required double centerX,
    required double centerY,
  }) {
    if (calibrationPoints.length < 4) {
      return DistortionParameters.identity;
    }

    // Calculer les distances de chaque point au centre
    final distances = <double>[];
    final angles = <double>[];

    for (final point in calibrationPoints) {
      final dx = point.x - centerX;
      final dy = point.y - centerY;
      distances.add(math.sqrt(dx * dx + dy * dy));
      angles.add(math.atan2(dy, dx));
    }

    // Trouver les axes majeur et mineur de l'ellipse
    double maxDist = 0, minDist = double.infinity;
    double maxAngle = 0;

    for (int i = 0; i < distances.length; i++) {
      if (distances[i] > maxDist) {
        maxDist = distances[i];
        maxAngle = angles[i];
      }
      if (distances[i] < minDist) {
        minDist = distances[i];
      }
    }

    // Calculer les facteurs de correction
    // On veut que maxDist et minDist deviennent égaux à expectedRadius
    final scaleX = expectedRadius / maxDist;
    final scaleY = expectedRadius / minDist;
    final circularityRatio = minDist / maxDist;

    return DistortionParameters(
      scaleX: scaleX.clamp(0.5, 2.0),
      scaleY: scaleY.clamp(0.5, 2.0),
      rotation: maxAngle,
      centerX: centerX,
      centerY: centerY,
      circularityRatio: circularityRatio,
    );
  }

  /// Applique la correction de distorsion à une image
  ///
  /// [imagePath] : Chemin de l'image source
  /// [params] : Paramètres de distorsion calculés
  ///
  /// Retourne le chemin de l'image corrigée
  Future<String> applyCorrection(
    String imagePath,
    DistortionParameters params,
  ) async {
    final file = File(imagePath);
    final bytes = await file.readAsBytes();
    final image = img.decodeImage(bytes);

    if (image == null) {
      throw Exception('Impossible de décoder l\'image');
    }

    final correctedImage = _transformImage(image, params);

    // Sauvegarder l'image corrigée
    final tempDir = await getTemporaryDirectory();
    final timestamp = DateTime.now().millisecondsSinceEpoch;
    final outputPath = '${tempDir.path}/corrected_$timestamp.jpg';

    final outputFile = File(outputPath);
    await outputFile.writeAsBytes(img.encodeJpg(correctedImage, quality: 95));

    return outputPath;
  }

  /// Transforme l'image pour corriger la distorsion
  img.Image _transformImage(img.Image source, DistortionParameters params) {
    final width = source.width;
    final height = source.height;

    // Créer une nouvelle image de même taille
    final result = img.Image(width: width, height: height);

    // Centre de transformation en pixels
    final cx = params.centerX * width;
    final cy = params.centerY * height;

    // Précalculer cos/sin de la rotation
    final cosR = math.cos(params.rotation);
    final sinR = math.sin(params.rotation);

    // Pour chaque pixel de l'image destination, trouver le pixel source correspondant
    for (int y = 0; y < height; y++) {
      for (int x = 0; x < width; x++) {
        // Coordonnées relatives au centre
        final dx = x - cx;
        final dy = y - cy;

        // Rotation inverse
        final rx = dx * cosR + dy * sinR;
        final ry = -dx * sinR + dy * cosR;

        // Mise à l'échelle inverse (pour trouver d'où vient le pixel)
        final sx = rx / params.scaleX;
        final sy = ry / params.scaleY;

        // Rotation dans l'autre sens
        final fx = sx * cosR - sy * sinR;
        final fy = sx * sinR + sy * cosR;

        // Coordonnées source
        final srcX = fx + cx;
        final srcY = fy + cy;

        // Interpolation bilinéaire
        final pixel = _bilinearInterpolate(source, srcX, srcY);
        result.setPixel(x, y, pixel);
      }
    }

    return result;
  }

  /// Interpolation bilinéaire pour un échantillonnage de qualité
  img.Color _bilinearInterpolate(img.Image image, double x, double y) {
    final x0 = x.floor();
    final y0 = y.floor();
    final x1 = x0 + 1;
    final y1 = y0 + 1;

    // Vérifier les limites
    if (x0 < 0 || y0 < 0 || x1 >= image.width || y1 >= image.height) {
      // Retourner le pixel le plus proche pour les zones hors limites
      return image.getPixel(
        x.round().clamp(0, image.width - 1),
        y.round().clamp(0, image.height - 1),
      );
    }

    // Poids pour l'interpolation
    final wx = x - x0;
    final wy = y - y0;

    // Récupérer les 4 pixels voisins
    final p00 = image.getPixel(x0, y0);
    final p10 = image.getPixel(x1, y0);
    final p01 = image.getPixel(x0, y1);
    final p11 = image.getPixel(x1, y1);

    // Interpoler chaque canal
    final r = _lerp2D(
      p00.r.toDouble(),
      p10.r.toDouble(),
      p01.r.toDouble(),
      p11.r.toDouble(),
      wx,
      wy,
    );
    final g = _lerp2D(
      p00.g.toDouble(),
      p10.g.toDouble(),
      p01.g.toDouble(),
      p11.g.toDouble(),
      wx,
      wy,
    );
    final b = _lerp2D(
      p00.b.toDouble(),
      p10.b.toDouble(),
      p01.b.toDouble(),
      p11.b.toDouble(),
      wx,
      wy,
    );
    final a = _lerp2D(
      p00.a.toDouble(),
      p10.a.toDouble(),
      p01.a.toDouble(),
      p11.a.toDouble(),
      wx,
      wy,
    );

    return img.ColorRgba8(
      r.round().clamp(0, 255),
      g.round().clamp(0, 255),
      b.round().clamp(0, 255),
      a.round().clamp(0, 255),
    );
  }

  /// Interpolation linéaire 2D
  double _lerp2D(
    double v00,
    double v10,
    double v01,
    double v11,
    double wx,
    double wy,
  ) {
    final top = v00 * (1 - wx) + v10 * wx;
    final bottom = v01 * (1 - wx) + v11 * wx;
    return top * (1 - wy) + bottom * wy;
  }

  /// Applique une correction de perspective simple basée sur 4 points
  ///
  /// Cette méthode est utile quand la photo est prise en angle.
  /// [corners] : Les 4 coins de la cible dans l'ordre (haut-gauche, haut-droite, bas-droite, bas-gauche)
  Future<String> applyPerspectiveCorrection(
    String imagePath,
    List<({double x, double y})> corners,
  ) async {
    if (corners.length != 4) {
      throw ArgumentError('4 points de coin sont requis');
    }

    final file = File(imagePath);
    final bytes = await file.readAsBytes();
    final image = img.decodeImage(bytes);

    if (image == null) {
      throw Exception('Impossible de décoder l\'image');
    }

    final width = image.width;
    final height = image.height;

    // Convertir les coordonnées normalisées en pixels
    final srcCorners = corners
        .map((c) => (x: c.x * width, y: c.y * height))
        .toList();

    // Calculer la taille du rectangle destination
    // On prend la moyenne des largeurs et hauteurs
    final topWidth = _distance(srcCorners[0], srcCorners[1]);
    final bottomWidth = _distance(srcCorners[3], srcCorners[2]);
    final leftHeight = _distance(srcCorners[0], srcCorners[3]);
    final rightHeight = _distance(srcCorners[1], srcCorners[2]);

    final dstWidth = ((topWidth + bottomWidth) / 2).round();
    final dstHeight = ((leftHeight + rightHeight) / 2).round();

    // Créer l'image destination
    final result = img.Image(width: dstWidth, height: dstHeight);

    // Calculer la matrice de transformation perspective
    final matrix = _computePerspectiveMatrix(srcCorners, [
      (x: 0.0, y: 0.0),
      (x: dstWidth.toDouble(), y: 0.0),
      (x: dstWidth.toDouble(), y: dstHeight.toDouble()),
      (x: 0.0, y: dstHeight.toDouble()),
    ]);

    // Appliquer la transformation
    for (int y = 0; y < dstHeight; y++) {
      for (int x = 0; x < dstWidth; x++) {
        final src = _applyPerspectiveTransform(
          matrix,
          x.toDouble(),
          y.toDouble(),
        );

        if (src.x >= 0 && src.x < width && src.y >= 0 && src.y < height) {
          final pixel = _bilinearInterpolate(image, src.x, src.y);
          result.setPixel(x, y, pixel);
        }
      }
    }

    // Sauvegarder
    final tempDir = await getTemporaryDirectory();
    final timestamp = DateTime.now().millisecondsSinceEpoch;
    final outputPath = '${tempDir.path}/perspective_$timestamp.jpg';

    final outputFile = File(outputPath);
    await outputFile.writeAsBytes(img.encodeJpg(result, quality: 95));

    return outputPath;
  }

  double _distance(({double x, double y}) p1, ({double x, double y}) p2) {
    final dx = p2.x - p1.x;
    final dy = p2.y - p1.y;
    return math.sqrt(dx * dx + dy * dy);
  }

  /// Calcule la matrice de transformation perspective (homographie)
  List<double> _computePerspectiveMatrix(
    List<({double x, double y})> src,
    List<({double x, double y})> dst,
  ) {
    // Résolution du système linéaire pour trouver la matrice 3x3
    // Utilisation de la méthode DLT (Direct Linear Transform)

    final a = List<List<double>>.generate(8, (_) => List.filled(9, 0.0));

    for (int i = 0; i < 4; i++) {
      final sx = src[i].x;
      final sy = src[i].y;
      final dx = dst[i].x;
      final dy = dst[i].y;

      a[i * 2] = [-sx, -sy, -1, 0, 0, 0, dx * sx, dx * sy, dx];
      a[i * 2 + 1] = [0, 0, 0, -sx, -sy, -1, dy * sx, dy * sy, dy];
    }

    // Résolution par SVD simplifiée (on utilise une approximation)
    // Pour une implémentation complète, il faudrait une vraie décomposition SVD
    final h = _solveHomography(a);

    return h;
  }

  /// Résout le système linéaire pour trouver la matrice d'homographie 3x3.
  /// Utilise l'élimination de Gauss-Jordan avec pivot partiel pour la stabilité.
  List<double> _solveHomography(List<List<double>> a) {
    // Le système 'a' est de taille 8x9 (8 équations, 9 inconnues).
    // On fixe h8 = 1.0 pour résoudre le système, ce qui nous donne un système 8x8.
    final int n = 8;
    final List<List<double>> matrix = List.generate(
      n,
      (i) => List<double>.from(a[i]),
    );

    // Vecteur B (les constantes de l'autre côté de l'égalité)
    // Dans DLT, -h8 * dx (ou dy) devient le terme constant.
    final List<double> b = List.generate(n, (i) => -matrix[i][8]);

    // Élimination de Gauss-Jordan
    for (int i = 0; i < n; i++) {
      // Recherche du pivot (valeur maximale dans la colonne pour limiter les erreurs)
      int pivot = i;
      for (int j = i + 1; j < n; j++) {
        if (matrix[j][i].abs() > matrix[pivot][i].abs()) {
          pivot = j;
        }
      }

      // Échange des lignes (si nécessaire)
      final List<double> tempRow = matrix[i];
      matrix[i] = matrix[pivot];
      matrix[pivot] = tempRow;

      final double tempB = b[i];
      b[i] = b[pivot];
      b[pivot] = tempB;

      // Vérification de la singularité (division par zéro impossible)
      if (matrix[i][i].abs() < 1e-10) {
        return [1, 0, 0, 0, 1, 0, 0, 0, 1]; // Retourne identité si échec
      }

      // Normalisation de la ligne pivot
      for (int j = i + 1; j < n; j++) {
        final double factor = matrix[j][i] / matrix[i][i];
        b[j] -= factor * b[i];
        for (int k = i; k < n; k++) {
          matrix[j][k] -= factor * matrix[i][k];
        }
      }
    }

    // Substitution arrière
    final List<double> h = List.filled(9, 0.0);
    for (int i = n - 1; i >= 0; i--) {
      double sum = 0.0;
      for (int j = i + 1; j < n; j++) {
        sum += matrix[i][j] * h[j];
      }
      h[i] = (b[i] - sum) / matrix[i][i];
    }

    h[8] = 1.0; // Normalisation finale
    return h;
  }

  ({double x, double y}) _applyPerspectiveTransform(
    List<double> h,
    double x,
    double y,
  ) {
    final w = h[6] * x + h[7] * y + h[8];
    if (w.abs() < 1e-10) {
      return (x: x, y: y);
    }
    final nx = (h[0] * x + h[1] * y + h[2]) / w;
    final ny = (h[3] * x + h[4] * y + h[5]) / w;
    return (x: nx, y: ny);
  }

  /// Corrige la perspective en se basant sur la détection de cercles (ellipses)
  /// dans l'image.
  ///
  /// Cette méthode tente de détecter l'ellipse la plus proéminente (la cible)
  /// et calcule une transformation pour la rendre parfaitement circulaire.
  Future<String> correctPerspectiveUsingCircles(String imagePath) async {
    try {
      // 1. Charger l'image avec OpenCV
      final src = cv.imread(imagePath, flags: cv.IMREAD_COLOR);
      if (src.isEmpty) throw Exception("Impossible de charger l'image");

      // 2. Prétraitement
      final gray = cv.cvtColor(src, cv.COLOR_BGR2GRAY);
      final blurred = cv.gaussianBlur(gray, (5, 5), 0);

      // Canny edge detector avec seuil adaptatif (Otsu)
      final thresh = cv.threshold(
        blurred,
        0,
        255,
        cv.THRESH_BINARY | cv.THRESH_OTSU,
      );
      final edges = cv.canny(blurred, thresh.$1 * 0.5, thresh.$1);

      // 3. Trouver les contours
      final contoursResult = cv.findContours(
        edges,
        cv.RETR_EXTERNAL,
        cv.CHAIN_APPROX_SIMPLE,
      );
      final contours = contoursResult.$1;

      if (contours.isEmpty) return imagePath; // Pas de contours trouvés

      // 4. Trouver le meilleur candidat ellipse
      cv.RotatedRect? bestEllipse;
      double maxArea = 0;

      for (final contour in contours) {
        if (contour.length < 5) {
          continue; // fitEllipse nécessite au moins 5 points
        }

        final area = cv.contourArea(contour);
        if (area < 1000) continue; // Ignorer les trop petits bruits

        final ellipse = cv.fitEllipse(contour);

        // Critère de sélection: on cherche la plus grande ellipse qui est proche d'un cercle
        // Mais comme on veut corriger la distorsion, elle PEUT être aplatie.
        // Donc on prend juste la plus grande ellipse raisonnablement centrée.
        if (area > maxArea) {
          maxArea = area;
          bestEllipse = ellipse;
        }
      }

      if (bestEllipse == null) return imagePath;

      // 5. Calculer la transformation perspective
      // L'idée est de mapper les 4 sommets de l'ellipse détectée vers un cercle parfait.
      // Ou plus simplement, mapper le rectangle englobant de l'ellipse vers un carré.

      // Points source: les 4 coins du rotated rect de l'ellipse
      // Note: opencv_dart RotatedRect points() non dispo directement?
      // On peut utiliser boxPoints(ellipse)
      final boxPoints = cv.boxPoints(bestEllipse);
      // boxPoints returns Mat (4x2 float32)

      // Extraire les 4 points
      final List<cv.Point> srcPoints = [];

      for (int i = 0; i < boxPoints.length; i++) {
        // On accède directement au point à l'index i
        final point2f = boxPoints[i];

        // On convertit les coordonnées float en int pour cv.Point
        srcPoints.add(cv.Point(point2f.x.toInt(), point2f.y.toInt()));
      }

      // Trier les points pour avoir: TL, TR, BR, BL
      _sortPoints(srcPoints);

      // Dimensions cibles
      final side = math
          .max(bestEllipse.size.width, bestEllipse.size.height)
          .toInt();

      final List<cv.Point> dstPoints = [
        cv.Point(0, 0),
        cv.Point(side, 0),
        cv.Point(side, side),
        cv.Point(0, side),
      ];

      // Matrice de perspective
      final M = cv.getPerspectiveTransform(
        cv.VecPoint.fromList(srcPoints),
        cv.VecPoint.fromList(dstPoints),
      );

      // 6. Warper l'image
      final corrected = cv.warpPerspective(src, M, (side, side));

      // 7. Sauvegarder
      final tempDir = await getTemporaryDirectory();
      final timestamp = DateTime.now().millisecondsSinceEpoch;
      final outputPath = '${tempDir.path}/corrected_circle_$timestamp.jpg';

      cv.imwrite(outputPath, corrected);

      return outputPath;
    } catch (e) {
      // En cas d'erreur, retourner l'image originale
      debugPrint('Erreur correction perspective cercles: $e');
      return imagePath;
    }
  }

  /// Trie les points dans l'ordre: Top-Left, Top-Right, Bottom-Right, Bottom-Left
  void _sortPoints(List<cv.Point> points) {
    // Calculer le centre de gravité
    double cx = 0;
    double cy = 0;
    for (final p in points) {
      cx += p.x;
      cy += p.y;
    }
    cx /= points.length;
    cy /= points.length;

    points.sort((a, b) {
      // Trier par angle autour du centre
      final angleA = math.atan2(a.y - cy, a.x - cx);
      final angleB = math.atan2(b.y - cy, b.x - cx);
      return angleA.compareTo(angleB);
    });

    // Re-trier pour être sûr:
    points.sort((a, b) => (a.y + a.x).compareTo(b.y + b.x));
    final tl = points[0];
    final br = points[3];

    // Reste tr et bl
    final remaining = [points[1], points[2]];
    remaining.sort((a, b) => a.x.compareTo(b.x));
    final bl = remaining[0];
    final tr = remaining[1];

    points[0] = tl;
    points[1] = tr;
    points[2] = br;
    points[3] = bl;
  }

  /// Corrige la perspective en reformant le plus grand ovale (ellipse) en un cercle parfait,
  /// sans recadrer agressivement l'image entière.
  Future<String> correctPerspectiveUsingOvals(String imagePath) async {
    try {
      final src = cv.imread(imagePath, flags: cv.IMREAD_COLOR);
      if (src.isEmpty) throw Exception("Impossible de charger l'image");

      final gray = cv.cvtColor(src, cv.COLOR_BGR2GRAY);
      final blurred = cv.gaussianBlur(gray, (5, 5), 0);

      final thresh = cv.threshold(
        blurred,
        0,
        255,
        cv.THRESH_BINARY | cv.THRESH_OTSU,
      );
      final edges = cv.canny(blurred, thresh.$1 * 0.5, thresh.$1);

      final contoursResult = cv.findContours(
        edges,
        cv.RETR_EXTERNAL,
        cv.CHAIN_APPROX_SIMPLE,
      );
      final contours = contoursResult.$1;

      if (contours.isEmpty) return imagePath;

      cv.RotatedRect? bestEllipse;
      double maxArea = 0;

      for (final contour in contours) {
        if (contour.length < 5) continue;
        final area = cv.contourArea(contour);
        if (area < 1000) continue;

        final ellipse = cv.fitEllipse(contour);
        if (area > maxArea) {
          maxArea = area;
          bestEllipse = ellipse;
        }
      }

      if (bestEllipse == null) return imagePath;

      // The goal here is to morph the bestEllipse into a perfect circle, while
      // keeping the image the same size and the center of the ellipse in the same place.
      // We'll use the average of the width and height (or max) to define the target circle
      final targetRadius =
          math.max(bestEllipse.size.width, bestEllipse.size.height) / 2.0;

      // Extract the 4 bounding box points of the ellipse
      final boxPoints = cv.boxPoints(bestEllipse);
      final List<cv.Point> srcPoints = [];
      for (int i = 0; i < boxPoints.length; i++) {
        srcPoints.add(cv.Point(boxPoints[i].x.toInt(), boxPoints[i].y.toInt()));
      }
      _sortPoints(srcPoints);

      // Calculate the size of the perfectly squared output image
      final int side = (targetRadius * 2).toInt();

      final List<cv.Point> dstPoints = [
        cv.Point(0, 0), // Top-Left
        cv.Point(side, 0), // Top-Right
        cv.Point(side, side), // Bottom-Right
        cv.Point(0, side), // Bottom-Left
      ];

      // Morph the target region into a perfect square, cropping the rest of the image
      final M = cv.getPerspectiveTransform(
        cv.VecPoint.fromList(srcPoints),
        cv.VecPoint.fromList(dstPoints),
      );

      final corrected = cv.warpPerspective(src, M, (side, side));

      final tempDir = await getTemporaryDirectory();
      final timestamp = DateTime.now().millisecondsSinceEpoch;
      final outputPath = '${tempDir.path}/corrected_oval_$timestamp.jpg';

      cv.imwrite(outputPath, corrected);

      return outputPath;
    } catch (e) {
      debugPrint('Erreur correction perspective ovales: $e');
      return imagePath;
    }
  }

  /// Corrige la distorsion et la profondeur (perspective) en créant un maillage
  /// basé sur la concentricité des différents cercles de la cible pour trouver le meilleur plan.
  Future<String> correctPerspectiveWithConcentricMesh(String imagePath) async {
    try {
      final src = cv.imread(imagePath, flags: cv.IMREAD_COLOR);
      if (src.isEmpty) throw Exception("Impossible de charger l'image");

      final gray = cv.cvtColor(src, cv.COLOR_BGR2GRAY);
      final blurred = cv.gaussianBlur(gray, (5, 5), 0);
      final thresh = cv.threshold(
        blurred,
        0,
        255,
        cv.THRESH_BINARY | cv.THRESH_OTSU,
      );
      final edges = cv.canny(blurred, thresh.$1 * 0.5, thresh.$1);

      final contoursResult = cv.findContours(
        edges,
        cv.RETR_LIST,
        cv.CHAIN_APPROX_SIMPLE,
      );
      final contours = contoursResult.$1;
      if (contours.isEmpty) return imagePath;

      List<cv.RotatedRect> ellipses = [];
      for (final contour in contours) {
        if (contour.length < 5) continue;
        if (cv.contourArea(contour) < 500) continue;
        ellipses.add(cv.fitEllipse(contour));
      }

      if (ellipses.isEmpty) return imagePath;

      // Find the largest ellipse to serve as our central reference
      ellipses.sort(
        (a, b) => (b.size.width * b.size.height).compareTo(
          a.size.width * a.size.height,
        ),
      );
      final largestEllipse = ellipses.first;
      final maxDist =
          math.max(largestEllipse.size.width, largestEllipse.size.height) *
          0.15;

      // Group all ellipses that are roughly concentric with the largest one
      List<cv.RotatedRect> concentricGroup = [];
      for (final e in ellipses) {
        final dx = e.center.x - largestEllipse.center.x;
        final dy = e.center.y - largestEllipse.center.y;
        if (math.sqrt(dx * dx + dy * dy) < maxDist) {
          concentricGroup.add(e);
        }
      }

      if (concentricGroup.length < 2) {
        debugPrint(
          "Pas assez de cercles concentriques pour le maillage, utilisation de la méthode simple.",
        );
        return await correctPerspectiveUsingOvals(imagePath);
      }

      final targetRadius =
          math.max(largestEllipse.size.width, largestEllipse.size.height) / 2.0;
      final int side = (targetRadius * 2.4).toInt(); // Add padding
      final double cx = side / 2.0;
      final double cy = side / 2.0;

      List<cv.Point2f> srcPointsList = [];
      List<cv.Point2f> dstPointsList = [];

      for (final ellipse in concentricGroup) {
        final box = cv.boxPoints(ellipse);
        final m0 = cv.Point2f(
          (box[0].x + box[1].x) / 2,
          (box[0].y + box[1].y) / 2,
        );
        final m1 = cv.Point2f(
          (box[1].x + box[2].x) / 2,
          (box[1].y + box[2].y) / 2,
        );
        final m2 = cv.Point2f(
          (box[2].x + box[3].x) / 2,
          (box[2].y + box[3].y) / 2,
        );
        final m3 = cv.Point2f(
          (box[3].x + box[0].x) / 2,
          (box[3].y + box[0].y) / 2,
        );

        final d02 = math.sqrt(
          math.pow(m0.x - m2.x, 2) + math.pow(m0.y - m2.y, 2),
        );
        final d13 = math.sqrt(
          math.pow(m1.x - m3.x, 2) + math.pow(m1.y - m3.y, 2),
        );

        cv.Point2f maj1, maj2, min1, min2;
        double r;

        if (d02 > d13) {
          maj1 = m0;
          maj2 = m2;
          min1 = m1;
          min2 = m3;
          r = d02 / 2.0;
        } else {
          maj1 = m1;
          maj2 = m3;
          min1 = m0;
          min2 = m2;
          r = d13 / 2.0;
        }

        // Sort maj1 and maj2 so maj1 is left/top
        if ((maj1.x - maj2.x).abs() > (maj1.y - maj2.y).abs()) {
          if (maj1.x > maj2.x) {
            final t = maj1;
            maj1 = maj2;
            maj2 = t;
          }
        } else {
          if (maj1.y > maj2.y) {
            final t = maj1;
            maj1 = maj2;
            maj2 = t;
          }
        }

        // Sort min1 and min2 so min1 is top/left
        if ((min1.y - min2.y).abs() > (min1.x - min2.x).abs()) {
          if (min1.y > min2.y) {
            final t = min1;
            min1 = min2;
            min2 = t;
          }
        } else {
          if (min1.x > min2.x) {
            final t = min1;
            min1 = min2;
            min2 = t;
          }
        }

        srcPointsList.addAll([maj1, maj2, min1, min2]);
        dstPointsList.addAll([
          cv.Point2f(cx - r, cy),
          cv.Point2f(cx + r, cy),
          cv.Point2f(cx, cy - r),
          cv.Point2f(cx, cy + r),
        ]);

        // Add ellipse centers mapping perfectly to the origin to force concentric depth alignment
        srcPointsList.add(cv.Point2f(ellipse.center.x, ellipse.center.y));
        dstPointsList.add(cv.Point2f(cx, cy));
      }

      // We explicitly convert points to VecPoint to use findHomography standard binding
      final srcVec = cv.VecPoint.fromList(
        srcPointsList.map((p) => cv.Point(p.x.toInt(), p.y.toInt())).toList(),
      );
      final dstVec = cv.VecPoint.fromList(
        dstPointsList.map((p) => cv.Point(p.x.toInt(), p.y.toInt())).toList(),
      );

      final M = cv.findHomography(
        cv.Mat.fromVec(srcVec),
        cv.Mat.fromVec(dstVec),
        method: cv.RANSAC,
      );

      if (M.isEmpty) {
        return await correctPerspectiveUsingOvals(imagePath);
      }

      final corrected = cv.warpPerspective(src, M, (side, side));

      final tempDir = await getTemporaryDirectory();
      final timestamp = DateTime.now().millisecondsSinceEpoch;
      final outputPath = '${tempDir.path}/corrected_mesh_$timestamp.jpg';
      cv.imwrite(outputPath, corrected);

      return outputPath;
    } catch (e) {
      debugPrint('Erreur correction perspective maillage concentrique: $e');
      return imagePath;
    }
  }

  /// Corrige la perspective en détectant les 4 coins de la feuille (quadrilatère)
  ///
  /// Cette méthode cherche le plus grand polygone à 4 côtés (le bord du papier)
  /// et le déforme pour en faire un carré parfait.
  Future<String> correctPerspectiveUsingQuadrilateral(String imagePath) async {
    try {
      final src = cv.imread(imagePath, flags: cv.IMREAD_COLOR);
      if (src.isEmpty) throw Exception("Impossible de charger l'image");

      final gray = cv.cvtColor(src, cv.COLOR_BGR2GRAY);
      // Flou plus important pour ignorer les détails internes (cercles, trous)
      final blurred = cv.gaussianBlur(gray, (9, 9), 0);

      // Canny edge detector
      final thresh = cv.threshold(
        blurred,
        0,
        255,
        cv.THRESH_BINARY | cv.THRESH_OTSU,
      );
      final edges = cv.canny(blurred, thresh.$1 * 0.5, thresh.$1);

      // Pour la détection de la feuille (les bords peuvent être discontinus à cause de l'éclairage)
      final kernel = cv.getStructuringElement(cv.MORPH_RECT, (5, 5));
      final closedEdges = cv.morphologyEx(edges, cv.MORPH_CLOSE, kernel);

      // Find contours
      final contoursResult = cv.findContours(
        closedEdges,
        cv.RETR_EXTERNAL,
        cv.CHAIN_APPROX_SIMPLE,
      );
      final contours = contoursResult.$1;

      cv.VecPoint? bestQuad;
      double maxArea = 0;

      final minArea = src.rows * src.cols * 0.1; // Au moins 10% de l'image

      for (final contour in contours) {
        final area = cv.contourArea(contour);
        if (area < minArea) continue;

        final peri = cv.arcLength(contour, true);
        // Approximation polygonale (tolérance = 2% à 5% du périmètre)
        final approx = cv.approxPolyDP(contour, 0.04 * peri, true);

        if (approx.length == 4) {
          if (area > maxArea) {
            maxArea = area;
            bestQuad = approx;
          }
        }
      }

      // Fallback
      if (bestQuad == null) {
        debugPrint(
          "Aucun papier quadrilatère détecté, on utilise les cercles à la place.",
        );
        return await correctPerspectiveUsingCircles(imagePath);
      }

      // Convert to List<cv.Point>
      final List<cv.Point> srcPoints = [];
      for (int i = 0; i < bestQuad.length; i++) {
        srcPoints.add(bestQuad[i]);
      }

      _sortPoints(srcPoints);

      // Calculate max width and height
      double widthA = _distanceCV(srcPoints[2], srcPoints[3]);
      double widthB = _distanceCV(srcPoints[1], srcPoints[0]);
      int dstWidth = math.max(widthA, widthB).toInt();

      double heightA = _distanceCV(srcPoints[1], srcPoints[2]);
      double heightB = _distanceCV(srcPoints[0], srcPoints[3]);
      int dstHeight = math.max(heightA, heightB).toInt();

      // Since standard target paper forms a square, we force the resulting warp to be a perfect square.
      int side = math.max(dstWidth, dstHeight);

      final List<cv.Point> dstPoints = [
        cv.Point(0, 0),
        cv.Point(side, 0),
        cv.Point(side, side),
        cv.Point(0, side),
      ];

      final M = cv.getPerspectiveTransform(
        cv.VecPoint.fromList(srcPoints),
        cv.VecPoint.fromList(dstPoints),
      );

      final corrected = cv.warpPerspective(src, M, (side, side));

      final tempDir = await getTemporaryDirectory();
      final timestamp = DateTime.now().millisecondsSinceEpoch;
      final outputPath = '${tempDir.path}/corrected_quad_$timestamp.jpg';

      cv.imwrite(outputPath, corrected);

      return outputPath;
    } catch (e) {
      debugPrint('Erreur correction perspective quadrilatère: $e');
      // Fallback
      return await correctPerspectiveUsingCircles(imagePath);
    }
  }

  double _distanceCV(cv.Point p1, cv.Point p2) {
    final dx = p2.x - p1.x;
    final dy = p2.y - p1.y;
    return math.sqrt(dx * dx + dy * dy);
  }
}
