import 'dart:async';
import 'dart:io';

import 'package:flutter/material.dart';
import 'package:flutter/services.dart';

import 'package:audio_service/audio_service.dart';
import 'package:intl/intl.dart';
import 'package:namico_db_wrapper/namico_db_wrapper.dart';
import 'package:on_audio_query/on_audio_query.dart';

import 'package:namida/class/faudiomodel.dart';
import 'package:namida/class/file_matcher.dart';
import 'package:namida/class/folder.dart';
import 'package:namida/class/library_group.dart';
import 'package:namida/class/library_item_map.dart';
import 'package:namida/class/split_config.dart';
import 'package:namida/class/track.dart';
import 'package:namida/class/video.dart';
import 'package:namida/controller/current_color.dart';
import 'package:namida/controller/directory_index.dart';
import 'package:namida/controller/folders_controller.dart';
import 'package:namida/controller/history_controller.dart';
import 'package:namida/controller/music_web_server/music_web_server_base.dart';
import 'package:namida/controller/navigator_controller.dart';
import 'package:namida/controller/platform/namida_channel/namida_channel.dart';
import 'package:namida/controller/platform/tags_extractor/tags_extractor.dart';
import 'package:namida/controller/player_controller.dart';
import 'package:namida/controller/scroll_search_controller.dart';
import 'package:namida/controller/search_sort_controller.dart';
import 'package:namida/controller/settings_controller.dart';
import 'package:namida/controller/settings_search_controller.dart';
import 'package:namida/controller/tagger_controller.dart';
import 'package:namida/core/constants.dart';
import 'package:namida/core/enums.dart';
import 'package:namida/core/extensions.dart';
import 'package:namida/core/functions.dart';
import 'package:namida/core/namida_converter_ext.dart';
import 'package:namida/core/translations/language.dart';
import 'package:namida/core/utils.dart';
import 'package:namida/ui/widgets/library/track_tile.dart';
import 'package:namida/ui/widgets/settings/indexer_settings.dart';

part 'indexer_artwork_extract_strategies.dart';

class Indexer<T extends Track> {
  static Indexer get inst => _instance;
  static final Indexer _instance = Indexer._internal();
  Indexer._internal();

  bool get _defaultUseMediaStore => NamidaFeaturesVisibility.onAudioQueryAvailable && settings.useMediaStore.value;
  bool get _includeVideosAsTracks => settings.includeVideos.value;
  bool get _isArtworkCachingEnabled => settings.cacheArtworks.value;
  bool get isNetworkArtworkCachingEnabled => true;

  Future<void> _clearTracksDBAndReOpen() async {
    await _tracksDBManager.deleteEverything();
  }

  late final _tracksDBManager = DBWrapper.openFromInfo(
    fileInfo: AppPaths.TRACKS_DB_INFO,
    config: const DBConfig(createIfNotExist: true),
  );
  late final _trackStatsDBManager = DBWrapper.openFromInfo(
    fileInfo: AppPaths.TRACKS_STATS_DB_INFO,
    config: const DBConfig(createIfNotExist: true),
  );

  final isIndexing = false.obs;

  final allAudioFiles = <String>{}.obs;
  final filteredForSizeDurationTracks = 0.obs;
  final duplicatedTracksLength = 0.obs;
  final tracksExcludedByNoMedia = 0.obs;

  final artworksInStorage = 0.obs;
  // final colorPalettesInStorage = 0.obs;

  final artworksSizeInStorage = 0.obs;

  final mainMapsGroup = LibraryGroup<T>();
  LibraryItemMapRaw<AlbumIdentifierWrapper> get mainMapAlbums => mainMapsGroup.mainMapAlbums;
  LibraryItemMap get mainMapArtists => mainMapsGroup.mainMapArtists;
  LibraryItemMap get mainMapAlbumArtists => mainMapsGroup.mainMapAlbumArtists;
  LibraryItemMap get mainMapComposer => mainMapsGroup.mainMapComposer;
  LibraryItemMap get mainMapGenres => mainMapsGroup.mainMapGenres;
  RxMap<Folder, List<T>> get mainMapFoldersTracksAndVideos => mainMapsGroup.mainMapFoldersTracksAndVideos;
  RxMap<Folder, List<T>> get mainMapFoldersTracks => mainMapsGroup.mainMapFoldersTracks;
  RxMap<VideoFolder, List<Video>> get mainMapFoldersVideos => mainMapsGroup.mainMapFoldersVideos;

  LibraryItemMap getArtistMapFor(MediaType type) {
    return switch (type) {
      MediaType.artist => Indexer.inst.mainMapArtists,
      MediaType.albumArtist => Indexer.inst.mainMapAlbumArtists,
      MediaType.composer => Indexer.inst.mainMapComposer,
      _ => Indexer.inst.mainMapArtists,
    };
  }

  final tracksInfoList = <T>[].obs;

  /// tracks map used for lookup
  var allTracksMappedByPath = <String, TrackExtended>{};
  final trackStatsMap = <Track, TrackStats>{}.obs;

  var allFolderCovers = <Folder, String>{}; // {directoryPath, imagePath}
  var allTracksMappedByYTID = <String, List<T>>{};

  /// Used to prevent duplicated track (by filename).
  final Map<String, bool> _currentFileNamesMap = {};

  late final _audioQuery = OnAudioQuery();

  List<T> recentlyAddedTracksSorted() {
    final alltracks = tracksInfoList.value.toList();
    alltracks.sort((a, b) {
      var result = b.dateModified.compareTo(a.dateModified);
      if (result == 0) result = b.dateAdded.compareTo(a.dateAdded);
      return result;
    });
    return alltracks;
  }

  String? getFallbackFolderArtworkPath({required Folder folder}) {
    return this.allFolderCovers[folder]; // folder hash already resolves extra/removed path separator
  }

  bool imageObtainedBefore(String imagePath) => _pendingArtworksCompressed[imagePath] != null || _pendingArtworksFullRes[imagePath] != null;

  /// {imagePath: (TrackExtended, id)};
  final _backupMediaStoreIDS = <String, (Track, int)>{};
  final artworksBytesMap = <String, Uint8List?>{};
  final artworksFilesMap = <String, File?>{};
  final _pendingArtworksCompressed = <String, Completer<void>>{};
  final _pendingArtworksFullRes = <String, Completer<void>>{};

  Future<FArtwork> getArtwork({
    required String? imagePath,
    Track? track,
    bool checkFileFirst = true,
    required bool compressed,
    int? size,
  }) async {
    if (imagePath == null && track == null) return FArtwork.dummy();

    final strategy = _getArtworkStrategy(
      imagePath: imagePath,
      track: track,
    );

    return await strategy.getArtwork(
      imagePath: imagePath,
      track: track,
      checkFileFirst: checkFileFirst,
      size: size,
      compressed: compressed,
    );
  }

  _ArtworkExtractStrategy _getArtworkStrategy({
    required String? imagePath,
    required Track? track,
  }) {
    // -- network tracks
    if (track != null && track.isNetwork) {
      return _NetworkBasedArtworkExtractStrategy(this);
    }

    // -- media store available
    if (_defaultUseMediaStore && imagePath != null) {
      return _MediaStoreArtworkExtractStrategy(this);
    }

    // -- file based
    return _FileBasedArtworkExtractStrategy(this);
  }

  Future<void> prepareTracksFile({bool startupBoost = false}) async {
    if (startupBoost) {
      final completer = Completer<void>();
      unawaited(_prepareTracksFile(completer));
      return await completer.future;
    } else {
      return await _prepareTracksFile();
    }
  }

  Future<void> _prepareTracksFile([Completer<void>? completer]) async {
    _fetchMediaStoreTracks(); // to fill ids map

    final tracksDBPath = AppPaths.TRACKS_DB_INFO.file.path;
    if (await File(tracksDBPath).existsAndValid((4 + 12) * 1024) || await File(AppPaths.TRACKS_OLD).existsAndValid()) {
      isIndexing.value = true;
      // -- only block load if the track file exists..
      await _readTrackData(completer);
      await sortMediaTracksAndSubListsAfterHistoryPrepared();
      await _sortAll();
      isIndexing.value = false;

      if (settings.refreshOnStartup.value) {
        this.refreshLibraryAndCheckForDiff(allowDeletion: false, showFinishedSnackbar: false);
      } else {
        // main reason is to refresh fallback cover
        this.getAudioFiles();
      }
    } else {
      // -- otherwise it get into normally and start indexing.
      await File(tracksDBPath).create();
      completer?.completeIfWasnt();
      refreshLibraryAndCheckForDiff(forceReIndex: true, useMediaStore: _defaultUseMediaStore);
    }
  }

  void rebuildTracksAfterSplitConfigChanges() async {
    final splitConfig = _createSplitConfig();
    final keysList = allTracksMappedByPath.keys.toFixedList();
    for (final trPath in keysList) {
      final oldtr = allTracksMappedByPath[trPath]!;
      allTracksMappedByPath[trPath] = oldtr.copyWith(
        artistsList: Indexer.splitArtist(
          title: oldtr.title,
          originalArtist: oldtr.originalArtist,
          config: splitConfig.artistsConfig,
        ),
        genresList: Indexer.splitGenre(
          oldtr.originalGenre,
          config: splitConfig.genresConfig,
        ),
        moodList: Indexer.splitGeneral(
          oldtr.originalMood,
          config: splitConfig.generalConfig,
        ),
        tagsList: Indexer.splitGeneral(
          oldtr.originalTags,
          config: splitConfig.generalConfig,
        ),
        generatePathHash: TagsExtractor.defaultUniqueArtworkHash,
      );
    }
    await _afterIndexing();
    tracksInfoList.refresh();
  }

  void rebuildTracksAfterExtractFeatArtistChanges() async {
    final artistsSplitConfig = ArtistsSplitConfig.settings();
    final keysList = allTracksMappedByPath.keys.toFixedList();
    for (final trPath in keysList) {
      final oldtr = allTracksMappedByPath[trPath]!;
      allTracksMappedByPath[trPath] = oldtr.copyWith(
        artistsList: Indexer.splitArtist(
          title: oldtr.title,
          originalArtist: oldtr.originalArtist,
          config: artistsSplitConfig,
        ),
        generatePathHash: TagsExtractor.defaultUniqueArtworkHash,
      );
    }
    await _afterIndexing();
    tracksInfoList.refresh();
  }

  void resortAllAfterIgnoreCommonPrefixChange() async {
    await _afterIndexing();
    tracksInfoList.refresh();
  }

