// 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 'dart:ui' as ui show Image, Codec, FrameInfo;
import 'dart:ui' show hashValues;

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

/// A [dart:ui.Image] object with its corresponding scale.
///
/// ImageInfo objects are used by [ImageStream] objects to represent the
/// actual data of the image once it has been obtained.
///
/// The receiver of an [ImageInfo] object must call [dispose]. To safely share
/// the object with other clients, use the [clone] method before calling
/// dispose.
@immutable
class ImageInfo {
  /// Creates an [ImageInfo] object for the given [image] and [scale].
  ///
  /// Both the [image] and the [scale] must not be null.
  ///
  /// The [debugLabel] may be used to identify the source of this image.
  const ImageInfo({ required this.image, this.scale = 1.0, this.debugLabel })
    : assert(image != null),
      assert(scale != null);

  /// Creates an [ImageInfo] with a cloned [image].
  ///
  /// Once all outstanding references to the [image] are disposed, it is no
  /// longer safe to access properties of it or attempt to draw it. Clones serve
  /// to create new references to the underlying image data that can safely be
  /// disposed without knowledge of whether some other reference holder will
  /// still need access to the underlying image. Once a client disposes of its
  /// own image reference, it can no longer access the image, but other clients
  /// will be able to access their own references.
  ///
  /// This method must be used in cases where a client holding an [ImageInfo]
  /// needs to share the image info object with another client and will still
  /// need to access the underlying image data at some later point, e.g. to
  /// share it again with another client.
  ///
  /// See also:
  ///
  ///  * [Image.clone], which describes how and why to clone images.
  ImageInfo clone() {
    return ImageInfo(
      image: image.clone(),
      scale: scale,
      debugLabel: debugLabel,
    );
  }

  /// Whether this [ImageInfo] is a [clone] of the `other`.
  ///
  /// This method is a convenience wrapper for [Image.isCloneOf], and is useful
  /// for clients that are trying to determine whether new layout or painting
  /// logic is required when receiving a new image reference.
  ///
  /// {@tool snippet}
  ///
  /// The following sample shows how to appropriately check whether the
  /// [ImageInfo] reference refers to new image data or not.
  ///
  /// ```dart
  /// ImageInfo? _imageInfo;
  /// set imageInfo (ImageInfo? value) {
  ///   // If the image reference is exactly the same, do nothing.
  ///   if (value == _imageInfo) {
  ///     return;
  ///   }
  ///   // If it is a clone of the current reference, we must dispose of it and
  ///   // can do so immediately. Since the underlying image has not changed,
  ///   // We don't have any additional work to do here.
  ///   if (value != null && _imageInfo != null && value.isCloneOf(_imageInfo!)) {
  ///     value.dispose();
  ///     return;
  ///   }
  ///   _imageInfo?.dispose();
  ///   _imageInfo = value;
  ///   // Perform work to determine size, or paint the image.
  /// }
  /// ```
  /// {@end-tool}
  bool isCloneOf(ImageInfo other) {
    return other.image.isCloneOf(image)
        && scale == scale
        && other.debugLabel == debugLabel;
  }

  /// The raw image pixels.
  ///
  /// This is the object to pass to the [Canvas.drawImage],
  /// [Canvas.drawImageRect], or [Canvas.drawImageNine] methods when painting
  /// the image.
  final ui.Image image;

  /// The linear scale factor for drawing this image at its intended size.
  ///
  /// The scale factor applies to the width and the height.
  ///
  /// {@template flutter.painting.imageInfo.scale}
  /// For example, if this is 2.0, it means that there are four image pixels for
  /// every one logical pixel, and the image's actual width and height (as given
  /// by the [dart:ui.Image.width] and [dart:ui.Image.height] properties) are
  /// double the height and width that should be used when painting the image
  /// (e.g. in the arguments given to [Canvas.drawImage]).
  /// {@endtemplate}
  final double scale;

  /// A string used for debugging purposes to identify the source of this image.
  final String? debugLabel;

  /// Disposes of this object.
  ///
  /// Once this method has been called, the object should not be used anymore,
  /// and no clones of it or the image it contains can be made.
  void dispose() {
    assert((image.debugGetOpenHandleStackTraces()?.length ?? 1) > 0);
    image.dispose();
  }

