import 'dart:async';
import 'package:flutter/foundation.dart';
import 'package:flutter/widgets.dart';
import 'package:http/http.dart' as http;
import 'composition.dart';
import 'frame_rate.dart';
import 'lottie.dart';
import 'lottie_delegates.dart';
import 'options.dart';
import 'providers/asset_provider.dart';
import 'providers/file_provider.dart';
import 'providers/load_image.dart';
import 'providers/lottie_provider.dart';
import 'providers/memory_provider.dart';
import 'providers/network_provider.dart';
import 'render_cache.dart';

typedef LottieFrameBuilder =
    Widget Function(
      BuildContext context,
      Widget child,
      LottieComposition? composition,
    );

/// Signature used by [Lottie.errorBuilder] to create a replacement widget to
/// render instead of the image.
typedef LottieErrorWidgetBuilder =
    Widget Function(BuildContext context, Object error, StackTrace? stackTrace);

/// A widget that displays a Lottie animation.
///
/// Several constructors are provided for the various ways that a Lottie file
/// can be provided:
///
///  * [Lottie], for obtaining a composition from a [LottieProvider].
///  * [Lottie.asset], for obtaining a Lottie file from an [AssetBundle]
///    using a key.
///  * [Lottie.network], for obtaining a lottie file from a URL.
///  * [Lottie.file], for obtaining a lottie file from a [File].
///  * [Lottie.memory], for obtaining a lottie file from a [Uint8List].
///
class LottieBuilder extends StatefulWidget {
  const LottieBuilder({
    super.key,
    required this.lottie,
    this.controller,
    this.frameRate,
    this.animate,
    this.reverse,
    this.repeat,
    this.delegates,
    this.options,
    this.onLoaded,
    this.frameBuilder,
    this.errorBuilder,
    this.width,
    this.height,
    this.fit,
    this.alignment,
    this.addRepaintBoundary,
    this.filterQuality,
    this.onWarning,
    this.renderCache,
  });

  /// Creates a widget that displays an [LottieComposition] obtained from the network.
  LottieBuilder.network(
    String src, {
    http.Client? client,
    Map<String, String>? headers,
    this.controller,
    this.frameRate,
    this.animate,
    this.reverse,
    this.repeat,
    this.delegates,
    this.options,
    LottieImageProviderFactory? imageProviderFactory,
    this.onLoaded,
    super.key,
    this.frameBuilder,
    this.errorBuilder,
    this.width,
    this.height,
    this.fit,
    this.alignment,
    this.addRepaintBoundary,
    this.filterQuality,
    this.onWarning,
    LottieDecoder? decoder,
    this.renderCache,
    bool? backgroundLoading,
  }) : lottie = NetworkLottie(
         src,
         client: client,
         headers: headers,
         imageProviderFactory: imageProviderFactory,
         decoder: decoder,
         backgroundLoading: backgroundLoading,
       );

  /// Creates a widget that displays an [LottieComposition] obtained from a [File].
  ///
  /// Either the [width] and [height] arguments should be specified, or the
  /// widget should be placed in a context that sets tight layout constraints.
  /// Otherwise, the image dimensions will change as the animation is loaded, which
  /// will result in ugly layout changes.
  ///
  /// On Android, this may require the
  /// `android.permission.READ_EXTERNAL_STORAGE` permission.
  ///
  LottieBuilder.file(
    Object file, {
    this.controller,
    this.frameRate,
    this.animate,
    this.reverse,
    this.repeat,
    this.delegates,
    this.options,
    LottieImageProviderFactory? imageProviderFactory,
    this.onLoaded,
    super.key,
    this.frameBuilder,
    this.errorBuilder,
    this.width,
    this.height,
    this.fit,
    this.alignment,
    this.addRepaintBoundary,
    this.filterQuality,
    this.onWarning,
    LottieDecoder? decoder,
    this.renderCache,
    bool? backgroundLoading,
  }) : lottie = FileLottie(
         file,
         imageProviderFactory: imageProviderFactory,
         decoder: decoder,
         backgroundLoading: backgroundLoading,
       );

