// Copyright 2020 Ben Hills and the project contributors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import 'package:pinepods_mobile/bloc/podcast/audio_bloc.dart';
import 'package:pinepods_mobile/bloc/podcast/episode_bloc.dart';
import 'package:pinepods_mobile/bloc/podcast/podcast_bloc.dart';
import 'package:pinepods_mobile/bloc/podcast/queue_bloc.dart';
import 'package:pinepods_mobile/bloc/settings/settings_bloc.dart';
import 'package:pinepods_mobile/entities/downloadable.dart';
import 'package:pinepods_mobile/entities/episode.dart';
import 'package:pinepods_mobile/l10n/L.dart';
import 'package:pinepods_mobile/services/audio/audio_player_service.dart';
import 'package:pinepods_mobile/state/queue_event_state.dart';
import 'package:pinepods_mobile/ui/podcast/episode_details.dart';
import 'package:pinepods_mobile/ui/podcast/transport_controls.dart';
import 'package:pinepods_mobile/ui/widgets/action_text.dart';
import 'package:pinepods_mobile/ui/widgets/tile_image.dart';
import 'package:pinepods_mobile/ui/utils/player_utils.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_dialogs/flutter_dialogs.dart';
import 'package:intl/intl.dart' show DateFormat;
import 'package:provider/provider.dart';
import 'package:rxdart/rxdart.dart';

/// This class builds a tile for each episode in the podcast feed.
class EpisodeTile extends StatelessWidget {
  final Episode episode;
  final bool download;
  final bool play;
  final bool playing;
  final bool queued;

  const EpisodeTile({
    super.key,
    required this.episode,
    required this.download,
    required this.play,
    this.playing = false,
    this.queued = false,
  });

  @override
  Widget build(BuildContext context) {
    final mediaQueryData = MediaQuery.of(context);

    if (mediaQueryData.accessibleNavigation) {
      if (defaultTargetPlatform == TargetPlatform.iOS) {
        return _CupertinoAccessibleEpisodeTile(
          episode: episode,
          download: download,
          play: play,
          playing: playing,
          queued: queued,
        );
      } else {
        return _AccessibleEpisodeTile(
          episode: episode,
          download: download,
          play: play,
          playing: playing,
          queued: queued,
        );
      }
    } else {
      return ExpandableEpisodeTile(
        episode: episode,
        download: download,
        play: play,
        playing: playing,
        queued: queued,
      );
    }
  }
}

/// An EpisodeTitle is built with an [ExpansionTile] widget and displays the episode's
/// basic details, thumbnail and play button.
///
/// It can then be expanded to present addition information about the episode and further
/// controls.
///
/// TODO: Replace [Opacity] with [Container] with a transparent colour.
class ExpandableEpisodeTile extends StatefulWidget {
  final Episode episode;
  final bool download;
  final bool play;
  final bool playing;
  final bool queued;

  const ExpandableEpisodeTile({
    super.key,
    required this.episode,
    required this.download,
    required this.play,
    this.playing = false,
    this.queued = false,
  });

  @override
  State<ExpandableEpisodeTile> createState() => _ExpandableEpisodeTileState();
}

class _ExpandableEpisodeTileState extends State<ExpandableEpisodeTile> {
  bool expanded = false;