  @override
  String toString() => '${debugLabel != null ? '$debugLabel ' : ''}$image @ ${debugFormatDouble(scale)}x';

  @override
  int get hashCode => hashValues(image, scale, debugLabel);

  @override
  bool operator ==(Object other) {
    if (other.runtimeType != runtimeType)
      return false;
    return other is ImageInfo
        && other.image == image
        && other.scale == scale
        && other.debugLabel == debugLabel;
  }
}

/// Interface for receiving notifications about the loading of an image.
///
/// This class overrides [operator ==] and [hashCode] to compare the individual
/// callbacks in the listener, meaning that if you add an instance of this class
/// as a listener (e.g. via [ImageStream.addListener]), you can instantiate a
/// _different_ instance of this class when you remove the listener, and the
/// listener will be properly removed as long as all associated callbacks are
/// equal.
///
/// Used by [ImageStream] and [ImageStreamCompleter].
@immutable
class ImageStreamListener {
  /// Creates a new [ImageStreamListener].
  ///
  /// The [onImage] parameter must not be null.
  const ImageStreamListener(
    this.onImage, {
    this.onChunk,
    this.onError,
  }) : assert(onImage != null);

  /// Callback for getting notified that an image is available.
  ///
  /// This callback may fire multiple times (e.g. if the [ImageStreamCompleter]
  /// that drives the notifications fires multiple times). An example of such a
  /// case would be an image with multiple frames within it (such as an animated
  /// GIF).
  ///
  /// For more information on how to interpret the parameters to the callback,
  /// see the documentation on [ImageListener].
  ///
  /// See also:
  ///
  ///  * [onError], which will be called instead of [onImage] if an error occurs
  ///    during loading.
  final ImageListener onImage;

  /// Callback for getting notified when a chunk of bytes has been received
  /// during the loading of the image.
  ///
  /// This callback may fire many times (e.g. when used with a [NetworkImage],
  /// where the image bytes are loaded incrementally over the wire) or not at
  /// all (e.g. when used with a [MemoryImage], where the image bytes are
  /// already available in memory).
  ///
  /// This callback may also continue to fire after the [onImage] callback has
  /// fired (e.g. for multi-frame images that continue to load after the first
  /// frame is available).
  final ImageChunkListener? onChunk;

  /// Callback for getting notified when an error occurs while loading an image.
  ///
  /// If an error occurs during loading, [onError] will be called instead of
  /// [onImage].
  ///
  /// If [onError] is called and does not throw, then the error is considered to
  /// be handled. An error handler can explicitly rethrow the exception reported
  /// to it to safely indicate that it did not handle the exception.
  ///
  /// If an image stream has no listeners that handled the error when the error
  /// was first encountered, then the error is reported using
  /// [FlutterError.reportError], with the [FlutterErrorDetails.silent] flag set
  /// to true.
  final ImageErrorListener? onError;

  @override
  int get hashCode => hashValues(onImage, onChunk, onError);

  @override
  bool operator ==(Object other) {
    if (other.runtimeType != runtimeType)
      return false;
    return other is ImageStreamListener
        && other.onImage == onImage
        && other.onChunk == onChunk
        && other.onError == onError;
  }
}

/// Signature for callbacks reporting that an image is available.
///
/// Used in [ImageStreamListener].
///
/// The `image` argument contains information about the image to be rendered.
/// The implementer of [ImageStreamListener.onImage] is expected to call dispose
/// on the [ui.Image] it receives.
///
/// The `synchronousCall` argument is true if the listener is being invoked
/// during the call to `addListener`. This can be useful if, for example,
/// [ImageStream.addListener] is invoked during a frame, so that a new rendering
/// frame is requested if the call was asynchronous (after the current frame)
/// and no rendering frame is requested if the call was synchronous (within the
/// same stack frame as the call to [ImageStream.addListener]).
typedef ImageListener = void Function(ImageInfo image, bool synchronousCall);

/// Signature for listening to [ImageChunkEvent] events.
///
/// Used in [ImageStreamListener].
typedef ImageChunkListener = void Function(ImageChunkEvent event);

/// Signature for reporting errors when resolving images.
///
/// Used in [ImageStreamListener], as well as by [ImageCache.putIfAbsent] and
/// [precacheImage], to report errors.
typedef ImageErrorListener = void Function(Object exception, StackTrace? stackTrace);