  Future<void> refreshLibraryAndCheckForDiff({
    Set<String>? currentFiles,
    bool forceReIndex = false,
    bool? useMediaStore,
    bool allowDeletion = true,
    bool showFinishedSnackbar = true,
  }) async {
    if (isIndexing.value) {
      snackyy(title: lang.note, message: lang.anotherProcessIsRunning);
      return;
    }

    isIndexing.value = true;
    useMediaStore ??= _defaultUseMediaStore;

    (Set<String>, Set<String>)? differenceStats;

    if (forceReIndex || tracksInfoList.isEmpty) {
      await _fetchAllSongsAndWriteToFile(
        audioFiles: {},
        deletedPaths: {},
        forceReIndex: true,
        useMediaStore: useMediaStore,
      );
    } else {
      currentFiles ??= await getAudioFiles();
      final newFiles = getNewFoundPaths(currentFiles);
      final deletedPaths = allowDeletion ? getDeletedPaths(currentFiles) : <String>{};
      differenceStats = (newFiles, deletedPaths);

      await _fetchAllSongsAndWriteToFile(
        audioFiles: newFiles,
        deletedPaths: deletedPaths,
        forceReIndex: false,
        useMediaStore: useMediaStore,
      );
    }

    await _afterIndexing();
    await _tracksDBManager.checkpoint().ignoreError();
    isIndexing.value = false;

    final isEmpty = tracksInfoList.value.isEmpty;
    if (showFinishedSnackbar || isEmpty) {
      if (isEmpty) {
        snackyy(
          title: lang.done,
          message: lang.noTracksFound,
          button: SnackbarButton(
            text: lang.addFolder,
            function: () {
              SettingsSearchController.inst
                  .onResultTap(
                    settingPage: SettingSubpageEnum.indexer,
                    key: IndexerSettingsKeysGlobal.addFolder,
                    context: namida.context!,
                  )
                  .ignoreError();
              const IndexerSettings().promptAddFolderType();
            },
          ),
        );
      } else {
        final msgParts = [lang.finishedUpdatingLibrary];
        if (differenceStats != null) {
          int newFilesLength = 0;
          int deletedFilesLength = 0;
          // -- only show actual stats since filtering can apply to new paths
          // -- eg: there could be 10 new files, but were filtered so it's false to say 10 new files were added

          for (final n in differenceStats.$1) {
            if (allTracksMappedByPath.containsKey(n)) {
              newFilesLength++;
            }
          }
          for (final n in differenceStats.$2) {
            if (!allTracksMappedByPath.containsKey(n)) {
              deletedFilesLength++;
            }
          }

          if (newFilesLength == 0 && deletedFilesLength == 0) {
            msgParts.add('${lang.local}: ${lang.noChangesFound}');
          } else {
            msgParts.add('${lang.newLabel}: ${newFilesLength.displayFilesKeyword}');
            msgParts.add('${lang.deleted}/${lang.filtered}: ${deletedFilesLength.displayFilesKeyword}');
          }
          void addIfNonZero(String text, int value) {
            if (value > 0) {
              msgParts.add('$text: $value');
            }
          }

          addIfNonZero(lang.duplicatedTracks, this.duplicatedTracksLength.value);
          addIfNonZero(lang.tracksExcludedByNomedia, this.tracksExcludedByNoMedia.value);
          addIfNonZero(lang.filteredBySizeAndDuration, this.filteredForSizeDurationTracks.value);
        }
        snackyy(title: lang.done, message: msgParts.join('\n'));
      }
    }
  }

  /// Adds all tracks inside [tracksInfoList] to their respective album, artist, etc..
  /// & sorts all media.
  Future<void> _afterIndexing() async {
    final mediaSorters = {for (final e in MediaType.values) e: SearchSortController.inst.getMediaTracksSortingComparables(e)};
    this.mainMapsGroup.fillAll(tracksInfoList.value, (tr) => tr.toTrackExt(), settings.albumIdentifiers.value);
    this.mainMapsGroup.sortAllSync(mediaSorters, settings.mediaItemsTrackSortingReverse.value, tracksInfoList.value);
    this.mainMapsGroup.refreshAll();
    FoldersController.tracksAndVideos.onMapChanged(mainMapFoldersTracksAndVideos.value);
    FoldersController.tracks.onMapChanged(mainMapFoldersTracks.value);
    FoldersController.videos.onMapChanged(mainMapFoldersVideos.value);
    _refreshMediaTracksSubListsAfterSort(mediaSorters.keys);

    await _sortAll();
  }

  Future<void> sortMediaTracksSubLists(List<MediaType> medias) async {
    final sorters = {for (final e in medias) e: SearchSortController.inst.getMediaTracksSortingComparables(e)};
    final mediaItemsTrackSortingReverse = settings.mediaItemsTrackSortingReverse.value;
    this.mainMapsGroup.sortAllSync(sorters, mediaItemsTrackSortingReverse, tracksInfoList.value);
    _refreshMediaTracksSubListsAfterSort(sorters.keys); // -- vip vro
    this.mainMapsGroup.refreshAll(); // to refresh sublists as well
  }

  void _refreshMediaTracksSubListsAfterSort(Iterable<MediaType> sortedMedias) {
    for (final e in sortedMedias) {
      final fn = switch (e) {
        MediaType.track => () => SearchSortController.inst.searchTracks(LibraryTab.tracks.textSearchController?.text ?? ''),
        MediaType.album ||
        MediaType.artist ||
        MediaType.albumArtist ||
        MediaType.composer ||
        MediaType.genre => () => SearchSortController.inst.searchMedia(e.toLibraryTab().textSearchController?.text ?? '', e),
        MediaType.folder => FoldersController.tracksAndVideos.refreshAfterSorting,
        MediaType.folderMusic => FoldersController.tracks.refreshAfterSorting,
        MediaType.folderVideo => FoldersController.videos.refreshAfterSorting,
        _ => null,
      };
      fn?.call();
    }
  }

  List<MediaType> _getMediaTypeSortThatDependOnHistory() {
    final requiredToSort = <MediaType>[];
    for (final e in settings.mediaItemsTrackSorting.entries) {
      for (final sort in e.value) {
        if (sort.requiresHistory) {
          requiredToSort.add(e.key);
          break;
        }
      }
    }
    return requiredToSort;
  }

  /// re-sorts media subtracks that depend on history.
  Future<void> sortMediaTracksAndSubListsAfterHistoryPrepared() async {
    if (HistoryController.inst.isHistoryLoaded && this.mainMapsGroup.didFill) {
      final requiredToSort = _getMediaTypeSortThatDependOnHistory();
      await sortMediaTracksSubLists(requiredToSort);

      for (final e in MediaType.values) {
        final sortRequiresHistory = switch (e) {
          MediaType.track => settings.mediaItemsTrackSorting.value[MediaType.track]?.firstOrNull?.requiresHistory ?? false,
          MediaType.album => settings.albumSort.value.requiresHistory,
          MediaType.artist => settings.artistSort.value.requiresHistory,
          MediaType.albumArtist => settings.artistSort.value.requiresHistory,
          MediaType.composer => settings.artistSort.value.requiresHistory,
          MediaType.genre => settings.genreSort.value.requiresHistory,
          MediaType.playlist => settings.playlistSort.value.requiresHistory,
          MediaType.folder => settings.mediaItemsTrackSorting.value[MediaType.folder]?.firstOrNull?.requiresHistory ?? false,
          MediaType.folderMusic => settings.mediaItemsTrackSorting.value[MediaType.folderMusic]?.firstOrNull?.requiresHistory ?? false,
          MediaType.folderVideo => settings.mediaItemsTrackSorting.value[MediaType.folderVideo]?.firstOrNull?.requiresHistory ?? false,
          MediaType.mood => false,
          MediaType.tag => false,
          MediaType.rating => false,
        };
        if (sortRequiresHistory) SearchSortController.inst.sortMedia(e);
      }
    }
  }

  Future<void> _sortAll() => SearchSortController.inst.sortAll();

  /// Removes Specific tracks from their corresponding media, useful when updating track metadata or reindexing a track.
  void _removeThisTrackFromAlbumGenreArtistEtc(Track tr) {
    final trExt = tr.toTrackExt();
    void removeAndDeleteEmpty<K>(Map<K, List<Track>> map, K key) {
      final list = map[key];
      if (list != null) {
        list.remove(tr);
        if (list.isEmpty) {
          map.remove(key);
        }
      }
    }

    for (var identifier in trExt.albumsIdentifiersModified) {
      removeAndDeleteEmpty(mainMapAlbums.value, identifier);
    }
    for (var artist in trExt.artistsList) {
      removeAndDeleteEmpty(mainMapArtists.value, artist);
    }
    removeAndDeleteEmpty(mainMapAlbumArtists.value, trExt.albumArtist);
    removeAndDeleteEmpty(mainMapComposer.value, trExt.composer);
    for (var genre in trExt.genresList) {
      removeAndDeleteEmpty(mainMapGenres.value, genre);
    }

    tr is Video ? removeAndDeleteEmpty(mainMapFoldersVideos.value, tr.folder) : removeAndDeleteEmpty(mainMapFoldersTracks.value, tr.folder);
    removeAndDeleteEmpty(mainMapFoldersTracksAndVideos.value, tr.folder);

    _currentFileNamesMap.remove(tr.filename);
  }