  @override
  Widget build(BuildContext context) {
    final theme = Theme.of(context);
    final textTheme = Theme.of(context).textTheme;
    final episodeBloc = Provider.of<EpisodeBloc>(context);
    final queueBloc = Provider.of<QueueBloc>(context);

    return ExpansionTile(
      tilePadding: const EdgeInsets.fromLTRB(16.0, 0.0, 8.0, 0.0),
      key: Key('PT${widget.episode.guid}'),
      onExpansionChanged: (isExpanded) {
        setState(() {
          expanded = isExpanded;
        });
      },
      trailing: Opacity(
        opacity: widget.episode.played ? 0.5 : 1.0,
        child: EpisodeTransportControls(
          episode: widget.episode,
          download: widget.download,
          play: widget.play,
        ),
      ),
      leading: ExcludeSemantics(
        child: Stack(
          alignment: Alignment.bottomLeft,
          fit: StackFit.passthrough,
          children: <Widget>[
            Opacity(
              opacity: widget.episode.played ? 0.5 : 1.0,
              child: TileImage(
                url: widget.episode.thumbImageUrl ?? widget.episode.imageUrl!,
                size: 56.0,
                highlight: widget.episode.highlight,
              ),
            ),
            SizedBox(
              height: 5.0,
              width: 56.0 * (widget.episode.percentagePlayed / 100),
              child: Container(
                color: Theme.of(context).primaryColor,
              ),
            ),
          ],
        ),
      ),
      subtitle: Opacity(
        opacity: widget.episode.played ? 0.5 : 1.0,
        child: EpisodeSubtitle(widget.episode),
      ),
      title: Opacity(
        opacity: widget.episode.played ? 0.5 : 1.0,
        child: Text(
          widget.episode.title!,
          overflow: TextOverflow.ellipsis,
          maxLines: 2,
          softWrap: false,
          style: textTheme.bodyMedium,
        ),
      ),
      children: <Widget>[
        Align(
          alignment: Alignment.centerLeft,
          child: Padding(
            padding: const EdgeInsets.symmetric(
              horizontal: 16.0,
              vertical: 4.0,
            ),
            child: Text(
              widget.episode.descriptionText!,
              overflow: TextOverflow.ellipsis,
              softWrap: false,
              maxLines: 5,
              style: Theme.of(context).textTheme.bodyLarge!.copyWith(
                    fontSize: 14,
                    fontWeight: FontWeight.normal,
                  ),
            ),
          ),
        ),
        Padding(
          padding: const EdgeInsets.fromLTRB(0.0, 4.0, 0.0, 8.0),
          child: Row(
            mainAxisAlignment: MainAxisAlignment.center,
            crossAxisAlignment: CrossAxisAlignment.start,
            children: <Widget>[
              Expanded(
                child: TextButton(
                  style: TextButton.styleFrom(
                    padding: EdgeInsets.zero,
                    shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(4.0)),
                  ),
                  onPressed: widget.episode.downloaded
                      ? () {
                          showPlatformDialog<void>(
                            context: context,
                            useRootNavigator: false,
                            builder: (_) => BasicDialogAlert(
                              title: Text(
                                L.of(context)!.delete_episode_title,
                              ),
                              content: Text(L.of(context)!.delete_episode_confirmation),
                              actions: <Widget>[
                                BasicDialogAction(
                                  title: ActionText(
                                    L.of(context)!.cancel_button_label,
                                  ),
                                  onPressed: () {
                                    Navigator.pop(context);
                                  },
                                ),
                                BasicDialogAction(
                                  title: ActionText(
                                    L.of(context)!.delete_button_label,
                                  ),
                                  iosIsDefaultAction: true,
                                  iosIsDestructiveAction: true,
                                  onPressed: () {
                                    episodeBloc.deleteDownload(widget.episode);
                                    Navigator.pop(context);
                                  },
                                ),
                              ],
                            ),
                          );
                        }
                      : null,
                  child: Column(
                    children: <Widget>[
                      Icon(
                        Icons.delete_outline,
                        semanticLabel: L.of(context)!.delete_episode_button_label,
                        size: 22,
                      ),
                      const Padding(
                        padding: EdgeInsets.symmetric(vertical: 2.0),
                      ),
                      ExcludeSemantics(
                        child: Text(
                          L.of(context)!.delete_label,
                          textAlign: TextAlign.center,
                          style: const TextStyle(
                            fontWeight: FontWeight.normal,
                          ),
                        ),
                      ),
                    ],
                  ),
                ),
              ),
              Expanded(
                child: TextButton(
                  style: TextButton.styleFrom(
                    padding: EdgeInsets.zero,
                    shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(0.0)),
                  ),
                  onPressed: widget.playing
                      ? null
                      : () {
                          if (widget.queued) {
                            queueBloc.queueEvent(QueueRemoveEvent(episode: widget.episode));
                          } else {
                            queueBloc.queueEvent(QueueAddEvent(episode: widget.episode));
                          }
                        },
                  child: Column(
                    children: <Widget>[
                      Icon(
                        widget.queued ? Icons.playlist_add_check_outlined : Icons.playlist_add_outlined,
                        semanticLabel: widget.queued
                            ? L.of(context)!.semantics_remove_from_queue
                            : L.of(context)!.semantics_add_to_queue,
                        size: 22,
                      ),
                      const Padding(
                        padding: EdgeInsets.symmetric(vertical: 2.0),
                      ),
                      ExcludeSemantics(
                        child: Text(
                          widget.queued ? 'Remove' : 'Add',
                          textAlign: TextAlign.center,
                          style: const TextStyle(
                            fontWeight: FontWeight.normal,
                          ),
                        ),
                      ),
                    ],
                  ),
                ),
              ),
              Expanded(
                child: TextButton(
                  style: TextButton.styleFrom(
                    padding: EdgeInsets.zero,
                    shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(0.0)),
                  ),
                  onPressed: () {
                    episodeBloc.togglePlayed(widget.episode);
                  },
                  child: Column(
                    mainAxisAlignment: MainAxisAlignment.center,
                    crossAxisAlignment: CrossAxisAlignment.center,
                    children: <Widget>[
                      Icon(
                        widget.episode.played ? Icons.unpublished_outlined : Icons.check_circle_outline,
                        size: 22,
                      ),
                      const Padding(
                        padding: EdgeInsets.symmetric(vertical: 2.0),
                      ),
                      Text(
                        widget.episode.played ? L.of(context)!.mark_unplayed_label : L.of(context)!.mark_played_label,
                        textAlign: TextAlign.center,
                        style: const TextStyle(
                          fontWeight: FontWeight.normal,
                        ),
                      ),
                    ],
                  ),
                ),
              ),
              Expanded(
                child: TextButton(
                  style: TextButton.styleFrom(
                    padding: EdgeInsets.zero,
                    shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(0.0)),
                  ),
                  onPressed: () {
                    showModalBottomSheet<void>(
                        barrierLabel: L.of(context)!.scrim_episode_details_selector,
                        context: context,
                        backgroundColor: theme.bottomAppBarTheme.color,
                        isScrollControlled: true,
                        shape: const RoundedRectangleBorder(
                          borderRadius: BorderRadius.only(
                            topLeft: Radius.circular(10.0),
                            topRight: Radius.circular(10.0),
                          ),
                        ),
                        builder: (context) {
                          return EpisodeDetails(
                            episode: widget.episode,
                          );
                        });
                  },
                  child: Column(
                    mainAxisAlignment: MainAxisAlignment.center,
                    crossAxisAlignment: CrossAxisAlignment.center,
                    children: <Widget>[
                      const Icon(
                        Icons.unfold_more_outlined,
                        size: 22,
                      ),
                      const Padding(
                        padding: EdgeInsets.symmetric(vertical: 2.0),
                      ),
                      Text(
                        L.of(context)!.more_label,
                        textAlign: TextAlign.center,
                        style: const TextStyle(
                          fontWeight: FontWeight.normal,
                        ),
                      ),
                    ],
                  ),
                ),
              ),
            ],
          ),
        ),
      ],
    );
  }
}