/// An immutable notification of image bytes that have been incrementally loaded.
///
/// Chunk events represent progress notifications while an image is being
/// loaded (e.g. from disk or over the network).
///
/// See also:
///
///  * [ImageChunkListener], the means by which callers get notified of
///    these events.
@immutable
class ImageChunkEvent with Diagnosticable {
  /// Creates a new chunk event.
  const ImageChunkEvent({
    required this.cumulativeBytesLoaded,
    required this.expectedTotalBytes,
  }) : assert(cumulativeBytesLoaded >= 0),
       assert(expectedTotalBytes == null || expectedTotalBytes >= 0);

  /// The number of bytes that have been received across the wire thus far.
  final int cumulativeBytesLoaded;

  /// The expected number of bytes that need to be received to finish loading
  /// the image.
  ///
  /// This value is not necessarily equal to the expected _size_ of the image
  /// in bytes, as the bytes required to load the image may be compressed.
  ///
  /// This value will be null if the number is not known in advance.
  ///
  /// When this value is null, the chunk event may still be useful as an
  /// indication that data is loading (and how much), but it cannot represent a
  /// loading completion percentage.
  final int? expectedTotalBytes;

  @override
  void debugFillProperties(DiagnosticPropertiesBuilder properties) {
    super.debugFillProperties(properties);
    properties.add(IntProperty('cumulativeBytesLoaded', cumulativeBytesLoaded));
    properties.add(IntProperty('expectedTotalBytes', expectedTotalBytes));
  }
}

/// A handle to an image resource.
///
/// ImageStream represents a handle to a [dart:ui.Image] object and its scale
/// (together represented by an [ImageInfo] object). The underlying image object
/// might change over time, either because the image is animating or because the
/// underlying image resource was mutated.
///
/// ImageStream objects can also represent an image that hasn't finished
/// loading.
///
/// ImageStream objects are backed by [ImageStreamCompleter] objects.
///
/// The [ImageCache] will consider an image to be live until the listener count
/// drops to zero after adding at least one listener. The
/// [ImageStreamCompleter.addOnLastListenerRemovedCallback] method is used for
/// tracking this information.
///
/// See also:
///
///  * [ImageProvider], which has an example that includes the use of an
///    [ImageStream] in a [Widget].
class ImageStream with Diagnosticable {
  /// Create an initially unbound image stream.
  ///
  /// Once an [ImageStreamCompleter] is available, call [setCompleter].
  ImageStream();

  /// The completer that has been assigned to this image stream.
  ///
  /// Generally there is no need to deal with the completer directly.
  ImageStreamCompleter? get completer => _completer;
  ImageStreamCompleter? _completer;

  List<ImageStreamListener>? _listeners;

  /// Assigns a particular [ImageStreamCompleter] to this [ImageStream].
  ///
  /// This is usually done automatically by the [ImageProvider] that created the
  /// [ImageStream].
  ///
  /// This method can only be called once per stream. To have an [ImageStream]
  /// represent multiple images over time, assign it a completer that
  /// completes several images in succession.
  void setCompleter(ImageStreamCompleter value) {
    assert(_completer == null);
    _completer = value;
    if (_listeners != null) {
      final List<ImageStreamListener> initialListeners = _listeners!;
      _listeners = null;
      initialListeners.forEach(_completer!.addListener);
    }
  }

  /// Adds a listener callback that is called whenever a new concrete [ImageInfo]
  /// object is available. If a concrete image is already available, this object
  /// will call the listener synchronously.
  ///
  /// If the assigned [completer] completes multiple images over its lifetime,
  /// this listener will fire multiple times.
  ///
  /// {@template flutter.painting.imageStream.addListener}
  /// The listener will be passed a flag indicating whether a synchronous call
  /// occurred. If the listener is added within a render object paint function,
  /// then use this flag to avoid calling [RenderObject.markNeedsPaint] during
  /// a paint.
  ///
  /// If a duplicate `listener` is registered N times, then it will be called N
  /// times when the image stream completes (whether because a new image is
  /// available or because an error occurs). Likewise, to remove all instances
  /// of the listener, [removeListener] would need to called N times as well.
  ///
  /// When a `listener` receives an [ImageInfo] object, the `listener` is
  /// responsible for disposing of the [ImageInfo.image].
  /// {@endtemplate}
  void addListener(ImageStreamListener listener) {
    if (_completer != null)
      return _completer!.addListener(listener);
    _listeners ??= <ImageStreamListener>[];
    _listeners!.add(listener);
  }

