part of 'tags_extractor.dart';

// ignore: unused_element
class _TagsExtractorAndroid extends TagsExtractor {
  _TagsExtractorAndroid._init() {
    _channel = const MethodChannel('faudiotagger');
  }

  @override
  Future<void> initializeForWrite() async {}

  @override
  Future<void> disposeForWrite() async {}

  late MethodChannel _channel;

  Timer? _logsSetTimer;
  int _logsSetRetries = 5;
  @override
  Future<void> updateLogsPath() async {
    final logsParentDir = File(AppPaths.LOGS_TAGGER).parent;
    if (!await logsParentDir.exists()) return;

    _logsSetTimer?.cancel();
    _logsSetRetries = 5;
    _logsSetTimer = Timer.periodic(const Duration(seconds: 1), (timer) async {
      try {
        await _channel.invokeMethod("setLogFile", {"path": AppPaths.LOGS_TAGGER});
        timer.cancel();
      } catch (_) {
        _logsSetRetries--;
      }
      if (_logsSetRetries <= 0) timer.cancel();
    });
  }

  FAudioModel _getFallbackFAudioModel(String path, Map<String, dynamic>? info) {
    FArtwork? artwork;
    if (info != null) {
      artwork = FArtwork.fromMap(info);
    }

    if (artwork == null || !artwork.hasArtwork) {
      // since normal artworks require data to build a filename, we here fallback to filename of original path.
      final possibleFile = FileParts.join(AppDirs.ARTWORKS, '${path.getFilename}.png');
      final bytesValue = info?['artwork'];
      final bytes = bytesValue is Uint8List ? bytesValue : null;
      artwork = FArtwork(file: possibleFile, bytes: bytes);
    }

    return FAudioModel.dummy(info?["path"] as String? ?? path, artwork);
  }

  Future<FAudioModel> _readAllData({
    required String path,
    required String? artworkDirectory,
    required bool extractArtwork,
    bool overrideArtwork = false,
  }) async {
    final map = await _channel.invokeMethod<Map<Object?, Object?>?>("readAllData", {
      "path": path,
      "artworkDirectory": artworkDirectory,
      "extractArtwork": extractArtwork,
      "overrideArtwork": overrideArtwork,
      "uniqueArtworkHash": TagsExtractor.defaultUniqueArtworkHash,
      "artworkIdentifiers": TagsExtractor.defaultGroupArtworksByAlbum ? TagsExtractor.defaultAlbumIdentifier.map((e) => e.index).toFixedList() : null,
    });
    try {
      return FAudioModel.fromMap(map!.cast());
    } catch (e) {
      return _getFallbackFAudioModel(path, map?.cast());
    }
  }

  @override
  Future<FArtwork?> extractArtwork({required String trackPath, required bool isVideo}) async {
    Uint8List? bytes;
    try {
      bytes = await _channel.invokeMethod<Uint8List?>("getArtwork", {
        "path": trackPath,
      });
    } catch (_) {}

    if (bytes == null) {
      final File? tempFile = await TagsExtractor.extractThumbnailCustom(
        trackPath: trackPath,
        filename: null,
        artworkDirectory: null,
        isVideo: isVideo,
      );
      bytes = await tempFile?.readAsBytes();
      tempFile?.tryDeleting();
    }

    return bytes == null ? null : FArtwork(bytes: bytes);
  }

