import 'dart:math' as math;
import 'dart:ui';

import 'package:google_mlkit_object_detection/google_mlkit_object_detection.dart';
import 'package:image/image.dart' as img;

import 'card_corner_refiner.dart';

/// TCG card aspect (width / height), ~63×88 mm.
const double _kCardAspect = 63 / 88;

/// Detected card-like region in image pixel coordinates.
class CardBox {
  const CardBox({
    required this.rect,
    required this.confidence,
    this.label,
    this.corners,
  });

  /// Axis-aligned outer bounds (always set).
  final Rect rect;
  final double confidence;
  final String? label;

  /// Tilted card quad in image pixels, ordered TL → TR → BR → BL.
  /// When null, [rect] corners are used.
  final List<Offset>? corners;

  /// Effective lock quad (tilted when refined).
  List<Offset> get quad {
    final c = corners;
    if (c != null && c.length == 4) return c;
    return [
      rect.topLeft,
      rect.topRight,
      rect.bottomRight,
      rect.bottomLeft,
    ];
  }

  CardBox copyWith({
    Rect? rect,
    double? confidence,
    String? label,
    List<Offset>? corners,
    bool clearCorners = false,
  }) {
    return CardBox(
      rect: rect ?? this.rect,
      confidence: confidence ?? this.confidence,
      label: label ?? this.label,
      corners: clearCorners ? null : (corners ?? this.corners),
    );
  }
}

/// ML Kit object detection on still images (post-capture lock).
class CardDetector {
  CardDetector() {
    _detector = ObjectDetector(
      options: ObjectDetectorOptions(
        mode: DetectionMode.single,
        classifyObjects: true,
        multipleObjects: true,
      ),
    );
  }

  late final ObjectDetector _detector;

  Future<List<CardBox>> detectFile(String path) async {
    final input = InputImage.fromFilePath(path);
    final objects = await _detector.processImage(input);
    final boxes = <CardBox>[];
    for (final obj in objects) {
      final conf = obj.labels.isEmpty
          ? 0.5
          : obj.labels
              .map((l) => l.confidence)
              .reduce((a, b) => a > b ? a : b);
      final label = obj.labels.isEmpty ? null : obj.labels.first.text;
      boxes.add(
        CardBox(
          rect: Rect.fromLTRB(
            obj.boundingBox.left.toDouble(),
            obj.boundingBox.top.toDouble(),
            obj.boundingBox.right.toDouble(),
            obj.boundingBox.bottom.toDouble(),
          ),
          confidence: conf,
          label: label,
        ),
      );
    }
    boxes.sort((a, b) => b.confidence.compareTo(a.confidence));
    return boxes;
  }

  /// Prefer largest reasonably confident box; bias toward TCG portrait aspect.
  CardBox? pickBest(List<CardBox> boxes, {Size? imageSize}) {
    if (boxes.isEmpty) return null;
    CardBox? best;
    var bestScore = -1.0;
    for (final b in boxes) {
      final area = b.rect.width * b.rect.height;
      final imgArea = imageSize == null
          ? area
          : (imageSize.width * imageSize.height).clamp(1.0, double.infinity);
      final fill = area / imgArea;
      if (fill < 0.04 || fill > 0.98) continue;
      final aspect = b.rect.width / b.rect.height;
      final short = math.min(aspect, 1 / aspect);
      final long = math.max(aspect, 1 / aspect);
      final ratio = short / long;
      // Closer to 63/88 (~0.72) scores higher; square half-art scores lower.
      final aspectScore =
          1.0 - ((ratio - _kCardAspect).abs() / _kCardAspect).clamp(0.0, 1.0);
      final score = b.confidence * 0.3 + fill * 0.4 + aspectScore * 0.3;
      if (score > bestScore) {
        bestScore = score;
        best = b;
      }
    }
    return best ?? boxes.first;
  }

  /// When ML only framed the colorful upper half, grow height to portrait TCG.
  static Rect expandToCardAspect(Rect r, Size imageSize) {
    final aspect = r.width / math.max(r.height, 1.0);
    // Already tall enough for a portrait card.
    if (aspect <= _kCardAspect * 1.12) {
      return r.intersect(Rect.fromLTWH(0, 0, imageSize.width, imageSize.height));
    }

    final targetH = r.width / _kCardAspect;
    // Keep the detected top (art header) and extend downward into the text box.
    var top = r.top;
    var bottom = top + targetH;
    if (bottom > imageSize.height) {
      bottom = imageSize.height;
      top = (bottom - targetH).clamp(0.0, imageSize.height);
    }
    if (top < 0) {
      top = 0;
      bottom = math.min(imageSize.height, targetH);
    }
    return Rect.fromLTRB(r.left, top, r.right, bottom)
        .intersect(Rect.fromLTWH(0, 0, imageSize.width, imageSize.height));
  }

  /// Refine [box] into a tilted quad using pixel edges inside the AABB.
  /// Falls back to a content-shrunk AABB when the blob looks like half a card.
  CardBox refineCorners(img.Image image, CardBox box) {
    final imageSize = Size(image.width.toDouble(), image.height.toDouble());
    final normalized = expandToCardAspect(box.rect, imageSize);
    final corners = CardCornerRefiner.refine(image, normalized);
    if (corners != null) {
      return box.copyWith(rect: normalized, corners: corners);
    }
    final shrunk = CardCornerRefiner.shrinkAabb(image, normalized);
    if (shrunk != null) {
      return box.copyWith(rect: shrunk, clearCorners: true);
    }
    return box.copyWith(rect: normalized, clearCorners: true);
  }

  Future<void> dispose() => _detector.close();
}