  /// Stops listening for events from this stream's [ImageStreamCompleter].
  ///
  /// If [listener] has been added multiple times, this removes the _first_
  /// instance of the listener.
  void removeListener(ImageStreamListener listener) {
    if (_completer != null)
      return _completer!.removeListener(listener);
    assert(_listeners != null);
    for (int i = 0; i < _listeners!.length; i += 1) {
      if (_listeners![i] == listener) {
        _listeners!.removeAt(i);
        break;
      }
    }
  }

  /// Returns an object which can be used with `==` to determine if this
  /// [ImageStream] shares the same listeners list as another [ImageStream].
  ///
  /// This can be used to avoid un-registering and re-registering listeners
  /// after calling [ImageProvider.resolve] on a new, but possibly equivalent,
  /// [ImageProvider].
  ///
  /// The key may change once in the lifetime of the object. When it changes, it
  /// will go from being different than other [ImageStream]'s keys to
  /// potentially being the same as others'. No notification is sent when this
  /// happens.
  Object get key => _completer ?? this;

  @override
  void debugFillProperties(DiagnosticPropertiesBuilder properties) {
    super.debugFillProperties(properties);
    properties.add(ObjectFlagProperty<ImageStreamCompleter>(
      'completer',
      _completer,
      ifPresent: _completer?.toStringShort(),
      ifNull: 'unresolved',
    ));
    properties.add(ObjectFlagProperty<List<ImageStreamListener>>(
      'listeners',
      _listeners,
      ifPresent: '${_listeners?.length} listener${_listeners?.length == 1 ? "" : "s" }',
      ifNull: 'no listeners',
      level: _completer != null ? DiagnosticLevel.hidden : DiagnosticLevel.info,
    ));
    _completer?.debugFillProperties(properties);
  }
}

/// An opaque handle that keeps an [ImageStreamCompleter] alive even if it has
/// lost its last listener.
///
/// To create a handle, use [ImageStreamCompleter.keepAlive].
///
/// Such handles are useful when an image cache needs to keep a completer alive
/// but does not actually have a listener subscribed, or when a widget that
/// displays an image needs to temporarily unsubscribe from the completer but
/// may re-subscribe in the future, for example when the [TickerMode] changes.
class ImageStreamCompleterHandle {
  ImageStreamCompleterHandle._(ImageStreamCompleter this._completer) {
    _completer!._keepAliveHandles += 1;
  }

  ImageStreamCompleter? _completer;

  /// Call this method to signal the [ImageStreamCompleter] that it can now be
  /// disposed when its last listener drops.
  ///
  /// This method must only be called once per object.
  void dispose() {
    assert(_completer != null);
    assert(_completer!._keepAliveHandles > 0);
    assert(!_completer!._disposed);

    _completer!._keepAliveHandles -= 1;
    _completer!._maybeDispose();
    _completer = null;
  }
}

/// Base class for those that manage the loading of [dart:ui.Image] objects for
/// [ImageStream]s.
///
/// [ImageStreamListener] objects are rarely constructed directly. Generally, an
/// [ImageProvider] subclass will return an [ImageStream] and automatically
/// configure it with the right [ImageStreamCompleter] when possible.
abstract class ImageStreamCompleter with Diagnosticable {
  final List<ImageStreamListener> _listeners = <ImageStreamListener>[];
  ImageInfo? _currentImage;
  FlutterErrorDetails? _currentError;

  /// A string identifying the source of the underlying image.
  String? debugLabel;

  /// Whether any listeners are currently registered.
  ///
  /// Clients should not depend on this value for their behavior, because having
  /// one listener's logic change when another listener happens to start or stop
  /// listening will lead to extremely hard-to-track bugs. Subclasses might use
  /// this information to determine whether to do any work when there are no
  /// listeners, however; for example, [MultiFrameImageStreamCompleter] uses it
  /// to determine when to iterate through frames of an animated image.
  ///
  /// Typically this is used by overriding [addListener], checking if
  /// [hasListeners] is false before calling `super.addListener()`, and if so,
  /// starting whatever work is needed to determine when to notify listeners;
  /// and similarly, by overriding [removeListener], checking if [hasListeners]
  /// is false after calling `super.removeListener()`, and if so, stopping that
  /// same work.
  @protected
  @visibleForTesting
  bool get hasListeners => _listeners.isNotEmpty;