/// This is an accessible version of the episode tile that uses Apple theming.
/// When the tile is tapped, an iOS menu will appear with the relevant options.
class _CupertinoAccessibleEpisodeTile extends StatefulWidget {
  final Episode episode;
  final bool download;
  final bool play;
  final bool playing;
  final bool queued;

  const _CupertinoAccessibleEpisodeTile({
    required this.episode,
    required this.download,
    required this.play,
    this.playing = false,
    this.queued = false,
  });

  @override
  State<_CupertinoAccessibleEpisodeTile> createState() => _CupertinoAccessibleEpisodeTileState();
}

class _CupertinoAccessibleEpisodeTileState extends State<_CupertinoAccessibleEpisodeTile> {
  bool expanded = false;

  @override
  Widget build(BuildContext context) {
    final theme = Theme.of(context);
    final textTheme = Theme.of(context).textTheme;
    final audioBloc = Provider.of<AudioBloc>(context, listen: false);
    final episodeBloc = Provider.of<EpisodeBloc>(context);
    final podcastBloc = Provider.of<PodcastBloc>(context);
    final queueBloc = Provider.of<QueueBloc>(context);

    return StreamBuilder<_PlayerControlState>(
        stream: Rx.combineLatest2(audioBloc.playingState!, audioBloc.nowPlaying!,
            (AudioState audioState, Episode? episode) => _PlayerControlState(audioState, episode)),
        builder: (context, snapshot) {
          if (!snapshot.hasData) {
            return Container();
          }

          final audioState = snapshot.data!.audioState;
          final nowPlaying = snapshot.data!.episode;
          final currentlyPlaying = nowPlaying?.guid == widget.episode.guid && audioState == AudioState.playing;
          final currentlyPaused = nowPlaying?.guid == widget.episode.guid && audioState == AudioState.pausing;

          return Semantics(
            button: true,
            child: ListTile(
              key: Key('PT${widget.episode.guid}'),
              leading: ExcludeSemantics(
                child: Stack(
                  alignment: Alignment.bottomLeft,
                  fit: StackFit.passthrough,
                  children: <Widget>[
                    Opacity(
                      opacity: widget.episode.played ? 0.5 : 1.0,
                      child: TileImage(
                        url: widget.episode.thumbImageUrl ?? widget.episode.imageUrl!,
                        size: 56.0,
                        highlight: widget.episode.highlight,
                      ),
                    ),
                    SizedBox(
                      height: 5.0,
                      width: 56.0 * (widget.episode.percentagePlayed / 100),
                      child: Container(
                        color: Theme.of(context).primaryColor,
                      ),
                    ),
                  ],
                ),
              ),
              subtitle: Opacity(
                opacity: widget.episode.played ? 0.5 : 1.0,
                child: EpisodeSubtitle(widget.episode),
              ),
              title: Opacity(
                opacity: widget.episode.played ? 0.5 : 1.0,
                child: Text(
                  widget.episode.title!,
                  overflow: TextOverflow.ellipsis,
                  maxLines: 2,
                  softWrap: false,
                  style: textTheme.bodyMedium,
                ),
              ),
              onTap: () {
                showCupertinoModalPopup<void>(
                  context: context,
                  builder: (BuildContext context) {
                    return CupertinoActionSheet(
                      actions: <Widget>[
                        if (currentlyPlaying)
                          CupertinoActionSheetAction(
                            isDefaultAction: true,
                            onPressed: () {
                              audioBloc.transitionState(TransitionState.pause);
                              Navigator.pop(context, 'Cancel');
                            },
                            child: Text(L.of(context)!.pause_button_label),
                          ),
                        if (currentlyPaused)
                          CupertinoActionSheetAction(
                            isDefaultAction: true,
                            onPressed: () {
                              audioBloc.transitionState(TransitionState.play);
                              Navigator.pop(context, 'Cancel');
                            },
                            child: Text(L.of(context)!.resume_button_label),
                          ),
                        if (!currentlyPlaying && !currentlyPaused)
                          CupertinoActionSheetAction(
                            isDefaultAction: true,
                            onPressed: () {
                              final settings = Provider.of<SettingsBloc>(context, listen: false).currentSettings;
                              audioBloc.play(widget.episode);
                              optionalShowNowPlaying(context, settings);
                              Navigator.pop(context, 'Cancel');
                            },
                            child: widget.episode.downloaded
                                ? Text(L.of(context)!.play_download_button_label)
                                : Text(L.of(context)!.play_button_label),
                          ),
                        if (widget.episode.downloadState == DownloadState.queued ||
                            widget.episode.downloadState == DownloadState.downloading)
                          CupertinoActionSheetAction(
                            isDefaultAction: false,
                            onPressed: () {
                              episodeBloc.deleteDownload(widget.episode);
                              Navigator.pop(context, 'Cancel');
                            },
                            child: Text(L.of(context)!.cancel_download_button_label),
                          ),
                        if (widget.episode.downloadState != DownloadState.downloading)
                          CupertinoActionSheetAction(
                            isDefaultAction: false,
                            onPressed: () {
                              if (widget.episode.downloaded) {
                                episodeBloc.deleteDownload(widget.episode);
                                Navigator.pop(context, 'Cancel');
                              } else {
                                podcastBloc.downloadEpisode(widget.episode);
                                Navigator.pop(context, 'Cancel');
                              }
                            },
                            child: widget.episode.downloaded
                                ? Text(L.of(context)!.delete_episode_button_label)
                                : Text(L.of(context)!.download_episode_button_label),
                          ),
                        if (!currentlyPlaying && !widget.queued)
                          CupertinoActionSheetAction(
                            isDefaultAction: false,
                            onPressed: () {
                              queueBloc.queueEvent(QueueAddEvent(episode: widget.episode));
                              Navigator.pop(context, 'Cancel');
                            },
                            child: Text(L.of(context)!.semantics_add_to_queue),
                          ),
                        if (!currentlyPlaying && widget.queued)
                          CupertinoActionSheetAction(
                            isDefaultAction: false,
                            onPressed: () {
                              queueBloc.queueEvent(QueueRemoveEvent(episode: widget.episode));
                              Navigator.pop(context, 'Cancel');
                            },
                            child: Text(L.of(context)!.semantics_remove_from_queue),
                          ),
                        if (widget.episode.played)
                          CupertinoActionSheetAction(
                            isDefaultAction: false,
                            onPressed: () {
                              episodeBloc.togglePlayed(widget.episode);
                              Navigator.pop(context, 'Cancel');
                            },
                            child: Text(L.of(context)!.semantics_mark_episode_unplayed),
                          ),
                        if (!widget.episode.played)
                          CupertinoActionSheetAction(
                            isDefaultAction: false,
                            onPressed: () {
                              episodeBloc.togglePlayed(widget.episode);
                              Navigator.pop(context, 'Cancel');
                            },
                            child: Text(L.of(context)!.semantics_mark_episode_played),
                          ),
                        CupertinoActionSheetAction(
                          isDefaultAction: false,
                          onPressed: () {
                            Navigator.pop(context, 'Cancel');
                            showModalBottomSheet<void>(
                                context: context,
                                barrierLabel: L.of(context)!.scrim_episode_details_selector,
                                backgroundColor: theme.bottomAppBarTheme.color,
                                isScrollControlled: true,
                                shape: const RoundedRectangleBorder(
                                  borderRadius: BorderRadius.only(
                                    topLeft: Radius.circular(10.0),
                                    topRight: Radius.circular(10.0),
                                  ),
                                ),
                                builder: (context) {
                                  return EpisodeDetails(
                                    episode: widget.episode,
                                  );
                                });
                          },
                          child: Text(L.of(context)!.episode_details_button_label),
                        ),
                      ],
                      cancelButton: CupertinoActionSheetAction(
                        isDefaultAction: false,
                        onPressed: () {
                          Navigator.pop(context, 'Close');
                        },
                        child: Text(L.of(context)!.close_button_label),
                      ),
                    );
                  },
                );
              },
            ),
          );
        });
  }
}

