// lib/editor/engine/brush.dart
//
// Data-driven, Krita-compatible brush model — the extensibility seam for the
// pen engine. Each [BrushKind] maps to an immutable [BrushProfile] that fully
// describes how a stroke is captured (pressure pre-warp) and rendered
// (perfect_freehand geometry params + caps/taper). Adding a brush = adding one
// const entry to [kBrushPresets]; no render-path branching.
//
// Mirrors Krita's sensor→curve design (Pixel brush: each property is driven by
// a sensor through a response curve). Here the response curve is a pure power
// law `p^gamma` applied to pressure BEFORE perfect_freehand (rnote's
// `PressureCurve`: Pow2 = quadratic, Sqrt = √p), and the geometry knobs are
// perfect_freehand's `thinning`/`streamline`/`smoothing`/caps. A future `.kpp`
// (Krita brush preset) importer can produce [BrushProfile]s from the same
// fields — see TODO(brush-kpp-import).
//
// Source spec: docs/research/pen-brush-spec.md §1 (rnote pressure curve) and §4
// (per-brush perfect_freehand option tables). The numbers below are lifted from
// that spec verbatim.

/// The four selectable brushes. Extensible: add a kind here + a preset in
/// [kBrushPresets]. The eraser is NOT a brush — it stays a separate tool.
enum BrushKind {
  /// Strong pressure→width (rnote Pow2 / quadratic), soft taper, solid ink.
  fountainPen,

  /// Near-constant thin width; pressure carries opacity in a later increment.
  ballpoint,

  /// Broad, flat width, translucent, square (uncapped) ends.
  highlighter,

  /// Moderate width + (later) opacity from pressure (rnote Sqrt / √p), scratchy.
  pencil,
}

/// Immutable, const description of one brush.
///
/// The capture path reads [pressureGamma] (the rnote power-law warp applied via
/// `PressureCurve(gamma: pressureGamma)` BEFORE perfect_freehand) and the render
/// path reads the perfect_freehand geometry fields ([pfThinning], [pfStreamline],
/// [pfSmoothing], [simulatePressure]) plus the cap/taper flags.
///
/// [opacity] / [blendMultiply] are carried NOW for the later compositing
/// increment but are NOT yet applied to rendered geometry — see
/// TODO(brush-opacity) at the render sites.
class BrushProfile {
  const BrushProfile({
    required this.kind,
    required this.baseWidthFraction,
    required this.pressureGamma,
    required this.pfThinning,
    required this.pfStreamline,
    required this.pfSmoothing,
    required this.simulatePressure,
    required this.capStart,
    required this.capEnd,
    required this.taper,
    required this.opacity,
    required this.blendMultiply,
  });

  /// Which brush this profile is for.
  final BrushKind kind;

  /// Suggested base stroke width as a fraction of page width (so it scales with
  /// zoom, matching `PenStroke.width`). The editors may override with their own
  /// configured pen/highlighter widths; this is the spec's nominal default
  /// (spec §4 diameters, expressed as a page-width fraction).
  final double baseWidthFraction;

  /// rnote `PressureCurve` exponent applied to raw pressure at CAPTURE, before
  /// perfect_freehand. `2.0` = Pow2 (quadratic, fountain pen); `0.5` = Sqrt
  /// (pencil); `1.0` = Linear (ballpoint / highlighter). Fed through the
  /// existing `PressureCurve(gamma: …)` — no new pow function (spec §1).
  final double pressureGamma;

  /// perfect_freehand `thinning`: how strongly (pre-warped) pressure modulates
  /// width. `0.0` = constant width (highlighter); high = wide dynamic range
  /// (fountain pen) (spec §4).
  final double pfThinning;

  /// perfect_freehand `streamline`: EMA low-pass on input positions (spec §4).
  final double pfStreamline;

  /// perfect_freehand `smoothing`: outline corner-softening (spec §4).
  final double pfSmoothing;

  /// perfect_freehand `simulatePressure`: when true, fakes pressure from
  /// velocity. All four presets ship `false` so REAL stylus pressure (already
  /// pre-warped by [pressureGamma]) drives width (spec §4). The render path
  /// still falls back to simulation when the device reports NO usable pressure.
  final bool simulatePressure;

  /// Round cap on the start of the stroke (false = square end, highlighter).
  final bool capStart;

