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

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

import 'card_detector.dart';

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

class CropResult {
  const CropResult({
    required this.bytes,
    required this.width,
    required this.height,
    required this.usedDetection,
  });

  final Uint8List bytes;
  final int width;
  final int height;
  final bool usedDetection;
}

/// Crop + orientation correct from detection box (or guide fallback).
class CardCropper {
  /// [box] in source image pixel coords. If null, uses centered guide.
  /// When [box] has tilted [CardBox.corners], crops the quad interior
  /// via perspective warp (exactly the green lock region).
  static Future<CropResult> cropAndCorrect({
    required String imagePath,
    CardBox? box,
    double padding = 0.0,
  }) async {
    return computeCrop(imagePath, box, padding);
  }

  static Future<CropResult> computeCrop(
    String imagePath,
    CardBox? box,
    double padding,
  ) async {
    final raw = await File(imagePath).readAsBytes();
    var decoded = img.decodeImage(raw);
    if (decoded == null) {
      throw StateError('Failed to decode image: $imagePath');
    }

    // Bake EXIF orientation into pixels.
    decoded = img.bakeOrientation(decoded);

    final usedDetection = box != null;
    img.Image out;

    if (box != null && box.corners != null && box.corners!.length == 4) {
      out = _perspectiveCrop(decoded, box.quad);
    } else if (box != null) {
      final w = decoded.width.toDouble();
      final h = decoded.height.toDouble();
      var cropRect = _expandAndClamp(box.rect, w, h, padding);
      cropRect = _fitAspect(cropRect, w, h, kCardAspect);
      out = _axisCrop(decoded, cropRect);
    } else {
      final w = decoded.width.toDouble();
      final h = decoded.height.toDouble();
      out = _axisCrop(decoded, _guideRect(w, h, kCardAspect));
    }

    // Mild straighten: if wider than tall but we expect portrait card, rotate.
    if (out.width > out.height * 1.05) {
      out = img.copyRotate(out, angle: 90);
    }

    // Resize to consistent card-proportional size so hash is scale-invariant.
    const cropW = 280;
    const cropH = 390;
    out = img.copyResize(out, width: cropW, height: cropH,
        interpolation: img.Interpolation.cubic);

    // Trim border fringe so desktop/table edges don't leak into the hash.
    const borderTrim = 10;
    out = img.copyCrop(out,
        x: borderTrim,
        y: borderTrim,
        width: cropW - borderTrim * 2,
        height: cropH - borderTrim * 2);

    final bytes = Uint8List.fromList(img.encodeJpg(out, quality: 98));
    return CropResult(
      bytes: bytes,
      width: out.width,
      height: out.height,
      usedDetection: usedDetection,
    );
  }

  /// Perspective-warp the interior of TL→TR→BR→BL into a flat card image.
  ///
  /// Corner order from detection is image-space; a tilted shot can map the
  /// card's long edge as "top". Re-orient so output is always portrait.
  static img.Image _perspectiveCrop(img.Image src, List<Offset> quad) {
    var tl = quad[0];
    var tr = quad[1];
    var br = quad[2];
    var bl = quad[3];

    double avg(double a, double b) => (a + b) / 2;
    var topW = avg((tr - tl).distance, (br - bl).distance);
    var sideH = avg((bl - tl).distance, (br - tr).distance);

    // Short edge = card width, long edge = card height (TCG portrait).
    if (topW > sideH) {
      final nTl = tr;
      final nTr = br;
      final nBr = bl;
      final nBl = tl;
      tl = nTl;
      tr = nTr;
      br = nBr;
      bl = nBl;
      final t = topW;
      topW = sideH;
      sideH = t;
    }

    // Prefer the edge that sits higher in the photo as the card top.
    final topY = avg(tl.dy, tr.dy);
    final botY = avg(bl.dy, br.dy);
    if (topY > botY) {
      final nTl = br;
      final nTr = bl;
      final nBr = tl;
      final nBl = tr;
      tl = nTl;
      tr = nTr;
      br = nBr;
      bl = nBl;
    }

    var outW = topW.round().clamp(32, 4096);
    var outH = sideH.round().clamp(32, 4096);

    // Prefer portrait TCG aspect while preserving measured size scale.
    const targetAspect = kCardAspect;
    if (outW / outH > targetAspect * 1.08) {
      outW = (outH * targetAspect).round().clamp(32, 4096);
    } else if (outW / outH < targetAspect / 1.08) {
      outH = (outW / targetAspect).round().clamp(32, 4096);
    }

    final dst = img.Image(width: outW, height: outH);
    var out = img.copyRectify(
      src,
      topLeft: img.Point(tl.dx, tl.dy),
      topRight: img.Point(tr.dx, tr.dy),
      bottomLeft: img.Point(bl.dx, bl.dy),
      bottomRight: img.Point(br.dx, br.dy),
      interpolation: img.Interpolation.cubic,
      toImage: dst,
    );

    // Safety net if mapping still came out landscape.
    if (out.width > out.height * 1.05) {
      out = img.copyRotate(out, angle: 90);
    }
    return out;
  }

  static img.Image _axisCrop(img.Image decoded, Rect cropRect) {
    final x = cropRect.left.round().clamp(0, decoded.width - 1);
    final y = cropRect.top.round().clamp(0, decoded.height - 1);
    final cw = cropRect.width.round().clamp(1, decoded.width - x);
    final ch = cropRect.height.round().clamp(1, decoded.height - y);
    return img.copyCrop(decoded, x: x, y: y, width: cw, height: ch);
  }

  static Rect _expandAndClamp(Rect r, double w, double h, double pad) {
    final dx = r.width * pad;
    final dy = r.height * pad;
    return Rect.fromLTRB(
      (r.left - dx).clamp(0, w),
      (r.top - dy).clamp(0, h),
      (r.right + dx).clamp(0, w),
      (r.bottom + dy).clamp(0, h),
    );
  }

  /// Shrink the oversized side to match [aspect] (avoid growing into background).
  static Rect _fitAspect(Rect r, double w, double h, double aspect) {
    final cx = r.center.dx;
    final cy = r.center.dy;
    var rw = r.width;
    var rh = r.height;
    if (rw / rh > aspect) {
      rw = rh * aspect;
    } else {
      rh = rw / aspect;
    }
    final scale = math.min(1.0, math.min(w / rw, h / rh));
    rw *= scale;
    rh *= scale;
    return Rect.fromCenter(center: Offset(cx, cy), width: rw, height: rh)
        .intersect(Rect.fromLTWH(0, 0, w, h));
  }

  static Rect _guideRect(double w, double h, double aspect) {
    // ~70% of the shorter side as card height.
    final cardH = math.min(h * 0.72, w / aspect * 0.9);
    final cardW = cardH * aspect;
    return Rect.fromCenter(
      center: Offset(w / 2, h / 2),
      width: cardW,
      height: cardH,
    );
  }
}
