library netcache_image;

import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'dart:typed_data';

import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
import 'package:path_provider/path_provider.dart';
import 'package:crypto/crypto.dart';
import 'package:connectivity_plus/connectivity_plus.dart';

/// A Flutter widget for loading network images with offline support and smart caching.
///
/// This widget automatically caches images after the first load and can load them
/// from cache when offline. It also supports preloading, retry mechanisms, and
/// customizable placeholder and error widgets.
///
/// Example:
/// ```dart
/// NetCacheImage(
///   url: 'https://example.com/image.jpg',
///   placeholder: Icon(Icons.image),
///   errorWidget: Icon(Icons.broken_image),
/// )
/// ```
class NetCacheImage extends StatefulWidget {
  /// The URL of the image to load.
  final String url;

  /// Widget to display while the image is loading.
  final Widget? placeholder;

  /// Widget to display when the image fails to load.
  final Widget? errorWidget;

  /// Duration after which cached images expire.
  /// Defaults to 7 days.
  final Duration? cacheExpiration;

  /// Maximum number of retry attempts for failed requests.
  /// Defaults to 3.
  final int maxRetries;

  /// Delay between retry attempts.
  /// Defaults to 2 seconds.
  final Duration retryDelay;

  /// Creates a NetCacheImage widget.
  ///
  /// The [url] parameter is required and must be a valid HTTP/HTTPS URL.
  const NetCacheImage({
    super.key,
    required this.url,
    this.placeholder,
    this.errorWidget,
    this.cacheExpiration,
    this.maxRetries = 3,
    this.retryDelay = const Duration(seconds: 2),
  });

  /// Preloads multiple images in advance.
  ///
  /// This method downloads and caches the specified images so they are
  /// available offline. Returns a Future that completes when all images
  /// have been processed.
  ///
  /// Example:
  /// ```dart
  /// await NetCacheImage.preload([
  ///   'https://example.com/img1.jpg',
  ///   'https://example.com/img2.jpg',
  /// ]);
  /// ```
  static Future<void> preload(
    List<String> urls, {
    Duration? cacheExpiration,
    int maxRetries = 3,
    Duration retryDelay = const Duration(seconds: 2),
  }) async {
    final futures = urls.map(
      (url) => _ImageCache.instance.loadImage(
        url,
        cacheExpiration: cacheExpiration,
        maxRetries: maxRetries,
        retryDelay: retryDelay,
      ),
    );
    await Future.wait(futures);
  }

  /// Clears all cached images.
  ///
  /// This method removes all cached images from the device storage.
  static Future<void> clearCache() async {
    await _ImageCache.instance.clearCache();
  }

  /// Clears expired cached images.
  ///
  /// This method removes only the cached images that have expired
  /// based on their cache expiration policy.
  static Future<void> clearExpiredCache() async {
    await _ImageCache.instance.clearExpiredCache();
  }

  /// Gets the total size of cached images in bytes.
  ///
  /// Returns the total size of all cached images in bytes.
  static Future<int> getCacheSize() async {
    return await _ImageCache.instance.getCacheSize();
  }

  @override
  State<NetCacheImage> createState() => _NetCacheImageState();
}

class _NetCacheImageState extends State<NetCacheImage> {
  late Future<Uint8List?> _imageFuture;

  @override
  void initState() {
    super.initState();
    _imageFuture = _ImageCache.instance.loadImage(
      widget.url,
      cacheExpiration: widget.cacheExpiration,
      maxRetries: widget.maxRetries,
      retryDelay: widget.retryDelay,
    );
  }

  @override
  void didUpdateWidget(NetCacheImage oldWidget) {
    super.didUpdateWidget(oldWidget);
    if (oldWidget.url != widget.url) {
      _imageFuture = _ImageCache.instance.loadImage(
        widget.url,
        cacheExpiration: widget.cacheExpiration,
        maxRetries: widget.maxRetries,
        retryDelay: widget.retryDelay,
      );
    }
  }