/// This is an accessible version of the episode tile that uses Android theming.
/// When the tile is tapped, an Android dialog menu will appear with the relevant
/// options.
class _AccessibleEpisodeTile extends StatefulWidget {
  final Episode episode;
  final bool download;
  final bool play;
  final bool playing;
  final bool queued;

  const _AccessibleEpisodeTile({
    required this.episode,
    required this.download,
    required this.play,
    this.playing = false,
    this.queued = false,
  });

  @override
  State<_AccessibleEpisodeTile> createState() => _AccessibleEpisodeTileState();
}

class _AccessibleEpisodeTileState extends State<_AccessibleEpisodeTile> {
  bool expanded = false;

  @override
  Widget build(BuildContext context) {
    final theme = Theme.of(context);
    final textTheme = Theme.of(context).textTheme;
    final audioBloc = Provider.of<AudioBloc>(context, listen: false);
    final episodeBloc = Provider.of<EpisodeBloc>(context);
    final podcastBloc = Provider.of<PodcastBloc>(context);
    final queueBloc = Provider.of<QueueBloc>(context);

    return StreamBuilder<_PlayerControlState>(
        stream: Rx.combineLatest2(audioBloc.playingState!, audioBloc.nowPlaying!,
            (AudioState audioState, Episode? episode) => _PlayerControlState(audioState, episode)),
        builder: (context, snapshot) {
          if (!snapshot.hasData) {
            return Container();
          }

          final audioState = snapshot.data!.audioState;
          final nowPlaying = snapshot.data!.episode;
          final currentlyPlaying = nowPlaying?.guid == widget.episode.guid && audioState == AudioState.playing;
          final currentlyPaused = nowPlaying?.guid == widget.episode.guid && audioState == AudioState.pausing;

          return ListTile(
            key: Key('PT${widget.episode.guid}'),
            onTap: () {
              showDialog(
                context: context,
                builder: (BuildContext context) {
                  return Semantics(
                    header: true,
                    child: SimpleDialog(
                      //TODO: Fix this - should not be hardcoded text
                      title: const Text('Episode Actions'),
                      children: <Widget>[
                        if (currentlyPlaying)
                          SimpleDialogOption(
                            onPressed: () {
                              audioBloc.transitionState(TransitionState.pause);
                              Navigator.pop(context, '');
                            },
                            padding: const EdgeInsets.symmetric(vertical: 16.0, horizontal: 24.0),
                            child: Text(L.of(context)!.pause_button_label),
                          ),
                        if (currentlyPaused)
                          SimpleDialogOption(
                            onPressed: () {
                              audioBloc.transitionState(TransitionState.play);
                              Navigator.pop(context, '');
                            },
                            padding: const EdgeInsets.symmetric(vertical: 16.0, horizontal: 24.0),
                            child: Text(L.of(context)!.resume_button_label),
                          ),
                        if (!currentlyPlaying && !currentlyPaused && widget.episode.downloaded)
                          SimpleDialogOption(
                            onPressed: () {
                              final settings = Provider.of<SettingsBloc>(context, listen: false).currentSettings;
                              audioBloc.play(widget.episode);
                              optionalShowNowPlaying(context, settings);
                              Navigator.pop(context, '');
                            },
                            padding: const EdgeInsets.symmetric(vertical: 16.0, horizontal: 24.0),
                            child: Text(L.of(context)!.play_download_button_label),
                          ),
                        if (!currentlyPlaying && !currentlyPaused && !widget.episode.downloaded)
                          SimpleDialogOption(
                            onPressed: () {
                              final settings = Provider.of<SettingsBloc>(context, listen: false).currentSettings;
                              audioBloc.play(widget.episode);
                              optionalShowNowPlaying(context, settings);
                              Navigator.pop(context, '');
                            },
                            padding: const EdgeInsets.symmetric(vertical: 16.0, horizontal: 24.0),
                            child: Text(L.of(context)!.play_button_label),
                          ),
                        if (widget.episode.downloadState == DownloadState.queued ||
                            widget.episode.downloadState == DownloadState.downloading)
                          SimpleDialogOption(
                            onPressed: () {
                              episodeBloc.deleteDownload(widget.episode);
                              Navigator.pop(context, '');
                            },
                            padding: const EdgeInsets.symmetric(vertical: 16.0, horizontal: 24.0),
                            child: Text(L.of(context)!.cancel_download_button_label),
                          ),
                        if (widget.episode.downloadState != DownloadState.downloading && widget.episode.downloaded)
                          SimpleDialogOption(
                            onPressed: () {
                              episodeBloc.deleteDownload(widget.episode);
                              Navigator.pop(context, '');
                            },
                            padding: const EdgeInsets.symmetric(vertical: 16.0, horizontal: 24.0),
                            child: Text(L.of(context)!.delete_episode_button_label),
                          ),
                        if (widget.episode.downloadState != DownloadState.downloading && !widget.episode.downloaded)
                          SimpleDialogOption(
                            onPressed: () {
                              podcastBloc.downloadEpisode(widget.episode);
                              Navigator.pop(context, '');
                            },
                            padding: const EdgeInsets.symmetric(vertical: 16.0, horizontal: 24.0),
                            child: Text(L.of(context)!.download_episode_button_label),
                          ),
                        if (!currentlyPlaying && !widget.queued)
                          SimpleDialogOption(
                            onPressed: () {
                              queueBloc.queueEvent(QueueAddEvent(episode: widget.episode));
                              Navigator.pop(context, '');
                            },
                            padding: const EdgeInsets.symmetric(vertical: 16.0, horizontal: 24.0),
                            child: Text(L.of(context)!.semantics_add_to_queue),
                          ),
                        if (!currentlyPlaying && widget.queued)
                          SimpleDialogOption(
                            onPressed: () {
                              queueBloc.queueEvent(QueueRemoveEvent(episode: widget.episode));
                              Navigator.pop(context, '');
                            },
                            padding: const EdgeInsets.symmetric(vertical: 16.0, horizontal: 24.0),
                            child: Text(L.of(context)!.semantics_remove_from_queue),
                          ),
                        if (widget.episode.played)
                          SimpleDialogOption(
                            onPressed: () {
                              episodeBloc.togglePlayed(widget.episode);
                              Navigator.pop(context, '');
                            },
                            padding: const EdgeInsets.symmetric(vertical: 16.0, horizontal: 24.0),
                            child: Text(L.of(context)!.semantics_mark_episode_unplayed),
                          ),
                        if (!widget.episode.played)
                          SimpleDialogOption(
                            onPressed: () {
                              episodeBloc.togglePlayed(widget.episode);
                              Navigator.pop(context, '');
                            },
                            padding: const EdgeInsets.symmetric(vertical: 16.0, horizontal: 24.0),
                            child: Text(L.of(context)!.semantics_mark_episode_played),
                          ),
                        SimpleDialogOption(
                          onPressed: () {
                            Navigator.pop(context, '');
                            showModalBottomSheet<void>(
                                context: context,
                                barrierLabel: L.of(context)!.scrim_episode_details_selector,
                                backgroundColor: theme.bottomAppBarTheme.color,
                                isScrollControlled: true,
                                shape: const RoundedRectangleBorder(
                                  borderRadius: BorderRadius.only(
                                    topLeft: Radius.circular(10.0),
                                    topRight: Radius.circular(10.0),
                                  ),
                                ),
                                builder: (context) {
                                  return EpisodeDetails(
                                    episode: widget.episode,
                                  );
                                });
                          },
                          padding: const EdgeInsets.symmetric(vertical: 16.0, horizontal: 24.0),
                          child: Text(L.of(context)!.episode_details_button_label),
                        ),
                        SimpleDialogOption(
                          padding: const EdgeInsets.symmetric(vertical: 16.0, horizontal: 24.0),
                          // child: Text(L.of(context)!.close_button_label),
                          child: Align(
                            alignment: Alignment.centerRight,
                            child: TextButton(
                              child: ActionText(L.of(context)!.close_button_label),
                              onPressed: () {
                                Navigator.pop(context, '');
                              },
                            ),
                          ),
                        ),
                      ],
                    ),
                  );
                },
              );
            },
            leading: ExcludeSemantics(
              child: Stack(
                alignment: Alignment.bottomLeft,
                fit: StackFit.passthrough,
                children: <Widget>[
                  Opacity(
                    opacity: widget.episode.played ? 0.5 : 1.0,
                    child: TileImage(
                      url: widget.episode.thumbImageUrl ?? widget.episode.imageUrl!,
                      size: 56.0,
                      highlight: widget.episode.highlight,
                    ),
                  ),
                  SizedBox(
                    height: 5.0,
                    width: 56.0 * (widget.episode.percentagePlayed / 100),
                    child: Container(
                      color: Theme.of(context).primaryColor,
                    ),
                  ),
                ],
              ),
            ),
            subtitle: Opacity(
              opacity: widget.episode.played ? 0.5 : 1.0,
              child: EpisodeSubtitle(widget.episode),
            ),
            title: Opacity(
              opacity: widget.episode.played ? 0.5 : 1.0,
              child: Text(
                widget.episode.title!,
                overflow: TextOverflow.ellipsis,
                maxLines: 2,
                softWrap: false,
                style: textTheme.bodyMedium,
              ),
            ),
          );
        });
  }
}

