// lib/editor/pdf/pen_capture_region.dart
//
// Per-pointer-kind input transport for the BadNote PDF editor (plan §3.5).
//
// PROBLEM
// -------
// pdfrx's PdfViewer uses an internal InteractiveViewer-style pan/zoom driven by
// a GestureDetector (onScaleStart/Update/End). A widget layered on top of the
// viewer that wants to *draw* with the pen would normally compete with that
// recognizer in Flutter's **gesture arena** — and the scale recognizer is
// greedy, so a freehand pen drag would frequently be claimed by the viewer
// (pen → pan) or starve touch scrolling. pdfrx's own
// `PdfOverlayInteractionRegion` is tap-oriented only and gives us no freehand
// drag stream that bypasses the arena.
//
// SOLUTION (arena-bypass, per-kind hit-test split)
// ------------------------------------------------
// We do NOT use a GestureDetector for pen capture. Instead we install a custom
// RenderProxyBox (`_RenderPenCapture`) whose hit-test answer is conditioned on
// the pointer device kind of the pointer-DOWN currently being routed:
//
//   * stylus / invertedStylus  -> hitTest returns TRUE  -> this box becomes the
//                                 pointer's hit-test target and receives the
//                                 entire down/move/up stream directly via
//                                 RenderObject.handleEvent, NOT through the
//                                 gesture arena (we never add a recognizer).
//
//   * touch / mouse / trackpad -> hitTest returns FALSE -> the hit-test
//                                 continues past us to the pdfrx viewer
//                                 underneath, so single-finger touch scroll and
//                                 pinch-zoom reach pdfrx untouched.
//
// WHY A CUSTOM BINDING IS REQUIRED (the only correct seam)
// --------------------------------------------------------
// Flutter only hit-tests a pointer on its PointerDownEvent, then CACHES the
// resulting HitTestResult and reuses it for every subsequent move/up
// (GestureBinding._handlePointerEventImmediately). Crucially,
// `hitTestInView(result, position, viewId)` is passed only the *position*, NOT
// the event — so a RenderBox.hitTest cannot read the pointer's kind from its
// arguments or from the HitTestResult. `pointerRouter.addGlobalRoute` fires
// during dispatchEvent, which is AFTER hit-test, so it is too late to influence
// the DOWN routing.
//
// The single reliable, fully-public seam is `GestureBinding.handlePointerEvent`
// (virtual), which is called with the live event immediately before the
// synchronous hit-test of that same event. [PenCaptureBinding] overrides it to
// stash the in-flight pointer kind into [PenCaptureBinding.currentPointerKind]
// before delegating to super; `_RenderPenCapture.hitTest` reads that field. The
// app must install [PenCaptureBinding] in main() (see spike_main.dart). If a
// non-PenCapture binding is in use, [currentPointerKind] stays null and the
// region defaults to NOT capturing — so touch scrolling is never accidentally
// stolen.
//
// The `captureEnabled` flag gates capture by editor mode (plan §6.1: Browse
// never captures pen; Draw/Type do). When disabled the box is hit-test
// transparent for all kinds.

import 'package:flutter/gestures.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter/widgets.dart';

/// Pointer kinds this region captures (plan §3.5 / §6.2: pen only; mouse is
/// never captured for drawing — pen required).
const Set<PointerDeviceKind> kPenCaptureKinds = {
  PointerDeviceKind.stylus,
  PointerDeviceKind.invertedStylus,
};

/// A [WidgetsFlutterBinding] that records the device kind of the pointer event
/// currently being routed, so that kind-gated hit-testing
/// ([PenCaptureRegion]) can consult it during the synchronous DOWN hit-test.
///
/// Install in main():
/// ```dart
/// void main() {
///   PenCaptureBinding.ensureInitialized();
///   pdfrxFlutterInitialize();
///   runApp(const SpikeApp());
/// }
/// ```
class PenCaptureBinding extends WidgetsFlutterBinding {
  /// The device kind of the pointer event currently being handled by
  /// [handlePointerEvent], valid for the duration of the synchronous hit-test
  /// the framework performs for that event. Null when no binding override is
  /// active or between events.
  static PointerDeviceKind? currentPointerKind;