  /// Creates a widget that displays an [LottieComposition] obtained from an [AssetBundle].
  LottieBuilder.asset(
    String name, {
    this.controller,
    this.frameRate,
    this.animate,
    this.reverse,
    this.repeat,
    this.delegates,
    this.options,
    LottieImageProviderFactory? imageProviderFactory,
    this.onLoaded,
    super.key,
    AssetBundle? bundle,
    this.frameBuilder,
    this.errorBuilder,
    this.width,
    this.height,
    this.fit,
    this.alignment,
    String? package,
    this.addRepaintBoundary,
    this.filterQuality,
    this.onWarning,
    LottieDecoder? decoder,
    this.renderCache,
    bool? backgroundLoading,
  }) : lottie = AssetLottie(
         name,
         bundle: bundle,
         package: package,
         imageProviderFactory: imageProviderFactory,
         decoder: decoder,
         backgroundLoading: backgroundLoading,
       );

  /// Creates a widget that displays an [LottieComposition] obtained from a [Uint8List].
  LottieBuilder.memory(
    Uint8List bytes, {
    this.controller,
    this.frameRate,
    this.animate,
    this.reverse,
    this.repeat,
    this.delegates,
    this.options,
    LottieImageProviderFactory? imageProviderFactory,
    this.onLoaded,
    this.errorBuilder,
    super.key,
    this.frameBuilder,
    this.width,
    this.height,
    this.fit,
    this.alignment,
    this.addRepaintBoundary,
    this.filterQuality,
    this.onWarning,
    LottieDecoder? decoder,
    this.renderCache,
    bool? backgroundLoading,
  }) : lottie = MemoryLottie(
         bytes,
         imageProviderFactory: imageProviderFactory,
         decoder: decoder,
         backgroundLoading: backgroundLoading,
       );

  /// The lottie animation to load.
  /// Example of providers: [AssetLottie], [NetworkLottie], [FileLottie], [MemoryLottie]
  final LottieProvider lottie;

  /// A callback called when the LottieComposition has been loaded.
  /// You can use this callback to set the correct duration on the AnimationController
  /// with `composition.duration`
  final void Function(LottieComposition)? onLoaded;

  /// The animation controller of the Lottie animation.
  /// The animated value will be mapped to the `progress` property of the
  /// Lottie animation.
  final Animation<double>? controller;

  /// The number of frames per second to render.
  /// Use `FrameRate.composition` to use the original frame rate of the Lottie composition (default)
  /// Use `FrameRate.max` to advance the animation progression at every frame.
  final FrameRate? frameRate;

  /// If no controller is specified, this value indicate whether or not the
  /// Lottie animation should be played automatically (default to true).
  /// If there is an animation controller specified, this property has no effect.
  ///
  /// See [repeat] to control whether the animation should repeat.
  final bool? animate;

  /// Specify that the automatic animation should repeat in a loop (default to true).
  /// The property has no effect if [animate] is false or [controller] is not null.
  final bool? repeat;

  /// Specify that the automatic animation should repeat in a loop in a "reverse"
  /// mode (go from start to end and then continuously from end to start).
  /// It default to false.
  /// The property has no effect if [animate] is false, [repeat] is false or [controller] is not null.
  final bool? reverse;

  /// A group of options to further customize the lottie animation.
  /// - A [text] delegate to dynamically change some text displayed in the animation
  /// - A value callback to change the properties of the animation at runtime.
  /// - A text style factory to map between a font family specified in the animation
  ///   and the font family in your assets.
  final LottieDelegates? delegates;

  /// Some options to enable/disable some feature of Lottie
  /// - enableMergePaths: Enable merge path support
  /// - enableApplyingOpacityToLayers: Enable layer-level opacity
  final LottieOptions? options;