class EpisodeTransportControls extends StatelessWidget {
  final Episode episode;
  final bool download;
  final bool play;

  const EpisodeTransportControls({
    super.key,
    required this.episode,
    required this.download,
    required this.play,
  });

  @override
  Widget build(BuildContext context) {
    final buttons = <Widget>[];

    if (download) {
      buttons.add(Semantics(
        container: true,
        child: DownloadControl(
          episode: episode,
        ),
      ));
    }

    if (play) {
      buttons.add(Semantics(
        container: true,
        child: PlayControl(
          episode: episode,
        ),
      ));
    }

    return SizedBox(
      width: (buttons.length * 48.0),
      child: Row(
        children: <Widget>[...buttons],
      ),
    );
  }
}

class EpisodeSubtitle extends StatelessWidget {
  final Episode episode;
  final String date;
  final Duration length;

  EpisodeSubtitle(this.episode, {super.key})
      : date = episode.publicationDate == null
            ? ''
            : DateFormat(episode.publicationDate!.year == DateTime.now().year ? 'd MMM' : 'd MMM yyyy')
                .format(episode.publicationDate!),
        length = Duration(seconds: episode.duration);

  @override
  Widget build(BuildContext context) {
    final textTheme = Theme.of(context).textTheme;
    var timeRemaining = episode.timeRemaining;

    String title;

    if (length.inSeconds > 0) {
      if (length.inSeconds < 60) {
        title = '$date • ${length.inSeconds} sec';
      } else {
        title = '$date • ${length.inMinutes} min';
      }
    } else {
      title = date;
    }

    if (timeRemaining.inSeconds > 0) {
      if (timeRemaining.inSeconds < 60) {
        title = '$title / ${timeRemaining.inSeconds} sec left';
      } else {
        title = '$title / ${timeRemaining.inMinutes} min left';
      }
    }

    return Padding(
      padding: const EdgeInsets.only(top: 4.0),
      child: Text(
        title,
        overflow: TextOverflow.ellipsis,
        softWrap: false,
        style: textTheme.bodySmall,
      ),
    );
  }
}

/// This class acts as a wrapper between the current audio state and
/// downloadables. Saves all that nesting of StreamBuilders.
class _PlayerControlState {
  final AudioState audioState;
  final Episode? episode;

  _PlayerControlState(this.audioState, this.episode);
}
