// lib/editor/pdf/spike_editor_pane.dart
//
// THROWAWAY M1 spike widget (plan §10 / MUST #2, #4, #5). Hosts a pdfrx
// PdfViewer.file and exercises the three things the M1 gate must prove:
//
//  1. Coordinate correctness (MUST #2): a `pageOverlaysBuilder` paints a
//     diagnostic crosshair at normalized (0.5, 0.5) using
//     `canvas.scale(size.width, size.height)`, with the CustomPaint sized to
//     `pageRect.size` (plan §2.1). This dot MUST sit at the visual page center
//     at every zoom level. `coordinate_assertion_test.dart` asserts this.
//
//  2. Pen/touch arbitration (MUST #3): a `viewerOverlayBuilder` wraps a
//     `PenCaptureRegion` so pen events draw a live viewer-level stroke while
//     touch scrolls and pinch zooms — same overlay, no mode switch.
//
//  3. Ink-overlay build cost (MUST #5): a toggle injects ~N synthetic strokes
//     per page (from dense_strokes.json) into the page overlay so the perf
//     bench can measure BUILD time with a non-trivial ui.Picture per page.
//
// This file is NOT production code and is excluded from the real editor.

import 'dart:convert';
import 'dart:io';

import 'package:flutter/material.dart';
import 'package:pdfrx/pdfrx.dart';

import 'pen_capture_region.dart';

/// Normalized page-space point the diagnostic marker is painted at. The M1
/// coordinate assertion checks this maps to the page-center pixel at all zooms.
const Offset kMarkerNormalized = Offset(0.5, 0.5);

/// A single captured pen sample in normalized page space, tagged with its page.
class _PenSample {
  const _PenSample(this.pageIndex, this.normalized);
  final int pageIndex;
  final Offset normalized;
}

/// Spike editor pane. Provide a [pdfPath] to a local PDF (e.g.
/// test/assets/large_300p.pdf). [denseStrokesAsset] is a filesystem PATH to the
/// synthetic ink load (MUST #5); if null the ink-load toggle is inert.
class SpikeEditorPane extends StatefulWidget {
  const SpikeEditorPane({
    super.key,
    required this.pdfPath,
    this.denseStrokesAsset,
    this.strokesPerPage = 300,
    this.strokeCountKey = '2000',
    this.onViewerReady,
    this.controller,
  });

  final String pdfPath;
  final String? denseStrokesAsset;
  final int strokesPerPage;

  /// Which top-level array in dense_strokes.json to draw from ("2000"/"5000").
  final String strokeCountKey;

  /// Forwarded from pdfrx once the document is laid out and interactive.
  final void Function(PdfDocument document, PdfViewerController controller)?
      onViewerReady;

  /// Optional externally-owned controller (tests drive zoom through this).
  final PdfViewerController? controller;

  @override
  State<SpikeEditorPane> createState() => SpikeEditorPaneState();
}

class SpikeEditorPaneState extends State<SpikeEditorPane> {
  late final PdfViewerController _controller =
      widget.controller ?? PdfViewerController();

  /// Live pen strokes captured via PenCaptureRegion (viewer-level overlay).
  final List<List<_PenSample>> _penStrokes = <List<_PenSample>>[];
  List<_PenSample>? _activeStroke;

  /// Synthetic strokes for the ink-load gate, lazily loaded. Each entry is a
  /// list of normalized polylines (one stroke = list of points).
  List<List<Offset>>? _syntheticStrokes;
  bool _inkLoadEnabled = false;
  bool _loadingSynthetic = false;

  bool get inkLoadEnabled => _inkLoadEnabled;

  /// Toggle the dense synthetic-ink overlay (MUST #5). Loads the asset on first
  /// enable. Public so the perf bench can drive it programmatically.
  Future<void> setInkLoad(bool enabled) async {
    if (enabled && _syntheticStrokes == null) {
      await _loadSyntheticStrokes();
    }
    if (mounted) setState(() => _inkLoadEnabled = enabled);
  }