  /// Round cap on the end of the stroke (false = square end, highlighter).
  final bool capEnd;

  /// Whether the ends taper to a point (fountain pen) (spec §4).
  final bool taper;

  /// Per-stroke opacity hint in [0,1]. CARRIED NOW, applied in a later
  /// increment — see TODO(brush-opacity). `1.0` = solid.
  final double opacity;

  /// Whether the brush should composite with `BlendMode.multiply` (highlighter
  /// build-up / marker feel). CARRIED NOW, applied later — TODO(brush-opacity).
  final bool blendMultiply;
}

/// The 4 brush presets, populated from the spec §4 tables.
///
/// Widths are the spec's logical-px diameters re-expressed as page-width
/// fractions against the project's ~1000px logical page (the existing
/// pen/highlighter widths are 0.006 / 0.02). Fountain pen ≈ pen (0.006),
/// highlighter ≈ 0.02 so the existing pen/highlighter visuals are PRESERVED as
/// the fountainPen/highlighter presets (no regression).
const Map<BrushKind, BrushProfile> kBrushPresets = {
  // Fountain pen — spec §4: size~6, thinning 0.9, smoothing 0.55,
  // streamline 0.45, simulatePressure false, taper on, pressure pre-warped to
  // p² (Pow2 / quadratic = pressureGamma 2.0). Solid ink (opacity 1.0).
  BrushKind.fountainPen: BrushProfile(
    kind: BrushKind.fountainPen,
    baseWidthFraction: 0.006,
    pressureGamma: 2.0,
    pfThinning: 0.9,
    pfStreamline: 0.45,
    pfSmoothing: 0.55,
    simulatePressure: false,
    capStart: true,
    capEnd: true,
    taper: true,
    opacity: 1.0,
    blendMultiply: false,
  ),
  // Ballpoint — spec §4: size~2.2, thinning 0.15, smoothing 0.5,
  // streamline 0.55, near-constant width, linear pressure (gamma 1.0). Pressure
  // → opacity is deferred (TODO(brush-opacity)); opacity hint carried at 1.0
  // until then so geometry-only ballpoint is solid (not invisible).
  BrushKind.ballpoint: BrushProfile(
    kind: BrushKind.ballpoint,
    baseWidthFraction: 0.0022,
    pressureGamma: 1.0,
    pfThinning: 0.15,
    pfStreamline: 0.55,
    pfSmoothing: 0.5,
    simulatePressure: false,
    capStart: true,
    capEnd: true,
    taper: false,
    opacity: 1.0,
    blendMultiply: false,
  ),
  // Highlighter — spec §4: size~22, thinning 0.0 (constant width),
  // smoothing 0.4, streamline 0.5, square (uncapped) ends, translucent +
  // multiply build-up. opacity 0.35 / blendMultiply true carried now; the
  // existing highlighter ships a 0x80 (50%) color alpha at capture, so the
  // 0.35 hint is NOT yet applied — see TODO(brush-opacity).
  BrushKind.highlighter: BrushProfile(
    kind: BrushKind.highlighter,
    baseWidthFraction: 0.02,
    pressureGamma: 1.0,
    pfThinning: 0.0,
    pfStreamline: 0.5,
    pfSmoothing: 0.4,
    simulatePressure: false,
    capStart: false,
    capEnd: false,
    taper: false,
    opacity: 0.35,
    blendMultiply: true,
  ),
  // Pencil — spec §4: size~3, thinning 0.5, smoothing 0.5, streamline 0.4,
  // pressure pre-warped to √p (Sqrt = pressureGamma 0.5). Pressure→opacity and
  // paper grain are deferred (TODO(brush-opacity) / TODO(brush-texture));
  // opacity hint 0.9 carried now, not yet applied.
  BrushKind.pencil: BrushProfile(
    kind: BrushKind.pencil,
    baseWidthFraction: 0.003,
    pressureGamma: 0.5,
    pfThinning: 0.5,
    pfStreamline: 0.4,
    pfSmoothing: 0.5,
    simulatePressure: false,
    capStart: true,
    capEnd: true,
    taper: false,
    opacity: 0.9,
    blendMultiply: false,
  ),
};

/// Resolve the [BrushProfile] for [kind] (always present; const map).
BrushProfile brushProfileFor(BrushKind kind) => kBrushPresets[kind]!;