  void _addTheseTracksToAlbumGenreArtistEtc(Map<TrackExtended, TrackExtended?> tracksMap) {
    final mainMapAlbums = this.mainMapAlbums.value;
    final mainMapArtists = this.mainMapArtists.value;
    final mainMapAlbumArtists = this.mainMapAlbumArtists.value;
    final mainMapComposer = this.mainMapComposer.value;
    final mainMapGenres = this.mainMapGenres.value;
    final mainMapFoldersTracksAndVideos = this.mainMapFoldersTracksAndVideos.value;
    final mainMapFoldersTracks = this.mainMapFoldersTracks.value;
    final mainMapFoldersVideos = this.mainMapFoldersVideos.value;

    final addedItemsLists = <MediaType, ({Map<dynamic, List<dynamic>> map, List<dynamic> newKeys, Set<dynamic> modifiedKeys})>{
      MediaType.album: (map: mainMapAlbums, newKeys: [], modifiedKeys: {}),
      MediaType.artist: (map: mainMapArtists, newKeys: [], modifiedKeys: {}),
      MediaType.albumArtist: (map: mainMapAlbumArtists, newKeys: [], modifiedKeys: {}),
      MediaType.composer: (map: mainMapComposer, newKeys: [], modifiedKeys: {}),
      MediaType.genre: (map: mainMapGenres, newKeys: [], modifiedKeys: {}),
      MediaType.folder: (map: mainMapFoldersTracksAndVideos, newKeys: [], modifiedKeys: {}),
      MediaType.folderMusic: (map: mainMapFoldersTracks, newKeys: [], modifiedKeys: {}),
      MediaType.folderVideo: (map: mainMapFoldersVideos, newKeys: [], modifiedKeys: {}),
    };

    void removeCustom<K, E>(MediaType type, Map<K, List<E>> map, K key, E item) {
      final list = map[key];
      if (list != null) {
        list.remove(item);
        if (list.isEmpty) {
          map.remove(key);
          addedItemsLists[type]!.modifiedKeys.add(key);
        }
      }
    }

    // -- this gurantees that [newlyAddedList] will not contain duplicates.
    void addCustom<K, E>(MediaType type, Map<K, List<E>> map, K? oldKey, K newKey, E item) {
      if (oldKey == newKey) return;
      if (oldKey != null) removeCustom(type, map, oldKey, item);
      final list = map[newKey];
      if (list == null) {
        map[newKey] = [item];
        addedItemsLists[type]!.newKeys.add(newKey);
        addedItemsLists[type]!.modifiedKeys.add(newKey);
      } else {
        if (!list.contains(item)) {
          list.add(item);
          addedItemsLists[type]!.modifiedKeys.add(newKey);
        }
      }
    }

    (List<D> newOnes, List<D> oldOnes) differenceLists<D>(List<D> newOnes, List<D> oldOnes) {
      final oldOnesCopy = List<D>.from(oldOnes);
      final newOnesFinal = <D>[];
      for (var element in newOnes) {
        final alreadyExistedInOld = oldOnesCopy.remove(element);
        if (!alreadyExistedInOld) newOnesFinal.add(element);
      }
      return (newOnesFinal, oldOnesCopy);
    }

    for (final e in tracksMap.entries) {
      final newtr = e.key;
      final oldtr = e.value;
      final oldTrack = oldtr?.asTrack();
      final newTrack = newtr.asTrack();

      // -- Assigning Albums
      final newOldAlbums = oldtr == null ? (newtr.albumsIdentifiersModified, const []) : differenceLists(newtr.albumsIdentifiersModified, oldtr.albumsIdentifiersModified);
      for (final alNew in newOldAlbums.$1) {
        addCustom(MediaType.album, mainMapAlbums, null, alNew, newTrack);
      }
      for (final alOld in newOldAlbums.$2) {
        removeCustom(MediaType.album, mainMapAlbums, null, alOld);
      }

      // -- Assigning Artists
      final newOldArtists = oldtr == null ? (newtr.artistsList, const []) : differenceLists(newtr.artistsList, oldtr.artistsList);

      for (final arNew in newOldArtists.$1) {
        addCustom(MediaType.artist, mainMapArtists, null, arNew, newTrack);
      }
      for (final arOld in newOldArtists.$2) {
        removeCustom(MediaType.artist, mainMapArtists, arOld, oldTrack);
      }
      addCustom(MediaType.albumArtist, mainMapAlbumArtists, oldTrack?.albumArtist, newTrack.albumArtist, newTrack);
      addCustom(MediaType.composer, mainMapComposer, oldTrack?.composer, newTrack.composer, newTrack);

      // -- Assigning Genres
      final newOldGenres = oldtr == null ? (newtr.genresList, const []) : differenceLists(newtr.genresList, oldtr.genresList);
      for (final genNew in newOldGenres.$1) {
        addCustom(MediaType.genre, mainMapGenres, null, genNew, newTrack);
      }
      for (final genOld in newOldGenres.$2) {
        removeCustom(MediaType.genre, mainMapGenres, genOld, oldTrack);
      }

      // -- Assigning Folders
      newTrack is Video
          ? addCustom(MediaType.folderVideo, mainMapFoldersVideos, oldTrack?.folder, newTrack.folder, newTrack)
          : addCustom(MediaType.folderMusic, mainMapFoldersTracks, oldTrack?.folder, newTrack.folder, newTrack);

      addCustom(MediaType.folder, mainMapFoldersTracksAndVideos, oldTrack?.folder, newTrack.folder, newTrack);
    }

    for (final sec in addedItemsLists.entries) {
      final type = sec.key;
      final modifiedKeys = sec.value.modifiedKeys;

      if (modifiedKeys.isNotEmpty) SearchSortController.inst.sortMedia(type); // main list sorting
      if (modifiedKeys.isNotEmpty) {
        final map = sec.value.map as Map<dynamic, List<T>>;
        final sorters = type == MediaType.albumArtist || type == MediaType.composer
            ? SearchSortController.inst.getMediaTracksSortingComparables(MediaType.artist)
            : SearchSortController.inst.getMediaTracksSortingComparables(type);
        final reverse = settings.mediaItemsTrackSortingReverse.value[type] ?? false;

        // -- similar approach to [LibraryGroup.sortAllSync], complicated but much better performance
        final affectedTracks = <T>[];
        final trackIndex = <T, int>{};
        for (final k in modifiedKeys) {
          final list = map[k];
          if (list == null) continue;
          for (final track in list) {
            if (!trackIndex.containsKey(track)) {
              trackIndex[track] = affectedTracks.length;
              affectedTracks.add(track);
            }
          }
        }

        final precomputedKeys = List.generate(
          sorters.length,
          (sorterIndex) => List.generate(
            affectedTracks.length,
            (i) => sorters[sorterIndex](affectedTracks[i]),
            growable: false,
          ),
          growable: false,
        );

        for (final k in modifiedKeys) {
          final list = map[k];
          if (list == null) continue;
          if (reverse) {
            list.sort((a, b) {
              final aIndex = trackIndex[a]!;
              final bIndex = trackIndex[b]!;
              for (final key in precomputedKeys) {
                final cmp = key[bIndex].compareTo(key[aIndex]);
                if (cmp != 0) return cmp;
              }
              return 0;
            });
          } else {
            list.sort((a, b) {
              final aIndex = trackIndex[a]!;
              final bIndex = trackIndex[b]!;
              for (final key in precomputedKeys) {
                final cmp = key[aIndex].compareTo(key[bIndex]);
                if (cmp != 0) return cmp;
              }
              return 0;
            });
          }
        }
      }
    }

    if (addedItemsLists[MediaType.folder]?.newKeys.isNotEmpty == true) FoldersController.tracksAndVideos.onMapChanged(mainMapFoldersTracksAndVideos);
    if (addedItemsLists[MediaType.folderMusic]?.newKeys.isNotEmpty == true) FoldersController.tracks.onMapChanged(mainMapFoldersTracks);
    if (addedItemsLists[MediaType.folderVideo]?.newKeys.isNotEmpty == true) FoldersController.videos.onMapChanged(mainMapFoldersVideos);
  }

  static Future<TrackExtended?> convertServerTagToTrack({
    required String path,
    required String server,
    required FileStatsAdv stats,
    required FAudioModel trackInfo,
    required bool tryExtractingFromFilename,
    int minDur = 0,
    int minSize = 0,
    required TrackExtended? Function() onMinDurTrigger,
    required TrackExtended? Function() onMinSizeTrigger,
    required TrackExtended? Function(String err) onError,
    SplitArtistGenreConfigsWrapper? splittersConfigs,
  }) {
    return Indexer.convertTagToTrack(
      trackPath: path,
      server: server,
      stats: stats,
      trackInfo: trackInfo,
      tryExtractingFromFilename: tryExtractingFromFilename,
      minDur: minDur,
      minSize: minSize,
      onMinDurTrigger: onMinDurTrigger,
      onMinSizeTrigger: onMinSizeTrigger,
      onError: onError,
      splittersConfigs: splittersConfigs,
    );
  }

  static Future<TrackExtended?> convertTagToTrack({
    required String trackPath,
    String? server,
    FileStatsAdv? stats,
    required FAudioModel trackInfo,
    required bool tryExtractingFromFilename,
    int minDur = 0,
    int minSize = 0,
    required TrackExtended? Function() onMinDurTrigger,
    required TrackExtended? Function() onMinSizeTrigger,
    required TrackExtended? Function(String err) onError,
    SplitArtistGenreConfigsWrapper? splittersConfigs,
  }) async {
    // -- most methods dont throw, except for timeout
    try {
      // -- returns null early depending on size [byte] or duration [seconds]
      FileStatsAdv? fileStat;
      try {
        fileStat = stats ?? FileStatsAdv.fromFileStat(await File(trackPath).stat());
        if (minSize > 0 && (fileStat.size ?? 0) < minSize) {
          return onMinSizeTrigger();
        }
      } catch (_) {}

      late TrackExtended finalTrackExtended;

      if (trackInfo.hasError && !tryExtractingFromFilename) return null;

      final initialTrack = TrackExtended(
        title: UnknownTags.TITLE,
        originalArtist: UnknownTags.ARTIST,
        artistsList: [UnknownTags.ARTIST],
        originalAlbum: UnknownTags.ALBUM,
        albumsList: [UnknownTags.ALBUM],
        albumArtist: UnknownTags.ALBUMARTIST,
        originalGenre: UnknownTags.GENRE,
        genresList: [UnknownTags.GENRE],
        composer: UnknownTags.COMPOSER,
        originalMood: UnknownTags.MOOD,
        moodList: [UnknownTags.MOOD],
        trackNo: 0,
        trackTo: 0,
        durationMS: 0,
        year: 0,
        yearText: '',
        size: fileStat?.size ?? 0,
        dateAdded: fileStat?.creationDate?.millisecondsSinceEpoch ?? 0,
        dateModified: fileStat?.modified?.millisecondsSinceEpoch ?? 0,
        path: trackPath,
        comment: '',
        description: '',
        synopsis: '',
        bitrate: 0,
        sampleRate: 0,
        bits: 0,
        isLossless: null,
        format: '',
        channels: '',
        discNo: 0,
        discTo: 0,
        language: '',
        lyrics: '',
        label: '',
        bpm: 0,
        rating: 0.0,
        originalTags: null,
        tagsList: [],
        gainData: null,
        sortInfo: null,
        albumsIdentifiersWrappers: [],
        isVideo: trackPath.isVideo(),
        hashKey: TrackExtended.generateHashKeyIfEnabled(null, trackPath, null),
        server: server,
      );
      if (!trackInfo.hasError) {
        int durationInMS = trackInfo.durationMS ?? 0;
        if (minDur != 0 && durationInMS != 0 && durationInMS < minDur * 1000) {
          return onMinDurTrigger();
        }

        final tags = trackInfo.tags;

        splittersConfigs ??= _createSplitConfig();

        final album = tags.album;
        final albumArtist = tags.albumArtist;
        final yearText = tags.year;

        // -- Split Albums
        final albums = splitAlbum(
          album,
          config: splittersConfigs.albumConfig,
        );

        // -- Split Artists
        final artists = splitArtist(
          title: tags.title,
          originalArtist: tags.artist,
          config: splittersConfigs.artistsConfig,
        );

        // -- Split Genres
        final genres = splitGenre(
          tags.genre,
          config: splittersConfigs.genresConfig,
        );

        // -- Split Moods
        final moods = splitGeneral(
          tags.mood,
          config: splittersConfigs.generalConfig,
        );

        // -- Split Tags
        final tagsEmbedded = splitGeneral(
          tags.tags,
          config: splittersConfigs.generalConfig,
        );

        final trackNoParsed = TrackExtended.parseTrackNumber(trackInfo.tags.trackNumber);
        final discNoParsed = TrackExtended.parseTrackNumber(trackInfo.tags.discNumber);

        String? trimOrNull(String? value) => value == null ? value : value.trimAll();
        String? nullifyEmpty(String? value) => value == '' ? null : value;
        String? doMagic(String? value) => nullifyEmpty(trimOrNull(value));

        finalTrackExtended = initialTrack.copyWith(
          title: doMagic(tags.title),
          originalArtist: doMagic(tags.artist),
          artistsList: artists,
          originalAlbum: doMagic(tags.album),
          albumsList: albums,
          albumArtist: doMagic(tags.albumArtist),
          originalGenre: doMagic(tags.genre),
          genresList: genres,
          originalMood: doMagic(tags.mood),
          moodList: moods,
          composer: doMagic(tags.composer),
          trackNo: trackNoParsed?.$1,
          trackTo: trackNoParsed?.$2 ?? TrackExtended.parseTrackNumber(trackInfo.tags.trackTotal)?.$1,
          durationMS: durationInMS,
          year: TrackExtended.enforceYearFormat(yearText),
          yearText: yearText,
          comment: tags.comment,
          description: tags.description,
          synopsis: tags.synopsis,
          bitrate: trackInfo.bitRate,
          sampleRate: trackInfo.sampleRate,
          bits: trackInfo.bits,
          isLossless: trackInfo.isLossless,
          format: trackInfo.format,
          channels: trackInfo.channels,
          discNo: discNoParsed?.$1,
          discTo: discNoParsed?.$2,
          language: tags.language,
          lyrics: tags.lyrics,
          label: tags.recordLabel,
          bpm: tags.bpm,
          rating: tags.ratingPercentage,
          originalTags: tags.tags,
          tagsList: tagsEmbedded,
          albumsIdentifiersWrappers: AlbumIdentifierWrapper.fromAlbums(
            albums: albums,
            albumArtist: albumArtist ?? '',
            year: yearText ?? '',
          ),
          gainData: tags.gainData,
          sortInfo: tags.sortInfo,
          generatePathHash: TagsExtractor.defaultUniqueArtworkHash,
        );

        // ----- if the title || artist weren't found in the tag fields
        final isTitleEmpty = finalTrackExtended.title == UnknownTags.TITLE;
        final isArtistEmpty = finalTrackExtended.originalArtist == UnknownTags.ARTIST;
        if (isTitleEmpty || isArtistEmpty) {
          final extractedName = FileMatcher.getTitleAndArtistFromFilename(trackPath.getFilenameWOExt);
          final newTitle = isTitleEmpty ? extractedName.$1 : null;
          final newArtists = isArtistEmpty ? [extractedName.$2] : null;
          finalTrackExtended = finalTrackExtended.copyWith(
            title: newTitle,
            originalArtist: newArtists?.first,
            artistsList: newArtists,
            generatePathHash: TagsExtractor.defaultUniqueArtworkHash,
          );
        }
      } else {
        // --- Adding dummy track with info extracted from filename.
        final titleAndArtist = FileMatcher.getTitleAndArtistFromFilename(trackPath.getFilenameWOExt);
        final title = titleAndArtist.$1;
        final artist = titleAndArtist.$2;
        finalTrackExtended = initialTrack.copyWith(
          title: title,
          originalArtist: artist,
          artistsList: [artist],
          generatePathHash: TagsExtractor.defaultUniqueArtworkHash,
        );
      }

      return finalTrackExtended;
    } catch (e) {
      return onError(e.toString());
    }
  }

