content: import 'dart:math';
  
  import 'package:collection/collection.dart';
  import 'package:drift/drift.dart';
  import 'package:hooks_riverpod/hooks_riverpod.dart';
  import 'package:media_kit/media_kit.dart';
  import 'package:spotube/extensions/list.dart';
  import 'package:spotube/models/database/database.dart';
  import 'package:spotube/models/metadata/metadata.dart';
  import 'package:spotube/models/playback/track_sources.dart';
  import 'package:spotube/provider/audio_player/state.dart';
  import 'package:spotube/provider/blacklist_provider.dart';
  import 'package:spotube/provider/database/database.dart';
  import 'package:spotube/provider/discord_provider.dart';
  import 'package:spotube/provider/server/track_sources.dart';
  import 'package:spotube/services/audio_player/audio_player.dart';
  import 'package:spotube/services/logger/logger.dart';
  
  class AudioPlayerNotifier extends Notifier<AudioPlayerState> {
    BlackListNotifier get _blacklist => ref.read(blacklistProvider.notifier);
  
    void _assertAllowedTracks(Iterable<SpotubeTrackObject> tracks) {
      assert(
        tracks.every(
          (track) =>
              track is SpotubeFullTrackObject || track is SpotubeLocalTrackObject,
        ),
        'All tracks must be either SpotubeFullTrackObject or SpotubeLocalTrackObject',
      );
    }
  
    void _assertAllowedTrack(SpotubeTrackObject tracks) {
      assert(
        tracks is SpotubeFullTrackObject || tracks is SpotubeLocalTrackObject,
        'Track must be either SpotubeFullTrackObject or SpotubeLocalTrackObject',
      );
    }
  
    Future<void> _syncSavedState() async {
      final database = ref.read(databaseProvider);
  
      var playerState =
          await database.select(database.audioPlayerStateTable).getSingleOrNull();
  
      if (playerState == null) {
        await database.into(database.audioPlayerStateTable).insert(
              AudioPlayerStateTableCompanion.insert(
                playing: audioPlayer.isPlaying,
                loopMode: audioPlayer.loopMode,
                shuffled: audioPlayer.isShuffled,
                collections: <String>[],
                tracks: const Value(<SpotubeTrackObject>[]),
                currentIndex: const Value(0),
                id: const Value(0),
              ),
            );
  
        playerState =
            await database.select(database.audioPlayerStateTable).getSingle();
      } else {
        await audioPlayer.setLoopMode(playerState.loopMode);
        await audioPlayer.setShuffle(playerState.shuffled);
      }
  
      final tracks = playerState.tracks;
      final currentIndex = playerState.currentIndex;
  
      if (tracks.isEmpty && state.tracks.isNotEmpty) {
        await _updatePlayerState(
          AudioPlayerStateTableCompanion(
            tracks: Value(state.tracks),
            currentIndex: Value(currentIndex),
          ),
        );
      } else if (tracks.isNotEmpty) {
        state = state.copyWith(
          tracks: tracks,
          currentIndex: currentIndex,
        );
        await audioPlayer.openPlaylist(
          tracks.asMediaList(),
          initialIndex: currentIndex,
          autoPlay: false,
        );
      }
  
      if (playerState.collections.isNotEmpty) {
        state = state.copyWith(
          collections: playerState.collections,
        );
      }
    }
  
    Future<void> _updatePlayerState(
      AudioPlayerStateTableCompanion companion,
    ) async {
      final database = ref.read(databaseProvider);
  
      await (database.update(database.audioPlayerStateTable)
            ..where((tb) => tb.id.equals(0)))
          .write(companion);
    }
  
    @override
    build() {
      final subscriptions = [
        audioPlayer.playingStream.listen((playing) async {
          try {
            state = state.copyWith(playing: playing);
  
            await _updatePlayerState(
              AudioPlayerStateTableCompanion(
                playing: Value(playing),
              ),
            );
          } catch (e, stack) {
            AppLogger.reportError(e, stack);
          }
        }),
        audioPlayer.loopModeStream.listen((loopMode) async {
          try {
            state = state.copyWith(loopMode: loopMode);
  
            await _updatePlayerState(
              AudioPlayerStateTableCompanion(
                loopMode: Value(loopMode),
              ),
            );
          } catch (e, stack) {
            AppLogger.reportError(e, stack);
          }
        }),
        audioPlayer.shuffledStream.listen((shuffled) async {
          try {
            state = state.copyWith(shuffled: shuffled);
  
            await _updatePlayerState(
              AudioPlayerStateTableCompanion(
                shuffled: Value(shuffled),
              ),
            );
          } catch (e, stack) {
            AppLogger.reportError(e, stack);
          }
        }),
        audioPlayer.playlistStream.listen((playlist) async {
          try {
            // Playlist and state has to be in sync. This is only meant for
            // the shuffle/re-ordering indices to be in sync
            if (playlist.medias.length != state.tracks.length) {
              AppLogger.log.w(
                "Playlist length does not match state tracks length. Ignoring... "
                "Playlist length: ${playlist.medias.length}, "
                "State tracks length: ${state.tracks.length}",
              );
              return;
            }
  
            final trackGroupedById = groupBy(
              state.tracks,
              (query) => query.id,
            );
  
            final tracks = <SpotubeTrackObject>[];
  
            for (final media in playlist.medias) {
              final trackQuery = TrackSourceQuery.parseUri(media.uri);
              final track = trackGroupedById[trackQuery.id]?.firstOrNull;
              if (track != null) {
                tracks.add(track);
              }
            }
  
            if (tracks.length != state.tracks.length) {
              AppLogger.log.w("Mismatch in tracks after reordering/shuffling.");
              final missingTracks =
                  state.tracks.where((track) => !tracks.contains(track)).toList();
              AppLogger.log.w(
                "Missing tracks: ${missingTracks.map((e) => e.id).join(", ")}",
              );
            }
  
            state = state.copyWith(
              tracks: tracks,
              currentIndex: playlist.index,
            );
  
            await _updatePlayerState(
              AudioPlayerStateTableCompanion(
                currentIndex: Value(state.currentIndex),
                tracks: Value(state.tracks),
              ),
            );
          } catch (e, stack) {
            AppLogger.reportError(e, stack);
          }
        }),
      ];
  
      _syncSavedState();
  
      ref.onDispose(() {
        for (final subscription in subscriptions) {
          subscription.cancel();
        }
      });
  
      return AudioPlayerState(
        loopMode: audioPlayer.loopMode,
        playing: audioPlayer.isPlaying,
        shuffled: audioPlayer.isShuffled,
        tracks: [],
        collections: [],
      );
    }
  
    // Collection related methods
    Future<void> addCollections(List<String> collectionIds) async {
      state = state.copyWith(collections: [
        ...state.collections,
        ...collectionIds,
      ]);
  
      await _updatePlayerState(
        AudioPlayerStateTableCompanion(
          collections: Value(state.collections),
        ),
      );
    }
  
    Future<void> addCollection(String collectionId) async {
      await addCollections([collectionId]);
    }
  
    Future<void> removeCollections(List<String> collectionIds) async {
      state = state.copyWith(
        collections: state.collections
            .where((element) => !collectionIds.contains(element))
            .toList(),
      );
  
      await _updatePlayerState(
        AudioPlayerStateTableCompanion(
          collections: Value(state.collections),
        ),
      );
    }
  
    Future<void> removeCollection(String collectionId) async {
      await removeCollections([collectionId]);
    }
  
    Future<void> addTracksAtFirst(
      Iterable<SpotubeTrackObject> tracks, {
      bool allowDuplicates = false,
    }) async {
      _assertAllowedTracks(tracks);
      if (state.tracks.length == 1) {
        return addTracks(tracks);
      }
  
      final addableTracks = _blacklist.filter(tracks).where(
            (track) =>
                allowDuplicates ||
                !state.tracks.any((element) => _compareTracks(element, track)),
          );
  
      state = state.copyWith(
        tracks: [...addableTracks, ...state.tracks],
      );
  
      for (int i = 0; i < addableTracks.length; i++) {
        final track = addableTracks.elementAt(i);
  
        await audioPlayer.addTrackAt(
          SpotubeMedia(track),
          max(state.currentIndex, 0) + i + 1,
        );
      }
  
      await _updatePlayerState(
        AudioPlayerStateTableCompanion(
          tracks: Value(state.tracks),
          currentIndex: Value(max(state.currentIndex, 0)),
        ),
      );
    }
  
    Future<void> addTrack(SpotubeTrackObject track) async {
      _assertAllowedTrack(track);
  
      if (_blacklist.contains(track)) return;
      if (state.tracks.any((element) => _compareTracks(element, track))) return;
  
      state = state.copyWith(
        tracks: [...state.tracks, track],
      );
  
      await audioPlayer.addTrack(SpotubeMedia(track));
  
      await _updatePlayerState(
        AudioPlayerStateTableCompanion(
          tracks: Value(state.tracks),
          currentIndex: Value(max(state.currentIndex, 0)),
        ),
      );
    }
  
    Future<void> addTracks(Iterable<SpotubeTrackObject> tracks) async {
      _assertAllowedTracks(tracks);
  
      tracks = _blacklist.filter(tracks).toList();
      state = state.copyWith(
        tracks: [...state.tracks, ...tracks],
      );
  
      for (final track in tracks) {
        await audioPlayer.addTrack(SpotubeMedia(track));
      }
  
      await _updatePlayerState(
        AudioPlayerStateTableCompanion(
          tracks: Value(state.tracks),
          currentIndex: Value(max(state.currentIndex, 0)),
        ),
      );
    }
  
    Future<void> removeTrack(String trackId) async {
      final index = state.tracks.indexWhere((element) => element.id == trackId);
  
      if (index == -1) return;
  
      state = state.copyWith(
        tracks: List.of(state.tracks)..removeAt(index),
      );
  
      await audioPlayer.removeTrack(index);
  
      await _updatePlayerState(
        AudioPlayerStateTableCompanion(
          tracks: Value(state.tracks),
          currentIndex: Value(max(state.currentIndex, 0)),
        ),
      );
    }
  
    Future<void> removeTracks(Iterable<String> trackIds) async {
      final trackIndexes = state.tracks
          .where((element) => trackIds.any((trackId) => trackId == element.id))
          .mapIndexed((index, element) => index);
  
      final tracks = state.tracks.where(
        (element) => !trackIds.contains(element.id),
      );
  
      state = state.copyWith(
        tracks: tracks.toList(),
      );
  
      for (final index in trackIndexes) {
        await audioPlayer.removeTrack(index);
      }
  
      await _updatePlayerState(
        AudioPlayerStateTableCompanion(
          tracks: Value(state.tracks),
          currentIndex: Value(max(state.currentIndex, 0)),
        ),
      );
    }
  
    bool _compareTracks(SpotubeTrackObject a, SpotubeTrackObject b) {
      if ((a is SpotubeLocalTrackObject && b is! SpotubeLocalTrackObject) ||
          (a is! SpotubeLocalTrackObject && b is SpotubeLocalTrackObject)) {
        return false;
      }
  
      return a is SpotubeLocalTrackObject && b is SpotubeLocalTrackObject
          ? (a).path == (b).path
          : a.id == b.id;
    }
  
    Future<void> load(
      List<SpotubeTrackObject> tracks, {
      int initialIndex = 0,
      bool autoPlay = false,
    }) async {
      _assertAllowedTracks(tracks);
  
      final medias = _blacklist
          .filter(tracks)
          .toList()
          .asMediaList()
          .unique((a, b) => a.uri == b.uri);
  
      // Giving the initial track a boost so MediaKit won't skip
      // because of timeout
      final intendedActiveTrack = medias.elementAt(initialIndex);
      if (intendedActiveTrack.track is! SpotubeLocalTrackObject) {
        await ref.read(
          trackSourcesProvider(
            TrackSourceQuery.fromTrack(
                intendedActiveTrack.track as SpotubeFullTrackObject),
          ).future,
        );
      }
  
      if (medias.isEmpty) return;
  
      state = state.copyWith(
        // These are filtered tracks as well
        tracks: medias.map((media) => media.track).toList(),
        currentIndex: initialIndex,
        collections: [],
      );
  
      await audioPlayer.openPlaylist(
        medias,
        initialIndex: initialIndex,
        autoPlay: autoPlay,
      );
  
      await _updatePlayerState(
        AudioPlayerStateTableCompanion(
          tracks: Value(state.tracks),
          currentIndex: Value(max(state.currentIndex, 0)),
        ),
      );
    }
  
    Future<void> swapActiveSource() async {
      if (state.tracks.isEmpty || state.activeTrack is! SpotubeFullTrackObject) {
        return;
      }
  
      final currentIndex = state.currentIndex;
      final currentTrack = state.activeTrack as SpotubeFullTrackObject;
      final swappedMedia = SpotubeMedia(currentTrack);
  
      await audioPlayer.addTrackAt(swappedMedia, currentIndex + 1);
      await audioPlayer.skipToNext();
      await audioPlayer.removeTrack(currentIndex);
    }
  
    Future<void> jumpToTrack(SpotubeTrackObject track) async {
      final index =
          state.tracks.toList().indexWhere((element) => element.id == track.id);
      if (index == -1) return;
      await audioPlayer.jumpTo(index);
    }
  
    Future<void> moveTrack(int oldIndex, int newIndex) async {
      if (oldIndex == newIndex ||
          newIndex < 0 ||
          oldIndex < 0 ||
          newIndex > state.tracks.length - 1 ||
          oldIndex > state.tracks.length - 1) {
        return;
      }
  
      await audioPlayer.moveTrack(oldIndex, newIndex);
    }
  
    Future<void> stop() async {
      state = state.copyWith(
        tracks: [],
        currentIndex: 0,
        collections: [],
        loopMode: PlaylistMode.none,
        playing: false,
        shuffled: false,
      );
      await audioPlayer.stop();
      await _updatePlayerState(
        AudioPlayerStateTableCompanion(
          tracks: Value(state.tracks),
          currentIndex: const Value(0),
          collections: const Value(<String>[]),
          loopMode: const Value(PlaylistMode.none),
          playing: const Value(false),
          shuffled: const Value(false),
        ),
      );
      ref.read(discordProvider.notifier).clear();
    }
  }
  
  final audioPlayerProvider =
      NotifierProvider<AudioPlayerNotifier, AudioPlayerState>(
    () => AudioPlayerNotifier(),
  );
  