import 'package:cached_network_image/cached_network_image.dart';
import 'package:collection/collection.dart';
import 'package:flutter/material.dart';

import '../app_progress_indicator.dart';
import 'pm_document_view.dart';
import 'pm_node.dart';

/// Renders raw ProseMirror JSON and memoises the parsed tree across rebuilds.
/// Parsing decodes base64 images; doing it in build() would re-decode them on
/// every rebuild — call sites should use this instead of PmNode.fromJson.
///
/// Images are precached before a document is presented, so the reader never
/// sees images pop in and text reflow downwards. Until the first document is
/// ready a spinner shows; on updates the previous document stays visible until
/// the new one is fully precached (no spinner flash on background refreshes).
/// A timeout keeps a dead network image from blocking the swap forever (the
/// image then renders with its broken-image fallback).
class PmJsonView extends StatefulWidget {
  final Map<String, dynamic> json;
  final void Function(String href)? onLinkTap;

  const PmJsonView({required this.json, this.onLinkTap, super.key});

  static const Duration precacheTimeout = Duration(seconds: 8);

  @override
  State<PmJsonView> createState() => _PmJsonViewState();
}

class _PmJsonViewState extends State<PmJsonView> {
  PmNode? _shown;
  PmNode? _pending;
  List<ImageProvider> _pendingProviders = const [];
  bool _precacheStarted = false;
  int _generation = 0;

  @override
  void initState() {
    super.initState();
    _parse();
  }

  @override
  void didUpdateWidget(PmJsonView oldWidget) {
    super.didUpdateWidget(oldWidget);
    if (identical(oldWidget.json, widget.json)) return;
    // Background refreshes deliver a new map instance with identical content;
    // re-parsing would re-decode every base64 image for a no-op swap.
    if (const DeepCollectionEquality().equals(oldWidget.json, widget.json)) {
      return;
    }
    _parse();
    _precache();
  }

  @override
  void didChangeDependencies() {
    super.didChangeDependencies();
    _precache();
  }

  void _parse() {
    final doc = PmNode.fromJson(widget.json);
    final providers = <ImageProvider>[];
    _collectProviders(doc, providers);
    _generation++;
    _precacheStarted = false;
    if (providers.isEmpty) {
      _shown = doc;
      _pending = null;
      _pendingProviders = const [];
    } else {
      _pending = doc;
      _pendingProviders = providers;
    }
  }

  void _precache() {
    final pending = _pending;
    if (pending == null || _precacheStarted) return;
    _precacheStarted = true;
    final generation = _generation;
    Future.wait([
          for (final provider in _pendingProviders)
            precacheImage(provider, context, onError: (_, _) {}),
        ])
        .timeout(PmJsonView.precacheTimeout, onTimeout: () => const [])
        .whenComplete(() {
          if (mounted && generation == _generation) {
            setState(() {
              _shown = pending;
              _pending = null;
              _pendingProviders = const [];
            });
          }
        });
  }

  void _collectProviders(PmNode node, List<ImageProvider> out) {
    if (node is PmImage) {
      final bytes = node.bytes;
      if (bytes != null) {
        out.add(MemoryImage(bytes));
      } else if (node.src.startsWith('http')) {
        out.add(CachedNetworkImageProvider(node.src));
      }
    }
    for (final child in node.children) {
      _collectProviders(child, out);
    }
  }

  @override
  Widget build(BuildContext context) {
    final shown = _shown;
    if (shown == null) {
      return const SizedBox(
        height: 160,
        child: Center(child: AppProgressIndicator.large()),
      );
    }
    return PmDocumentView(doc: shown, onLinkTap: widget.onLinkTap);
  }
}