  Future<TrackExtended?> getTrackInfo({
    required String trackPath,
    int minDur = 0,
    int minSize = 0,
    required TrackExtended? Function() onMinDurTrigger,
    required TrackExtended? Function() onMinSizeTrigger,
    bool tryExtractingFromFilename = true,
    required bool isNetwork,
    String? networkId,
  }) async {
    final res = await NamidaTaggerController.inst.extractMetadata(
      trackPath: trackPath,
      extractArtwork: false,
      overrideArtwork: false,
      isVideo: trackPath.isVideo(),
      isNetwork: isNetwork,
      networkId: networkId,
    );
    if (res.hasError) return null;
    return convertTagToTrack(
      trackPath: trackPath,
      trackInfo: res,
      tryExtractingFromFilename: tryExtractingFromFilename,
      minDur: minDur,
      minSize: minSize,
      onMinDurTrigger: onMinDurTrigger,
      onMinSizeTrigger: onMinSizeTrigger,
      onError: (_) => null,
    );
  }

  void _addTrackToLists(TrackExtended trackExt, FArtwork? artwork) {
    final tr = trackExt.asTrack() as T;
    final alreadyExists = allTracksMappedByPath.containsKey(tr.path);
    allTracksMappedByPath[tr.path] = trackExt;
    unawaited(_tracksDBManager.put(tr.path, trackExt.toJsonWithoutPath()));
    _currentFileNamesMap[trackExt.path.getFilename] = true;

    if (!alreadyExists) {
      tracksInfoList.add(tr);
      SearchSortController.inst.trackSearchList.add(tr);
      allTracksMappedByYTID.addForce(trackExt.youtubeID, tr);
    } else {
      final list = allTracksMappedByYTID[trackExt.youtubeID] ??= [];
      if (!list.contains(tr)) {
        list.add(tr);
      }
    }

    if (artwork != null && artwork.file != null) {
      artworksInStorage.value++;
      if (artwork.size != null) artworksSizeInStorage.value += artwork.size!;
    }
  }

  /// Removes track entries from related lists, this doesNOT delete tracks from system or remove stats entries
  Future<void> removeTracksFromLibrary(Iterable<Selectable> tracksToRemove, {required bool isFromDelete}) async {
    if (tracksToRemove.isEmpty) return;
    IOSink? recentlyDeletedFileWrite;
    if (isFromDelete) {
      final recentlyDeletedFile = File("${AppDirs.RECENTLY_DELETED}${DateFormat('yyyy_MM_dd HH_mm_ss').format(DateTime.now())} - (${tracksToRemove.length}).txt");
      recentlyDeletedFileWrite = recentlyDeletedFile.openWrite(mode: FileMode.writeOnlyAppend);
    }
    final tracksToRemoveCopy = tracksToRemove.toFixedList();
    for (final trS in tracksToRemoveCopy) {
      final tr = trS.track;
      recentlyDeletedFileWrite?.writeln(tr.path);
      _removeThisTrackFromAlbumGenreArtistEtc(tr);
      allTracksMappedByYTID.remove(tr.youtubeID);
      tracksInfoList.value.remove(tr);
      SearchSortController.inst.trackSearchList.value.remove(tr);
      SearchSortController.inst.trackSearchTemp.value.remove(tr);
      allTracksMappedByPath.remove(tr.path);
      unawaited(_tracksDBManager.delete(tr.path));
      TrackTileManager.rebuildTrackInfo(tr);
      if (tr.isPhysical) this.scanMediaStore(tr.path);
    }

    this.tracksInfoList.refresh();
    this.mainMapsGroup.refreshAll();
    SearchSortController.inst.trackSearchList.refresh();
    SearchSortController.inst.trackSearchTemp.refresh();
    FoldersController.tracksAndVideos.currentFolder.refresh();
    FoldersController.tracks.currentFolder.refresh();
    FoldersController.videos.currentFolder.refresh();
    recentlyDeletedFileWrite?.flush().then((_) => recentlyDeletedFileWrite?.close());

    SearchSortController.inst.refreshPortsIfNecessary();
  }

  Future<void> reindexTracks({
    required List<PhysicalMedia> tracks,
    bool updateArtwork = false,
    required void Function(bool didExtract) onProgress,
    required void Function(int tracksLength) onFinish,
    bool tryExtractingFromFilename = true,
  }) async {
    final tracksReal = <Track>[];
    final tracksRealPaths = <String>[];
    final tracksMissing = <Track>[];
    final finalNewOldTracks = <TrackExtended, TrackExtended?>{};
    for (final s in tracks) {
      bool exists = false;
      final tr = s.track;
      try {
        exists = await tr.exists();
      } catch (_) {}
      if (exists) {
        tracksReal.add(tr);
        tracksRealPaths.add(tr.path);
      } else {
        tracksMissing.add(tr);
      }
      TrackTileManager.rebuildTrackInfo(tr);
    }

    if (updateArtwork) {
      Indexer.clearMemoryImageCache();
    }

    for (var _ in tracksMissing) {
      onProgress(false);
    }

    final keyWrapper = ExtractingPathKey.create();
    final stream = await NamidaTaggerController.inst.extractMetadataAsStream(
      paths: tracksRealPaths,
      keyWrapper: keyWrapper,
      extractArtwork: updateArtwork,
      overrideArtwork: updateArtwork,
      isNetwork: false,
    );
    final splitConfigs = _createSplitConfig();
    await for (final item in stream) {
      final path = item.tags.path;
      final trext = await convertTagToTrack(
        trackPath: path,
        trackInfo: item,
        tryExtractingFromFilename: tryExtractingFromFilename,
        onMinDurTrigger: () => null,
        onMinSizeTrigger: () => null,
        onError: (_) => null,
        splittersConfigs: splitConfigs,
      );
      if (item.hasError) {
        onProgress(false);
      } else {
        final tr = Track.orVideo(path);
        final oldTr = tr.toTrackExtOrNull();
        allTracksMappedByYTID.remove(tr.youtubeID);
        _currentFileNamesMap.remove(path.getFilename);
        // _removeThisTrackFromAlbumGenreArtistEtc(tr);
        if (trext != null) {
          finalNewOldTracks[trext] = oldTr;
          _addTrackToLists(trext, item.tags.artwork);
        }
        onProgress(true);
      }
    }

    _addTheseTracksToAlbumGenreArtistEtc(finalNewOldTracks);
    Player.inst.refreshNotification();
    _sortAndRefreshTracks();
    onFinish(finalNewOldTracks.length);

    SearchSortController.inst.refreshPortsIfNecessary();
  }

  Future<void> updateTrackMetadata({
    required Map<T, TrackExtended> tracksMap,
    bool artworkWasEdited = true,
  }) async {
    final newTracks = <T>[];

    if (artworkWasEdited) {
      Indexer.clearMemoryImageCache();
    }

    final finalNewOldTracks = <TrackExtended, TrackExtended?>{};

    for (final e in tracksMap.entries) {
      final ot = e.key;
      finalNewOldTracks[e.value] = ot.toTrackExtOrNull();
      final nt = e.value.asTrack() as T;
      newTracks.add(nt);
      allTracksMappedByPath[ot.path] = e.value;
      unawaited(_tracksDBManager.put(ot.path, e.value.toJsonWithoutPath()));
      allTracksMappedByYTID.addForce(e.value.youtubeID, ot);
      // _currentFileNamesMap.remove(ot.filename); // same path alr
      // _currentFileNamesMap[nt.filename] = true; // --^
      TrackTileManager.rebuildTrackInfo(ot);

      if (artworkWasEdited) {
        // artwork extraction is not our business
        CurrentColor.inst.reExtractTrackColorPalette(track: ot, newNC: null, imagePath: ot.pathToImage);
      }
    }
    _addTheseTracksToAlbumGenreArtistEtc(finalNewOldTracks);
    _sortAndRefreshTracks();
    tracksInfoList.refresh();

    SearchSortController.inst.refreshPortsIfNecessary();
    final globalSearchText = ScrollSearchController.inst.searchTextEditingController.text;
    if (globalSearchText.isNotEmpty) SearchSortController.inst.searchAll(globalSearchText);
  }

