part of '../video_controls.dart';

final Expando<LatestAsyncWrite<String>> _subtitleVisibilityWrites = Expando<LatestAsyncWrite<String>>();

extension _PlexVideoControlsTrackMethods on _PlexVideoControlsState {
  void _toggleSubtitles() {
    final currentTrack = widget.player.state.track.subtitle;
    // No-op if no subtitle track is selected
    if (currentTrack == null || currentTrack.id == 'no') return;

    _setSubtitleVisibility(!_subtitlesVisible);
  }

  void _onSubtitleTrackChanged(SubtitleTrack track) {
    // Reset visibility when user explicitly picks a new subtitle track
    if (track.id != 'no' && !_subtitlesVisible) {
      _setSubtitleVisibility(true);
    }
    widget.onSubtitleTrackChanged?.call(track);
  }

  void _setSubtitleVisibility(bool visible) {
    final targetPlayer = widget.player;
    final coordinator = _subtitleVisibilityWrites[targetPlayer] ??= LatestAsyncWrite<String>();
    final writeToken = coordinator.begin('sub-visibility');
    final generation = ++_subtitleVisibilityWriteGeneration;
    _setControlsState(() {
      _subtitlesVisible = visible;
    });

    unawaited(() async {
      try {
        final committed = await coordinator.commitIfLatest('sub-visibility', writeToken, () async {
          await targetPlayer.setProperty('sub-visibility', visible ? 'yes' : 'no');
          if (mounted && targetPlayer == widget.player) {
            // Preserve every successfully executed mutation as the rollback
            // baseline, even when a newer optimistic toggle is queued.
            _confirmedSubtitlesVisible = visible;
          }
        });
        if (!committed ||
            !mounted ||
            generation != _subtitleVisibilityWriteGeneration ||
            targetPlayer != widget.player) {
          return;
        }
      } catch (error, stackTrace) {
        appLogger.w('Failed to update subtitle visibility', error: error, stackTrace: stackTrace);
        if (!mounted || generation != _subtitleVisibilityWriteGeneration || targetPlayer != widget.player) {
          return;
        }
        _setControlsState(() {
          _subtitlesVisible = _confirmedSubtitlesVisible;
        });
      }
    }());
  }

  void _toggleShader() {
    final shaderService = widget.shaderService;
    if (shaderService == null || !shaderService.isSupported) return;

    final shaderProvider = context.read<ShaderProvider>();
    final targetPreset = resolveShaderTogglePreset(
      currentPreset: shaderService.currentPreset,
      savedPreset: shaderProvider.savedPreset,
      allPresets: shaderProvider.allPresets,
    );

    if (targetPreset.isEnabled && widget.isAmbientLightingEnabled) {
      widget.onToggleAmbientLighting?.call();
    }

    unawaited(
      shaderService
          .applyPreset(targetPreset)
          .then((_) async {
            if (!mounted) return;
            if (targetPreset.isEnabled) {
              await shaderProvider.setPreset(targetPreset);
            } else {
              shaderProvider.setCurrentPreset(targetPreset);
            }
            if (!mounted) return;
            // ignore: no-empty-block - setState triggers rebuild to reflect shader changes
            _setControlsState(() {});
            widget.onShaderChanged?.call();
          })
          .catchError((Object e, StackTrace st) {
            appLogger.w('Failed to toggle shader preset', error: e, stackTrace: st);
          }),
    );
  }

  void _nextAudioTrack() {
    if (!widget.canControl) return;
    widget.onCycleAudioTrack?.call();
  }

  void _nextSubtitleTrack() {
    if (!widget.canControl) return;
    widget.onCycleSubtitleTrack?.call();
  }

  void _nextChapter() => _seekToNextChapter();

  void _previousChapter() => _seekToPreviousChapter();