  /// We must avoid disposing a completer if it has never had a listener, even
  /// if all [keepAlive] handles get disposed.
  bool _hadAtLeastOneListener = false;

  /// Adds a listener callback that is called whenever a new concrete [ImageInfo]
  /// object is available or an error is reported. If a concrete image is
  /// already available, or if an error has been already reported, this object
  /// will notify the listener synchronously.
  ///
  /// If the [ImageStreamCompleter] completes multiple images over its lifetime,
  /// this listener's [ImageStreamListener.onImage] will fire multiple times.
  ///
  /// {@macro flutter.painting.imageStream.addListener}
  void addListener(ImageStreamListener listener) {
    _checkDisposed();
    _hadAtLeastOneListener = true;
    _listeners.add(listener);
    if (_currentImage != null) {
      try {
        listener.onImage(_currentImage!.clone(), true);
      } catch (exception, stack) {
        reportError(
          context: ErrorDescription('by a synchronously-called image listener'),
          exception: exception,
          stack: stack,
        );
      }
    }
    if (_currentError != null && listener.onError != null) {
      try {
        listener.onError!(_currentError!.exception, _currentError!.stack);
      } catch (newException, newStack) {
        if (newException != _currentError!.exception) {
          FlutterError.reportError(
            FlutterErrorDetails(
              exception: newException,
              library: 'image resource service',
              context: ErrorDescription('by a synchronously-called image error listener'),
              stack: newStack,
            ),
          );
        }
      }
    }
  }

  int _keepAliveHandles = 0;
  /// Creates an [ImageStreamCompleterHandle] that will prevent this stream from
  /// being disposed at least until the handle is disposed.
  ///
  /// Such handles are useful when an image cache needs to keep a completer
  /// alive but does not itself have a listener subscribed, or when a widget
  /// that displays an image needs to temporarily unsubscribe from the completer
  /// but may re-subscribe in the future, for example when the [TickerMode]
  /// changes.
  ImageStreamCompleterHandle keepAlive() {
    _checkDisposed();
    return ImageStreamCompleterHandle._(this);
  }

  /// Stops the specified [listener] from receiving image stream events.
  ///
  /// If [listener] has been added multiple times, this removes the _first_
  /// instance of the listener.
  ///
  /// Once all listeners have been removed and all [keepAlive] handles have been
  /// disposed, this image stream is no longer usable.
  void removeListener(ImageStreamListener listener) {
    _checkDisposed();
    for (int i = 0; i < _listeners.length; i += 1) {
      if (_listeners[i] == listener) {
        _listeners.removeAt(i);
        break;
      }
    }
    if (_listeners.isEmpty) {
      final List<VoidCallback> callbacks = _onLastListenerRemovedCallbacks.toList();
      for (final VoidCallback callback in callbacks) {
        callback();
      }
      _onLastListenerRemovedCallbacks.clear();
      _maybeDispose();
    }
  }

  bool _disposed = false;
  void _maybeDispose() {
    if (!_hadAtLeastOneListener || _disposed || _listeners.isNotEmpty || _keepAliveHandles != 0) {
      return;
    }

    _currentImage?.dispose();
    _currentImage = null;
    _disposed = true;
  }

  void _checkDisposed() {
    if (_disposed) {
      throw StateError(
        'Stream has been disposed.\n'
        'An ImageStream is considered disposed once at least one listener has '
        'been added and subsequently all listeners have been removed and no '
        'handles are outstanding from the keepAlive method.\n'
        'To resolve this error, maintain at least one listener on the stream, '
        'or create an ImageStreamCompleterHandle from the keepAlive '
        'method, or create a new stream for the image.',
      );
    }
  }

  final List<VoidCallback> _onLastListenerRemovedCallbacks = <VoidCallback>[];

  /// Adds a callback to call when [removeListener] results in an empty
  /// list of listeners and there are no [keepAlive] handles outstanding.
  ///
  /// This callback will never fire if [removeListener] is never called.
  void addOnLastListenerRemovedCallback(VoidCallback callback) {
    assert(callback != null);
    _checkDisposed();
    _onLastListenerRemovedCallbacks.add(callback);
  }