  @override
  Future<FAudioModel> extractMetadata({
    required String trackPath,
    required bool extractArtwork,
    required String? artworkDirectory,
    Set<AlbumIdentifier>? identifiers,
    bool overrideArtwork = false,
    required bool isVideo,
    bool tagger = true,
    FAudioModel? trackInfo,
    required bool isNetwork,
    String? networkId,
  }) async {
    if (trackInfo == null && tagger && !isVideo) {
      trackInfo = await _readAllData(
        path: trackPath,
        artworkDirectory: artworkDirectory,
        extractArtwork: extractArtwork,
        overrideArtwork: overrideArtwork,
      );
    }

    FArtwork artwork = trackInfo?.tags.artwork ?? FArtwork();
    if (extractArtwork && (artwork.file == null && artwork.bytes == null)) {
      if (artworkDirectory != null) {
        // specified directory to save in, the file is expected to exist here.
        File? artworkFile = artwork.file;
        // no need to allow if overrideArtwork, it's alr done on native side atp.
        if (artworkFile == null || !await artworkFile.exists()) {
          final filename = TagsExtractor.buildImageFilename(
            path: trackPath,
            identifiers: identifiers,
            isNetwork: isNetwork,
            networkId: networkId,
            infoCallback: () => (
              albumName: trackInfo?.tags.album,
              albumArtist: trackInfo?.tags.albumArtist,
              year: trackInfo?.tags.year,
              title: trackInfo?.tags.title,
              artist: trackInfo?.tags.artist,
            ),
            hashKeyCallback: () => trackPath.toFastHashKey(),
            parentDirPath: artworkDirectory,
          );
          final File? thumbFile = await TagsExtractor.extractThumbnailCustom(
            trackPath: trackPath,
            filename: filename,
            artworkDirectory: artworkDirectory,
            isVideo: isVideo,
            overrideOldArtwork: overrideArtwork,
          );
          artwork.file = thumbFile;
        }
      } else {
        // -- otherwise the artwork should be within info as bytes.
        Uint8List? artworkBytes = artwork.bytes;
        if (artworkBytes == null || artworkBytes.isEmpty) {
          final File? tempFile = await TagsExtractor.extractThumbnailCustom(
            trackPath: trackPath,
            filename: null,
            artworkDirectory: null,
            isVideo: isVideo,
            overrideOldArtwork: overrideArtwork,
          );
          artwork.bytes = await tempFile?.readAsBytes();
          tempFile?.tryDeleting();
        }
      }
    }

    if (trackInfo == null || trackInfo.hasError || !trackInfo.tags.isValid) {
      final ffmpegInfo = await ffmpegController.ffmpegExtractMetadata(trackPath);

      if (ffmpegInfo != null && isVideo) {
        try {
          final stats = await File(trackPath).stat();
          videoController.addLocalVideoFileInfoToCacheMap(trackPath, ffmpegInfo, stats);
        } catch (_) {}
      }
      final newTrackInfo = ffmpegInfo == null ? FAudioModel.dummy(trackPath, artwork) : ffmpegInfo.toFAudioModel(artwork: artwork);
      trackInfo = newTrackInfo.merge(trackInfo);
    }

    return trackInfo;
  }

