import 'dart:math';

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

import '../models/ink_point.dart';
import '../models/ink_stroke.dart';
import '../models/pen_tool.dart';
import '../models/pointer_device_kind.dart';
import '../models/pressure_curve.dart';
import '../utils/stroke_stabilizer.dart';

/// Controls whether the canvas accepts drawing input or passes events through.
enum InteractionMode { draw, navigate }

class InkCanvas extends StatefulWidget {
  final List<InkStroke> strokes;
  final void Function(InkStroke stroke)? onStrokeComplete;
  final void Function(String strokeId, List<InkStroke> replacements)? onErase;
  final PenTool tool;
  final Color color;
  final double strokeWidth;
  final PressureCurve pressureCurve;
  final StabilizationLevel stabilizationLevel;
  final bool filled;
  final InteractionMode interactionMode;
  final Rect? viewportBounds;

  const InkCanvas({
    super.key,
    required this.strokes,
    this.onStrokeComplete,
    this.onErase,
    this.tool = PenTool.pen,
    this.color = Colors.black,
    this.strokeWidth = 2.0,
    this.pressureCurve = PressureCurve.linear,
    this.stabilizationLevel = StabilizationLevel.none,
    this.filled = false,
    this.interactionMode = InteractionMode.draw,
    this.viewportBounds,
  });

  @override
  State<InkCanvas> createState() => _InkCanvasState();
}

class _InkCanvasState extends State<InkCanvas> {
  final List<InkPoint> _currentPoints = [];
  bool _isDrawing = false;
  PenTool? _activeTool;
  StrokeStabilizer? _stabilizer;

  /// Start point for shape tools.
  InkPoint? _shapeStart;

  /// Whether the active tool is a shape tool (needs only 2 points).
  bool get _isShapeTool {
    final t = _activeTool ?? widget.tool;
    return t == PenTool.rectangle ||
        t == PenTool.ellipse ||
        t == PenTool.line ||
        t == PenTool.arrow;
  }

  /// Whether the active tool is the text tool.
  bool get _isTextTool {
    return (_activeTool ?? widget.tool) == PenTool.text;
  }

  @override
  void initState() {
    super.initState();
    _stabilizer = StrokeStabilizer(level: widget.stabilizationLevel);
  }

  @override
  void didUpdateWidget(InkCanvas oldWidget) {
    super.didUpdateWidget(oldWidget);
    if (oldWidget.stabilizationLevel != widget.stabilizationLevel) {
      _stabilizer = StrokeStabilizer(level: widget.stabilizationLevel);
    }
  }

  InputDeviceKind _mapKind(PointerDeviceKind kind) {
    switch (kind) {
      case PointerDeviceKind.touch:
        return InputDeviceKind.touch;
      case PointerDeviceKind.mouse:
        return InputDeviceKind.mouse;
      case PointerDeviceKind.stylus:
        return InputDeviceKind.stylus;
      case PointerDeviceKind.invertedStylus:
        return InputDeviceKind.invertedStylus;
      case PointerDeviceKind.trackpad:
        return InputDeviceKind.trackpad;
      case PointerDeviceKind.unknown:
        return InputDeviceKind.unknown;
    }
  }

  InkPoint _makePoint(PointerEvent event) {
    return InkPoint(
      x: event.localPosition.dx,
      y: event.localPosition.dy,
      pressure: event.pressure,
      tilt: event is PointerMoveEvent ? event.tilt : 0.0,
      timestamp: event.timeStamp.inMicroseconds,
      pointerDeviceKind: _mapKind(event.kind),
    );
  }

  void _handlePointerDown(PointerDownEvent event) {
    if (event.kind == PointerDeviceKind.trackpad) return;

    // In navigate mode, no drawing at all — pass all events through.
    if (widget.interactionMode == InteractionMode.navigate) return;

    // In draw mode: stylus and mouse draw, touch passes through for scrolling.
    if (event.kind == PointerDeviceKind.touch) return;

    _isDrawing = true;
    _activeTool = widget.tool;

    if (event.kind == PointerDeviceKind.invertedStylus) {
      _activeTool = PenTool.eraser;
    }

    final point = _makePoint(event);

    if (_activeTool == PenTool.eraser) {
      _eraseAt(point);
    } else if (_isTextTool) {
      // Text tool: record position, handled on pointer up
      _shapeStart = point;
    } else if (_isShapeTool) {
      // Shape tool: record start point
      _shapeStart = point;
      _stabilizer?.reset();
      setState(() {
        _currentPoints.clear();
        _currentPoints.add(point);
      });
    } else {
      // Freehand tools (pen, marker, highlighter)
      _stabilizer?.reset();
      final smoothed = _stabilizer?.filter(point) ?? point;
      setState(() {
        _currentPoints.clear();
        _currentPoints.add(smoothed);
      });
    }
  }