  /// Removes a callback previously supplied to
  /// [addOnLastListenerRemovedCallback].
  void removeOnLastListenerRemovedCallback(VoidCallback callback) {
    assert(callback != null);
    _checkDisposed();
    _onLastListenerRemovedCallbacks.remove(callback);
  }

  /// Calls all the registered listeners to notify them of a new image.
  @protected
  @pragma('vm:notify-debugger-on-exception')
  void setImage(ImageInfo image) {
    _checkDisposed();
    _currentImage?.dispose();
    _currentImage = image;

    if (_listeners.isEmpty)
      return;
    // Make a copy to allow for concurrent modification.
    final List<ImageStreamListener> localListeners =
        List<ImageStreamListener>.from(_listeners);
    for (final ImageStreamListener listener in localListeners) {
      try {
        listener.onImage(image.clone(), false);
      } catch (exception, stack) {
        reportError(
          context: ErrorDescription('by an image listener'),
          exception: exception,
          stack: stack,
        );
      }
    }
  }

  /// Calls all the registered error listeners to notify them of an error that
  /// occurred while resolving the image.
  ///
  /// If no error listeners (listeners with an [ImageStreamListener.onError]
  /// specified) are attached, or if the handlers all rethrow the exception
  /// verbatim (with `throw exception`), a [FlutterError] will be reported using
  /// [FlutterError.reportError].
  ///
  /// The `context` should be a string describing where the error was caught, in
  /// a form that will make sense in English when following the word "thrown",
  /// as in "thrown while obtaining the image from the network" (for the context
  /// "while obtaining the image from the network").
  ///
  /// The `exception` is the error being reported; the `stack` is the
  /// [StackTrace] associated with the exception.
  ///
  /// The `informationCollector` is a callback (of type [InformationCollector])
  /// that is called when the exception is used by [FlutterError.reportError].
  /// It is used to obtain further details to include in the logs, which may be
  /// expensive to collect, and thus should only be collected if the error is to
  /// be logged in the first place.
  ///
  /// The `silent` argument causes the exception to not be reported to the logs
  /// in release builds, if passed to [FlutterError.reportError]. (It is still
  /// sent to error handlers.) It should be set to true if the error is one that
  /// is expected to be encountered in release builds, for example network
  /// errors. That way, logs on end-user devices will not have spurious
  /// messages, but errors during development will still be reported.
  ///
  /// See [FlutterErrorDetails] for further details on these values.
  @pragma('vm:notify-debugger-on-exception')
  void reportError({
    DiagnosticsNode? context,
    required Object exception,
    StackTrace? stack,
    InformationCollector? informationCollector,
    bool silent = false,
  }) {
    _currentError = FlutterErrorDetails(
      exception: exception,
      stack: stack,
      library: 'image resource service',
      context: context,
      informationCollector: informationCollector,
      silent: silent,
    );

    // Make a copy to allow for concurrent modification.
    final List<ImageErrorListener> localErrorListeners = _listeners
        .map<ImageErrorListener?>((ImageStreamListener listener) => listener.onError)
        .whereType<ImageErrorListener>()
        .toList();

    bool handled = false;
    for (final ImageErrorListener errorListener in localErrorListeners) {
      try {
        errorListener(exception, stack);
        handled = true;
      } catch (newException, newStack) {
        if (newException != exception) {
          FlutterError.reportError(
            FlutterErrorDetails(
              context: ErrorDescription('when reporting an error to an image listener'),
              library: 'image resource service',
              exception: newException,
              stack: newStack,
            ),
          );
        }
      }
    }
    if (!handled) {
      FlutterError.reportError(_currentError!);
    }
  }

  /// Calls all the registered [ImageChunkListener]s (listeners with an
  /// [ImageStreamListener.onChunk] specified) to notify them of a new
  /// [ImageChunkEvent].
  @protected
  void reportImageChunkEvent(ImageChunkEvent event) {
    _checkDisposed();
    if (hasListeners) {
      // Make a copy to allow for concurrent modification.
      final List<ImageChunkListener> localListeners = _listeners
          .map<ImageChunkListener?>((ImageStreamListener listener) => listener.onChunk)
          .whereType<ImageChunkListener>()
          .toList();
      for (final ImageChunkListener listener in localListeners) {
        listener(event);
      }
    }
  }