  Future<void> _loadSyntheticStrokes() async {
    final asset = widget.denseStrokesAsset;
    if (asset == null || _loadingSynthetic) return;
    _loadingSynthetic = true;
    try {
      // [asset] is a filesystem path (e.g. test/assets/dense_strokes.json),
      // not a bundled rootBundle key — regenerate via tool/gen_dense_strokes.dart.
      final raw = await File(asset).readAsString();
      final decoded = jsonDecode(raw) as Map<String, dynamic>;
      final strokesJson =
          (decoded[widget.strokeCountKey] as List<dynamic>? ?? const []);
      final result = <List<Offset>>[];
      for (final s in strokesJson) {
        final points = (s as Map<String, dynamic>)['points'] as List<dynamic>;
        final poly = <Offset>[];
        for (final p in points) {
          final pt = p as Map<String, dynamic>;
          poly.add(Offset(
            (pt['x'] as num).toDouble(),
            (pt['y'] as num).toDouble(),
          ));
        }
        if (poly.length >= 2) result.add(poly);
      }
      _syntheticStrokes = result;
    } finally {
      _loadingSynthetic = false;
    }
  }

  // --- Pen capture (viewer-level) ---------------------------------------

  void _onPenEvent(PointerEvent event) {
    // Convert global → document → which page + normalized page coords.
    final doc = _controller.globalToDocument(event.position);
    if (doc == null) return;
    final hit = _documentToPage(doc);
    if (hit == null) return;

    if (event is PointerDownEvent) {
      _activeStroke = <_PenSample>[hit];
      _penStrokes.add(_activeStroke!);
      setState(() {});
    } else if (event is PointerMoveEvent) {
      _activeStroke?.add(hit);
      setState(() {});
    } else if (event is PointerUpEvent || event is PointerCancelEvent) {
      _activeStroke = null;
    }
  }

  /// Maps a document-space point to (pageIndex, normalized-in-page) using the
  /// controller's page layout rects (document coordinates). Returns null if the
  /// point is outside every page box.
  _PenSample? _documentToPage(Offset doc) {
    if (!_controller.isReady) return null;
    final rects = _controller.layout.pageLayouts;
    for (var i = 0; i < rects.length; i++) {
      final r = rects[i];
      if (r.contains(doc)) {
        final nx = ((doc.dx - r.left) / r.width).clamp(0.0, 1.0);
        final ny = ((doc.dy - r.top) / r.height).clamp(0.0, 1.0);
        return _PenSample(i, Offset(nx, ny));
      }
    }
    return null;
  }

  @override
  Widget build(BuildContext context) {
    return Stack(
      children: [
        PdfViewer.file(
          widget.pdfPath,
          controller: _controller,
          params: PdfViewerParams(
            onViewerReady: widget.onViewerReady,
            // (1) Per-page overlay: diagnostic center marker + optional synthetic
            // ink. CustomPaint is sized to pageRect.size so canvas.scale maps
            // normalized [0,1] → zoomed pixels (plan §2.1).
            pageOverlaysBuilder: (context, pageRectInViewer, page) {
              final pageIndex = page.pageNumber - 1;
              return [
                SizedBox.fromSize(
                  size: pageRectInViewer.size,
                  child: CustomPaint(
                    painter: _SpikeInkPainter(
                      synthetic:
                          _inkLoadEnabled ? _strokesForPage(pageIndex) : null,
                    ),
                  ),
                ),
              ];
            },
            // (2) Viewer-level overlay: pen capture + live pen rendering. Touch
            // falls through to pdfrx for scroll/zoom (per-kind hit-test split).
            viewerOverlayBuilder: (context, size, handleLinkTap) {
              return [
                Positioned.fill(
                  child: PenCaptureRegion(
                    onPenEvent: _onPenEvent,
                    child: IgnorePointer(
                      child: CustomPaint(
                        size: size,
                        painter: _LivePenPainter(
                          strokes: _penStrokes,
                          controller: _controller,
                        ),
                      ),
                    ),
                  ),
                ),
              ];
            },
          ),
        ),
      ],
    );
  }

  /// Deterministic per-page slice of the synthetic stroke pool so each page
  /// shows ~[widget.strokesPerPage] strokes without loading 300× the data.
  List<List<Offset>> _strokesForPage(int pageIndex) {
    final pool = _syntheticStrokes;
    if (pool == null || pool.isEmpty) return const [];
    final n = widget.strokesPerPage.clamp(0, pool.length);
    final start = (pageIndex * n) % pool.length;
    final out = <List<Offset>>[];
    for (var i = 0; i < n; i++) {
      out.add(pool[(start + i) % pool.length]);
    }
    return out;
  }