  void _handlePointerMove(PointerMoveEvent event) {
    if (!_isDrawing) return;

    final point = _makePoint(event);

    if (_activeTool == PenTool.eraser) {
      _eraseAt(point);
    } else if (_isTextTool) {
      // No preview for text tool
      return;
    } else if (_isShapeTool) {
      // Shape preview: keep only start + current
      setState(() {
        if (_currentPoints.length >= 2) {
          _currentPoints[1] = point;
        } else {
          _currentPoints.add(point);
        }
      });
    } else {
      // Freehand
      final smoothed = _stabilizer?.filter(point) ?? point;
      setState(() {
        _currentPoints.add(smoothed);
      });
    }
  }

  void _handlePointerUp(PointerUpEvent event) {
    if (!_isDrawing) return;
    _isDrawing = false;

    final activeTool = _activeTool ?? widget.tool;

    if (activeTool == PenTool.eraser) {
      // Nothing to finalize
    } else if (_isTextTool) {
      if (_shapeStart != null) {
        _showTextDialog(_shapeStart!);
      }
    } else if (_isShapeTool) {
      // Shape: finalize with start + end points
      if (_currentPoints.length >= 2) {
        final stroke = InkStroke(
          id: _generateId(),
          points: List.from(_currentPoints),
          tool: activeTool,
          color: _getColorForTool(activeTool).toARGB32(),
          strokeWidth: widget.strokeWidth,
          createdAt: DateTime.now(),
          filled: widget.filled,
        );
        widget.onStrokeComplete?.call(stroke);
      }
    } else if (_currentPoints.isNotEmpty) {
      // Freehand
      final stroke = InkStroke(
        id: _generateId(),
        points: List.from(_currentPoints),
        tool: activeTool,
        color: _getColorForTool(activeTool).toARGB32(),
        strokeWidth: activeTool == PenTool.highlighter
            ? widget.strokeWidth * 3
            : widget.strokeWidth,
        createdAt: DateTime.now(),
      );
      widget.onStrokeComplete?.call(stroke);
    }

    setState(() {
      _currentPoints.clear();
      _shapeStart = null;
    });
  }

  void _showTextDialog(InkPoint position) {
    final controller = TextEditingController();
    showDialog(
      context: context,
      builder: (context) {
        return AlertDialog(
          title: const Text('Add Text'),
          content: TextField(
            controller: controller,
            autofocus: true,
            decoration: const InputDecoration(hintText: 'Enter text...'),
            maxLines: null,
          ),
          actions: [
            TextButton(
              onPressed: () => Navigator.of(context).pop(),
              child: const Text('Cancel'),
            ),
            TextButton(
              onPressed: () {
                final text = controller.text.trim();
                if (text.isNotEmpty) {
                  final stroke = InkStroke(
                    id: _generateId(),
                    points: [position],
                    tool: PenTool.text,
                    color: widget.color.toARGB32(),
                    strokeWidth: widget.strokeWidth,
                    createdAt: DateTime.now(),
                    textContent: text,
                    fontSize: widget.strokeWidth * 7,
                  );
                  widget.onStrokeComplete?.call(stroke);
                }
                Navigator.of(context).pop();
              },
              child: const Text('OK'),
            ),
          ],
        );
      },
    );
  }

  void _eraseAt(InkPoint point) {
    final eraserRadius = widget.strokeWidth * 3;

    // Collect all (strokeId, replacements) pairs before invoking any callback,
    // to avoid ConcurrentModificationError when the parent's onErase triggers
    // a setState that mutates widget.strokes mid-iteration.
    final toErase = <(String, List<InkStroke>)>[];

    for (final stroke in widget.strokes) {
      if (stroke.tool == PenTool.eraser) continue;

      final erasedIndices = <int>{};
      for (int i = 0; i < stroke.points.length; i++) {
        final p = stroke.points[i];
        final dx = p.x - point.x;
        final dy = p.y - point.y;
        if (dx * dx + dy * dy < eraserRadius * eraserRadius) {
          erasedIndices.add(i);
        }
      }

      if (erasedIndices.isEmpty) continue;

      toErase.add((stroke.id, _splitStroke(stroke, erasedIndices)));
    }

    for (final (strokeId, replacements) in toErase) {
      widget.onErase?.call(strokeId, replacements);
    }
  }

