import 'dart:io';
import 'dart:ui' as ui;

import 'package:async/async.dart';
import 'package:flutter/widgets.dart';
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
import 'package:immich_mobile/infrastructure/loaders/image_request.dart';
import 'package:immich_mobile/infrastructure/repositories/settings.repository.dart';
import 'package:immich_mobile/presentation/widgets/images/local_image_provider.dart';
import 'package:immich_mobile/presentation/widgets/images/remote_image_provider.dart';
import 'package:immich_mobile/presentation/widgets/timeline/constants.dart';
import 'package:logging/logging.dart';

abstract class CancellableImageProvider<T extends Object> extends ImageProvider<T> {
  void cancel();
}

mixin CancellableImageProviderMixin<T extends Object> on CancellableImageProvider<T> {
  static final _log = Logger('CancellableImageProviderMixin');

  bool isCancelled = false;
  bool isFinished = false;
  ImageRequest? request;
  CancelableOperation<ImageInfo?>? cachedOperation;

  ImageInfo? getInitialImage(CancellableImageProvider provider) {
    final completer = CancelableCompleter<ImageInfo?>(onCancel: provider.cancel);
    final cachedStream = provider.resolve(const ImageConfiguration());
    ImageInfo? cachedImage;
    final listener = ImageStreamListener((image, synchronousCall) {
      if (synchronousCall) {
        cachedImage = image;
      }

      if (!completer.isCompleted) {
        completer.complete(image);
      }
    }, onError: completer.completeError);

    cachedStream.addListener(listener);
    if (cachedImage != null) {
      cachedStream.removeListener(listener);
      return cachedImage;
    }

    completer.operation.valueOrCancellation().whenComplete(() {
      cachedStream.removeListener(listener);
      cachedOperation = null;
    });
    cachedOperation = completer.operation;
    return null;
  }

  Stream<ImageInfo> loadRequest(ImageRequest request, ImageDecoderCallback decode, {required bool isFinal}) async* {
    if (isCancelled) {
      this.request = null;
      return;
    }

    try {
      final image = await request.load(decode);
      if (isCancelled || image == null) {
        image?.dispose();
        return;
      }
      isFinished = isFinal;
      yield image;
    } catch (e, stack) {
      if (isCancelled) {
        return;
      }
      if (isFinal) {
        isFinished = true;
        PaintingBinding.instance.imageCache.evict(this);
        rethrow;
      }
      _log.warning('Non-fatal image load error', e, stack);
    } finally {
      this.request = null;
    }
  }

  Future<ui.Codec?> loadCodecRequest(ImageRequest request, {required bool isFinal}) async {
    if (isCancelled) {
      this.request = null;
      return null;
    }

    try {
      final codec = await request.loadCodec();
      if (isCancelled || codec == null) {
        codec?.dispose();
        return null;
      }
      isFinished = isFinal;
      return codec;
    } catch (e) {
      if (isFinal) {
        isFinished = true;
        PaintingBinding.instance.imageCache.evict(this);
        rethrow;
      }
      return null;
    } finally {
      this.request = null;
    }
  }

  Stream<ImageInfo> initialImageStream() async* {
    final cachedOperation = this.cachedOperation;
    if (cachedOperation == null) {
      return;
    }

    try {
      final cachedImage = await cachedOperation.valueOrCancellation();
      if (cachedImage != null && !isCancelled) {
        yield cachedImage;
      }
    } catch (e, stack) {
      _log.severe('Error loading initial image', e, stack);
    } finally {
      this.cachedOperation = null;
    }
  }

  @override
  void cancel() {
    isCancelled = true;
    final hasActiveWork = !isFinished;

    final request = this.request;
    if (request != null) {
      this.request = null;
      request.cancel();
    }

    final operation = cachedOperation;
    if (operation != null) {
      cachedOperation = null;
      operation.cancel();
    }

    if (hasActiveWork) {
      PaintingBinding.instance.imageCache.evict(this);
    }
  }
}

ImageProvider getFullImageProvider(
  BaseAsset asset, {
  Size size = const Size(1080, 1920),
  bool edited = true,
  String? localFilePath,
}) {
  // Create new provider and cache it
  final ImageProvider provider;
  if (localFilePath != null) {
    provider = FileImage(File(localFilePath));
  } else if (_shouldUseLocalAsset(asset)) {
    final id = asset is LocalAsset ? asset.id : (asset as RemoteAsset).localId!;
    provider = LocalFullImageProvider(id: id, size: size, assetType: asset.type, isAnimated: asset.isAnimatedImage);
  } else {
    final String assetId;
    final String thumbhash;
    if (asset is LocalAsset && asset.hasRemote) {
      assetId = asset.remoteId!;
      thumbhash = "";
    } else if (asset is RemoteAsset) {
      assetId = asset.id;
      thumbhash = asset.thumbHash ?? "";
    } else {
      throw ArgumentError("Unsupported asset type: ${asset.runtimeType}");
    }
    provider = RemoteFullImageProvider(
      assetId: assetId,
      thumbhash: thumbhash,
      assetType: asset.type,
      isAnimated: asset.isAnimatedImage,
      edited: edited,
    );
  }

  return provider;
}

ImageProvider? getThumbnailImageProvider(BaseAsset asset, {Size size = kThumbnailResolution, bool edited = true}) {
  if (_shouldUseLocalAsset(asset)) {
    final id = asset is LocalAsset ? asset.id : (asset as RemoteAsset).localId!;
    return LocalThumbProvider(id: id, size: size, assetType: asset.type);
  }

  final assetId = asset is RemoteAsset ? asset.id : (asset as LocalAsset).remoteId;
  final thumbhash = asset is RemoteAsset ? asset.thumbHash ?? "" : "";
  return assetId != null ? RemoteImageProvider.thumbnail(assetId: assetId, thumbhash: thumbhash, edited: edited) : null;
}

bool _shouldUseLocalAsset(BaseAsset asset) =>
    asset.hasLocal &&
    (!asset.hasRemote || !SettingsRepository.instance.appConfig.image.preferRemote) &&
    !asset.isEdited;