  /// A builder function responsible for creating the widget that represents
  /// this lottie animation.
  ///
  /// If this is null, this widget will display a lottie animation that is painted as
  /// soon as it is available (and will appear to "pop" in
  /// if it becomes available asynchronously). Callers might use this builder to
  /// add effects to the animation (such as fading the animation in when it becomes
  /// available) or to display a placeholder widget while the animation is loading.
  ///
  /// To have finer-grained control over the way that an animation's loading
  /// progress is communicated to the user, see [loadingBuilder].
  ///
  /// {@template lottie.chainedBuildersExample}
  /// ```dart
  /// Lottie(
  ///   ...
  ///   frameBuilder: (BuildContext context, Widget child) {
  ///     return Padding(
  ///       padding: EdgeInsets.all(8.0),
  ///       child: child,
  ///     );
  ///   }
  /// )
  /// ```
  ///
  /// In this example, the widget hierarchy will contain the following:
  ///
  /// ```dart
  /// Center(
  ///   Padding(
  ///     padding: EdgeInsets.all(8.0),
  ///     child: <lottie>,
  ///   ),
  /// )
  /// ```
  /// {@endtemplate}
  ///
  /// {@tool snippet --template=stateless_widget_material}
  ///
  /// The following sample demonstrates how to use this builder to implement an
  /// animation that fades in once it's been loaded.
  ///
  /// This sample contains a limited subset of the functionality that the
  /// [FadeInImage] widget provides out of the box.
  ///
  /// ```dart
  /// @override
  /// Widget build(BuildContext context) {
  ///   return DecoratedBox(
  ///     decoration: BoxDecoration(
  ///       color: Colors.white,
  ///       border: Border.all(),
  ///       borderRadius: BorderRadius.circular(20),
  ///     ),
  ///     child: Lottie.network(
  ///       'https://example.com/animation.json',
  ///       frameBuilder: (BuildContext context, Widget child) {
  ///         if (wasSynchronouslyLoaded) {
  ///           return child;
  ///         }
  ///         return AnimatedOpacity(
  ///           child: child,
  ///           opacity: frame == null ? 0 : 1,
  ///           duration: const Duration(seconds: 1),
  ///           curve: Curves.easeOut,
  ///         );
  ///       },
  ///     ),
  ///   );
  /// }
  /// ```
  /// {@end-tool}
  ///
  final LottieFrameBuilder? frameBuilder;

  /// If non-null, require the lottie animation to have this width.
  ///
  /// If null, the lottie animation will pick a size that best preserves its intrinsic
  /// aspect ratio.
  ///
  /// It is strongly recommended that either both the [width] and the [height]
  /// be specified, or that the widget be placed in a context that sets tight
  /// layout constraints, so that the animation does not change size as it loads.
  /// Consider using [fit] to adapt the animation's rendering to fit the given width
  /// and height if the exact animation dimensions are not known in advance.
  final double? width;

  /// If non-null, require the lottie animation to have this height.
  ///
  /// If null, the lottie animation will pick a size that best preserves its intrinsic
  /// aspect ratio.
  ///
  /// It is strongly recommended that either both the [width] and the [height]
  /// be specified, or that the widget be placed in a context that sets tight
  /// layout constraints, so that the animation does not change size as it loads.
  /// Consider using [fit] to adapt the animation's rendering to fit the given width
  /// and height if the exact animation dimensions are not known in advance.
  final double? height;

  /// How to inscribe the animation into the space allocated during layout.
  ///
  /// The default varies based on the other fields. See the discussion at
  /// [paintImage].
  final BoxFit? fit;