  Future<T?> convertPathToTracksAndAddToListsSingle(String trackPath) async {
    final infoInLib = allTracksMappedByPath[trackPath];
    if (infoInLib != null) {
      return infoInLib.asTrack() as T;
    }

    final splitConfig = _createSplitConfig();

    Future<TrackExtended?> extractFunction(FAudioModel item) => convertTagToTrack(
      trackPath: item.tags.path,
      trackInfo: item,
      tryExtractingFromFilename: true,
      onMinDurTrigger: () => null,
      onMinSizeTrigger: () => null,
      onError: (_) => null,
      splittersConfigs: splitConfig,
    );

    final model = await NamidaTaggerController.inst.extractMetadata(
      trackPath: trackPath,
      extractArtwork: null,
      isVideo: trackPath.isVideo(),
      isNetwork: false,
    );
    final trext = await extractFunction(model);
    if (trext != null) {
      _addTrackToLists(trext, model.tags.artwork);
      _addTheseTracksToAlbumGenreArtistEtc({trext: null});
      // _sortAndRefreshTracks();
      SearchSortController.inst.refreshPortsIfNecessary();
      return trext.asTrack() as T;
    }

    return null;
  }

  Future<List<T>> convertPathsToTracksAndAddToLists(Iterable<String> tracksPathPre) async {
    final finalTracks = <T>[];
    final tracksToExtract = <String>[];
    final finalNewOldTracks = <TrackExtended, TrackExtended?>{};

    final orderLookup = <String, int>{};
    int index = 0;
    void onPath(String path) {
      final infoInLib = allTracksMappedByPath[path];
      if (infoInLib != null) {
        finalTracks.add(infoInLib.asTrack() as T);
      } else {
        tracksToExtract.add(path);
      }
      orderLookup[path] = index;
      index++;
    }

    for (final path in tracksPathPre) {
      final isDir = await Directory(path).exists().ignoreError() ?? false;
      if (isDir) {
        final files = await Directory(path).listAllIsolate(recursive: true).ignoreError() ?? [];
        for (final f in files) {
          if (f is File) onPath(f.path);
        }
      } else {
        onPath(path);
      }
    }

    if (tracksToExtract.isNotEmpty) {
      final splitConfig = _createSplitConfig();

      Future<TrackExtended?> extractFunction(FAudioModel item) => convertTagToTrack(
        trackPath: item.tags.path,
        trackInfo: item,
        tryExtractingFromFilename: true,
        onMinDurTrigger: () => null,
        onMinSizeTrigger: () => null,
        onError: (_) => null,
        splittersConfigs: splitConfig,
      );

      final keyWrapper = ExtractingPathKey.create();
      final stream = await NamidaTaggerController.inst.extractMetadataAsStream(
        paths: tracksToExtract,
        extractArtwork: null,
        keyWrapper: keyWrapper,
        isNetwork: false,
      );
      await for (final item in stream) {
        final p = item.tags.path;
        final obj = Track.orVideo(p);
        finalTracks.add(obj as T);
        final trext = await extractFunction(item);
        if (trext != null) {
          _addTrackToLists(trext, item.tags.artwork);
          finalNewOldTracks[trext] = null;
        }
      }
    }

    _addTheseTracksToAlbumGenreArtistEtc(finalNewOldTracks);
    _sortAndRefreshTracks();

    finalTracks.sortBy((e) => orderLookup[e.path] ?? 0);

    SearchSortController.inst.refreshPortsIfNecessary();

    return finalTracks;
  }

  void _sortAndRefreshTracks() {
    Player.inst.refreshRxVariables();
    Player.inst.refreshNotification();
    SearchSortController.inst.searchAll(ScrollSearchController.inst.searchTextEditingController.text);
    SearchSortController.inst.sortMedia(MediaType.track);
    this.mainMapsGroup.refreshAll();
  }

  Future<void> _clearLists() async {
    artworksInStorage.value = 0;
    artworksSizeInStorage.value = 0;
    tracksInfoList.clear();
    allTracksMappedByPath.clear();
    allTracksMappedByYTID.clear();
    _currentFileNamesMap.clear();
    await _clearTracksDBAndReOpen();

    SearchSortController.inst.sortMedia(MediaType.track);
    SearchSortController.inst.refreshPortsIfNecessary();
  }

  void _resetCounters() {
    filteredForSizeDurationTracks.value = 0;
    duplicatedTracksLength.value = 0;
    tracksExcludedByNoMedia.value = 0;
  }

  /// Removes [deletedPaths] and fetches [audioFiles].
  ///
  /// [bypassAllChecks] will bypass `duration`, `size` & similar filenames checks.
  ///
  /// Setting [forceReIndex] to `true` will require u to call [_afterIndexing],
  /// otherwise use [_addTheseTracksToAlbumGenreArtistEtc] with changed tracks only.
  Future<void> _fetchAllSongsAndWriteToFile({
    required Set<String> audioFiles,
    required Set<String> deletedPaths,
    required bool forceReIndex,
    required bool useMediaStore,
  }) async {
    _resetCounters();

    if (forceReIndex) {
      await _clearLists();
      if (!useMediaStore) audioFiles = await getAudioFiles();
    }

    printy("Audio Files New: ${audioFiles.length}");
    printy("Audio Files Deleted: ${deletedPaths.length}");

    if (deletedPaths.isNotEmpty) {
      tracksInfoList.removeWhere((tr) {
        final remove = deletedPaths.contains(tr.path);
        if (remove) unawaited(_tracksDBManager.delete(tr.path));
        return remove;
      });
    }

    final minDur = settings.indexMinDurationInSec.value; // Seconds
    final minSize = settings.indexMinFileSizeInB.value; // bytes
    final prevDuplicated = settings.preventDuplicatedTracks.value;
    if (useMediaStore) {
      final trs = await _fetchMediaStoreTracks();
      tracksInfoList.clear();
      allTracksMappedByPath.clear();
      _clearTracksDBAndReOpen();
      allTracksMappedByYTID.clear();
      _currentFileNamesMap.clear();
      for (var e in trs) {
        _addTrackToLists(e, null);
      }
    } else {
      NamidaTaggerController.inst.currentPathsBeingExtracted.clear();
      final audioFilesWithoutDuplicates = <String>[];
      if (prevDuplicated) {
        /// skip duplicated tracks according to filename
        for (final trackPath in audioFiles) {
          if (_currentFileNamesMap.containsKey(trackPath.getFilename)) {
            duplicatedTracksLength.value++;
          } else {
            audioFilesWithoutDuplicates.add(trackPath);
          }
        }
      }

      final finalAudios = prevDuplicated ? audioFilesWithoutDuplicates : audioFiles.toList();
      int listParts;
      if (Platform.isAndroid || Platform.isIOS) {
        listParts = (Platform.numberOfProcessors * 0.5).round().withMinimum(2);
      } else {
        // lil bit more luxurious on desktop
        listParts = (Platform.numberOfProcessors * 0.8).round().withMinimum(2);
      }
      final audioFilesParts = finalAudios.split(listParts);
      final audioFilesCompleters = List.generate(audioFilesParts.length, (_) => Completer<void>());
      final keyWrapper = ExtractingPathKey.create();

      Future<void> extractAll(List<String> chunkList) async {
        if (chunkList.isEmpty) return;

        final splittersConfigs = _createSplitConfig();
        Future<TrackExtended?> extractFunction(FAudioModel item) => convertTagToTrack(
          trackPath: item.tags.path,
          trackInfo: item,
          tryExtractingFromFilename: true,
          minDur: minDur,
          minSize: minSize,
          onMinDurTrigger: () {
            filteredForSizeDurationTracks.value++;
            return null;
          },
          onMinSizeTrigger: () {
            filteredForSizeDurationTracks.value++;
            return null;
          },
          onError: (_) => null,
          splittersConfigs: splittersConfigs,
        );

        final stream = await NamidaTaggerController.inst.extractMetadataAsStream(
          paths: chunkList,
          keyWrapper: keyWrapper,
          extractArtwork: null,
          overrideArtwork: false,
          isNetwork: false,
        );

        await for (final item in stream) {
          final trext = await extractFunction(item);
          if (trext != null) _addTrackToLists(trext, item.tags.artwork);
        }
      }

      audioFilesParts.loopAdv((part, partIndex) {
        extractAll(part).then((value) => audioFilesCompleters[partIndex].complete());
      });
      await Future.wait(audioFilesCompleters.map((e) => e.future));
    }

    final networkTrackMapsToRemove = await _addServerTracksIfAvailable(forceReIndex: forceReIndex).toList();

    /// doing some checks to remove unqualified tracks.
    /// removes tracks after changing `duration` or `size`.
    tracksInfoList.removeWhere((tr) {
      final remove = networkTrackMapsToRemove.any((map) => map.containsKey(tr.path)) || (tr.durationMS != 0 && tr.durationMS < minDur * 1000) || tr.size < minSize;
      if (remove) {
        allTracksMappedByPath.remove(tr.path);
        allTracksMappedByYTID.remove(tr.youtubeID);
        _currentFileNamesMap.remove(tr.path.getFilename);
        unawaited(_tracksDBManager.delete(tr.path));
      }
      return remove;
    });

    /// removes duplicated tracks after a refresh
    if (prevDuplicated) {
      final uniquedSet = <String>{};
      final lengthBefore = tracksInfoList.value.length;
      tracksInfoList.value.retainWhere((e) {
        final keep = uniquedSet.add(e.filename);
        if (!keep) unawaited(_tracksDBManager.delete(e.path));
        return keep;
      });
      final lengthAfter = tracksInfoList.value.length;
      final removedNumber = lengthBefore - lengthAfter;
      duplicatedTracksLength.value = removedNumber;
    }

    printy("FINAL: ${tracksInfoList.length}");

    _sortAndRefreshTracks();
    Indexer.createDefaultNamidaArtworkIfRequired();
    TrackTileManager.onTrackItemPropChange();
    SearchSortController.inst.refreshPortsIfNecessary();
  }