  /// Accumulates a list of strings describing the object's state. Subclasses
  /// should override this to have their information included in [toString].
  @override
  void debugFillProperties(DiagnosticPropertiesBuilder description) {
    super.debugFillProperties(description);
    description.add(DiagnosticsProperty<ImageInfo>('current', _currentImage, ifNull: 'unresolved', showName: false));
    description.add(ObjectFlagProperty<List<ImageStreamListener>>(
      'listeners',
      _listeners,
      ifPresent: '${_listeners.length} listener${_listeners.length == 1 ? "" : "s" }',
    ));
    description.add(FlagProperty('disposed', value: _disposed, ifTrue: '<disposed>'));
  }
}

/// Manages the loading of [dart:ui.Image] objects for static [ImageStream]s (those
/// with only one frame).
class OneFrameImageStreamCompleter extends ImageStreamCompleter {
  /// Creates a manager for one-frame [ImageStream]s.
  ///
  /// The image resource awaits the given [Future]. When the future resolves,
  /// it notifies the [ImageListener]s that have been registered with
  /// [addListener].
  ///
  /// The [InformationCollector], if provided, is invoked if the given [Future]
  /// resolves with an error, and can be used to supplement the reported error
  /// message (for example, giving the image's URL).
  ///
  /// Errors are reported using [FlutterError.reportError] with the `silent`
  /// argument on [FlutterErrorDetails] set to true, meaning that by default the
  /// message is only dumped to the console in debug mode (see [new
  /// FlutterErrorDetails]).
  OneFrameImageStreamCompleter(Future<ImageInfo> image, { InformationCollector? informationCollector })
      : assert(image != null) {
    image.then<void>(setImage, onError: (Object error, StackTrace stack) {
      reportError(
        context: ErrorDescription('resolving a single-frame image stream'),
        exception: error,
        stack: stack,
        informationCollector: informationCollector,
        silent: true,
      );
    });
  }
}

/// Manages the decoding and scheduling of image frames.
///
/// New frames will only be emitted while there are registered listeners to the
/// stream (registered with [addListener]).
///
/// This class deals with 2 types of frames:
///
///  * image frames - image frames of an animated image.
///  * app frames - frames that the flutter engine is drawing to the screen to
///    show the app GUI.
///
/// For single frame images the stream will only complete once.
///
/// For animated images, this class eagerly decodes the next image frame,
/// and notifies the listeners that a new frame is ready on the first app frame
/// that is scheduled after the image frame duration has passed.
///
/// Scheduling new timers only from scheduled app frames, makes sure we pause
/// the animation when the app is not visible (as new app frames will not be
/// scheduled).
///
/// See the following timeline example:
///
///     | Time | Event                                      | Comment                   |
///     |------|--------------------------------------------|---------------------------|
///     | t1   | App frame scheduled (image frame A posted) |                           |
///     | t2   | App frame scheduled                        |                           |
///     | t3   | App frame scheduled                        |                           |
///     | t4   | Image frame B decoded                      |                           |
///     | t5   | App frame scheduled                        | t5 - t1 < frameB_duration |
///     | t6   | App frame scheduled (image frame B posted) | t6 - t1 > frameB_duration |
///
class MultiFrameImageStreamCompleter extends ImageStreamCompleter {
  /// Creates a image stream completer.
  ///
  /// Immediately starts decoding the first image frame when the codec is ready.
  ///
  /// The `codec` parameter is a future for an initialized [ui.Codec] that will
  /// be used to decode the image.
  ///
  /// The `scale` parameter is the linear scale factor for drawing this frames
  /// of this image at their intended size.
  ///
  /// The `tag` parameter is passed on to created [ImageInfo] objects to
  /// help identify the source of the image.
  ///
  /// The `chunkEvents` parameter is an optional stream of notifications about
  /// the loading progress of the image. If this stream is provided, the events
  /// produced by the stream will be delivered to registered [ImageChunkListener]s
  /// (see [addListener]).
  MultiFrameImageStreamCompleter({
    required Future<ui.Codec> codec,
    required double scale,
    String? debugLabel,
    Stream<ImageChunkEvent>? chunkEvents,
    InformationCollector? informationCollector,
  }) : assert(codec != null),
       _informationCollector = informationCollector,
       _scale = scale {
    this.debugLabel = debugLabel;
    codec.then<void>(_handleCodecReady, onError: (Object error, StackTrace stack) {
      reportError(
        context: ErrorDescription('resolving an image codec'),
        exception: error,
        stack: stack,
        informationCollector: informationCollector,
        silent: true,
      );
    });
    if (chunkEvents != null) {
      chunkEvents.listen(reportImageChunkEvent,
        onError: (Object error, StackTrace stack) {
          reportError(
            context: ErrorDescription('loading an image'),
            exception: error,
            stack: stack,
            informationCollector: informationCollector,
            silent: true,
          );
        },
      );
    }
  }