  @override
  Widget build(BuildContext context) {
    return FutureBuilder<Uint8List?>(
      future: _imageFuture,
      builder: (context, snapshot) {
        if (snapshot.connectionState == ConnectionState.waiting) {
          return widget.placeholder ?? const SizedBox.shrink();
        }

        if (snapshot.hasError || snapshot.data == null) {
          return widget.errorWidget ?? const SizedBox.shrink();
        }

        return Image.memory(
          snapshot.data!,
          fit: BoxFit.cover,
          errorBuilder: (context, error, stackTrace) {
            return widget.errorWidget ?? const SizedBox.shrink();
          },
        );
      },
    );
  }
}

/// Internal cache manager for handling image caching.
class _ImageCache {
  static final _ImageCache instance = _ImageCache._internal();
  _ImageCache._internal();

  final Map<String, Completer<Uint8List?>> _loadingImages = {};
  Directory? _cacheDir;

  /// Gets the cache directory for storing images.
  Future<Directory> get _getCacheDir async {
    if (_cacheDir != null) return _cacheDir!;
    _cacheDir = await getTemporaryDirectory();
    final cacheDir = Directory('${_cacheDir!.path}/netcache_images');
    if (!await cacheDir.exists()) {
      await cacheDir.create(recursive: true);
    }
    return cacheDir;
  }

  /// Generates a cache key for the given URL.
  String _getCacheKey(String url) {
    return md5.convert(utf8.encode(url)).toString();
  }

  /// Gets the cache file for the given URL.
  Future<File> _getCacheFile(String url) async {
    final cacheDir = await _getCacheDir;
    final key = _getCacheKey(url);
    return File('${cacheDir.path}/$key');
  }

  /// Gets the metadata file for the given URL.
  Future<File> _getMetadataFile(String url) async {
    final cacheDir = await _getCacheDir;
    final key = _getCacheKey(url);
    return File('${cacheDir.path}/${key}_metadata.json');
  }

  /// Loads an image from cache or network.
  Future<Uint8List?> loadImage(
    String url, {
    Duration? cacheExpiration,
    int maxRetries = 3,
    Duration retryDelay = const Duration(seconds: 2),
  }) async {
    // Check if already loading
    if (_loadingImages.containsKey(url)) {
      return _loadingImages[url]!.future;
    }

    final completer = Completer<Uint8List?>();
    _loadingImages[url] = completer;

    try {
      final result = await _loadImageInternal(
        url,
        cacheExpiration: cacheExpiration,
        maxRetries: maxRetries,
        retryDelay: retryDelay,
      );
      completer.complete(result);
    } catch (e) {
      completer.completeError(e);
    } finally {
      _loadingImages.remove(url);
    }

    return completer.future;
  }

  /// Internal method to load image with retry logic.
  Future<Uint8List?> _loadImageInternal(
    String url, {
    Duration? cacheExpiration,
    int maxRetries = 3,
    Duration retryDelay = const Duration(seconds: 2),
  }) async {
    // Try to load from cache first
    final cachedData = await _loadFromCache(url, cacheExpiration);
    if (cachedData != null) {
      return cachedData;
    }

    // Try to load from network with retries
    Exception? lastException;
    for (int attempt = 0; attempt <= maxRetries; attempt++) {
      try {
        final data = await _loadFromNetwork(url);
        if (data != null) {
          await _saveToCache(url, data, cacheExpiration);
          return data;
        }
      } catch (e) {
        lastException = e as Exception;
        if (attempt < maxRetries) {
          await Future.delayed(retryDelay);
        }
      }
    }

    throw lastException ?? Exception('Failed to load image: $url');
  }

