// lib/editor/engine/shape_geometry.dart
//
// Pure geometry for the SHAPE tool. Each shape is generated as a list of
// NORMALIZED [PenPoint]s (the same model freehand strokes use), so a shape is
// just a [PenStroke] — it reuses stroke rendering, persistence, erase, and undo
// with NO new model or storage. Points carry a constant pressure (1.0) so the
// brush renders them at a steady width (shapes don't taper with pressure).
//
// All inputs/outputs are in normalized page coordinates ([0,1] x [0,1]); the
// caller wraps the points in a PenStroke with the current brush color/width.

import 'dart:math' as math;

import '../canvas/editor_tool.dart';
import '../canvas/pen_stroke.dart';
import 'brush.dart';

/// Number of points sampled around an ellipse. Kept as a const so tests can pin
/// it (spec: "ellipse = sampled points ~48"). The polyline is closed, so the
/// last point repeats the first ⇒ [kEllipseSamples] + 1 total points.
const int kEllipseSamples = 48;

/// Constant pressure baked into every shape point so the brush renders a steady
/// width (no pressure taper for geometric shapes).
const double _kShapePressure = 1.0;

/// Geometric shapes must NOT inherit fountain thinning/taper — force a near-
/// constant-width brush so line/rect/ellipse look like ruler ink.
const BrushKind kShapeBrush = BrushKind.ballpoint;

/// Generate the normalized polyline for [kind] spanning [start] → [end].
///
///   * [ShapeKind.line]      → 2 points.
///   * [ShapeKind.rectangle] → 5 points (closed: 4 corners + repeat of the
///     first), an axis-aligned box whose opposite corners are [start]/[end].
///   * [ShapeKind.ellipse]   → [kEllipseSamples] + 1 points (closed), inscribed
///     in the [start]→[end] bounding box.
///   * [ShapeKind.arrow]     → shaft (start → end) + two arrowhead segments,
///     emitted as a single polyline so it renders as one stroke.
List<PenPoint> generateShapePoints(ShapeKind kind, PenPoint start, PenPoint end) {
  switch (kind) {
    case ShapeKind.line:
      return [
        PenPoint(start.x, start.y, _kShapePressure),
        PenPoint(end.x, end.y, _kShapePressure),
      ];

    case ShapeKind.rectangle:
      final l = math.min(start.x, end.x);
      final r = math.max(start.x, end.x);
      final t = math.min(start.y, end.y);
      final b = math.max(start.y, end.y);
      return [
        PenPoint(l, t, _kShapePressure),
        PenPoint(r, t, _kShapePressure),
        PenPoint(r, b, _kShapePressure),
        PenPoint(l, b, _kShapePressure),
        PenPoint(l, t, _kShapePressure), // close
      ];

    case ShapeKind.ellipse:
      final cx = (start.x + end.x) / 2;
      final cy = (start.y + end.y) / 2;
      final rx = (end.x - start.x).abs() / 2;
      final ry = (end.y - start.y).abs() / 2;
      final pts = <PenPoint>[];
      for (var i = 0; i <= kEllipseSamples; i++) {
        final a = (i / kEllipseSamples) * 2 * math.pi;
        pts.add(PenPoint(
          cx + rx * math.cos(a),
          cy + ry * math.sin(a),
          _kShapePressure,
        ));
      }
      return pts;

    case ShapeKind.arrow:
      // Shaft start→end, then back up the shaft to draw the two head barbs so
      // the whole arrow is one continuous polyline (no pen lifts).
      final dx = end.x - start.x;
      final dy = end.y - start.y;
      final len = math.sqrt(dx * dx + dy * dy);
      final pts = <PenPoint>[
        PenPoint(start.x, start.y, _kShapePressure),
        PenPoint(end.x, end.y, _kShapePressure),
      ];
      if (len <= 1e-6) return pts; // degenerate: just the (near-zero) shaft
      // Arrowhead: barbs at ±[_kArrowAngle] from the reversed shaft direction,
      // [_kArrowHead] of the shaft length (capped) long.
      final ang = math.atan2(dy, dx);
      final head = math.min(len * _kArrowHeadFraction, _kArrowHeadMax);
      for (final sign in const [1.0, -1.0]) {
        final a = ang + math.pi + sign * _kArrowAngle;
        pts.add(PenPoint(
          end.x + head * math.cos(a),
          end.y + head * math.sin(a),
          _kShapePressure,
        ));
        pts.add(PenPoint(end.x, end.y, _kShapePressure)); // back to the tip
      }
      return pts;
  }
}

/// Arrowhead barb length as a fraction of the shaft length.
const double _kArrowHeadFraction = 0.25;

/// Hard cap on the barb length (normalized) so a long arrow's head stays sane.
const double _kArrowHeadMax = 0.06;

/// Half-angle of the arrowhead barbs from the shaft (radians ≈ 28°).
const double _kArrowAngle = 0.5;

/// Return a copy of [points] translated by ([dx],[dy]) in normalized coords,
/// preserving pressure/tilt. Used by the SELECT tool to drag a stroke.
List<PenPoint> translatePoints(List<PenPoint> points, double dx, double dy) =>
    [for (final p in points) PenPoint(p.x + dx, p.y + dy, p.pressure, tilt: p.tilt)];

/// A translated copy of [stroke] (its points shifted by [dx],[dy]); color,
/// width, kind, and brush are preserved.
PenStroke translateStroke(PenStroke stroke, double dx, double dy) => PenStroke(
      points: translatePoints(stroke.points, dx, dy),
      color: stroke.color,
      width: stroke.width,
      kind: stroke.kind,
      brush: stroke.brush,
    );

/// Tight normalized bounds of [stroke]'s points, or null when it has no points.
/// Used by the SELECT tool to draw the selection bounding box.
({double left, double top, double right, double bottom})? penStrokeBounds(
    PenStroke stroke) {
  if (stroke.points.isEmpty) return null;
  var l = double.infinity, t = double.infinity;
  var r = double.negativeInfinity, b = double.negativeInfinity;
  for (final p in stroke.points) {
    if (p.x < l) l = p.x;
    if (p.y < t) t = p.y;
    if (p.x > r) r = p.x;
    if (p.y > b) b = p.y;
  }
  return (left: l, top: t, right: r, bottom: b);
}