  List<InkStroke> _splitStroke(InkStroke stroke, Set<int> erasedIndices) {
    final segments = <List<InkPoint>>[];
    List<InkPoint> currentSegment = [];

    for (int i = 0; i < stroke.points.length; i++) {
      if (erasedIndices.contains(i)) {
        if (currentSegment.isNotEmpty) {
          segments.add(currentSegment);
          currentSegment = [];
        }
      } else {
        currentSegment.add(stroke.points[i]);
      }
    }

    if (currentSegment.isNotEmpty) {
      segments.add(currentSegment);
    }

    final replacements = <InkStroke>[];
    for (final segment in segments) {
      if (segment.length >= 2) {
        replacements.add(
          InkStroke(
            id: _generateId(),
            points: segment,
            tool: stroke.tool,
            color: stroke.color,
            strokeWidth: stroke.strokeWidth,
            createdAt: stroke.createdAt,
            filled: stroke.filled,
            textContent: stroke.textContent,
            fontSize: stroke.fontSize,
          ),
        );
      }
    }

    return replacements;
  }

  Color _getColorForTool(PenTool tool) {
    switch (tool) {
      case PenTool.marker:
        return widget.color.withAlpha(77);
      case PenTool.highlighter:
        return const Color(0x80FFFF00);
      case PenTool.pen:
      case PenTool.eraser:
      case PenTool.rectangle:
      case PenTool.ellipse:
      case PenTool.line:
      case PenTool.arrow:
      case PenTool.text:
        return widget.color;
    }
  }

  String _generateId() {
    return const Uuid().v4();
  }

  @override
  Widget build(BuildContext context) {
    return Listener(
      onPointerDown: _handlePointerDown,
      onPointerMove: _handlePointerMove,
      onPointerUp: _handlePointerUp,
      child: CustomPaint(
        painter: _InkPainter(
          strokes: widget.strokes,
          currentPoints: _currentPoints,
          currentTool: _activeTool ?? widget.tool,
          currentColor: _getColorForTool(_activeTool ?? widget.tool),
          currentStrokeWidth:
              (_activeTool ?? widget.tool) == PenTool.highlighter
              ? widget.strokeWidth * 3
              : widget.strokeWidth,
          pressureCurve: widget.pressureCurve,
          filled: widget.filled,
          viewportBounds: widget.viewportBounds,
        ),
        size: Size.infinite,
      ),
    );
  }
}

class _InkPainter extends CustomPainter {
  final List<InkStroke> strokes;
  final List<InkPoint> currentPoints;
  final PenTool currentTool;
  final Color currentColor;
  final double currentStrokeWidth;
  final PressureCurve pressureCurve;
  final bool filled;
  final Rect? viewportBounds;

  _InkPainter({
    required this.strokes,
    required this.currentPoints,
    required this.currentTool,
    required this.currentColor,
    required this.currentStrokeWidth,
    required this.pressureCurve,
    required this.filled,
    this.viewportBounds,
  });

  bool _strokeInViewport(InkStroke stroke, Rect viewport) {
    if (stroke.points.isEmpty) return false;
    double minX = double.infinity, minY = double.infinity;
    double maxX = double.negativeInfinity, maxY = double.negativeInfinity;
    for (final p in stroke.points) {
      if (p.x < minX) minX = p.x;
      if (p.y < minY) minY = p.y;
      if (p.x > maxX) maxX = p.x;
      if (p.y > maxY) maxY = p.y;
    }
    return viewport.overlaps(Rect.fromLTRB(minX, minY, maxX, maxY));
  }

  @override
  void paint(Canvas canvas, Size size) {
    for (final stroke in strokes) {
      if (stroke.tool == PenTool.eraser) continue;
      if (viewportBounds != null &&
          !_strokeInViewport(stroke, viewportBounds!)) {
        continue;
      }
      _drawStroke(
        canvas,
        stroke.points,
        stroke.tool,
        Color(stroke.color),
        stroke.strokeWidth,
        true,
        stroke.filled,
        stroke.textContent,
        stroke.fontSize,
      );
    }

    if (currentPoints.isNotEmpty && currentTool != PenTool.eraser) {
      _drawStroke(
        canvas,
        currentPoints,
        currentTool,
        currentColor,
        currentStrokeWidth,
        false,
        filled,
        null,
        14.0,
      );
    }
  }