  // Note: PdfViewerController is not a Listenable/ChangeNotifier we own a
  // lifecycle for; pdfrx attaches/detaches it via the PdfViewer. No dispose().
}

/// Paints the diagnostic center marker (always) plus synthetic ink (when the
/// MUST #5 load is enabled), in normalized [0,1] page space scaled to the
/// CustomPaint size (== zoomed page box). This is what the coordinate assertion
/// inspects.
class _SpikeInkPainter extends CustomPainter {
  _SpikeInkPainter({this.synthetic});

  final List<List<Offset>>? synthetic;

  @override
  void paint(Canvas canvas, Size size) {
    canvas.save();
    // Map normalized [0,1] → zoomed pixels (plan §2.1).
    canvas.scale(size.width, size.height);

    // Synthetic ink load (MUST #5): a non-trivial set of polylines per page.
    final syn = synthetic;
    if (syn != null && syn.isNotEmpty) {
      final inkPaint = Paint()
        ..color = const Color(0x5500AAFF)
        ..style = PaintingStyle.stroke
        // Stroke width is in normalized units post-scale; keep it page-relative
        // and hairline-ish so 300 strokes are visible but cheap.
        ..strokeWidth = 0.002
        ..strokeCap = StrokeCap.round;
      for (final poly in syn) {
        if (poly.length < 2) continue;
        final path = Path()..moveTo(poly.first.dx, poly.first.dy);
        for (var i = 1; i < poly.length; i++) {
          path.lineTo(poly[i].dx, poly[i].dy);
        }
        canvas.drawPath(path, inkPaint);
      }
    }

    canvas.restore();

    // Diagnostic crosshair at normalized (0.5,0.5) — drawn in PIXEL space (after
    // restore) so its line thickness is constant on screen and its CENTER is at
    // exactly size.width*0.5, size.height*0.5. The coordinate assertion checks
    // this pixel.
    final center = Offset(
      size.width * kMarkerNormalized.dx,
      size.height * kMarkerNormalized.dy,
    );
    final markerPaint = Paint()
      ..color = const Color(0xFFFF0066)
      ..strokeWidth = 2.0
      ..style = PaintingStyle.stroke;
    const arm = 16.0;
    canvas.drawLine(
        center.translate(-arm, 0), center.translate(arm, 0), markerPaint);
    canvas.drawLine(
        center.translate(0, -arm), center.translate(0, arm), markerPaint);
    canvas.drawCircle(center, 3.0, Paint()..color = const Color(0xFFFF0066));
  }

  @override
  bool shouldRepaint(covariant _SpikeInkPainter oldDelegate) =>
      oldDelegate.synthetic != synthetic;
}

/// Paints live pen strokes captured by the PenCaptureRegion. Strokes are stored
/// in normalized page space, so for each sample we re-project page→document→
/// local each paint via the controller (keeps strokes glued to pages under
/// scroll/zoom — the §2.1 property, exercised at the viewer level here).
class _LivePenPainter extends CustomPainter {
  _LivePenPainter({required this.strokes, required this.controller})
      : super(repaint: controller);

  final List<List<_PenSample>> strokes;
  final PdfViewerController controller;

  @override
  void paint(Canvas canvas, Size size) {
    if (!controller.isReady) return;
    final rects = controller.layout.pageLayouts;
    final paint = Paint()
      ..color = const Color(0xFF1565C0)
      ..style = PaintingStyle.stroke
      ..strokeWidth = 3.0
      ..strokeCap = StrokeCap.round
      ..strokeJoin = StrokeJoin.round;

    for (final stroke in strokes) {
      Path? path;
      for (final s in stroke) {
        if (s.pageIndex >= rects.length) continue;
        final r = rects[s.pageIndex];
        // normalized page → document
        final docPt = Offset(
          r.left + s.normalized.dx * r.width,
          r.top + s.normalized.dy * r.height,
        );
        // document → local (viewer) coords
        final local = controller.documentToLocal(docPt);
        if (path == null) {
          path = Path()..moveTo(local.dx, local.dy);
        } else {
          path.lineTo(local.dx, local.dy);
        }
      }
      if (path != null) canvas.drawPath(path, paint);
    }
  }

  @override
  bool shouldRepaint(covariant _LivePenPainter oldDelegate) => true;
}
