// lib/editor/engine/stroke_model.dart
//
// Canonical, persistable stroke model for the BadNote editor engine.
//
// This is the single source of truth for ink strokes across the new own-canvas
// engine (screen render + export + persistence). It is a deliberate SUPERSET of
// both the in-memory live `PenStroke`/`PenPoint` (lib/editor/canvas/pen_stroke.dart)
// and the freezed/JSON `InkStroke`/`InkPoint` (lib/models/ink_stroke.dart) so the
// adapters below round-trip losslessly with `InkStroke` (SF1): `tilt`,
// `timestamp` and `pointerDeviceKind` are preserved, never dropped.
//
// Coordinate semantics (matching the live conventions):
//   * Point x/y are NORMALIZED to the page rectangle, i.e. in [0,1].
//   * Stroke `width` is a FRACTION of the page width, so it scales with zoom.

import 'package:freezed_annotation/freezed_annotation.dart';
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 '../canvas/pen_stroke.dart';

part 'stroke_model.freezed.dart';
part 'stroke_model.g.dart';

const _uuid = Uuid();

/// The drawing tools the engine knows about. Extensible; P0 uses these three.
enum EditorTool {
  @JsonValue('pen')
  pen,
  @JsonValue('highlighter')
  highlighter,
  @JsonValue('eraser')
  eraser,
}

/// A single captured sample of a stroke.
///
/// [x]/[y] are normalized to the page rectangle ([0,1]). The remaining fields
/// are a superset of [InkPoint] (nullable here so the live capture path can
/// leave them unset, while [InkStroke] data round-trips intact through the
/// adapters below).
@freezed
abstract class EditorPoint with _$EditorPoint {
  const factory EditorPoint({
    required double x,
    required double y,
    double? pressure,
    double? tilt,
    int? timestamp,
    InputDeviceKind? pointerDeviceKind,
  }) = _EditorPoint;

  factory EditorPoint.fromJson(Map<String, dynamic> json) =>
      _$EditorPointFromJson(json);
}

/// A committed stroke in normalized page coordinates.
///
/// [width] is a fraction of page width (matches live `PenStroke.width`).
@freezed
abstract class EditorStroke with _$EditorStroke {
  const EditorStroke._();

  factory EditorStroke({
    required String id,
    required List<EditorPoint> points,
    @Default(EditorTool.pen) EditorTool tool,
    @Default(0xFF000000) int color,
    @Default(0.003) double width,
    @Default(false) bool filled,
    String? textContent,
    @Default(14.0) double fontSize,
  }) = _EditorStroke;

  /// Convenience constructor that generates a uuid [id] when none is supplied.
  factory EditorStroke.create({
    String? id,
    required List<EditorPoint> points,
    EditorTool tool = EditorTool.pen,
    int color = 0xFF000000,
    double width = 0.003,
    bool filled = false,
    String? textContent,
    double fontSize = 14.0,
  }) =>
      EditorStroke(
        id: id ?? _uuid.v4(),
        points: points,
        tool: tool,
        color: color,
        width: width,
        filled: filled,
        textContent: textContent,
        fontSize: fontSize,
      );

  factory EditorStroke.fromJson(Map<String, dynamic> json) =>
      _$EditorStrokeFromJson(json);

  // ---- Adapters -----------------------------------------------------------

  /// Adapts an in-memory live [PenStroke] (normalized; carries tilt when the
  /// native pen plugin supplied it, else null; no timestamp/pointerDeviceKind).
  factory EditorStroke.fromPenStroke(PenStroke stroke, {String? id}) =>
      EditorStroke(
        id: id ?? _uuid.v4(),
        points: stroke.points
            .map((p) =>
                EditorPoint(x: p.x, y: p.y, pressure: p.pressure, tilt: p.tilt))
            .toList(),
        tool: switch (stroke.kind) {
          PenStrokeKind.pen => EditorTool.pen,
          PenStrokeKind.highlighter => EditorTool.highlighter,
        },
        color: stroke.color,
        width: stroke.width,
      );

  /// Lossless adapter from the freezed/JSON [InkStroke] model.
  factory EditorStroke.fromInkStroke(InkStroke stroke) => EditorStroke(
        id: stroke.id,
        points: stroke.points
            .map(
              (p) => EditorPoint(
                x: p.x,
                y: p.y,
                pressure: p.pressure,
                tilt: p.tilt,
                timestamp: p.timestamp,
                pointerDeviceKind: p.pointerDeviceKind,
              ),
            )
            .toList(),
        tool: _toolFromPenTool(stroke.tool),
        color: stroke.color,
        width: stroke.strokeWidth,
        filled: stroke.filled,
        textContent: stroke.textContent,
        fontSize: stroke.fontSize,
      );

  /// Lossless adapter to the freezed/JSON [InkStroke] model. Null superset
  /// fields fall back to [InkPoint]'s own defaults so the InkStroke round-trip
  /// (fromInkStroke → toInkStroke) reproduces the original exactly.
  InkStroke toInkStroke({DateTime? createdAt}) => InkStroke(
        id: id,
        points: points
            .map(
              (p) => InkPoint(
                x: p.x,
                y: p.y,
                pressure: p.pressure ?? 0.5,
                tilt: p.tilt ?? 0.0,
                timestamp: p.timestamp ?? 0,
                pointerDeviceKind:
                    p.pointerDeviceKind ?? InputDeviceKind.unknown,
              ),
            )
            .toList(),
        tool: _toolToPenTool(tool),
        color: color,
        strokeWidth: width,
        createdAt: createdAt ?? DateTime.fromMillisecondsSinceEpoch(0),
        filled: filled,
        textContent: textContent,
        fontSize: fontSize,
      );

  static EditorTool _toolFromPenTool(PenTool tool) => switch (tool) {
        PenTool.highlighter => EditorTool.highlighter,
        PenTool.eraser => EditorTool.eraser,
        _ => EditorTool.pen,
      };

  static PenTool _toolToPenTool(EditorTool tool) => switch (tool) {
        EditorTool.pen => PenTool.pen,
        EditorTool.highlighter => PenTool.highlighter,
        EditorTool.eraser => PenTool.eraser,
      };
}