  /// How to align the animation within its bounds.
  ///
  /// The alignment aligns the given position in the animation to the given position
  /// in the layout bounds. For example, an [Alignment] alignment of (-1.0,
  /// -1.0) aligns the animation to the top-left corner of its layout bounds, while an
  /// [Alignment] alignment of (1.0, 1.0) aligns the bottom right of the
  /// animation with the bottom right corner of its layout bounds. Similarly, an
  /// alignment of (0.0, 1.0) aligns the bottom middle of the animation with the
  /// middle of the bottom edge of its layout bounds.
  ///
  /// To display a subpart of an animation, consider using a [CustomPainter] and
  /// [Canvas.drawImageRect].
  ///
  /// If the [alignment] is [TextDirection]-dependent (i.e. if it is a
  /// [AlignmentDirectional]), then an ambient [Directionality] widget
  /// must be in scope.
  ///
  /// Defaults to [Alignment.center].
  ///
  /// See also:
  ///
  ///  * [Alignment], a class with convenient constants typically used to
  ///    specify an [AlignmentGeometry].
  ///  * [AlignmentDirectional], like [Alignment] for specifying alignments
  ///    relative to text direction.
  final AlignmentGeometry? alignment;

  /// Indicate to automatically add a `RepaintBoundary` widget around the animation.
  /// This allows to optimize the app performance by isolating the animation in its
  /// own `Layer`.
  ///
  /// This property is `true` by default.
  final bool? addRepaintBoundary;

  /// The quality of the image layer. See [FilterQuality]
  /// [FilterQuality.high] is highest quality but slowest.
  ///
  /// Defaults to [FilterQuality.low]
  final FilterQuality? filterQuality;

  /// A callback called when there is a warning during the loading or painting
  /// of the animation.
  final WarningCallback? onWarning;

  /// A builder function that is called if an error occurs during loading.
  ///
  /// If this builder is not provided, any exceptions will be reported to
  /// [FlutterError.onError]. If it is provided, the caller should either handle
  /// the exception by providing a replacement widget, or rethrow the exception.
  ///
  /// The following sample uses [errorBuilder] to show a '😢' in place of the
  /// image that fails to load, and prints the error to the console.
  ///
  /// ```dart
  /// Widget build(BuildContext context) {
  ///   return DecoratedBox(
  ///     decoration: BoxDecoration(
  ///       color: Colors.white,
  ///     ),
  ///     child: Lottie.network(
  ///       'https://example.does.not.exist/lottie.json',
  ///       errorBuilder: (BuildContext context, Object exception, StackTrace? stackTrace) {
  ///         // Appropriate logging or analytics, e.g.
  ///         // myAnalytics.recordError(
  ///         //   'An error occurred loading "https://example.does.not.exist/animation.json"',
  ///         //   exception,
  ///         //   stackTrace,
  ///         // );
  ///         return const Text('😢');
  ///       },
  ///     ),
  ///   );
  /// }
  /// ```
  final ImageErrorWidgetBuilder? errorBuilder;

  /// Opt-in to a special render mode where the frames of the animation are
  /// lazily rendered and kept in a cache.
  /// Subsequent runs of the animation will be cheaper to render.
  ///
  /// This is useful is the animation is complex and can consume lot of energy
  /// from the battery.
  /// This will trade an excessive CPU usage for an increase memory usage.
  /// The main use-case is a short and small (size on the screen) animation that is
  /// played repeatedly.
  ///
  /// There are 2 kinds of caches:
  /// - [RenderCache.raster]: keep the frame rasterized in the cache (as [dart:ui.Image]).
  ///   Subsequent runs of the animation are very cheap for both the CPU and GPU but it takes
  ///   a lot of memory (rendered_width * rendered_height * frame_rate * duration_of_the_animation).
  ///   This should only be used for very short and very small animations.
  /// - [RenderCache.drawingCommands]: keep the frame as a list of graphical operations ([dart:ui.Picture]).
  ///   Subsequent runs of the animation are cheaper for the CPU but not for the GPU.
  ///   Memory usage is a lot lower than RenderCache.raster.
  ///
  /// The render cache is managed internally and will release the memory once the
  /// animation disappear. The cache is shared between all animations.

