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

import 'package:flutter/material.dart';
import 'package:perfect_freehand/perfect_freehand.dart' as pf;

import '../models/ink_point.dart';
import '../models/ink_stroke.dart';
import '../models/pen_tool.dart';
import '../models/pressure_curve.dart';

/// Renders ink strokes to a PNG byte array for local OCR.
class StrokeRasterizer {
  static const _padding = 24.0;
  static const _defaultPressureCurve = PressureCurve.linear;

  /// Render [strokes] onto a white canvas and return PNG bytes, or null if empty.
  static Future<Uint8List?> render(List<InkStroke> strokes) async {
    final drawable = strokes
        .where((s) => s.tool != PenTool.eraser && s.points.isNotEmpty)
        .toList();
    if (drawable.isEmpty) return null;

    final bounds = _computeBounds(drawable);
    if (bounds == null) return null;

    final width = (bounds.width + _padding * 2).ceil().clamp(1, 4096);
    final height = (bounds.height + _padding * 2).ceil().clamp(1, 4096);
    final offset = Offset(_padding - bounds.left, _padding - bounds.top);

    final recorder = ui.PictureRecorder();
    final canvas = Canvas(recorder);
    canvas.drawRect(
      Rect.fromLTWH(0, 0, width.toDouble(), height.toDouble()),
      Paint()..color = Colors.white,
    );

    for (final stroke in drawable) {
      _drawStroke(canvas, stroke, offset);
    }

    final picture = recorder.endRecording();
    final image = await picture.toImage(width, height);
    final byteData = await image.toByteData(format: ui.ImageByteFormat.png);
    return byteData?.buffer.asUint8List();
  }

  static Rect? _computeBounds(List<InkStroke> strokes) {
    double? minX, minY, maxX, maxY;
    for (final stroke in strokes) {
      for (final p in stroke.points) {
        minX = minX == null ? p.x : min(minX, p.x);
        minY = minY == null ? p.y : min(minY, p.y);
        maxX = maxX == null ? p.x : max(maxX, p.x);
        maxY = maxY == null ? p.y : max(maxY, p.y);
      }
    }
    if (minX == null || minY == null || maxX == null || maxY == null) {
      return null;
    }
    return Rect.fromLTRB(minX, minY, maxX, maxY);
  }

  static List<InkPoint> _offsetPoints(List<InkPoint> points, Offset offset) {
    return points
        .map(
          (p) => InkPoint(
            x: p.x + offset.dx,
            y: p.y + offset.dy,
            pressure: p.pressure,
            timestamp: p.timestamp,
          ),
        )
        .toList();
  }

  static void _drawStroke(Canvas canvas, InkStroke stroke, Offset offset) {
    final points = _offsetPoints(stroke.points, offset);
    final color = Color(stroke.color);
    final tool = stroke.tool;

    switch (tool) {
      case PenTool.pen:
      case PenTool.marker:
      case PenTool.highlighter:
        _drawFreehand(canvas, points, tool, color, stroke.strokeWidth);
        break;
      case PenTool.rectangle:
        if (points.length >= 2) {
          _drawRect(canvas, points, color, stroke.strokeWidth, stroke.filled);
        } else {
          _drawFreehand(canvas, points, tool, color, stroke.strokeWidth);
        }
        break;
      case PenTool.ellipse:
        if (points.length >= 2) {
          _drawOval(canvas, points, color, stroke.strokeWidth, stroke.filled);
        } else {
          _drawFreehand(canvas, points, tool, color, stroke.strokeWidth);
        }
        break;
      case PenTool.line:
        if (points.length >= 2) {
          _drawLine(canvas, points, color, stroke.strokeWidth);
        } else {
          _drawFreehand(canvas, points, tool, color, stroke.strokeWidth);
        }
        break;
      case PenTool.arrow:
        if (points.length >= 2) {
          _drawArrow(canvas, points, color, stroke.strokeWidth);
        } else {
          _drawFreehand(canvas, points, tool, color, stroke.strokeWidth);
        }
        break;
      case PenTool.text:
        if (stroke.textContent != null && stroke.textContent!.isNotEmpty) {
          _drawText(
            canvas,
            points,
            stroke.textContent!,
            stroke.fontSize,
            color,
          );
        }
        break;
      case PenTool.eraser:
        break;
    }
  }