  TrackControlsState _buildTrackControlsState({
    required PlaybackStateProvider playbackState,
    required VoidCallback? onToggleAlwaysOnTop,
  }) {
    final versionQuality = effectiveVersionQualityControls(
      isOfflinePlayback: widget.isOfflinePlayback,
      availableVersions: widget.availableVersions,
      serverSupportsTranscoding: widget.serverSupportsTranscoding,
      isTranscoding: widget.isTranscoding,
      sourceAudioTracks: widget.sourceAudioTracks,
      selectedAudioStreamId: widget.selectedAudioStreamId,
      sourceSubtitleTracks: widget.sourceSubtitleTracks,
      selectedSubtitleChoice: widget.selectedSubtitleChoice,
    );
    final canSwitchSourceSubtitles = versionQuality.canSwitch && versionQuality.sourceSubtitleTracks.isNotEmpty;
    return TrackControlsState(
      availableVersions: versionQuality.availableVersions,
      selectedMediaIndex: widget.selectedMediaIndex,
      selectedQualityPreset: widget.selectedQualityPreset,
      serverSupportsTranscoding: versionQuality.serverSupportsTranscoding,
      isTranscoding: versionQuality.isTranscoding,
      sourceAudioTracks: versionQuality.sourceAudioTracks,
      selectedAudioStreamId: versionQuality.selectedAudioStreamId,
      sourceSubtitleTracks: canSwitchSourceSubtitles
          ? versionQuality.sourceSubtitleTracks
          : const <MediaSubtitleTrack>[],
      selectedSubtitleChoice: canSwitchSourceSubtitles ? versionQuality.selectedSubtitleChoice : null,
      selectedSecondarySubtitleStreamId: canSwitchSourceSubtitles ? widget.selectedSecondarySubtitleStreamId : null,
      sourceSubtitleSidecars: canSwitchSourceSubtitles
          ? widget.sourceSubtitleSidecars
          : const <PlaybackSubtitleSidecar>[],
      sourcePartId: canSwitchSourceSubtitles ? widget.sourcePartId : null,
      sourceDurationMs: widget.metadata.durationMs,
      boxFitMode: widget.boxFitMode,
      videoZoomScale: widget.videoZoomScale,
      audioSyncOffset: _audioSyncOffset,
      subtitleSyncOffset: _subtitleSyncOffset,
      isRotationLocked: _isRotationLocked,
      isFullscreen: _isFullscreen,
      isAlwaysOnTop: _isAlwaysOnTop,
      onTogglePIPMode: (_isPipSupported && !PlatformDetector.isTV()) ? widget.onTogglePIPMode : null,
      onCycleBoxFitMode: widget.onCycleBoxFitMode,
      onVideoZoomChanged: widget.onVideoZoomChanged,
      onResetVideoZoom: widget.onResetVideoZoom,
      onToggleRotationLock: _toggleRotationLock,
      onToggleScreenLock: _toggleScreenLock,
      onToggleFullscreen: _toggleFullscreen,
      onToggleAlwaysOnTop: onToggleAlwaysOnTop,
      onSwitchVersion: versionQuality.canSwitch ? (i) => _switchVersionAndQuality(newMediaIndex: i) : null,
      onSwitchQualityPreset: versionQuality.canSwitch ? (p) => _switchVersionAndQuality(newPreset: p) : null,
      onSwitchAudioStreamId: versionQuality.canSwitch ? (id) => _switchVersionAndQuality(newAudioStreamId: id) : null,
      onSwitchSubtitle: canSwitchSourceSubtitles
          ? (choice) => _switchVersionAndQuality(newSubtitleChoice: choice)
          : null,
      onAudioTrackChanged: widget.onAudioTrackChanged,
      onSubtitleTrackChanged: _onSubtitleTrackChanged,
      onSecondarySubtitleTrackChanged: widget.onSecondarySubtitleTrackChanged,
      onLoadSeekTimes: null,
      onCancelAutoHide: widget.chromeController.cancelAutoHide,
      onStartAutoHide: _startHideTimer,
      // Sync offsets are now driven by listenable rebuilds — the sheet writes
      // to SettingsService and the parent re-reads via `_audioSyncOffset` /
      // `_subtitleSyncOffset` getters. Callback kept for sheet API compat.
      onSyncOffsetChanged: null,
      serverId: widget.metadata.serverId,
      shaderService: widget.shaderService,
      onShaderChanged: widget.onShaderChanged,
      isAmbientLightingEnabled: widget.isAmbientLightingEnabled,
      onToggleAmbientLighting: widget.player.playerType != 'exoplayer' ? widget.onToggleAmbientLighting : null,
      canControl: widget.canControl,
      isLive: widget.isLive,
      subtitlesVisible: _subtitlesVisible,
      showQueueButton: playbackState.isQueueActive && widget.canNavigateMediaItems,
      onQueueItemSelected: playbackState.isQueueActive && widget.canNavigateMediaItems ? _onQueueItemSelected : null,
      ratingKey: widget.metadata.id,
      mediaTitle: widget.metadata.title,
      onSubtitleDownloaded: _onSubtitleDownloaded,
      // Plex proxies OpenSubtitles via its server-side plugin; Jellyfin
      // doesn't expose an equivalent so the Search Subtitles tile is hidden
      // for Jellyfin items. The check uses the registered client type for
      // this metadata's serverId.
      subtitleSearchSupported: _isPlexBackedMetadata(),
    );
  }

  /// True when the active server supports external subtitle search (Plex
  /// today). Requires a server id because the download callback needs the
  /// Plex client/token for that server.
  bool _isPlexBackedMetadata() {
    try {
      final serverId = widget.metadata.serverId;
      if (serverId == null) return false;
      final manager = context.read<MultiServerProvider>().serverManager;
      final c = manager.getClient(ServerId(serverId));
      return c?.capabilities.externalSubtitleSearch ?? false;
    } catch (_) {
      return false;
    }
  }

  Widget _buildTrackChapterControlsWidget({bool hideChaptersAndQueue = false}) {
    final playbackState = context.watch<PlaybackStateProvider>();
    final trackControlsState = _buildTrackControlsState(
      playbackState: playbackState,
      onToggleAlwaysOnTop: _toggleAlwaysOnTop,
    );

    return TrackChapterControls(
      player: widget.player,
      chapters: _chapters,
      chaptersLoaded: _chaptersLoaded,
      trackControlsState: trackControlsState,
      onSeekRequested: widget.onSeekRequested,
      onSeekCompleted: widget.onSeekCompleted,
      hideChaptersAndQueue: hideChaptersAndQueue,
    );
  }
}