  /// Ensures a [PenCaptureBinding] is the active binding and returns it.
  static WidgetsBinding ensureInitialized() {
    PenCaptureBinding();
    return WidgetsBinding.instance;
  }

  @override
  void handlePointerEvent(PointerEvent event) {
    // Set the kind BEFORE super performs the synchronous hit-test for a DOWN.
    currentPointerKind = event.kind;
    try {
      super.handlePointerEvent(event);
    } finally {
      // Leave currentPointerKind set to the last event's kind; it is only read
      // transiently during hit-test (which happens inside super for DOWNs).
      // We do not null it here because moves reuse the cached hit path and never
      // re-hit-test, so staleness between events is harmless.
    }
  }
}

/// A transparent capture region that forwards the full pointer stream for
/// stylus/invertedStylus pointers to [onPenEvent] while letting touch and mouse
/// pointers fall through to whatever is painted underneath (typically a pdfrx
/// `PdfViewer`).
///
/// See the file header for the arena-bypass rationale and binding requirement.
class PenCaptureRegion extends SingleChildRenderObjectWidget {
  const PenCaptureRegion({
    super.key,
    required this.onPenEvent,
    this.captureEnabled = true,
    required Widget child,
  }) : super(child: child);

  /// Called with every [PointerEvent] (down/move/up/cancel) of a captured pen
  /// pointer, delivered raw and in order without arena arbitration.
  final void Function(PointerEvent event) onPenEvent;

  /// When false the region is hit-test transparent for all pointer kinds, so
  /// even pen events fall through to the viewer underneath (e.g. Browse mode).
  final bool captureEnabled;

  @override
  RenderObject createRenderObject(BuildContext context) {
    return _RenderPenCapture(
      onPenEvent: onPenEvent,
      captureEnabled: captureEnabled,
    );
  }

  @override
  void updateRenderObject(BuildContext context, RenderObject renderObject) {
    (renderObject as _RenderPenCapture)
      ..onPenEvent = onPenEvent
      ..captureEnabled = captureEnabled;
  }
}

/// RenderProxyBox that conditionally participates in hit-testing based on the
/// in-flight pointer's device kind (via [PenCaptureBinding.currentPointerKind])
/// and forwards captured pen events.
class _RenderPenCapture extends RenderProxyBox {
  _RenderPenCapture({
    required void Function(PointerEvent) onPenEvent,
    required bool captureEnabled,
  })  : _onPenEvent = onPenEvent,
        _captureEnabled = captureEnabled;

  void Function(PointerEvent) _onPenEvent;
  set onPenEvent(void Function(PointerEvent) value) => _onPenEvent = value;

  bool _captureEnabled;
  set captureEnabled(bool value) => _captureEnabled = value;

  /// Returns true only for pen kinds while capture is enabled. Returning false
  /// continues the hit-test to the pdfrx viewer underneath, which is how
  /// touch/mouse reach it for scroll/zoom.
  ///
  /// We override [hitTest] directly (rather than relying on
  /// [hitTestChildren]+[hitTestSelf]) so the kind check is the single decision
  /// point and covers the entire box area, regardless of the translucent paint
  /// child.
  @override
  bool hitTest(BoxHitTestResult result, {required Offset position}) {
    if (!_captureEnabled) return false;
    if (!_shouldCaptureCurrentPointer()) return false;
    if (!size.contains(position)) return false;
    result.add(BoxHitTestEntry(this, position));
    return true;
  }

  /// Mirrors the predicate for any caller routing through the default
  /// RenderBox.hitTest dispatch (hitTestChildren then hitTestSelf).
  @override
  bool hitTestSelf(Offset position) =>
      _captureEnabled && _shouldCaptureCurrentPointer();

  bool _shouldCaptureCurrentPointer() {
    final kind = PenCaptureBinding.currentPointerKind;
    if (kind == null) return false; // No kind-aware binding → never steal touch.
    return kPenCaptureKinds.contains(kind);
  }

  @override
  void handleEvent(PointerEvent event, HitTestEntry entry) {
    if (!_captureEnabled) return;
    if (!kPenCaptureKinds.contains(event.kind)) return;
    _onPenEvent(event);
  }
}