  /// Streams Maps of tracks that should be deleted, cuz they no longer exist on the server
  Stream<Map<String, int>> _addServerTracksIfAvailable({required bool forceReIndex}) async* {
    bool didRefreshSearchList = false;

    final tracksByServer = _getTracksGroupedByServer();
    final configuredServers = <String>{};

    for (final dir in settings.directoriesToScan.value) {
      if (dir is DirectoryIndexServer) {
        final server = dir.toWebServer();
        if (server != null) {
          final dbKey = dir.toDbKey();
          configuredServers.add(dbKey);

          if (!didRefreshSearchList) {
            didRefreshSearchList = true;
            _refreshMediaTracksSubListsAfterSort([MediaType.track]);
          }

          final serverTracksInLibrary = tracksByServer[dbKey] ??= <String, int>{};

          await server.fetchAllMusicAndProcess(
            serverTracksInLibrary,
            (trExt) {
              _addTrackToLists(trExt, null);
              serverTracksInLibrary.remove(trExt.path); // still exists
            },
            forceReIndex: forceReIndex,
          );
          // -- now without the tracks that was seen again on server
          yield serverTracksInLibrary;
        }
      }
    }

    // -- remove tracks from servers that no longer exist
    for (final entry in tracksByServer.entries) {
      if (!configuredServers.contains(entry.key)) {
        yield entry.value;
      }
    }
  }

  Map<String, Map<String, int>> _getTracksGroupedByServer() {
    final map = <String, Map<String, int>>{};
    for (final trExt in allTracksMappedByPath.values) {
      final server = trExt.server;
      if (server != null && server.isNotEmpty) {
        (map[server] ??= <String, int>{})[trExt.path] = trExt.dateModified;
      }
    }
    return map;
  }

  Future<void> updateTrackDuration(Track track, Duration dur) async {
    final durInMS = dur.inMilliseconds;
    if (durInMS > 0 && track.durationMS != durInMS) {
      final trx = allTracksMappedByPath[track.path];
      if (trx != null) {
        final newTrExt = trx.copyWith(durationMS: durInMS, generatePathHash: TagsExtractor.defaultUniqueArtworkHash);
        allTracksMappedByPath[track.path] = newTrExt;
        unawaited(_tracksDBManager.put(track.path, newTrExt.toJsonWithoutPath()));
        tracksInfoList.refresh();
        SearchSortController.inst.trackSearchList.refresh();
        SearchSortController.inst.trackSearchTemp.refresh();
      }
      TrackTileManager.rebuildTrackInfo(track);
    }
  }

  static List<String> splitByCommaList(String listText) {
    final moodsFinalLookup = <String, bool>{};
    final moodsFinal = <String>[];
    final moodsPre = listText.split(',');
    for (var m in moodsPre) {
      if (m.isNotEmpty && m != ' ') {
        final cleaned = m.trimAll();
        if (moodsFinalLookup[cleaned] == null) {
          moodsFinalLookup[cleaned] = true;
          moodsFinal.add(cleaned);
        }
      }
    }
    return moodsFinal;
  }

  /// Returns new [TrackStats].
  Future<TrackStats> updateTrackStats(
    Track track, {
    String? ratingString,
    String? tagsString,
    String? moodsString,
    int? lastPositionInMs,
  }) async {
    if (ratingString != null || tagsString != null || moodsString != null) {
      TrackTileManager.rebuildTrackInfo(track);
    }

    final statsRaw = track.statsRaw;

    // fallback rating should not access tags rating (effectiveRating), otherwise it would stick and reindexing won't solve it
    final rating = ratingString != null
        ? ratingString.isEmpty
              ? null
              : int.tryParse(ratingString) ?? statsRaw?.rating
        : statsRaw?.rating;
    final tags = tagsString != null ? splitByCommaList(tagsString) : track.effectiveTags;
    final moods = moodsString != null ? splitByCommaList(moodsString) : track.effectiveMoods;
    lastPositionInMs ??= track.lastPlayedPositionInMs ?? 0;
    final newStats = TrackStats(
      track: track,
      rating: rating?.clampInt(0, 100) ?? 0,
      tags: tags,
      moods: moods,
      lastPositionInMs: lastPositionInMs,
      audioTrackId: statsRaw?.audioTrackId,
    );
    trackStatsMap[track] = newStats;
    unawaited(_trackStatsDBManager.put(track.path, newStats.toJsonWithoutTrack()));
    return newStats;
  }

  Future<void> updateTrackAudioTrackId(
    Track track, {
    required String? audioTrackId,
  }) async {
    final stats = track.statsRaw;
    final newStats = TrackStats(
      track: track,
      rating: stats?.rating ?? 0,
      tags: stats?.tags,
      moods: stats?.moods,
      lastPositionInMs: stats?.lastPositionInMs ?? 0,
      audioTrackId: audioTrackId,
    );
    trackStatsMap[track] = newStats;
    unawaited(_trackStatsDBManager.put(track.path, newStats.toJsonWithoutTrack()));
  }

  Future<void> moveStatsPath(Map<Track, Track> oldNewTrack) async {
    for (final oldNew in oldNewTrack.entries) {
      final oldValue = trackStatsMap[oldNew.key];
      if (oldValue != null) {
        trackStatsMap.value[oldNew.value] = oldValue;
        trackStatsMap.refresh();
      }
      final oldValueDB = await _trackStatsDBManager.get(oldNew.key.path);
      await _trackStatsDBManager.put(oldNew.value.path, oldValueDB);
      // await _trackStatsDBManager.delete(oldNew.key.path); // not so important ig, preserve just in case
    }
  }

  Future<void> moveStatsDirectory(
    String normalizedOldDir,
    String normalizedNewDir, {
    Iterable<String>? forThesePathsOnly,
    bool ensureNewFileExists = false,
  }) async {
    final pathsOnlySet = forThesePathsOnly?.toSet();
    final existenceCache = <String, bool>{};
    final toUpdate = <Track, Track>{};

    for (final track in trackStatsMap.value.keys.toFixedList()) {
      final normalizedPath = replaceFunctionNormalizePath(track.path);
      final shouldUpdate = replaceFunctionForUpdatedPaths(
        track.path,
        normalizedOldDir,
        normalizedNewDir,
        pathsOnlySet,
        ensureNewFileExists,
        existenceCache,
      );
      if (shouldUpdate) {
        final newPath = replaceFunctionGetNewPath(normalizedPath, normalizedOldDir, normalizedNewDir);
        toUpdate[track] = Track.fromTypeParameter(track.runtimeType, newPath);
      }
    }

    if (toUpdate.isEmpty) return;

    for (final entry in toUpdate.entries) {
      final stats = trackStatsMap.value.remove(entry.key);
      if (stats != null) trackStatsMap.value[entry.value] = stats;
    }
    trackStatsMap.refresh();

    for (final oldNew in toUpdate.entries) {
      final oldValueDB = await _trackStatsDBManager.get(oldNew.key.path);
      await _trackStatsDBManager.put(oldNew.value.path, oldValueDB);
      // await _trackStatsDBManager.delete(oldNew.key.path); // not so important ig, preserve just in case
    }
  }

  Set<String> getAllLibraryMoods() {
    final items = <String>{};
    _loopLibraryMoods((name, tr) => items.add(name));
    return items;
  }

  Set<String> getAllLibraryTags() {
    final items = <String>{};
    _loopLibraryTags((name, tr) => items.add(name));
    return items;
  }

  List<Track>? getTracksForMood(String? name) {
    if (name == null) return null;
    final trs = <Track>[];
    _loopLibraryMoods((currentName, tr) {
      if (currentName == name) {
        trs.add(tr);
      }
    });
    return trs;
  }

  List<Track>? getTracksForTag(String? name) {
    if (name == null) return null;
    final trs = <Track>[];
    _loopLibraryTags((currentName, tr) {
      if (currentName == name) {
        trs.add(tr);
      }
    });
    return trs;
  }

  List<Track>? getTracksForRating(String? name) {
    if (name == null) return null;
    final trs = <Track>[];
    _loopLibraryRatings((currentName, tr) {
      if (currentName == name) {
        trs.add(tr);
      }
    });
    return trs;
  }

  Map<String, List<Track>> getTracksGroupedByMoods({bool sort = true}) {
    final allAvailableMap = <String, List<Track>>{};
    _loopLibraryMoods((mood, tr) => allAvailableMap.addForce(mood, tr));
    if (sort) allAvailableMap.sortByReverse((e) => e.value.length);
    return allAvailableMap;
  }

  void _loopLibraryMoods(void Function(String name, Track tr) callback) {
    final tempSet = <(Track, String)>{};

    // -- from stats map
    for (final e in Indexer.inst.trackStatsMap.value.entries) {
      final tr = e.key;
      if (tr.hasInfoInLibrary()) {
        final moods = e.value.moods;
        if (moods != null) {
          for (var mood in moods) {
            if (tempSet.add((tr, mood))) {
              callback(mood, tr);
            }
          }
        }
      }
    }

    // -- from track embedded tag
    for (var tr in allTracksInLibrary) {
      for (var mood in tr.moodList) {
        if (tempSet.add((tr, mood))) {
          callback(mood, tr);
        }
      }
    }
  }

  Map<String, List<Track>> getTracksGroupedByTags({bool sort = true}) {
    final allAvailableMap = <String, List<Track>>{};
    _loopLibraryTags((tag, tr) => allAvailableMap.addForce(tag, tr));
    if (sort) allAvailableMap.sortByReverse((e) => e.value.length);
    return allAvailableMap;
  }

  void _loopLibraryTags(void Function(String name, Track tr) callback) {
    final tempSet = <(Track, String)>{};

    // -- from stats map
    for (final e in Indexer.inst.trackStatsMap.value.entries) {
      final tr = e.key;
      if (tr.hasInfoInLibrary()) {
        final tags = e.value.tags;
        if (tags != null) {
          for (var tag in tags) {
            if (tempSet.add((tr, tag))) {
              callback(tag, tr);
            }
          }
        }
      }
    }

    // -- from track embedded tag
    for (var tr in allTracksInLibrary) {
      for (var tag in tr.tagsList) {
        if (tempSet.add((tr, tag))) {
          callback(tag, tr);
        }
      }
    }
  }

  Map<String, List<Track>> getTracksGroupedByRatings({bool sort = true}) {
    final allAvailableMap = <String, List<Track>>{};
    _loopLibraryRatings((rating, tr) => allAvailableMap.addForce(rating, tr));
    if (sort) allAvailableMap.sortByReverse((e) => int.tryParse(e.key) ?? 0);
    return allAvailableMap;
  }

  void _loopLibraryRatings(void Function(String name, Track tr) callback) {
    for (var tr in allTracksInLibrary) {
      callback(tr.effectiveRating.toString(), tr);
    }
  }