  ui.Codec? _codec;
  final double _scale;
  final InformationCollector? _informationCollector;
  ui.FrameInfo? _nextFrame;
  // When the current was first shown.
  late Duration _shownTimestamp;
  // The requested duration for the current frame;
  Duration? _frameDuration;
  // How many frames have been emitted so far.
  int _framesEmitted = 0;
  Timer? _timer;

  // Used to guard against registering multiple _handleAppFrame callbacks for the same frame.
  bool _frameCallbackScheduled = false;

  void _handleCodecReady(ui.Codec codec) {
    _codec = codec;
    assert(_codec != null);

    if (hasListeners) {
      _decodeNextFrameAndSchedule();
    }
  }

  void _handleAppFrame(Duration timestamp) {
    _frameCallbackScheduled = false;
    if (!hasListeners)
      return;
    assert(_nextFrame != null);
    if (_isFirstFrame() || _hasFrameDurationPassed(timestamp)) {
      _emitFrame(ImageInfo(
        image: _nextFrame!.image.clone(),
        scale: _scale,
        debugLabel: debugLabel,
      ));
      _shownTimestamp = timestamp;
      _frameDuration = _nextFrame!.duration;
      _nextFrame!.image.dispose();
      _nextFrame = null;
      final int completedCycles = _framesEmitted ~/ _codec!.frameCount;
      if (_codec!.repetitionCount == -1 || completedCycles <= _codec!.repetitionCount) {
        _decodeNextFrameAndSchedule();
      }
      return;
    }
    final Duration delay = _frameDuration! - (timestamp - _shownTimestamp);
    _timer = Timer(delay * timeDilation, () {
      _scheduleAppFrame();
    });
  }

  bool _isFirstFrame() {
    return _frameDuration == null;
  }

  bool _hasFrameDurationPassed(Duration timestamp) {
    return timestamp - _shownTimestamp >= _frameDuration!;
  }

  Future<void> _decodeNextFrameAndSchedule() async {
    // This will be null if we gave it away. If not, it's still ours and it
    // must be disposed of.
    _nextFrame?.image.dispose();
    _nextFrame = null;
    try {
      _nextFrame = await _codec!.getNextFrame();
    } catch (exception, stack) {
      reportError(
        context: ErrorDescription('resolving an image frame'),
        exception: exception,
        stack: stack,
        informationCollector: _informationCollector,
        silent: true,
      );
      return;
    }
    if (_codec!.frameCount == 1) {
      // ImageStreamCompleter listeners removed while waiting for next frame to
      // be decoded.
      // There's no reason to emit the frame without active listeners.
      if (!hasListeners) {
        return;
      }
      // This is not an animated image, just return it and don't schedule more
      // frames.
      _emitFrame(ImageInfo(
        image: _nextFrame!.image.clone(),
        scale: _scale,
        debugLabel: debugLabel,
      ));
      _nextFrame!.image.dispose();
      _nextFrame = null;
      return;
    }
    _scheduleAppFrame();
  }

  void _scheduleAppFrame() {
    if (_frameCallbackScheduled) {
      return;
    }
    _frameCallbackScheduled = true;
    SchedulerBinding.instance!.scheduleFrameCallback(_handleAppFrame);
  }

  void _emitFrame(ImageInfo imageInfo) {
    setImage(imageInfo);
    _framesEmitted += 1;
  }

  @override
  void addListener(ImageStreamListener listener) {
    if (!hasListeners && _codec != null && (_currentImage == null || _codec!.frameCount > 1))
      _decodeNextFrameAndSchedule();
    super.addListener(listener);
  }

  @override
  void removeListener(ImageStreamListener listener) {
    super.removeListener(listener);
    if (!hasListeners) {
      _timer?.cancel();
      _timer = null;
    }
  }
}
