content: // ignore_for_file: non_nullable_equals_parameter, depend_on_referenced_packages, implementation_imports
  import 'dart:async';
  import 'dart:io';
  import 'dart:ui' as ui show Codec;
  import 'package:extended_image_library/src/extended_image_provider.dart';
  import 'package:extended_image_library/src/platform.dart';
  import 'package:flutter/foundation.dart';
  import 'package:flutter/widgets.dart';
  import 'package:http_client_helper/http_client_helper.dart';
  import 'package:mangayomi/services/http/m_client.dart';
  import 'package:path/path.dart';
  import 'package:path_provider/path_provider.dart';
  import 'package:extended_image_library/src/network/extended_network_image_provider.dart'
      as image_provider;
  
  class CustomExtendedNetworkImageProvider
      extends ImageProvider<image_provider.ExtendedNetworkImageProvider>
      with ExtendedImageProvider<image_provider.ExtendedNetworkImageProvider>
      implements image_provider.ExtendedNetworkImageProvider {
    /// Creates an object that fetches the image at the given URL.
    ///
    /// The arguments must not be null.
    CustomExtendedNetworkImageProvider(
      this.url, {
      this.scale = 1.0,
      this.headers,
      this.cache = true,
      this.retries = 3,
      this.timeLimit,
      this.timeRetry = const Duration(milliseconds: 100),
      this.cacheKey,
      this.printError = true,
      this.cacheRawData = false,
      this.cancelToken,
      this.imageCacheName,
      this.imageCacheFolderName,
      this.cacheMaxAge = const Duration(days: 30),
      this.showCloudFlareError = false,
    });
  
    /// The name of [ImageCache], you can define custom [ImageCache] to store this provider.
    @override
    final String? imageCacheName;
  
    /// Whether cache raw data if you need to get raw data directly.
    /// For example, we need raw image data to edit,
    /// but [ui.Image.toByteData()] is very slow. So we cache the image
    /// data here.
    @override
    final bool cacheRawData;
  
    /// The time limit to request image
    @override
    final Duration? timeLimit;
  
    /// The time to retry to request
    @override
    final int retries;
  
    /// The time duration to retry to request
    @override
    final Duration timeRetry;
  
    /// Whether cache image to local
    @override
    final bool cache;
  
    /// The URL from which the image will be fetched.
    @override
    final String url;
  
    /// The scale to place in the [ImageInfo] object of the image.
    @override
    final double scale;
  
    /// The HTTP headers that will be used with [HttpClient.get] to fetch image from network.
    @override
    final Map<String, String>? headers;
  
    /// The token to cancel network request
    @override
    final CancellationToken? cancelToken;
  
    /// Custom cache key
    @override
    final String? cacheKey;
  
    /// print error
    @override
    final bool printError;
  
    /// The max duration to cahce image.
    /// After this time the cache is expired and the image is reloaded.
    @override
    final Duration? cacheMaxAge;
  
    final bool showCloudFlareError;
  
    final String? imageCacheFolderName;
  
    @override
    ImageStreamCompleter loadImage(
      image_provider.ExtendedNetworkImageProvider key,
      ImageDecoderCallback decode,
    ) {
      // Ownership of this controller is handed off to [_loadAsync]; it is that
      // method's responsibility to close the controller's stream when the image
      // has been loaded or an error is thrown.
      final StreamController<ImageChunkEvent> chunkEvents =
          StreamController<ImageChunkEvent>();
  
      return MultiFrameImageStreamCompleter(
        codec: _loadAsync(
          key as CustomExtendedNetworkImageProvider,
          chunkEvents,
          decode,
        ),
        scale: key.scale,
        chunkEvents: chunkEvents.stream,
        debugLabel: key.url,
        informationCollector: () {
          return <DiagnosticsNode>[
            DiagnosticsProperty<ImageProvider>('Image provider', this),
            DiagnosticsProperty<image_provider.ExtendedNetworkImageProvider>(
              'Image key',
              key,
            ),
          ];
        },
      );
    }
  
    @override
    Future<CustomExtendedNetworkImageProvider> obtainKey(
      ImageConfiguration configuration,
    ) {
      return SynchronousFuture<CustomExtendedNetworkImageProvider>(this);
    }
  
    Future<ui.Codec> _loadAsync(
      CustomExtendedNetworkImageProvider key,
      StreamController<ImageChunkEvent> chunkEvents,
      ImageDecoderCallback decode,
    ) async {
      assert(key == this);
      final String md5Key = cacheKey ?? keyToMd5(key.url);
      ui.Codec? result;
      if (cache) {
        try {
          final Uint8List? data = await _loadCache(key, chunkEvents, md5Key);
          if (data != null) {
            result = await instantiateImageCodec(data, decode);
          }
        } catch (e) {
          if (kDebugMode) {
            print(e);
          }
        }
      }
  
      if (result == null) {
        try {
          final Uint8List? data = await _loadNetwork(key, chunkEvents);
          if (data != null) {
            result = await instantiateImageCodec(data, decode);
          }
        } catch (e) {
          if (kDebugMode) {
            print(e);
          }
        }
      }
  
      //Failed to load
      if (result == null) {
        //result = await ui.instantiateImageCodec(kTransparentImage);
        return Future<ui.Codec>.error(StateError('Failed to load $url.'));
      }
  
      return result;
    }
  
    /// Get the image from cache folder.
    Future<Uint8List?> _loadCache(
      CustomExtendedNetworkImageProvider key,
      StreamController<ImageChunkEvent>? chunkEvents,
      String md5Key,
    ) async {
      final Directory cacheImagesDirectory = Directory(
        join(
          (await getTemporaryDirectory()).path,
          'Mangayomi',
          imageCacheFolderName ?? 'cacheimagecover',
        ),
      );
      Uint8List? data;
      final File cacheFile = File(join(cacheImagesDirectory.path, md5Key));
      // exist, try to find cache image file
      if (cacheFile.existsSync()) {
        if (key.cacheMaxAge != null) {
          final DateTime now = DateTime.now();
          final DateTime lastModified = cacheFile.lastModifiedSync();
          if (now.difference(lastModified) > key.cacheMaxAge!) {
            cacheFile.deleteSync();
          } else {
            data = await cacheFile.readAsBytes();
          }
        } else {
          data = await cacheFile.readAsBytes();
        }
      } else if (!cacheImagesDirectory.existsSync()) {
        await cacheImagesDirectory.create(recursive: true);
      }
      // load from network
      if (data == null) {
        data = await _loadNetwork(key, chunkEvents);
        if (data != null) {
          // cache image file
          await File(join(cacheImagesDirectory.path, md5Key)).writeAsBytes(data);
        }
      }
  
      return data;
    }
  
    /// Get the image from network.
    Future<Uint8List?> _loadNetwork(
      CustomExtendedNetworkImageProvider key,
      StreamController<ImageChunkEvent>? chunkEvents,
    ) async {
      try {
        final Uri resolved = Uri.base.resolve(key.url);
        final StreamedResponse? response = await _tryGetResponse(resolved);
        List<int> bytes = [];
        final int total = response!.contentLength ?? 0;
        if (response.statusCode == HttpStatus.ok) {
          int received = 0;
          response.stream.asBroadcastStream();
          await for (var chunk in response.stream) {
            bytes.addAll(chunk);
            try {
              received += chunk.length;
              if (chunkEvents != null) {}
              chunkEvents!.add(
                ImageChunkEvent(
                  cumulativeBytesLoaded: received,
                  expectedTotalBytes: total,
                ),
              );
            } catch (e) {
              if (kDebugMode) {
                print(e);
              }
            }
          }
        } else {
          return null;
        }
  
        if (bytes.isEmpty) {
          return Future<Uint8List>.error(
            StateError('NetworkImage is an empty file: $resolved'),
          );
        }
  
        return Uint8List.fromList(bytes);
      } on OperationCanceledError catch (_) {
        if (kDebugMode) {
          print('User cancel request $url.');
        }
        return Future<Uint8List>.error(StateError('User cancel request $url.'));
      } catch (e) {
        if (kDebugMode) {
          print(e);
        }
      } finally {
        await chunkEvents?.close();
      }
      return null;
    }
  
    Future<StreamedResponse> _getResponse(Uri resolved) async {
      var request = Request('GET', resolved);
      request.headers.addAll(headers ?? {});
  
      StreamedResponse response = await MClient.init(
        showCloudFlareError: showCloudFlareError,
      ).send(request);
      if (response.statusCode != 200) {
        final res = await MClient.init(
          reqcopyWith: {'useDartHttpClient': true},
          showCloudFlareError: showCloudFlareError,
        ).send(response.request!);
        return res;
      }
  
      return response;
    }
  
    // Http get with cancel, delay try again
    Future<StreamedResponse?> _tryGetResponse(Uri resolved) async {
      cancelToken?.throwIfCancellationRequested();
      return await RetryHelper.tryRun<StreamedResponse>(
        () {
          return CancellationTokenSource.register(
            cancelToken,
            _getResponse(resolved),
          );
        },
        cancelToken: cancelToken,
        timeRetry: timeRetry,
        retries: retries,
      );
    }
  
    @override
    bool operator ==(dynamic other) {
      if (other.runtimeType != runtimeType) {
        return false;
      }
      return other is CustomExtendedNetworkImageProvider &&
          url == other.url &&
          scale == other.scale &&
          cacheRawData == other.cacheRawData &&
          timeLimit == other.timeLimit &&
          cancelToken == other.cancelToken &&
          timeRetry == other.timeRetry &&
          cache == other.cache &&
          cacheKey == other.cacheKey &&
          //headers == other.headers &&
          retries == other.retries &&
          imageCacheName == other.imageCacheName &&
          cacheMaxAge == other.cacheMaxAge;
    }
  
    @override
    int get hashCode => Object.hash(
      url,
      scale,
      cacheRawData,
      timeLimit,
      cancelToken,
      timeRetry,
      cache,
      cacheKey,
      //headers,
      retries,
      imageCacheName,
      cacheMaxAge,
    );
  
    @override
    String toString() => '$runtimeType("$url", scale: $scale)';
  
    @override
    /// Get network image data from cached
    Future<Uint8List?> getNetworkImageData({
      StreamController<ImageChunkEvent>? chunkEvents,
    }) async {
      final String uId = cacheKey ?? keyToMd5(url);
  
      if (cache) {
        return await _loadCache(this, chunkEvents, uId);
      }
  
      return await _loadNetwork(this, chunkEvents);
    }
  
    @override
    WebHtmlElementStrategy get webHtmlElementStrategy =>
        WebHtmlElementStrategy.fallback;
  }
  