  Future<void> _readTrackData([Completer<void>? completer]) async {
    tracksInfoList.clear(); // clearing for cases which refreshing library is required (like after changing separators)

    final mediaSorters = <MediaType, List<Comparable<dynamic> Function(Track)>>{};
    final mediaSortersReverse = settings.mediaItemsTrackSortingReverse.value;

    for (final e in MediaType.values) {
      mediaSorters[e] = SearchSortController.inst.getMediaTracksSortingComparables(e);
    }

    await [
      _IndexerIsolateExecuter._readTrackStatsDataSync.thready([AppPaths.TRACKS_STATS_DB_INFO, AppPaths.TRACKS_STATS_OLD]).then(
        (res) {
          trackStatsMap.value = res;
        },
      ),
      _IndexerIsolateExecuter._readTracksDataSync
          .thready([
            AppPaths.TRACKS_DB_INFO,
            AppPaths.TRACKS_OLD,
            _createSplitConfig(),
          ])
          .then(
            (value) async {
              allTracksMappedByPath = value.allTracksMappedByPath;
              allTracksMappedByYTID = value.allTracksMappedByYTID as Map<String, List<T>>;
              tracksInfoList.value = value.tracksInfoList as List<T>;

              mainMapsGroup.fillAll(
                tracksInfoList.value,
                (tr) =>
                    allTracksMappedByPath[tr.path] ??
                    kDummyExtendedTrack.copyWith(
                      title: tr.path.getFilenameWOExt,
                      path: tr.path,
                      generatePathHash: TagsExtractor.defaultUniqueArtworkHash,
                    ),
                settings.albumIdentifiers.value,
              );

              mainMapsGroup.sortAllSync(
                mediaSorters,
                mediaSortersReverse,
                tracksInfoList.value,
              );

              FoldersController.tracksAndVideos.onMapChanged(mainMapFoldersTracksAndVideos.value);
              FoldersController.tracksAndVideos.onFirstLoad();

              FoldersController.tracks.onMapChanged(mainMapFoldersTracks.value);
              FoldersController.tracks.onFirstLoad();
              FoldersController.videos.onMapChanged(mainMapFoldersVideos.value);
              FoldersController.videos.onFirstLoad();

              completer?.completeIfWasnt();

              SearchSortController.inst.refreshPortsIfNecessary(); // -- vip to refresh filtering
            },
          ),
    ].executeAllAndSilentReportErrors();

    printy("All Tracks Length From File: ${tracksInfoList.length}");
  }

  /// Same as [_readTrackData] but sorting happens on the spawned isolate, means everything needs to be copied instead of just tracks
  // Future<void> _readTrackDataWithOffloadSorting([Completer<void>? completer]) async {
  //   tracksInfoList.clear(); // clearing for cases which refreshing library is required (like after changing separators)

  //   final mediaSorters = <MediaType, List<Comparable<dynamic> Function(Track)>>{};
  //   final mediaSortersNOTSafeInIsolate = _getMediaTypeSortThatDependOnHistory();
  //   final mediaSortersReverse = settings.mediaItemsTrackSortingReverse.value;

  //   for (final e in MediaType.values) {
  //     if (!mediaSortersNOTSafeInIsolate.contains(e)) {
  //       mediaSorters[e] = SearchSortController.inst.getMediaTracksSortingComparables(e);
  //     }
  //   }

  //   final tracksRecievePort = ReceivePort();

  //   Future<void> handleRecieveTracks() async {
  //     try {
  //       final value = await tracksRecievePort.first;
  //       value as _TracksLoadResult;
  //       allTracksMappedByPath = value.allTracksMappedByPath;
  //       allTracksMappedByYTID = value.allTracksMappedByYTID as Map<String, List<T>>;
  //       tracksInfoList.value = value.tracksInfoList as List<T>;
  //       this.sortMediaTracksSubLists([MediaType.track]);
  //       completer?.completeIfWasnt();
  //     } catch (e, st) {
  //       completer?.completeErrorIfWasnt(e, st);
  //     } finally {
  //       tracksRecievePort.close();
  //     }
  //   }

  //   await [
  //     _IndexerIsolateExecuter._readTrackStatsDataSync.thready([AppPaths.TRACKS_STATS_DB_INFO, AppPaths.TRACKS_STATS_OLD]).then(
  //       (res) {
  //         trackStatsMap.value = res;
  //       },
  //     ),
  //     _IndexerIsolateExecuter._readTracksDataSync
  //         .thready([
  //           AppPaths.TRACKS_DB_INFO,
  //           AppPaths.TRACKS_OLD,
  //           _createSplitConfig(),
  //           mediaSorters,
  //           mediaSortersReverse,
  //           settings.albumIdentifiers.value,
  //           TagsExtractor.defaultUniqueArtworkHash,
  //           tracksRecievePort.sendPort,
  //         ])
  //         .then(
  //           (libraryGroup) async {
  //             mainMapsGroup.updateFrom(libraryGroup);

  //             FoldersController.tracksAndVideos.onMapChanged(mainMapFoldersTracksAndVideos.value);
  //             FoldersController.tracksAndVideos.onFirstLoad();

  //             FoldersController.tracks.onMapChanged(mainMapFoldersTracks.value);
  //             FoldersController.tracks.onFirstLoad();
  //             FoldersController.videos.onMapChanged(mainMapFoldersVideos.value);
  //             FoldersController.videos.onFirstLoad();

  //             _refreshMediaTracksSubListsAfterSort(mediaSorters.keys);

  //             SearchSortController.inst.refreshPortsIfNecessary(); // -- vip to refresh filtering
  //           },
  //         ),
  //     handleRecieveTracks(),
  //   ].executeAllAndSilentReportErrors();

  //   printy("All Tracks Length From File: ${tracksInfoList.length}");
  // }

  static List<String> splitArtist({
    required String? title,
    required String? originalArtist,
    required ArtistsSplitConfig config,
  }) {
    final allArtists = <String>{};

    final artistsOrg = config.splitText(originalArtist, fallback: UnknownTags.ARTIST);
    allArtists.addAll(artistsOrg);

    if (config.addFeatArtist) {
      final List<String>? moreArtists = title?.split(RegExp(r'\(ft\. |\[ft\. |\(feat\. |\[feat\. \]', caseSensitive: false));
      if (moreArtists != null && moreArtists.length > 1) {
        final extractedFeatArtists = moreArtists[1].split(RegExp(r'\)|\]')).first;
        final artists = config.splitText(extractedFeatArtists, fallback: null);
        allArtists.addAll(artists);
      }
    }
    return allArtists.toList();
  }

  static List<String> splitGenre(
    String? originalGenre, {
    required GenresSplitConfig config,
  }) {
    return config.splitText(
      originalGenre,
      fallback: UnknownTags.GENRE,
    );
  }

  static List<String> splitGeneral(
    String? originalText, {
    required GeneralSplitConfig config,
  }) {
    return config.splitText(
      originalText,
      fallback: null,
    );
  }

  static List<String> splitAlbum(
    String? originalText, {
    required SimpleSplitConfig config,
  }) {
    return config.splitText(
      originalText,
      fallback: null,
    );
  }

  Iterable<String> _getPhysicalMedias() => tracksInfoList.value.where((tr) => tr.isPhysical).map((t) => t.path);
  Set<String> getNewFoundPaths(Set<String> currentFiles) => currentFiles.difference(Set.of(_getPhysicalMedias()));
  Set<String> getDeletedPaths(Set<String> currentFiles) => Set.of(_getPhysicalMedias()).difference(currentFiles);

  /// [strictNoMedia] forces all subdirectories to follow the same result of the parent.
  ///
  /// ex: if (.nomedia) was found in [/storage/0/Music/],
  /// then subdirectories [/storage/0/Music/folder1/], [/storage/0/Music/folder2/] & [/storage/0/Music/folder2/subfolder/] will be excluded too.
  Future<Set<String>> getAudioFiles({bool strictNoMedia = true}) async {
    tracksExcludedByNoMedia.value = 0;

    final extensions = _includeVideosAsTracks ? NamidaFileExtensionsWrapper.audioAndVideo : NamidaFileExtensionsWrapper.audio;
    final dirsFilterer = DirsFileFilter(
      extensions: extensions,
      imageExtensions: NamidaFileExtensionsWrapper.image,
      blacklistExtensions: settings.extensionsBlacklist.value,
      strictNoMedia: strictNoMedia,
    );
    final result = await dirsFilterer.filter();

    final allPaths = result.allPaths;

    allAudioFiles.value = allPaths;
    tracksExcludedByNoMedia.value += result.excludedByNoMedia.length;
    allFolderCovers = result.folderCovers;

    printy("Paths Found: ${allPaths.length}");
    return allPaths;
  }

  Future<void> scanMediaStore(String path) async {
    if (NamidaFeaturesVisibility.onAudioQueryAvailable) {
      try {
        await _audioQuery.scanMedia(path);
      } catch (_) {}
    }
  }

  Future<List<TrackExtended>> _fetchMediaStoreTracks() async {
    if (!_defaultUseMediaStore) return [];
    final allMusic = await _audioQuery.querySongs();
    // -- folders selected will be ignored when [_defaultUseMediaStore] is enabled.
    allMusic.retainWhere(
      (element) => settings.directoriesToExclude.value.every(
        (dir) => !element.data.startsWith(dir.sourceRaw),
      ) /* && settings.directoriesToScan.any((dir) => element.data.startsWith(dir)) */,
    );
    final tracks = <TrackExtended>[];

    final splitConfig = SplitArtistGenreConfigsWrapper.settings();

    for (final e in allMusic) {
      final map = e.getMap;
      final album = e.album;
      final albums = album == null
          ? <String>[]
          : Indexer.splitAlbum(
              album,
              config: splitConfig.albumConfig,
            );
      final albumArtist = map['album_artist'] as String?;
      final artist = e.artist;
      final artists = artist == null
          ? <String>[]
          : Indexer.splitArtist(
              title: e.title,
              originalArtist: artist,
              config: splitConfig.artistsConfig,
            );
      final genre = e.genre;
      final genres = genre == null
          ? <String>[]
          : Indexer.splitGenre(
              genre,
              config: splitConfig.genresConfig,
            );
      final mood = map['mood'];
      final moods = mood == null
          ? <String>[]
          : Indexer.splitGeneral(
              mood,
              config: splitConfig.generalConfig,
            );
      final tag = map['tag'] ?? map['tags'];
      final tags = tag == null
          ? <String>[]
          : Indexer.splitGeneral(
              tag,
              config: splitConfig.generalConfig,
            );
      final bitrate = map['bitrate'] as int?;
      final disc = map['disc_number'] as int?;
      final discTo = map['disc_total'] as int?;
      final trackTo = map['track_total'] as int?;
      final yearString = map['year'] as String?;
      final path = e.data;
      final trext = TrackExtended(
        title: e.title,
        originalArtist: e.artist ?? UnknownTags.ARTIST,
        artistsList: artists,
        originalAlbum: album ?? UnknownTags.ALBUM,
        albumsList: albums,
        albumArtist: albumArtist ?? UnknownTags.ALBUMARTIST,
        originalGenre: e.genre ?? UnknownTags.GENRE,
        genresList: genres,
        originalMood: mood ?? '',
        moodList: moods,
        composer: e.composer ?? '',
        trackNo: e.track ?? 0,
        trackTo: trackTo ?? 0,
        durationMS: e.duration ?? 0, // `e.duration` => milliseconds
        year: TrackExtended.enforceYearFormat(yearString) ?? 0,
        yearText: yearString ?? '',
        size: e.size,
        dateAdded: e.dateAdded ?? 0,
        dateModified: e.dateModified ?? 0,
        path: path,
        comment: '',
        description: '',
        synopsis: '',
        bitrate: bitrate == null ? 0 : bitrate ~/ 1000,
        sampleRate: 0,
        bits: 0,
        isLossless: null,
        format: '',
        channels: '',
        discNo: disc ?? 0,
        discTo: discTo ?? 0,
        language: '',
        lyrics: '',
        label: '',
        bpm: 0,
        rating: 0.0,
        originalTags: tag,
        tagsList: tags,
        gainData: null,
        sortInfo: null,
        albumsIdentifiersWrappers: AlbumIdentifierWrapper.fromAlbums(
          albums: albums,
          albumArtist: albumArtist ?? '',
          year: yearString ?? '',
        ),
        isVideo: e.data.isVideo(),
        hashKey: TrackExtended.generateHashKeyIfEnabled(null, path, null),
        server: null,
      );
      tracks.add(trext);
      _backupMediaStoreIDS[trext.pathToImage] = (trext.asTrack(), e.id);
    }
    return tracks;
  }