  void _drawStroke(
    Canvas canvas,
    List<InkPoint> points,
    PenTool tool,
    Color color,
    double strokeWidth,
    bool isComplete,
    bool strokeFilled,
    String? textContent,
    double fontSize,
  ) {
    if (points.isEmpty) return;

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

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

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

    final outlinePoints = pf.getStroke(
      pfPoints,
      options: pf.StrokeOptions(
        size: strokeWidth,
        thinning: thinning,
        smoothing: 0.5,
        streamline: 0.5,
        // 2.x defaults: no taper + capped ends (was taperStart/End:0 +
        // capStart/End:true in 1.0.4).
        simulatePressure: tool != PenTool.marker && tool != PenTool.highlighter,
        isComplete: isComplete,
      ),
    );

    if (outlinePoints.isEmpty) return;

    final path = Path();
    path.moveTo(outlinePoints[0].dx, outlinePoints[0].dy);

    for (int i = 1; i < outlinePoints.length; i++) {
      path.lineTo(outlinePoints[i].dx, outlinePoints[i].dy);
    }
    path.close();

    final paint = Paint()
      ..color = color
      ..style = PaintingStyle.fill
      ..isAntiAlias = true;

    canvas.drawPath(path, paint);
  }

  void _drawRect(
    Canvas canvas,
    List<InkPoint> points,
    Color color,
    double strokeWidth,
    bool strokeFilled,
  ) {
    final rect = Rect.fromPoints(
      Offset(points[0].x, points[0].y),
      Offset(points[1].x, points[1].y),
    );

    final paint = Paint()
      ..color = color
      ..strokeWidth = strokeWidth
      ..isAntiAlias = true
      ..style = strokeFilled ? PaintingStyle.fill : PaintingStyle.stroke;

    canvas.drawRect(rect, paint);
  }

  void _drawOval(
    Canvas canvas,
    List<InkPoint> points,
    Color color,
    double strokeWidth,
    bool strokeFilled,
  ) {
    final rect = Rect.fromPoints(
      Offset(points[0].x, points[0].y),
      Offset(points[1].x, points[1].y),
    );

    final paint = Paint()
      ..color = color
      ..strokeWidth = strokeWidth
      ..isAntiAlias = true
      ..style = strokeFilled ? PaintingStyle.fill : PaintingStyle.stroke;

    canvas.drawOval(rect, paint);
  }

  void _drawLine(
    Canvas canvas,
    List<InkPoint> points,
    Color color,
    double strokeWidth,
  ) {
    final paint = Paint()
      ..color = color
      ..strokeWidth = strokeWidth
      ..isAntiAlias = true
      ..style = PaintingStyle.stroke
      ..strokeCap = StrokeCap.round;

    canvas.drawLine(
      Offset(points[0].x, points[0].y),
      Offset(points[1].x, points[1].y),
      paint,
    );
  }

  void _drawArrow(
    Canvas canvas,
    List<InkPoint> points,
    Color color,
    double strokeWidth,
  ) {
    final p1 = Offset(points[0].x, points[0].y);
    final p2 = Offset(points[1].x, points[1].y);

    final paint = Paint()
      ..color = color
      ..strokeWidth = strokeWidth
      ..isAntiAlias = true
      ..style = PaintingStyle.stroke
      ..strokeCap = StrokeCap.round;

    // Main line
    canvas.drawLine(p1, p2, paint);

    // Arrowhead
    final dx = p2.dx - p1.dx;
    final dy = p2.dy - p1.dy;
    final angle = atan2(dy, dx);
    final arrowLength = strokeWidth * 5;
    const arrowAngle = pi / 6; // 30 degrees

    final arrowP1 = Offset(
      p2.dx - arrowLength * cos(angle - arrowAngle),
      p2.dy - arrowLength * sin(angle - arrowAngle),
    );
    final arrowP2 = Offset(
      p2.dx - arrowLength * cos(angle + arrowAngle),
      p2.dy - arrowLength * sin(angle + arrowAngle),
    );

    canvas.drawLine(p2, arrowP1, paint);
    canvas.drawLine(p2, arrowP2, paint);
  }

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

  @override
  bool shouldRepaint(covariant _InkPainter oldDelegate) {
    if (strokes.length != oldDelegate.strokes.length) return true;
    if (currentPoints.length != oldDelegate.currentPoints.length) return true;
    for (int i = 0; i < strokes.length; i++) {
      final a = strokes[i], b = oldDelegate.strokes[i];
      if (a.id != b.id ||
          a.color != b.color ||
          a.strokeWidth != b.strokeWidth ||
          a.tool != b.tool) {
        return true;
      }
    }
    return currentTool != oldDelegate.currentTool;
  }
}