  /// Loads image data from cache.
  Future<Uint8List?> _loadFromCache(
    String url,
    Duration? cacheExpiration,
  ) async {
    try {
      final cacheFile = await _getCacheFile(url);
      final metadataFile = await _getMetadataFile(url);

      if (!await cacheFile.exists() || !await metadataFile.exists()) {
        return null;
      }

      // Check expiration
      if (cacheExpiration != null) {
        final metadata = await _loadMetadata(metadataFile);
        if (metadata != null) {
          final now = DateTime.now();
          final cachedAt = DateTime.parse(metadata['cachedAt']);
          if (now.difference(cachedAt) > cacheExpiration) {
            await cacheFile.delete();
            await metadataFile.delete();
            return null;
          }
        }
      }

      return await cacheFile.readAsBytes();
    } catch (e) {
      return null;
    }
  }

  /// Loads image data from network.
  Future<Uint8List?> _loadFromNetwork(String url) async {
    // Check connectivity
    final connectivity = await Connectivity().checkConnectivity();
    if (connectivity == ConnectivityResult.none) {
      throw Exception('No internet connection');
    }

    final response = await http.get(Uri.parse(url));
    if (response.statusCode == 200) {
      return response.bodyBytes;
    } else {
      throw Exception('HTTP ${response.statusCode}: ${response.reasonPhrase}');
    }
  }

  /// Saves image data to cache.
  Future<void> _saveToCache(
    String url,
    Uint8List data,
    Duration? cacheExpiration,
  ) async {
    try {
      final cacheFile = await _getCacheFile(url);
      final metadataFile = await _getMetadataFile(url);

      await cacheFile.writeAsBytes(data);
      await _saveMetadata(metadataFile, cacheExpiration);
    } catch (e) {
      // Ignore cache save errors
    }
  }

  /// Loads metadata from file.
  Future<Map<String, dynamic>?> _loadMetadata(File metadataFile) async {
    try {
      final content = await metadataFile.readAsString();
      return json.decode(content) as Map<String, dynamic>;
    } catch (e) {
      return null;
    }
  }

  /// Saves metadata to file.
  Future<void> _saveMetadata(
    File metadataFile,
    Duration? cacheExpiration,
  ) async {
    final metadata = {
      'cachedAt': DateTime.now().toIso8601String(),
      'expiration': cacheExpiration?.inMilliseconds,
    };
    await metadataFile.writeAsString(json.encode(metadata));
  }

  /// Clears all cached images.
  Future<void> clearCache() async {
    try {
      final cacheDir = await _getCacheDir;
      if (await cacheDir.exists()) {
        await cacheDir.delete(recursive: true);
        await cacheDir.create();
      }
    } catch (e) {
      // Ignore clear errors
    }
  }

  /// Clears expired cached images.
  Future<void> clearExpiredCache() async {
    try {
      final cacheDir = await _getCacheDir;
      if (!await cacheDir.exists()) return;

      final files = await cacheDir.list().toList();
      for (final file in files) {
        if (file is File && file.path.endsWith('_metadata.json')) {
          final metadata = await _loadMetadata(file);
          if (metadata != null) {
            final cachedAt = DateTime.parse(metadata['cachedAt']);
            final expirationMs = metadata['expiration'] as int?;
            if (expirationMs != null) {
              final expiration = Duration(milliseconds: expirationMs);
              final now = DateTime.now();
              if (now.difference(cachedAt) > expiration) {
                // Delete both metadata and image files
                await file.delete();
                final imageFile = File(
                  file.path.replaceAll('_metadata.json', ''),
                );
                if (await imageFile.exists()) {
                  await imageFile.delete();
                }
              }
            }
          }
        }
      }
    } catch (e) {
      // Ignore clear errors
    }
  }

  /// Gets the total size of cached images in bytes.
  Future<int> getCacheSize() async {
    try {
      final cacheDir = await _getCacheDir;
      if (!await cacheDir.exists()) return 0;

      int totalSize = 0;
      final files = await cacheDir.list().toList();
      for (final file in files) {
        if (file is File && !file.path.endsWith('_metadata.json')) {
          totalSize += await file.length();
        }
      }
      return totalSize;
    } catch (e) {
      return 0;
    }
  }
}
