// lib/editor/canvas/pen_interactive_viewer.dart
//
// A focused fork of Flutter 3.44's InteractiveViewer, adapted for the pen-first
// canvas (clean-room model shared with Saber). Two deliberate changes vs stock:
//
//  1. The pan/zoom ScaleGestureRecognizer is restricted to NON-stylus devices
//     (`supportedDevices` excludes stylus / invertedStylus). The pen therefore
//     never reaches this recognizer — it only draws via the canvas `Listener`.
//     This removes the gesture-arena fight and, crucially, the one-frame
//     "pan-steal" where a stylus stroke's first frame was consumed as a pan
//     (the "写字识别成单击" feel bug) because stock InteractiveViewer's
//     `panEnabled` only updated a frame after the stroke had begun.
//
//  2. The per-frame scale change is clamped (`_kMin/_MaxScaleChangePerFrame`).
//     Stock InteractiveViewer already damps focal jitter and guards the pan
//     branch, but a single-frame multi-touch glitch can still spike
//     `details.scale`, popping the zoom bigger/smaller for one frame and then
//     snapping back (the reported pinch flicker). Clamping the per-update change
//     swallows that spike without affecting a real (gradual) pinch, since scale
//     is tracked absolutely from gesture start and simply catches up next frame.
//
// Everything else (scale-about-focal math, pan, fling inertia, mouse-wheel zoom)
// is Flutter's proven logic. The boundary/rotation/panAxis machinery is dropped
// because this canvas always uses an infinite boundary, free pan, and no
// rotation — so that code was provably a no-op here.

import 'dart:math' as math;

import 'package:flutter/foundation.dart' show clampDouble;
import 'package:flutter/gestures.dart';
import 'package:flutter/physics.dart';
import 'package:flutter/widgets.dart';

import 'input_diagnostics.dart';

/// Devices allowed to pan/zoom. Stylus + invertedStylus are excluded so the pen
/// is owned exclusively by the drawing `Listener`.
const Set<PointerDeviceKind> _kPanZoomDevices = <PointerDeviceKind>{
  PointerDeviceKind.touch,
  PointerDeviceKind.mouse,
  PointerDeviceKind.trackpad,
  PointerDeviceKind.unknown,
};

/// A real pinch changes scale only modestly per frame (≲1.15x at 60fps). A frame
/// demanding far more than this is a Windows multi-touch position glitch, not
/// intent — that frame is dropped so the zoom can't pop and snap back.
const double _kScaleGlitchHi = 1.4;
const double _kScaleGlitchLo = 1 / _kScaleGlitchHi;

/// During a 2-finger gesture the focal point (finger midpoint) should move
/// smoothly. A single-frame local jump beyond this is a Windows touch misread,
/// and the frame is dropped (position-jump guard).
const double _kFocalGlitchPx = 250.0;

const double _kDrag = 0.0000135;

enum _GestureType { pan, scale }

/// Pan + zoom for the pen canvas. The pen never reaches this widget's gesture
/// recognizer; only touch / mouse / trackpad pan and zoom the shared transform.
class PenInteractiveViewer extends StatefulWidget {
  const PenInteractiveViewer({
    super.key,
    required this.transformationController,
    required this.child,
    this.minScale = 0.5,
    this.maxScale = 8.0,
    this.panEnabled = true,
    this.scaleEnabled = true,
    this.scaleFactor = kDefaultMouseScrollToScaleFactor,
    this.interactionEndFrictionCoefficient = _kDrag,
  })  : assert(minScale > 0),
        assert(maxScale >= minScale);

  final TransformationController transformationController;
  final Widget child;
  final double minScale;
  final double maxScale;
  final bool panEnabled;
  final bool scaleEnabled;
  final double scaleFactor;
  final double interactionEndFrictionCoefficient;

  @override
  State<PenInteractiveViewer> createState() => _PenInteractiveViewerState();
}

