// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import 'dart:async';

import 'package:flutter/painting.dart';
import 'package:flutter/scheduler.dart';

import 'disposable_build_context.dart';
import 'framework.dart';
import 'scrollable.dart';

/// An [ImageProvider] that makes use of
/// [Scrollable.recommendDeferredLoadingForContext] to avoid loading images when
/// rapidly scrolling.
///
/// This provider assumes that its wrapped [imageProvider] correctly uses the
/// [ImageCache], and does not attempt to re-acquire or decode images in the
/// cache.
///
/// Calling [resolve] on this provider will cause it to obtain the image key
/// and then check the following:
///
///   1. If the returned [ImageStream] has been completed, end. This can happen
///      if the caller sets the completer on the stream.
///   2. If the [ImageCache] has a completer for the key for this image, ask the
///      wrapped provider to resolve.
///      This can happen if the image was precached, or another [ImageProvider]
///      already resolved the same image.
///   3. If the [context] has been disposed, end. This can happen if the caller
///      has been disposed and is no longer interested in resolving the image.
///   4. If the widget is scrolling with high velocity at this point in time,
///      wait until the beginning of the next frame and go back to step 1.
///   5. Delegate loading the image to the wrapped provider and finish.
///
/// If the cycle ends at steps 1 or 3, the [ImageStream] will never be marked as
/// complete and listeners will not be notified.
///
/// The [Image] widget wraps its incoming providers with this provider to avoid
/// overutilization of resources for images that would never appear on screen or
/// only be visible for a very brief period.
@optionalTypeArgs
class ScrollAwareImageProvider<T extends Object> extends ImageProvider<T> {
  /// Creates a [ScrollAwareImageProvider].
  ///
  /// The [context] object is the [BuildContext] of the [State] using this
  /// provider. It is used to determine scrolling velocity during [resolve]. It
  /// must not be null.
  ///
  /// The [imageProvider] is used to create a key and load the image. It must
  /// not be null, and is assumed to interact with the cache in the normal way
  /// that [ImageProvider.resolveStreamForKey] does.
  const ScrollAwareImageProvider({
    required this.context,
    required this.imageProvider,
  }) : assert(context != null),
       assert(imageProvider != null);

  /// The context that may or may not be enclosed by a [Scrollable].
  ///
  /// Once [DisposableBuildContext.dispose] is called on this context,
  /// the provider will stop trying to resolve the image if it has not already
  /// been resolved.
  final DisposableBuildContext context;

  /// The wrapped image provider to delegate [obtainKey] and [load] to.
  final ImageProvider<T> imageProvider;

  @override
  void resolveStreamForKey(
    ImageConfiguration configuration,
    ImageStream stream,
    T key,
    ImageErrorListener handleError,
  ) {
    // Something managed to complete the stream, or it's already in the image
    // cache. Notify the wrapped provider and expect it to behave by not
    // reloading the image since it's already resolved.
    // Do this even if the context has gone out of the tree, since it will
    // update LRU information about the cache. Even though we never showed the
    // image, it was still touched more recently.
    // Do this before checking scrolling, so that if the bytes are available we
    // render them even though we're scrolling fast - there's no additional
    // allocations to do for texture memory, it's already there.
    if (stream.completer != null || PaintingBinding.instance!.imageCache!.containsKey(key)) {
      imageProvider.resolveStreamForKey(configuration, stream, key, handleError);
      return;
    }
    // The context has gone out of the tree - ignore it.
    if (context.context == null) {
      return;
    }
    // Something still wants this image, but check if the context is scrolling
    // too fast before scheduling work that might never show on screen.
    // Try to get to end of the frame callbacks of the next frame, and then
    // check again.
    if (Scrollable.recommendDeferredLoadingForContext(context.context!)) {
        SchedulerBinding.instance!.scheduleFrameCallback((_) {
          scheduleMicrotask(() => resolveStreamForKey(configuration, stream, key, handleError));
        });
        return;
    }
    // We are in the tree, we're not scrolling too fast, the cache doesn't
    // have our image, and no one has otherwise completed the stream.  Go.
    imageProvider.resolveStreamForKey(configuration, stream, key, handleError);
  }

  @override
  ImageStreamCompleter load(T key, DecoderCallback decode) => imageProvider.load(key, decode);

  @override
  Future<T> obtainKey(ImageConfiguration configuration) => imageProvider.obtainKey(configuration);
}