  static void _drawFreehand(
    Canvas canvas,
    List<InkPoint> points,
    PenTool tool,
    Color color,
    double strokeWidth,
  ) {
    final pfPoints = points
        .map(
          (p) => pf.PointVector(
            p.x,
            p.y,
            _defaultPressureCurve.apply(p.pressure).clamp(0.0, 1.0),
          ),
        )
        .toList();

    final thinning = (tool == PenTool.marker || tool == PenTool.highlighter)
        ? 0.0
        : 0.7;

    final outline = pf.getStroke(
      pfPoints,
      options: pf.StrokeOptions(
        size: strokeWidth,
        thinning: thinning,
        smoothing: 0.5,
        streamline: 0.5,
        simulatePressure: tool != PenTool.marker && tool != PenTool.highlighter,
        isComplete: true,
      ),
    );
    if (outline.isEmpty) return;

    final path = Path()..moveTo(outline[0].dx, outline[0].dy);
    for (var i = 1; i < outline.length; i++) {
      path.lineTo(outline[i].dx, outline[i].dy);
    }
    path.close();

    canvas.drawPath(
      path,
      Paint()
        ..color = color
        ..style = PaintingStyle.fill
        ..isAntiAlias = true,
    );
  }

  static void _drawRect(
    Canvas canvas,
    List<InkPoint> points,
    Color color,
    double strokeWidth,
    bool filled,
  ) {
    final rect = Rect.fromPoints(
      Offset(points[0].x, points[0].y),
      Offset(points[1].x, points[1].y),
    );
    canvas.drawRect(
      rect,
      Paint()
        ..color = color
        ..strokeWidth = strokeWidth
        ..style = filled ? PaintingStyle.fill : PaintingStyle.stroke
        ..isAntiAlias = true,
    );
  }

  static void _drawOval(
    Canvas canvas,
    List<InkPoint> points,
    Color color,
    double strokeWidth,
    bool filled,
  ) {
    final rect = Rect.fromPoints(
      Offset(points[0].x, points[0].y),
      Offset(points[1].x, points[1].y),
    );
    canvas.drawOval(
      rect,
      Paint()
        ..color = color
        ..strokeWidth = strokeWidth
        ..style = filled ? PaintingStyle.fill : PaintingStyle.stroke
        ..isAntiAlias = true,
    );
  }

  static void _drawLine(
    Canvas canvas,
    List<InkPoint> points,
    Color color,
    double strokeWidth,
  ) {
    canvas.drawLine(
      Offset(points[0].x, points[0].y),
      Offset(points[1].x, points[1].y),
      Paint()
        ..color = color
        ..strokeWidth = strokeWidth
        ..strokeCap = StrokeCap.round
        ..isAntiAlias = true,
    );
  }

  static void _drawArrow(
    Canvas canvas,
    List<InkPoint> points,
    Color color,
    double strokeWidth,
  ) {
    final start = Offset(points[0].x, points[0].y);
    final end = Offset(points[1].x, points[1].y);
    canvas.drawLine(
      start,
      end,
      Paint()
        ..color = color
        ..strokeWidth = strokeWidth
        ..strokeCap = StrokeCap.round
        ..isAntiAlias = true,
    );

    final angle = atan2(end.dy - start.dy, end.dx - start.dx);
    const headLength = 12.0;
    const headAngle = pi / 6;
    final p1 =
        end +
        Offset(
          -headLength * cos(angle - headAngle),
          -headLength * sin(angle - headAngle),
        );
    final p2 =
        end +
        Offset(
          -headLength * cos(angle + headAngle),
          -headLength * sin(angle + headAngle),
        );
    final head = Path()
      ..moveTo(end.dx, end.dy)
      ..lineTo(p1.dx, p1.dy)
      ..lineTo(p2.dx, p2.dy)
      ..close();
    canvas.drawPath(
      head,
      Paint()
        ..color = color
        ..style = PaintingStyle.fill,
    );
  }

  static void _drawText(
    Canvas canvas,
    List<InkPoint> points,
    String text,
    double fontSize,
    Color color,
  ) {
    if (points.isEmpty) return;
    final painter = TextPainter(
      text: TextSpan(
        text: text,
        style: TextStyle(color: color, fontSize: fontSize),
      ),
      textDirection: TextDirection.ltr,
    )..layout();
    painter.paint(canvas, Offset(points[0].x, points[0].y));
  }
}