  /// Any change in the configuration of the animation (delegates, frame rate etc...)
  /// will clear the cache entry.
  /// For RenderCache.raster, any change in the size will invalidate the cache entry. The cache
  /// use the final size visible on the screen (with all transforms applied).
  ///
  /// In order to not exceed the memory limit of a device, the raster cache is constrained
  /// to maximum 50MB. After that, animations are not cached anymore.
  final RenderCache? renderCache;

  @override
  State<LottieBuilder> createState() => _LottieBuilderState();

  @override
  void debugFillProperties(DiagnosticPropertiesBuilder properties) {
    super.debugFillProperties(properties);
    properties.add(DiagnosticsProperty<LottieProvider>('lottie', lottie));
    properties.add(DiagnosticsProperty<Function>('frameBuilder', frameBuilder));
    properties.add(DoubleProperty('width', width, defaultValue: null));
    properties.add(DoubleProperty('height', height, defaultValue: null));
    properties.add(EnumProperty<BoxFit>('fit', fit, defaultValue: null));
    properties.add(
      DiagnosticsProperty<AlignmentGeometry>(
        'alignment',
        alignment,
        defaultValue: null,
      ),
    );
  }
}

class _LottieBuilderState extends State<LottieBuilder> {
  Future<LottieComposition>? _loadingFuture;

  @override
  void didUpdateWidget(LottieBuilder oldWidget) {
    super.didUpdateWidget(oldWidget);

    if (oldWidget.lottie != widget.lottie) {
      _load();
    }
  }

  void _load() {
    var provider = widget.lottie;
    _loadingFuture = widget.lottie.load(context: context).then((composition) {
      // LottieProvider.load() can return a Synchronous future and the onLoaded
      // callback can call setState, so we wrap it in a microtask to avoid an
      // "!_isDirty" error.
      scheduleMicrotask(() {
        if (mounted && widget.lottie == provider) {
          var onWarning = widget.onWarning;
          composition.onWarning = onWarning;
          if (onWarning != null) {
            for (var warning in composition.warnings) {
              onWarning(warning);
            }
          }

          widget.onLoaded?.call(composition);
        }
      });

      return composition;
    });
  }

  @override
  Widget build(BuildContext context) {
    // We need to wait the first build instead of initState because AssetLottie
    // provider may call DefaultAssetBundle.of
    if (_loadingFuture == null) {
      _load();
    }
    return FutureBuilder<LottieComposition>(
      future: _loadingFuture,
      builder: (context, snapshot) {
        if (snapshot.hasError) {
          var errorBuilder = widget.errorBuilder;
          if (errorBuilder != null) {
            return errorBuilder(context, snapshot.error!, snapshot.stackTrace);
          } else if (kDebugMode) {
            return SizedBox(
              width: widget.width,
              height: widget.height,
              child: ErrorWidget(snapshot.error!),
            );
          }
        }

        var composition = snapshot.data;
        var animate = widget.animate;
        animate ??= (composition?.durationFrames ?? 0) > 1.0;

        Widget result = Lottie(
          composition: composition,
          controller: widget.controller,
          frameRate: widget.frameRate,
          animate: animate,
          reverse: widget.reverse,
          repeat: widget.repeat,
          delegates: widget.delegates,
          options: widget.options,
          width: widget.width,
          height: widget.height,
          fit: widget.fit,
          alignment: widget.alignment,
          addRepaintBoundary: widget.addRepaintBoundary,
          filterQuality: widget.filterQuality,
          renderCache: widget.renderCache,
        );

        if (widget.frameBuilder != null) {
          result = widget.frameBuilder!(context, result, composition);
        }

        return result;
      },
    );
  }

  @override
  void debugFillProperties(DiagnosticPropertiesBuilder description) {
    super.debugFillProperties(description);
    description.add(
      DiagnosticsProperty<Future<LottieComposition>>(
        'loadingFuture',
        _loadingFuture,
      ),
    );
  }
}
