

part of 'player_page.dart';

extension _PlayerPageLifecycle on _PlayerPageState {
  void _onPanelChanged() {
    if (_drawer.isOpen && _drawer.category == PlayerPanelCategory.episode) {
      WidgetsBinding.instance.addPostFrameCallback(
        (_) => _episodeBodyKey.currentState?.ensureLoaded(),
      );
    }
    if (mounted) setState(() {});
  }

  Future<void> _initialize() async {
    try {
      final directStream = widget.directStream;
      final activeSession = ref.read(activeSessionProvider);
      if (activeSession == null && directStream == null) {
        if (mounted) {
          setState(() => _errorMessage = '没有活跃的会话');
        }
        return;
      }
      AppLogger.debug(
        _tag,
        directStream != null
            ? 'init direct stream'
            : 'init server=${activeSession!.serverUrl} '
                  'userId=${activeSession.userId}',
      );

      _prewarmPlayerEngine();

      final bootstrap = await (
        ref.read(deviceIdProvider.future),
        ref.read(appVersionProvider.future),
        ref.read(playbackResolverProvider.future),
        ref.read(embyGatewayProvider.future),
      ).wait;
      final deviceId = bootstrap.$1;
      final appVersion = bootstrap.$2;
      final resolver = bootstrap.$3;
      final gateway = bootstrap.$4;
      if (!mounted) return;

      final traktDepsFuture = directStream == null
          ? _loadTraktDeps()
          : Future.value(null);
      final prefetchFuture = directStream == null
          ? _loadPrefetchedPlaybackInfo(activeSession!)
          : Future.value(null);

      final scrobbleStatus = ref.read(traktScrobbleStatusProvider.notifier);

      final pendingSeq = ref.read(pendingCrossServerSequenceProvider);
      if (pendingSeq != null) {
        ref.read(pendingCrossServerSequenceProvider.notifier).state = null;
      }

      final settingsFuture = (
        ref
            .read(subtitleSettingsProvider.future)
            .then(
              (value) => value,
              onError: (Object e) {
                AppLogger.warn(_tag, 'load subtitle settings failed', e);
                return const SubtitleStyleSettings();
              },
            ),
        Future.value(
          ref.read(playerSettingsProvider).value ?? const PlayerSettingsState(),
        ),
        ref
            .read(danmakuSettingsProvider.future)
            .then(
              (value) => value,
              onError: (Object e) {
                AppLogger.warn(_tag, 'load danmaku settings failed', e);
                return const DanmakuSettingsState();
              },
            ),
        ref
            .read(audioSettingsProvider.future)
            .then(
              (value) => value,
              onError: (Object e) {
                AppLogger.warn(_tag, 'load audio settings failed', e);
                return const AudioSettingsState();
              },
            ),
      ).wait;
      final Future<Playlist> playlistFuture;
      if (directStream != null) {
        final itemType = directStream.episode != null ? 'Episode' : 'Movie';
        final syntheticItem = EmbyRawItem(
          Id: widget.itemId,
          Name: directStream.title,
          Type: itemType,
          SeriesName: directStream.seriesName,
          IndexNumber: directStream.episode,
          RunTimeTicks: directStream.durationSeconds == null
              ? null
              : directStream.durationSeconds! * kTicksPerSecond,
        );
        playlistFuture = Future<Playlist>.value(
          SingleItemPlaylist(
            item: PlaylistItem(
              itemId: widget.itemId,
              displayLabel: directStream.title,
              playFromStart: true,
              preloadedItem: syntheticItem,
            ),
          ),
        );
      } else {
        playlistFuture = playlistFromLaunch(
          itemId: widget.itemId,
          mediaSourceId: widget.mediaSourceId,
          startPositionTicks: widget.startPositionTicks,
          preferredAudioStreamIndex: widget.preferredAudioStreamIndex,
          preferredSubtitleStreamIndex: widget.preferredSubtitleStreamIndex,
          playFromStart: widget.fromStart,
          crossServerSequence: pendingSeq,
          loadDetail: (id) {
            final serverId = ref.read(activeSessionProvider)?.serverId ?? '';
            return ref.read(
              mediaDetailDataProvider((serverId: serverId, itemId: id)).future,
            );
          },
          loadEpisodes: ({required seriesId, required seasonId}) {
            final serverId = ref.read(activeSessionProvider)?.serverId ?? '';
            return ref.read(
              seriesEpisodesDataProvider((
                serverId: serverId,
                seriesId: seriesId,
                seasonId: seasonId,
              )).future,
            );
          },
          loadSeasons: (seriesId) {
            final serverId = ref.read(activeSessionProvider)?.serverId ?? '';
            return ref.read(
              seriesSeasonsDataProvider((
                serverId: serverId,
                seriesId: seriesId,
              )).future,
            );
          },
        );
      }
      final ready = await (settingsFuture, playlistFuture).wait;
      final settings = ready.$1;
      _subtitleSettings = settings.$1;
      final playerSettings =
          ref.read(playerSettingsProvider).value ?? settings.$2;
      final danmakuSettings = settings.$3;
      if (!mounted) return;

      final audioSettings = settings.$4;
      _prefetchedPlaybackInfo = await prefetchFuture;
      final traktDeps = await traktDepsFuture;
      if (!mounted) return;

      _builder = PlaybackSessionBuilder(
        resolver: resolver,
        gateway: gateway,
        metadataResolver: MediaSourceMetadataResolver(
          probeService: MediaProbeService.instance,
        ),
        engineHolder: _engineHolder,
        readActiveSession: () {


          if (!mounted) throw const CancelledException('player unmounted');
          return ref.read(activeSessionProvider)!;
        },
        deviceId: deviceId,
        deviceName: kEmbyDeviceName,
        playerSettingsNotifier: _playerSettingsNotifier,
        audioSettingsNotifier: _audioSettingsNotifier,
        subtitleSettingsNotifier: ref.read(subtitleSettingsProvider.notifier),
        loadItemDetail: (serverId, itemId) async {
          if (!mounted) throw const CancelledException('player unmounted');
          final res = await ref.read(
            mediaDetailDataProvider((
              serverId: serverId,
              itemId: itemId,
            )).future,
          );
          return res.base;
        },
        readEngineOverride: () {
          if (!mounted) throw const CancelledException('player unmounted');
          return ref.read(playerEngineOverrideProvider).value ??
              PlayerEngineOverride.auto;
        },
        traktGateway: traktDeps?.gateway,
        traktQueue: traktDeps?.queue,
        traktAppVersion: appVersion,
        traktOnSuccess: scrobbleStatus.recordSuccess,
        traktOnError: scrobbleStatus.recordError,
      );

      _initialPlayerSettings = playerSettings;
      _initialDanmakuSettings = danmakuSettings;
      _initialAudioSettings = audioSettings;
      _panelSettings = danmakuSettings.panelSettings;

      _playlist = ready.$2;
      if (!mounted) {
        _playlist?.dispose();
        _playlist = null;
        return;
      }
      _playlist!.current.addListener(_onCurrentItemChanged);
      _onCurrentItemChanged();
    } catch (e, s) {
      AppLogger.error(_tag, 'initialize failed', e, s);
      if (mounted) setState(() => _errorMessage = formatUserError(e));
    }
  }


