// lib/editor/render/ink_picture_cache.dart
//
// Bounded LRU cache of ui.Picture objects keyed by "hostId:revision".
//
// Resolution-independent: ink is vector, so a single Picture is valid at any
// zoom level. There are NO DPI buckets.
//
// Evicted Pictures are disposed via a post-frame callback so Flutter's raster
// thread is never asked to delete a Picture it may still be reading.

import 'dart:collection';
import 'dart:ui' as ui;

import 'package:flutter/widgets.dart';

/// Bounded LRU cache of [ui.Picture]s keyed by a string (typically
/// `"$hostId:$revision"`).
///
/// Usage:
/// ```dart
/// final picture = cache.getOrBuild(hostId, store.revision, size, () {
///   final recorder = ui.PictureRecorder();
///   final canvas = ui.Canvas(recorder);
///   // … draw …
///   return recorder.endRecording();
/// });
/// canvas.drawPicture(picture);
/// ```
class InkPictureCache {
  InkPictureCache({int maxSize = 12}) : _maxSize = maxSize;

  final int _maxSize;

  // LinkedHashMap preserves insertion order; we move accessed entries to the
  // back so the front is always the least-recently used.
  final LinkedHashMap<String, ui.Picture> _cache =
      LinkedHashMap<String, ui.Picture>();

  /// Returns a cached [ui.Picture] for [key], or calls [build] to create one.
  ///
  /// The [key] should encode all inputs that affect the picture content (host
  /// id + revision, at minimum). [size] and [build] are only used on a cache
  /// miss.
  ui.Picture getOrBuild(
    String hostId,
    int revision,
    ui.Size size,
    ui.Picture Function() build,
  ) {
    final key = '$hostId:$revision';

    if (_cache.containsKey(key)) {
      // Promote to most-recently-used by reinserting at the back.
      final pic = _cache.remove(key)!;
      _cache[key] = pic;
      return pic;
    }

    final picture = build();
    _cache[key] = picture;

    // Evict least-recently-used entries beyond the cap.
    while (_cache.length > _maxSize) {
      final lruKey = _cache.keys.first;
      final evicted = _cache.remove(lruKey)!;
      _disposeDeferred(evicted);
    }

    return picture;
  }

  /// Disposes all cached Pictures, deferring the actual disposal to a
  /// post-frame callback so any in-flight raster work can complete.
  void dispose() {
    final pictures = List<ui.Picture>.from(_cache.values);
    _cache.clear();
    for (final pic in pictures) {
      _disposeDeferred(pic);
    }
  }

  static void _disposeDeferred(ui.Picture picture) {
    WidgetsBinding.instance.addPostFrameCallback((_) {
      picture.dispose();
    });
  }
}