  @override
  Future<Stream<FAudioModel>> extractMetadataAsStream({
    required List<String> paths,
    required ExtractingPathKey keyWrapper,
    required bool extractArtwork,
    required String? audioArtworkDirectory,
    required String? videoArtworkDirectory,
    bool overrideArtwork = false,
    required bool isNetwork,
  }) async {
    final streamKey = keyWrapper.next();
    StreamSubscription<dynamic>? streamSub;
    StreamSubscription<dynamic>? streamSubIndices;
    final identifiersSet = TagsExtractor.getAlbumIdentifiersSet();

    await _channel.invokeMethod("readAllDataAsStream", {
      "streamKey": streamKey,
      "paths": paths,
      "audioArtworkDirectory": audioArtworkDirectory,
      "videoArtworkDirectory": videoArtworkDirectory,
      "extractArtwork": extractArtwork,
      "overrideArtwork": overrideArtwork,
      "uniqueArtworkHash": TagsExtractor.defaultUniqueArtworkHash,
      "videoExtensions": NamidaFileExtensionsWrapper.video.extensions.toFixedList(),
      "artworkIdentifiers": TagsExtractor.defaultGroupArtworksByAlbum ? TagsExtractor.defaultAlbumIdentifier.map((e) => e.index).toFixedList() : null,
    });
    final usingStream = Completer<void>();
    int toExtract = paths.length;

    _streamControllers[streamKey] = StreamController<FAudioModel>();
    final streamController = _streamControllers[streamKey]!;

    Future<void> closeStreams() async {
      await usingStream.future;
      streamController.close();
      streamSub?.cancel();
      streamSubIndices?.cancel();
      _streamControllers.remove(streamKey);
      currentPathsBeingExtracted.remove(streamKey);
    }

    void onExtract(FAudioModel info, int index) {
      streamController.add(info);
      toExtract--;
      if (toExtract <= 0) {
        usingStream.completeIfWasnt();
        closeStreams();
      }
    }

    void ffmpegExtract({
      required String path,
      required int index,
      required bool isVideo,
      Map<String, dynamic>? trackInfoMap,
    }) {
      extractMetadata(
            trackPath: path,
            tagger: false,
            artworkDirectory: isVideo ? videoArtworkDirectory : audioArtworkDirectory,
            identifiers: identifiersSet,
            extractArtwork: extractArtwork,
            overrideArtwork: overrideArtwork,
            isVideo: isVideo,
            isNetwork: isNetwork,
            trackInfo: trackInfoMap == null ? null : FAudioModel.fromMap(trackInfoMap),
          )
          .catchError((e, st) {
            logger.error('ffmpeg fallback extracton failed', e: e, st: st);
            return _getFallbackFAudioModel(path, trackInfoMap);
          })
          .then((value) => onExtract(value, index));
    }

    final channelEvent = EventChannel('faudiotagger/stream/$streamKey');
    final channelEventIndices = EventChannel('faudiotagger/stream/$streamKey.index');
    streamSubIndices = channelEventIndices.receiveBroadcastStream().listen(
      (index) {
        currentPathsBeingExtracted[streamKey] = paths[index as int];
      },
    );
    streamSub = channelEvent.receiveBroadcastStream().listen((event) {
      final message = event as Map<Object?, Object?>;
      final map = message.cast<String, dynamic>();
      final path = map['path'] as String;
      final index = map['_i_'] as int;
      final isVideo = path.isVideo();
      if (map["ERROR_FAULTY"] == true) {
        ffmpegExtract(
          path: path,
          index: index,
          isVideo: isVideo,
        );
      } else if (isVideo) {
        // -- ensure artwork is extracted
        ffmpegExtract(
          path: path,
          index: index,
          isVideo: isVideo,
          trackInfoMap: map,
        );
      } else {
        try {
          onExtract(FAudioModel.fromMap(map), index);
        } catch (e) {
          onExtract(_getFallbackFAudioModel(path, map), index);
        }
      }
    });

    _channel.invokeMethod("streamReady", {"streamKey": streamKey, "count": paths.length}); // we ready to recieve
    return streamController.stream;
  }

  Future<String?> _writeTagsInternal({
    required String path,
    required FTags tags,
  }) async {
    try {
      return await _channel.invokeMethod<String?>("writeTags", {
        "path": path,
        "tags": tags.toMap(),
      });
    } catch (e) {
      return e.toString();
    }
  }

  @override
  Future<bool> writeTags({
    required String path,
    required FTags newTags,
    required String? commentToInsert,
    required String? oldComment,
    required bool displayFFmpegFallbackWarning,
  }) async {
    // -- 1. try tagger
    String? error = await _writeTagsInternal(
      path: path,
      tags: newTags,
    );

    bool didUpdate = error == null || error == '';

    if (!didUpdate) {
      // -- 2. try with ffmpeg
      final ffmpegTagsMap = commentToInsert != null && commentToInsert.isNotEmpty
          ? <String, String?>{
              FFMPEGTagField.comment.tagKey: oldComment == null || oldComment.isEmpty ? commentToInsert : '$commentToInsert\n$oldComment',
            }
          : FFMPEGTagField.createTagsMapfromFTag(newTags);
      didUpdate = await ffmpegController.ffmpegEditMetadata(
        path: path,
        tagsMap: ffmpegTagsMap,
      );

      final imageFile = newTags.artwork.file;
      if (imageFile != null) {
        await ffmpegController.editAudioThumbnail(audioPath: path, thumbnailPath: imageFile.path);
      }
      if (displayFFmpegFallbackWarning) {
        snackyy(
          title: lang.warning,
          message: 'FFMPEG was used. Some tags might not have been updated',
          isError: true,
        );
      }
    }
    return didUpdate;
  }
}
