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

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

import 'coin_contour_refiner.dart';

/// Detected coin region in image pixel coordinates.
class CoinBox {
  const CoinBox({
    required this.rect,
    required this.confidence,
    this.center,
    this.radius,
  });

  /// Axis-aligned outer bounds.
  final Rect rect;
  final double confidence;

  /// Refined circle (image pixels). When null, use [rect] inscribed circle.
  final Offset? center;
  final double? radius;

  Offset get effectiveCenter =>
      center ?? Offset(rect.center.dx, rect.center.dy);

  double get effectiveRadius =>
      radius ?? (math.min(rect.width, rect.height) / 2);

  CoinBox copyWith({
    Rect? rect,
    double? confidence,
    Offset? center,
    double? radius,
  }) {
    return CoinBox(
      rect: rect ?? this.rect,
      confidence: confidence ?? this.confidence,
      center: center ?? this.center,
      radius: radius ?? this.radius,
    );
  }
}

/// CoinSnap-style YOLOv5 detector (`assets/ml/detect.tflite`).
///
/// Input: `[1, 320, 320, 3]` float32, pixels / 255, letterbox resize.
/// Output: `[1, 6300, 6]` → `cx, cy, w, h, obj, cls` (already 0–1).
class CoinDetector {
  CoinDetector();

  static const int inputSize = 320;
  static const double confThreshold = 0.35;
  static const double iouThreshold = 0.45;
  static const String assetPath = 'assets/ml/detect.tflite';

  Interpreter? _interpreter;
  bool _loading = false;

  Future<void> ensureLoaded() async {
    if (_interpreter != null || _loading) return;
    _loading = true;
    try {
      _interpreter = await Interpreter.fromAsset(assetPath);
    } finally {
      _loading = false;
    }
  }

  Future<List<CoinBox>> detectImage(img.Image image) async {
    await ensureLoaded();
    final interp = _interpreter;
    if (interp == null) return const [];

    final prepared = _letterbox(image, inputSize);
    final input = _toInputTensor(prepared.canvas);
    // Nested list matches [1, 6300, 6] expected by tflite_flutter.
    final output = List.generate(
      1,
      (_) => List.generate(6300, (_) => List<double>.filled(6, 0.0)),
    );
    final inputNd = List.generate(
      1,
      (_) => List.generate(
        inputSize,
        (y) => List.generate(
          inputSize,
          (x) {
            final i = (y * inputSize + x) * 3;
            return [input[i], input[i + 1], input[i + 2]];
          },
        ),
      ),
    );

    interp.run(inputNd, output);

    final boxes = <CoinBox>[];
    final rows = output[0];
    for (final row in rows) {
      final score = row[4] * row[5];
      if (score < confThreshold) continue;

      // Normalized cx,cy,w,h on the letterboxed 320 canvas.
      final cx = row[0] * inputSize;
      final cy = row[1] * inputSize;
      final w = row[2] * inputSize;
      final h = row[3] * inputSize;

      // Map back from letterbox → original image pixels.
      final left = (cx - w / 2 - prepared.padX) / prepared.scale;
      final top = (cy - h / 2 - prepared.padY) / prepared.scale;
      final right = (cx + w / 2 - prepared.padX) / prepared.scale;
      final bottom = (cy + h / 2 - prepared.padY) / prepared.scale;

      final rect = Rect.fromLTRB(left, top, right, bottom).intersect(
        Rect.fromLTWH(0, 0, image.width.toDouble(), image.height.toDouble()),
      );
      if (rect.width < 8 || rect.height < 8) continue;

      boxes.add(CoinBox(rect: rect, confidence: score));
    }

    return _nms(boxes);
  }

  /// Prefer largest reasonably confident near-circular box.
  CoinBox? pickBest(List<CoinBox> boxes, {Size? imageSize}) {
    if (boxes.isEmpty) return null;
    CoinBox? 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.02 || fill > 0.95) continue;

      final aspect = b.rect.width / math.max(b.rect.height, 1.0);
      final roundScore = 1.0 - (aspect - 1.0).abs().clamp(0.0, 1.0);
      final score = b.confidence * 0.45 + fill * 0.25 + roundScore * 0.3;
      if (score > bestScore) {
        bestScore = score;
        best = b;
      }
    }
    return best ?? boxes.first;
  }

  /// Refine AABB into a circle using local edge intensity.
  CoinBox refineContour(img.Image image, CoinBox box) {
    final refined = CoinContourRefiner.refine(image, box.rect);
    if (refined == null) {
      final r = math.min(box.rect.width, box.rect.height) / 2;
      return box.copyWith(center: box.rect.center, radius: r);
    }
    final side = refined.radius * 2;
    final rect = Rect.fromCenter(
      center: refined.center,
      width: side,
      height: side,
    ).intersect(
      Rect.fromLTWH(0, 0, image.width.toDouble(), image.height.toDouble()),
    );
    return box.copyWith(
      rect: rect,
      center: refined.center,
      radius: refined.radius,
    );
  }

  Future<void> dispose() async {
    _interpreter?.close();
    _interpreter = null;
  }

  static Float32List _toInputTensor(img.Image canvas) {
    final out = Float32List(1 * inputSize * inputSize * 3);
    var i = 0;
    for (var y = 0; y < inputSize; y++) {
      for (var x = 0; x < inputSize; x++) {
        final p = canvas.getPixel(x, y);
        out[i++] = p.r / 255.0;
        out[i++] = p.g / 255.0;
        out[i++] = p.b / 255.0;
      }
    }
    return out;
  }

  static _Letterbox _letterbox(img.Image src, int size) {
    final scale = math.min(size / src.width, size / src.height);
    final nw = math.max(1, (src.width * scale).round());
    final nh = math.max(1, (src.height * scale).round());
    final resized = img.copyResize(
      src,
      width: nw,
      height: nh,
      interpolation: img.Interpolation.linear,
    );
    final canvas = img.Image(width: size, height: size);
    img.fill(canvas, color: img.ColorRgb8(114, 114, 114));
    final padX = ((size - nw) / 2).floor();
    final padY = ((size - nh) / 2).floor();
    img.compositeImage(canvas, resized, dstX: padX, dstY: padY);
    return _Letterbox(canvas: canvas, scale: scale, padX: padX.toDouble(), padY: padY.toDouble());
  }

  static List<CoinBox> _nms(List<CoinBox> boxes) {
    final sorted = [...boxes]..sort((a, b) => b.confidence.compareTo(a.confidence));
    final kept = <CoinBox>[];
    final suppressed = List<bool>.filled(sorted.length, false);
    for (var i = 0; i < sorted.length; i++) {
      if (suppressed[i]) continue;
      kept.add(sorted[i]);
      for (var j = i + 1; j < sorted.length; j++) {
        if (suppressed[j]) continue;
        if (_iou(sorted[i].rect, sorted[j].rect) > iouThreshold) {
          suppressed[j] = true;
        }
      }
    }
    return kept;
  }

  static double _iou(Rect a, Rect b) {
    final inter = a.intersect(b);
    if (inter.isEmpty) return 0;
    final interArea = inter.width * inter.height;
    final union = a.width * a.height + b.width * b.height - interArea;
    return union <= 0 ? 0 : interArea / union;
  }
}

class _Letterbox {
  const _Letterbox({
    required this.canvas,
    required this.scale,
    required this.padX,
    required this.padY,
  });

  final img.Image canvas;
  final double scale;
  final double padX;
  final double padY;
}
