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

import 'package:archive/archive.dart';
import 'package:crypto/crypto.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/services.dart';
import 'package:path_provider/path_provider.dart';

class FrameAnimationView extends StatefulWidget {
  final String framePath;

  final int frameRate;

  final FrameAnimationController? controller;

  final double? speed;

  final double? width;

  final double? height;

  const FrameAnimationView(
      {super.key,
      required this.framePath,
      this.frameRate = 25,
      this.controller,
      this.speed,
      this.width,
      this.height});

  @override
  State<StatefulWidget> createState() {
    return FrameAnimationViewState();
  }
}

class FrameAnimationViewState extends State<FrameAnimationView> {
  int _currentFrame = -1;
  List<File> imageFiles = [];
  List<ui.Image> images = [];
  Timer? _timer;
  StreamSubscription? _precacheStreamSubscription;

  @override
  void initState() {
    super.initState();
    widget.controller?.bindState(this);
    loadFrameFromAssets()
        .then((_) => initializeImageFiles())
        .then((_) => precacheImageFiles())
        .then((_) => widget.controller == null || widget.controller!.autoPlay
            ? startAnimation()
            : null)
        .catchError((error) => debugPrint('FrameAnimationView error: $error'));
  }

  @override
  void dispose() {
    super.dispose();
    _timer?.cancel();
    _precacheStreamSubscription?.cancel();
    imageFiles.clear();
    for (var image in images) {
      image.dispose();
    }
    images.clear();
  }

  @override
  Widget build(BuildContext context) {
    if (_timer == null ||
        _currentFrame < 0 ||
        images.isEmpty ||
        _currentFrame >= images.length) {
      return SizedBox(width: widget.width, height: widget.height);
    }
    return RawImage(
      image: images[_currentFrame],
      width: widget.width,
      height: widget.height,
    );
  }

  Future<void> loadFrameFromAssets() {
    if (widget.framePath.isEmpty) {
      throw Exception('framePath is null or empty');
    }
    if (widget.framePath.endsWith('.zip')) {
      return unZipToCache();
    } else {
      return copyToCache();
    }
  }

  Future<void> unZipToCache() async {
    // Load the zip file from assets
    final ByteData data = await rootBundle.load(widget.framePath);
    final List<int> bytes = data.buffer.asUint8List();

    // Decode the zip file
    final Archive archive = ZipDecoder().decodeBytes(bytes);

    // Create a cache directory
    final Directory cacheDir = await createCacheDirectory();

    // if the cache directory is not empty and frame images count is equal to the zip file's files count
    if (cacheDir.listSync().isNotEmpty &&
        cacheDir.listSync().length == archive.length) {
      return;
    }

    // Extract the contents of the zip file
    for (final ArchiveFile file in archive) {
      final String filename = file.name;
      final List<int> fileData = file.content as List<int>;

      final File newFile = File('${cacheDir.path}/$filename');
      if (newFile.existsSync()) {
        newFile.deleteSync();
      } else {
        newFile.createSync(recursive: true);
      }
      newFile.writeAsBytesSync(fileData);
    }
  }

  Future<void> copyToCache() async {
    // Read AssetManifest.json file
    final String manifestContent =
        await rootBundle.loadString('AssetManifest.json');

    // Parse JSON string into Map
    final Map<String, dynamic> manifestMap = json.decode(manifestContent);

    // Filter the Map to get file paths that start with folderPath
    final List<String> filePaths = manifestMap.keys
        .where((String key) => key.startsWith(widget.framePath))
        .toList();

    // Create a cache directory
    final Directory cacheDir = await createCacheDirectory();

    // if the cache directory is not empty and frame images count is equal to the zip file's files count
    if (cacheDir.listSync().isNotEmpty &&
        cacheDir.listSync().length == filePaths.length) {
      return;
    }

    // Copy the files to the cache directory
    for (final String path in filePaths) {
      final ByteData data = await rootBundle.load(path);
      final List<int> bytes = data.buffer.asUint8List();
      final File newFile = File('${cacheDir.path}/${path.split('/').last}');
      if (newFile.existsSync()) {
        newFile.deleteSync();
      } else {
        newFile.createSync(recursive: true);
      }
      newFile.writeAsBytesSync(bytes);
      imageFiles.add(newFile);
    }
  }

  Future<Directory> createCacheDirectory() async {
    // Get the temporary directory
    final Directory tempDir = await getTemporaryDirectory();

    // Create a new directory within the temporary directory
    final Directory cacheDir = Directory('${tempDir.path}/frame_anim_cache');

    // Check if the directory exists, if not, create it
    if (!await cacheDir.exists()) {
      await cacheDir.create(recursive: true);
    }

    // create unique directory by framePath's md5
    final Directory frameDir = Directory(
        '${cacheDir.path}/${md5.convert(utf8.encode(widget.framePath)).toString()}');

    // Check if the directory exists, if not, create it
    if (!await frameDir.exists()) {
      await frameDir.create(recursive: true);
    }

    return frameDir;
  }

  startAnimation() {
    if (imageFiles.isEmpty) {
      return;
    }

    if (_timer != null && _timer!.isActive) {
      return;
    }

    double speed = widget.speed ?? 1.0;

    _timer = Timer.periodic(
        Duration(milliseconds: 1000 ~/ (widget.frameRate * speed)), (_) {
      if (images.isEmpty) {
        return;
      }
      setState(() {
        int targetFrame = (_currentFrame + 1) % imageFiles.length;
        _currentFrame =
            targetFrame >= images.length ? images.length - 1 : targetFrame;
      });
    });
  }

  initializeImageFiles() async {
    final Directory cacheDir = await createCacheDirectory();

    imageFiles = cacheDir
        .listSync()
        .map((file) => File(file.path))
        .where((file) =>
            file.path.endsWith('.png') ||
            file.path.endsWith('.jpg') ||
            file.path.endsWith('.jpeg') ||
            file.path.endsWith('.webp'))
        .toList();

    if (imageFiles.isEmpty) {
      throw Exception('frame images not found');
    }
  }

  precacheImageFiles() {
    _precacheStreamSubscription =
        Stream<File>.fromIterable(imageFiles).asyncMap((file) async {
      final Uint8List bytes = await file.readAsBytes();
      final ui.Codec codec = await ui.instantiateImageCodec(bytes);
      final ui.FrameInfo frameInfo = await codec.getNextFrame();
      return frameInfo.image;
    }).listen((image) {
      images.add(image);
    });
  }
}

class FrameAnimationController {
  final bool autoPlay;

  FrameAnimationViewState? _state;

  FrameAnimationController({this.autoPlay = true});

  void play() {
    _state?.startAnimation();
  }

  void stop() {
    _state?._timer?.cancel();
  }

  bool get isPlaying => _state?._timer?.isActive ?? false;

  void bindState(FrameAnimationViewState state) {
    _state = state;
  }
}
