

part of 'player_page.dart';

extension _PlayerPageSources on _PlayerPageState {
  PlaybackRequest _itemToRequest(PlaylistItem item) {
    return PlaybackRequest(
      itemId: item.itemId,
      mediaSourceId: item.mediaSourceId,
      startPositionTicks: item.startPositionTicks,
      preferredAudioStreamIndex: item.preferredAudioStreamIndex,
      preferredSubtitleStreamIndex: item.preferredSubtitleStreamIndex,
      subtitleSettings: _subtitleSettings,
      playerSettings: _lastRequest?.playerSettings ?? _initialPlayerSettings,
      danmakuSettings: _lastRequest?.danmakuSettings ?? _initialDanmakuSettings,
      audioSettings: _lastRequest?.audioSettings ?? _initialAudioSettings,
      playFromStart: item.playFromStart,
      targetServer: item.serverIdentity,
      preloadedItem: item.preloadedItem,
      directStreamUrl: widget.directStream?.url,
      directStreamHeaders:
          widget.directStream?.headers ?? const <String, String>{},
    );
  }

  void _onCurrentItemChanged() {
    final pl = _playlist;
    if (pl == null) return;
    final item = pl.current.value;
    if (item.itemId.isEmpty) return;

    _relink.reset();
    unawaited(_switchTo(_itemToRequest(item)));
  }

  Future<void> _onEpisodePicked(EmbyRawItem episode) async {
    final pl = _playlist;
    if (pl == null) return;
    if (episode.Id == _session?.context.itemId) return;
    if (pl is SeriesEpisodePlaylist && episode.SeasonId != null) {
      await pl.goToInSeason(seasonId: episode.SeasonId!, episodeId: episode.Id);
    } else {
      await pl.goToItemId(episode.Id);
    }
  }

  Future<void> _switchVersion(String sourceId) async {
    if (sourceId == _session?.context.mediaSourceId) return;
    final base = _lastRequest;
    if (base == null) return;
    _relink.reset();
    final pos = _session?.engine.state.position;
    final resumeTicks = pos == null ? null : ticksFromDuration(pos);


    final req = base.copyWith(
      mediaSourceId: sourceId,
      startPositionTicks: (resumeTicks != null && resumeTicks > 0)
          ? resumeTicks
          : null,
      playFromStart: resumeTicks != null && resumeTicks <= 0,
    );
    await _switchTo(req);
  }


  int? _currentResumeTicks() {
    final pos = _session?.engine.state.position ?? Duration.zero;
    final ticks = ticksFromDuration(pos);
    return ticks > 0 ? ticks : null;
  }


  CrossServerSearchKey? _buildCrossServerKey(PlaybackContext ctx) {
    final activeServerId = ctx.serverId.isEmpty ? null : ctx.serverId;
    if (activeServerId == null) return null;
    final providerIds = ctx.providerIds;
    final type = ctx.itemType;
    if (type == 'Movie') {
      if (providerIds.isEmpty && ctx.itemName.isEmpty) return null;
      return (
        activeServerId: activeServerId,
        anchorType: 'Movie',
        providerIdQuery: encodeProviderIdsForKey(providerIds),
        seriesProviderIdQuery: '',
        parentIndexNumber: -1,
        indexNumber: -1,
        itemName: ctx.itemName,
        excludedServerId: ctx.serverId,
      );
    }
    if (type == 'Episode') {
      final s = ctx.parentIndexNumber;
      final e = ctx.itemIndexNumber;
      if (s == null || e == null) return null;
      final seriesProviderIds = ctx.seriesProviderIds;
      if (providerIds.isEmpty && seriesProviderIds.isEmpty) return null;
      return (
        activeServerId: activeServerId,
        anchorType: 'Episode',
        providerIdQuery: encodeProviderIdsForKey(providerIds),
        seriesProviderIdQuery: encodeProviderIdsForKey(seriesProviderIds),
        parentIndexNumber: s,
        indexNumber: e,
        itemName: ctx.itemName,
        excludedServerId: ctx.serverId,
      );
    }
    return null;
  }


  List<CrossServerSourceCard> _filteredCrossServerCards(
    List<CrossServerSourceCard> cards,
    PlaybackContext ctx,
  ) {
    final curServerId = ctx.serverId;
    final curSourceId = ctx.mediaSourceId;
    return cards
        .where(
          (c) => !(c.serverId == curServerId && c.mediaSourceId == curSourceId),
        )
        .toList(growable: false);
  }