class _PenInteractiveViewerState extends State<PenInteractiveViewer>
    with TickerProviderStateMixin {
  TransformationController get _transformer => widget.transformationController;

  final GlobalKey _childKey = GlobalKey();
  Animation<Offset>? _animation;
  Animation<double>? _scaleAnimation;
  late Offset _scaleAnimationFocalPoint;
  late AnimationController _controller;
  late AnimationController _scaleController;
  Offset? _referenceFocalPoint;
  double? _scaleStart;
  _GestureType? _gestureType;

  /// Number of pointers in the active gesture. When it changes (a finger lands
  /// or lifts, or a Windows touch dropout/re-acquire), we re-baseline instead of
  /// applying a frame whose scale/focal still refer to the old finger set.
  int _lastPointerCount = 0;

  /// The recognizer's cumulative `details.scale` and the absolute scale we last
  /// APPLIED, both as of the previous accepted frame. The pinch is driven
  /// absolutely from these + the gesture-start snapshot — we never read the live
  /// matrix back into the per-frame scale change. (Re-reading
  /// `getMaxScaleOnAxis()` per frame was the flicker source: a single transient
  /// mis-read/interleaved write made `desiredScale/liveScale` demand a ~1.3–1.4x
  /// jump for one frame and snap back. The glitch guard missed it because the
  /// spike sat just under the 1.4 threshold.)
  double _lastRawScale = 1.0;
  double _lastAppliedScale = 1.0;

  // --- Matrix helpers (infinite boundary → no clamping to bounds) -----------

  Matrix4 _matrixTranslate(Matrix4 matrix, Offset translation) {
    if (translation == Offset.zero) return matrix.clone();
    return matrix.clone()
      ..translateByDouble(translation.dx, translation.dy, 0, 1);
  }

  Matrix4 _matrixScale(Matrix4 matrix, double scale) {
    if (scale == 1.0) return matrix.clone();
    assert(scale != 0.0);
    final double currentScale = _transformer.value.getMaxScaleOnAxis();
    final double clampedTotalScale = clampDouble(
      currentScale * scale,
      widget.minScale,
      widget.maxScale,
    );
    final double clampedScale = clampedTotalScale / currentScale;
    return matrix.clone()
      ..scaleByDouble(clampedScale, clampedScale, clampedScale, 1);
  }

  bool _gestureIsSupported(_GestureType? gestureType) => switch (gestureType) {
        _GestureType.scale => widget.scaleEnabled,
        _GestureType.pan || null => widget.panEnabled,
      };

  _GestureType _getGestureType(ScaleUpdateDetails details) {
    final double scale = widget.scaleEnabled ? details.scale : 1.0;
    return (scale - 1).abs() > 0 ? _GestureType.scale : _GestureType.pan;
  }

  // --- Gesture lifecycle ----------------------------------------------------

  void _onScaleStart(ScaleStartDetails details) {
    if (_controller.isAnimating) {
      _controller.stop();
      _controller.reset();
      _animation?.removeListener(_handleInertiaAnimation);
      _animation = null;
    }
    if (_scaleController.isAnimating) {
      _scaleController.stop();
      _scaleController.reset();
      _scaleAnimation?.removeListener(_handleScaleAnimation);
      _scaleAnimation = null;
    }
    _gestureType = null;
    _lastPointerCount = details.pointerCount;
    _scaleStart = _transformer.value.getMaxScaleOnAxis();
    _referenceFocalPoint = _transformer.toScene(details.localFocalPoint);
    _lastRawScale = 1.0;
    _lastAppliedScale = _scaleStart!;
  }

  void _onScaleUpdate(ScaleUpdateDetails details) {
    final double scale = _transformer.value.getMaxScaleOnAxis();
    _scaleAnimationFocalPoint = details.localFocalPoint;

    // Re-baseline on any pointer-count change so a finger landing/lifting (or a
    // Windows touch dropout) can't make scale/focal jump from the stale set.
    // The transitional frame itself is skipped.
    if (details.pointerCount != _lastPointerCount) {
      _lastPointerCount = details.pointerCount;
      _scaleStart = _transformer.value.getMaxScaleOnAxis();
      _referenceFocalPoint = _transformer.toScene(details.localFocalPoint);
      _lastRawScale = details.scale;
      _lastAppliedScale = _scaleStart!;
      InputDiagnostics.instance.recordRebaseline();
      return;
    }

    final double focalJumpPx = details.focalPointDelta.distance;
    final Offset focalPointScene = _transformer.toScene(details.localFocalPoint);

    if (_gestureType == _GestureType.pan) {
      // A 2-finger gesture can start with no scale change; allow re-typing it.
      _gestureType = _getGestureType(details);
    } else {
      _gestureType ??= _getGestureType(details);
    }
    if (!_gestureIsSupported(_gestureType)) return;

    // Position-jump guard: during a pinch the focal midpoint should move
    // smoothly; a big single-frame jump is a touch misread → drop the frame.
    final bool focalDrop =
        details.pointerCount >= 2 && focalJumpPx > _kFocalGlitchPx;

    void record(double appliedChange, bool scaleDrop, bool focalDropped) {
      InputDiagnostics.instance.recordScaleFrame(
        rawScale: details.scale,
        pointerCount: details.pointerCount,
        currentScale: scale,
        appliedChange: appliedChange,
        focalJumpPx: focalJumpPx,
        scaleDrop: scaleDrop,
        focalDrop: focalDropped,
      );
    }

    switch (_gestureType!) {
      case _GestureType.scale:
        assert(_scaleStart != null);
        // Per-frame finger-motion ratio from the recognizer's OWN cumulative
        // scale — the clean, monotonic signal (verified against device logs).
        // Crucially we do NOT divide by the live matrix scale here: feeding
        // getMaxScaleOnAxis() back in is what let a single mis-read pop the zoom
        // and snap back. A ratio outside the glitch band is a real multi-touch
        // spike → drop the frame; absolute tracking means the next good frame
        // resumes from the true finger span, so the spike never shows.
        final double rawRatio =
            _lastRawScale > 0 ? details.scale / _lastRawScale : 1.0;
        final bool scaleDrop =
            rawRatio > _kScaleGlitchHi || rawRatio < _kScaleGlitchLo;
        if (scaleDrop || focalDrop) {
          record(1.0, scaleDrop, focalDrop);
          return;
        }

        // Drive the transform ABSOLUTELY from the gesture-start snapshot: the
        // target scale is `_scaleStart * details.scale`, and we re-anchor so the
        // scene point that was under the focal at gesture start stays under the
        // CURRENT focal (which also yields 2-finger pan for free). Closed form
        // for a pure scale+translate matrix — no inversion, no live read-back —
        // so an interleaved/transient matrix write can't survive into the next
        // frame: every frame is fully re-derived from clean inputs.
        final double targetScale = clampDouble(
          _scaleStart! * details.scale,
          widget.minScale,
          widget.maxScale,
        );
        final Offset focal = details.localFocalPoint;
        final double tx = focal.dx - targetScale * _referenceFocalPoint!.dx;
        final double ty = focal.dy - targetScale * _referenceFocalPoint!.dy;
        _transformer.value = Matrix4.identity()
          ..setEntry(0, 0, targetScale)
          ..setEntry(1, 1, targetScale)
          ..setEntry(2, 2, targetScale)
          ..setTranslationRaw(tx, ty, 0);

        final double applied =
            _lastAppliedScale > 0 ? targetScale / _lastAppliedScale : 1.0;
        _lastRawScale = details.scale;
        _lastAppliedScale = targetScale;
        record(applied, false, false);

      case _GestureType.pan:
        assert(_referenceFocalPoint != null);
        // Throw away near-scale frames so a stale reference can't jump the pan.
        if (details.scale != 1.0) return;
        if (focalDrop) {
          _referenceFocalPoint = _transformer.toScene(details.localFocalPoint);
          record(1.0, false, true);
          return;
        }
        final Offset translationChange =
            focalPointScene - _referenceFocalPoint!;
        _transformer.value =
            _matrixTranslate(_transformer.value, translationChange);
        _referenceFocalPoint = _transformer.toScene(details.localFocalPoint);
        record(1.0, false, false);
    }
  }

  void _onScaleEnd(ScaleEndDetails details) {
    _scaleStart = null;
    _referenceFocalPoint = null;
    _lastPointerCount = 0;
    _animation?.removeListener(_handleInertiaAnimation);
    _scaleAnimation?.removeListener(_handleScaleAnimation);
    _controller.reset();
    _scaleController.reset();

    if (!_gestureIsSupported(_gestureType)) return;

    switch (_gestureType) {
      case _GestureType.pan:
        if (details.velocity.pixelsPerSecond.distance < kMinFlingVelocity) {
          return;
        }
        final translationVector = _transformer.value.getTranslation();
        final Offset translation =
            Offset(translationVector.x, translationVector.y);
        final FrictionSimulation frictionSimulationX = FrictionSimulation(
          widget.interactionEndFrictionCoefficient,
          translation.dx,
          details.velocity.pixelsPerSecond.dx,
        );
        final FrictionSimulation frictionSimulationY = FrictionSimulation(
          widget.interactionEndFrictionCoefficient,
          translation.dy,
          details.velocity.pixelsPerSecond.dy,
        );
        final double tFinal = _getFinalTime(
          details.velocity.pixelsPerSecond.distance,
          widget.interactionEndFrictionCoefficient,
        );
        _animation = Tween<Offset>(
          begin: translation,
          end: Offset(frictionSimulationX.finalX, frictionSimulationY.finalX),
        ).animate(CurvedAnimation(parent: _controller, curve: Curves.decelerate));
        _controller.duration = Duration(milliseconds: (tFinal * 1000).round());
        _animation!.addListener(_handleInertiaAnimation);
        _controller.forward();
      case _GestureType.scale:
        if (details.scaleVelocity.abs() < 0.1) return;
        final double scale = _transformer.value.getMaxScaleOnAxis();
        final FrictionSimulation frictionSimulation = FrictionSimulation(
          widget.interactionEndFrictionCoefficient * widget.scaleFactor,
          scale,
          details.scaleVelocity / 10,
        );
        final double tFinal = _getFinalTime(
          details.scaleVelocity.abs(),
          widget.interactionEndFrictionCoefficient,
          effectivelyMotionless: 0.1,
        );
        _scaleAnimation = Tween<double>(
          begin: scale,
          end: frictionSimulation.x(tFinal),
        ).animate(
            CurvedAnimation(parent: _scaleController, curve: Curves.decelerate));
        _scaleController.duration = Duration(milliseconds: (tFinal * 1000).round());
        _scaleAnimation!.addListener(_handleScaleAnimation);
        _scaleController.forward();
      case null:
        break;
    }
  }

  // --- Mouse wheel / trackpad zoom ------------------------------------------

  void _receivedPointerSignal(PointerSignalEvent event) {
    final double scaleChange;
    if (event is PointerScrollEvent) {
      if (event.kind == PointerDeviceKind.trackpad) {
        // Trackpad scroll → pan.
        if (!_gestureIsSupported(_GestureType.pan)) return;
        final Offset localDelta = PointerEvent.transformDeltaViaPositions(
          untransformedEndPosition: event.position + event.scrollDelta,
          untransformedDelta: event.scrollDelta,
          transform: event.transform,
        );
        final Offset focalPointScene = _transformer.toScene(event.localPosition);
        final Offset newFocalPointScene =
            _transformer.toScene(event.localPosition - localDelta);
        _transformer.value = _matrixTranslate(
          _transformer.value,
          newFocalPointScene - focalPointScene,
        );
        return;
      }
      if (event.scrollDelta.dy == 0.0) return;
      scaleChange = math.exp(-event.scrollDelta.dy / widget.scaleFactor);
    } else if (event is PointerScaleEvent) {
      scaleChange = event.scale;
    } else {
      return;
    }
    if (!_gestureIsSupported(_GestureType.scale)) return;

    final Offset focalPointScene = _transformer.toScene(event.localPosition);
    _transformer.value = _matrixScale(_transformer.value, scaleChange);
    final Offset focalPointSceneScaled =
        _transformer.toScene(event.localPosition);
    _transformer.value = _matrixTranslate(
      _transformer.value,
      focalPointSceneScaled - focalPointScene,
    );
  }

  void _handleInertiaAnimation() {
    if (!_controller.isAnimating) {
      _animation?.removeListener(_handleInertiaAnimation);
      _animation = null;
      _controller.reset();
      return;
    }
    final translationVector = _transformer.value.getTranslation();
    final Offset translation = Offset(translationVector.x, translationVector.y);
    _transformer.value = _matrixTranslate(
      _transformer.value,
      _transformer.toScene(_animation!.value) - _transformer.toScene(translation),
    );
  }

  void _handleScaleAnimation() {
    if (!_scaleController.isAnimating) {
      _scaleAnimation?.removeListener(_handleScaleAnimation);
      _scaleAnimation = null;
      _scaleController.reset();
      return;
    }
    final double desiredScale = _scaleAnimation!.value;
    final double scaleChange =
        desiredScale / _transformer.value.getMaxScaleOnAxis();
    final Offset referenceFocalPoint =
        _transformer.toScene(_scaleAnimationFocalPoint);
    _transformer.value = _matrixScale(_transformer.value, scaleChange);
    final Offset focalPointSceneScaled =
        _transformer.toScene(_scaleAnimationFocalPoint);
    _transformer.value = _matrixTranslate(
      _transformer.value,
      focalPointSceneScaled - referenceFocalPoint,
    );
  }

  void _handleTransformation() => setState(() {});

  @override
  void initState() {
    super.initState();
    _controller = AnimationController(vsync: this);
    _scaleController = AnimationController(vsync: this);
    _transformer.addListener(_handleTransformation);
  }

  @override
  void didUpdateWidget(PenInteractiveViewer oldWidget) {
    super.didUpdateWidget(oldWidget);
    if (oldWidget.transformationController != widget.transformationController) {
      oldWidget.transformationController.removeListener(_handleTransformation);
      widget.transformationController.addListener(_handleTransformation);
    }
  }

  @override
  void dispose() {
    _controller.dispose();
    _scaleController.dispose();
    _transformer.removeListener(_handleTransformation);
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    Widget child = Transform(
      transform: _transformer.value,
      child: KeyedSubtree(key: _childKey, child: widget.child),
    );
    child = OverflowBox(
      alignment: Alignment.topLeft,
      minWidth: 0.0,
      minHeight: 0.0,
      maxWidth: double.infinity,
      maxHeight: double.infinity,
      child: child,
    );
    child = ClipRect(child: child);

    return Listener(
      onPointerSignal: _receivedPointerSignal,
      child: GestureDetector(
        behavior: HitTestBehavior.opaque,
        supportedDevices: _kPanZoomDevices,
        onScaleStart: _onScaleStart,
        onScaleUpdate: _onScaleUpdate,
        onScaleEnd: _onScaleEnd,
        trackpadScrollCausesScale: false,
        trackpadScrollToScaleFactor: Offset(0, -1 / widget.scaleFactor),
        child: child,
      ),
    );
  }
}

double _getFinalTime(double velocity, double drag,
    {double effectivelyMotionless = 10}) {
  return math.log(effectivelyMotionless / velocity) / math.log(drag / 100);
}