  void updateImageSizesInStorage({required int removedCount, required int removedSize}) {
    artworksInStorage.value -= removedCount;
    artworksSizeInStorage.value -= removedSize;
  }

  Future<void> calculateAllImageSizesInStorage() async {
    final stats = await _caclulateDirectoryInfoIsolate.thready({
      "dirPath": AppDirs.ARTWORKS,
      "token": RootIsolateToken.instance,
    });
    artworksInStorage.value = stats.$1;
    artworksSizeInStorage.value = stats.$2;
  }

  static (int, int) _caclulateDirectoryInfoIsolate(Map p) {
    final dirPath = p["dirPath"] as String;
    final token = p["token"] as RootIsolateToken;
    BackgroundIsolateBinaryMessenger.ensureInitialized(token);

    int totalCount = 0;
    int totalSize = 0;

    void calDirRecursive(Directory dir) {
      final files = dir.listSyncSafe();
      for (var f in files) {
        if (f is File) {
          try {
            totalSize += (f).lengthSync();
            totalCount++;
          } catch (_) {}
        } else {
          try {
            calDirRecursive(f as Directory);
          } catch (_) {}
        }
      }
    }

    calDirRecursive(Directory(dirPath));

    return (totalCount, totalSize);
  }

  // static int _caclulateDirectoryCountIsolate(String dirPath) {
  //   int totalCount = 0;

  // void calDirRecursive(Directory dir) {
  //   final files = dir.listSyncSafe();
  //   for (var f in files) {
  //     if (f is File) {
  //       totalCount++;
  //     } else {
  //       try {
  //         calDirRecursive(f as Directory);
  //       } catch (_) {}
  //     }
  //   }
  // }

  //   calDirRecursive(Directory(dirPath));

  //   return totalCount;
  // }

  // Future<void> updateColorPalettesSizeInStorage({String? newPalettePath}) async {
  //   if (newPalettePath != null) {
  //     colorPalettesInStorage.value++;
  //     return;
  //   }
  //   final count = await _caclulateDirectoryCountIsolate.thready(AppDirs.PALETTES);
  //   colorPalettesInStorage.value = count;
  // }

  Future<void> clearImageCache() async {
    await Directory(AppDirs.ARTWORKS).delete(recursive: true);
    await Directory(AppDirs.ARTWORKS).create();
    Indexer.createDefaultNamidaArtworkIfRequired();
    calculateAllImageSizesInStorage();
  }

  static Future<String> createDefaultNamidaArtworkIfRequired() async {
    if (!await File(AppPaths.NAMIDA_LOGO_LAYER).exists()) {
      final byteData = await rootBundle.load(NamidaChannel.defaultLayerIconForPlatform);
      final file = await File(AppPaths.NAMIDA_LOGO_LAYER).create(recursive: true);
      await file.writeAsBytes(byteData.buffer.asUint8List());
    }
    return AppPaths.NAMIDA_LOGO_LAYER;
  }

  static SplitArtistGenreConfigsWrapper _createSplitConfig() {
    return SplitArtistGenreConfigsWrapper.settings();
  }

  static void clearMemoryImageCache() {
    imageCache.clear();
    imageCache.clearLiveImages();
    if (Platform.isAndroid) AudioService.evictArtworkCache();
  }
}

class _IndexerIsolateExecuter {
  /// reading stats db containing track rating etc.
  static Future<Map<Track, TrackStats>> _readTrackStatsDataSync(List paramsList) async {
    final statsDbInfo = paramsList[0] as DbWrapperFileInfo;
    final oldJsonFilePath = paramsList[1] as String;

    final statsDBManager = await DBWrapper.openFromInfoSyncTry(
      fileInfo: statsDbInfo,
      config: const DBConfig(
        createIfNotExist: true,
        autoDisposeTimerDuration: null, // we close manually
      ),
    );

    final trackStatsMap = <Track, TrackStats>{};

    try {
      statsDBManager?.loadEverythingKeyed(
        (key, value) {
          final track = Track.fromJson(key, isVideo: value['v'] == true);
          final stats = TrackStats.fromJsonWithoutTrack(track, value);
          trackStatsMap[stats.track] = stats;
        },
      );

      // -- migrating tracks stats json to db
      final statsJsonFile = File(oldJsonFilePath);
      if (statsJsonFile.existsSync()) {
        final list = statsJsonFile.readAsJsonSync() as List?;
        if (list != null) {
          for (final item in list) {
            try {
              final trst = TrackStats.fromJson(item);
              if (trackStatsMap[trst.track] == null) {
                final jsonDetails = trst.toJsonWithoutTrack();
                if (jsonDetails != null) {
                  trackStatsMap[trst.track] = trst;
                  statsDBManager?.put(trst.track.path, trst.toJsonWithoutTrack());
                }
              }
            } catch (_) {}
          }
        }
        statsJsonFile.deleteSync();
      }
    } catch (_) {}
    statsDBManager?.close();
    return trackStatsMap;
  }

  /// Reading actual tracks db.
  static Future<_TracksLoadResult> _readTracksDataSync(List paramsList) async {
    final tracksDbInfo = paramsList[0] as DbWrapperFileInfo;
    final oldJsonFilePath = paramsList[1] as String;
    final splitconfig = paramsList[2] as SplitArtistGenreConfigsWrapper;
    // --------- enable only if sorting will be done here ---------
    // final mediaItemsTrackSorters = paramsList[3] as Map<MediaType, List<Comparable<dynamic> Function(Track)>>;
    // final mediaItemsTrackSortingReverse = paramsList[4] as Map<MediaType, bool>;
    // final albumIdentifiers = paramsList[5] as List<AlbumIdentifier>;
    // final generatePathHash = paramsList[6] as bool? ?? false;
    // final tracksInitPort = paramsList[7] as SendPort;
    // ------------------

    final tracksDBManager = await DBWrapper.openFromInfoSyncTry(
      fileInfo: tracksDbInfo,
      config: DBConfig(
        createIfNotExist: true,
        autoDisposeTimerDuration: null, // we close manually
      ),
    );
    final allTracksMappedByPath = <String, TrackExtended>{};
    final tracksInfoList = <Track>[];
    var allTracksMappedByYTID = <String, List<Track>>{};

    try {
      tracksDBManager!.loadEverythingKeyed(
        (path, item) {
          final trExt = TrackExtended.fromJson(
            path,
            item,
            splitConfig: splitconfig,
          );
          final track = trExt.asTrack();
          allTracksMappedByPath[track.path] = trExt;
          tracksInfoList.add(track);
          allTracksMappedByYTID.addForce(trExt.youtubeID, track);
        },
      );

      // -- migrating tracks json to db
      final tracksJsonFile = File(oldJsonFilePath);
      if (tracksJsonFile.existsSync()) {
        final list = tracksJsonFile.readAsJsonSync() as List?;
        if (list != null) {
          for (final item in list) {
            try {
              final trExt = TrackExtended.fromJson(
                item['path'] ?? '',
                item,
                splitConfig: splitconfig,
              );
              final track = trExt.asTrack();
              allTracksMappedByPath[track.path] = trExt;
              tracksInfoList.add(track);
              allTracksMappedByYTID.addForce(trExt.youtubeID, track);
              tracksDBManager.put(track.path, trExt.toJsonWithoutPath());
            } catch (_) {}
          }
        }
        tracksJsonFile.deleteSync();
      }
    } catch (_) {}

    if (tracksInfoList.isEmpty && allTracksMappedByPath.isEmpty) {
      // -- in case db disk image was malformed for example,
      // -- clear the db so that it be ready by the next time.
      try {
        tracksDBManager?.deleteEverything();
      } catch (_) {}
    }

    tracksDBManager?.close();

    return _TracksLoadResult(
      tracksInfoList: tracksInfoList,
      allTracksMappedByPath: allTracksMappedByPath,
      allTracksMappedByYTID: allTracksMappedByYTID,
    );

    // --------- enable only if sorting will be done here ---------
    // tracksInitPort.send(
    //   _TracksLoadResult(
    //     tracksInfoList: tracksInfoList,
    //     allTracksMappedByPath: allTracksMappedByPath,
    //     allTracksMappedByYTID: allTracksMappedByYTID,
    //   ),
    // );

    // // -- so that sorting works, the Indexer here is local to this isolate only
    // Indexer.inst.allTracksMappedByPath = allTracksMappedByPath;
    // Indexer.inst.tracksInfoList.value = tracksInfoList;
    // Indexer.inst.allTracksMappedByYTID = allTracksMappedByYTID;

    // final libraryGroup = LibraryGroup();
    // libraryGroup.fillAll(
    //   tracksInfoList,
    //   (tr) =>
    //       allTracksMappedByPath[tr.path] ??
    //       kDummyExtendedTrack.copyWith(
    //         title: tr.path.getFilenameWOExt,
    //         path: tr.path,
    //         generatePathHash: generatePathHash,
    //       ),
    //   albumIdentifiers,
    // );

    // libraryGroup.sortAllSync(
    //   mediaItemsTrackSorters,
    //   mediaItemsTrackSortingReverse,
    //   tracksInfoList,
    // );

    // return libraryGroup;
    // ------------------
  }
}

class _TracksLoadResult {
  final List<Track> tracksInfoList;
  final Map<String, TrackExtended> allTracksMappedByPath;
  final Map<String, List<Track>> allTracksMappedByYTID;

  const _TracksLoadResult({
    required this.tracksInfoList,
    required this.allTracksMappedByPath,
    required this.allTracksMappedByYTID,
  });
}

class FileStatsAdv {
  final DateTime? creationDate;
  final DateTime? modified;
  final int? size;

  const FileStatsAdv({
    required this.creationDate,
    required this.modified,
    required this.size,
  });

  factory FileStatsAdv.fromFileStat(FileStat stat) {
    return FileStatsAdv(
      creationDate: stat.creationDate,
      modified: stat.modified,
      size: stat.size,
    );
  }
}