  void _prewarmPlayerEngine() {
    if (_engineHolder.current != null) return;
    final override =
        ref.read(playerEngineOverrideProvider).value ??
        PlayerEngineOverride.auto;
    final kind = resolvePlayerEngineKind(
      override,
      isDolbyVision: false,
      videoCodec: null,
    );
    unawaited(() async {
      try {
        final engine = await _engineHolder.acquire(
          kind,
          () => PlaybackSessionBuilder.createEngine(kind),
        );
        AppLogger.debug(_tag, 'engine prewarmed kind=${kind.name}');
        if (engine is Media3PlayerEngine) {
          await engine.prewarmNative();
        }
      } catch (e) {
        AppLogger.debug(_tag, 'engine prewarm failed', e);
      }
    }());
  }

  Future<({AuthedTraktGateway gateway, TraktPendingSyncQueue queue})?>
  _loadTraktDeps() async {
    try {
      final traktGateway = ref.read(authedTraktGatewayProvider);
      final queue = await ref.read(traktSyncQueueProvider.future);
      unawaited(queue.drain());
      return (gateway: traktGateway, queue: queue);
    } catch (e) {
      AppLogger.warn(_tag, 'trakt deps unavailable, scrobble disabled', e);
      return null;
    }
  }

  Future<({int startTimeTicks, Map<String, dynamic> info})?>
  _loadPrefetchedPlaybackInfo(AuthedSession activeSession) async {
    try {
      final startTimeTicks = widget.fromStart
          ? 0
          : (widget.startPositionTicks ?? 0);
      final serverId = widget.serverId ?? activeSession.serverId;
      final key = (
        serverId: serverId,
        itemId: widget.itemId,
        mediaSourceId: widget.mediaSourceId,
        startTimeTicks: startTimeTicks,
      );
      AppLogger.debug(
        _tag,
        'playbackInfo prefetch await ${describePlaybackInfoPrefetchKey(key)}',
      );
      final prefetched = await ref.read(
        playbackInfoPrefetchProvider(key).future,
      );
      if (prefetched == null || prefetched.info.isEmpty) {
        AppLogger.debug(
          _tag,
          'playbackInfo prefetch miss ${describePlaybackInfoPrefetchKey(key)}',
        );
        return null;
      }
      AppLogger.debug(
        _tag,
        'playbackInfo prefetch hit ${describePlaybackInfoPrefetchKey(key)}',
      );
      return (startTimeTicks: prefetched.startTimeTicks, info: prefetched.info);
    } catch (e) {
      AppLogger.debug(_tag, 'playbackInfo prefetch failed; fallback', e);
      return null;
    }
  }

  int _prefetchStartTimeTicksFor(PlaybackRequest req) {
    if (req.playFromStart) return 0;
    final explicitTicks = req.startPositionTicks;
    if (explicitTicks != null && explicitTicks > 0) return explicitTicks;
    return req.preloadedItem?.UserData?.PlaybackPositionTicks ?? 0;
  }
}