  Future<void> _switchToCrossServer(CrossServerSourceCard card) async {
    if (_switchInFlight) return;
    _relink.reset();
    final resumeTicks = _currentResumeTicks();
    final identity = PlaybackServerIdentity.fromCard(card);

    final oldPlaylist = _playlist;
    oldPlaylist?.current.removeListener(_onCurrentItemChanged);
    final newPlaylist = CrossServerPlaylist(
      sequence: [
        CrossServerPlaylistEntry(
          serverId: card.serverId,
          serverName: card.serverName,
          serverUrl: card.serverUrl,
          token: card.token,
          userId: card.userId,
          itemId: card.landingItemId,
          label: card.item.Name,
          preloadedItem: card.item.Id == card.landingItemId ? card.item : null,
        ),
      ],
      initialIndex: 0,
    );
    _playlist = newPlaylist;
    try {
      oldPlaylist?.dispose();
    } catch (e) {
      AppLogger.warn(_tag, 'dispose old playlist failed', e);
    }

    final base = _lastRequest;
    final req = PlaybackRequest(
      itemId: card.landingItemId,
      mediaSourceId: card.mediaSourceId,
      startPositionTicks: resumeTicks,
      subtitleSettings: _subtitleSettings,
      playerSettings: base?.playerSettings ?? _initialPlayerSettings,
      danmakuSettings: base?.danmakuSettings ?? _initialDanmakuSettings,
      audioSettings: base?.audioSettings ?? _initialAudioSettings,
      playFromStart: resumeTicks == null,
      targetServer: identity,
    );
    newPlaylist.current.addListener(_onCurrentItemChanged);
    await _switchTo(req);
  }


  void _toggleVersionBar() {
    if (_versionBarOpen.value) {
      _versionBarOpen.value = false;
    } else {
      _drawer.close();
      _versionBarOpen.value = true;
    }
  }

  void _onAudioTrackSelected(PlaybackSession session, AudioTrack track) {
    final audioSettings = _lastRequest?.audioSettings ?? _initialAudioSettings;
    if (!audioSettings.rememberAudioTrack) return;
    final embyTracks = session.context.embyAudioTracks;
    if (embyTracks.isEmpty) return;
    var embyTrack = embyTracks.where((t) => t.index == track.index).firstOrNull;
    if (embyTrack == null) {
      final engineTracks = session.engine.state.tracks.embeddedAudio;
      final pos = engineTracks.indexWhere((t) => t.id == track.id);
      if (pos >= 0 && pos < embyTracks.length) embyTrack = embyTracks[pos];
    }
    if (embyTrack == null) return;
    unawaited(
      _audioSettingsNotifier.saveLastAudioTrack(
        session.context.memoryKey,
        RememberedAudioTrack(
          streamIndex: embyTrack.index,
          language: embyTrack.language,
          codec: embyTrack.codec,
          channels: embyTrack.channels,
          displayTitle: embyTrack.displayTitle,
          updatedAt: DateTime.now(),
        ),
      ),
    );
  }


  void _onSubtitleSelected(PlaybackSession session, int? streamIndex) {
    unawaited(session.selectSubtitleByStreamIndex(streamIndex));
    if (streamIndex == null) {
      unawaited(
        _subtitleSettingsNotifier.saveLastSubtitleTrack(
          session.context.memoryKey,
          RememberedSubtitleTrack(streamIndex: -1, updatedAt: DateTime.now()),
        ),
      );
      return;
    }
    final embyTrack = session.context.embySubtitles
        .where((track) => track.index == streamIndex)
        .firstOrNull;
    if (embyTrack == null) {
      AppLogger.warn(
        _tag,
        'subtitle memory: streamIdx=$streamIndex not in context.embySubtitles; '
        'skipping memory write',
      );
      return;
    }
    unawaited(
      _subtitleSettingsNotifier.saveLastSubtitleTrack(
        session.context.memoryKey,
        RememberedSubtitleTrack(
          streamIndex: embyTrack.index,
          language: embyTrack.language,
          title: embyTrack.title,
          updatedAt: DateTime.now(),
        ),
      ),
    );
  }
}
