use std::cell::RefCell;
use std::path::Path;
use std::rc::Rc;

use cosimo::{
    Datum, DatumKey, ScheduledSelectionKind, SchedulerConfig, schedule_datum_for_key,
    schedule_session_for_keys,
};
use wxdragon::prelude::*;

use crate::CosimoUi;
use crate::app_config::SoundNotificationPolicy;
use crate::app_state::AppState;
use crate::keyboard::is_ready_screen_refresh_key;
use crate::localization::Localizer;
use crate::menu_ui::set_menu_item_label;
use crate::notifications::{due_notification_kind, play_due_notification};
use crate::ready_list::{
    ReadyCardSort, ReadyCardSortDirection, ReadyStudyItemFilterState,
    ready_study_item_filter_counts, ready_study_item_labels_for_indices,
};
use crate::resident_tray::{ResidentTray, resident_tray_visible};
use crate::review_audio::{ReviewAudioSide, current_review_audio_file};
use crate::review_session::ReviewPhase;
use crate::review_ui::{MenuAvailability, ReadyScreenFocus, ReviewUi, set_ready_filter_choices};
use crate::runtime::current_time_and_seed;
use crate::time_format::format_relative_duration_rounded;

const READY_REFRESH_INTERVAL_MS: i32 = 60_000;

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum ReadyRefreshKeyDisposition {
    Refresh,
    Skip,
}

pub(crate) fn ready_to_start_summary_text(
    localizer: Localizer,
    total_cards: usize,
    review_db_path: &Path,
    minimum_cards_per_session: usize,
    scheduled_cards: Option<usize>,
    due_cards: Option<usize>,
) -> String {
    localizer.ready_to_start_summary_text(
        total_cards,
        &review_db_path.display().to_string(),
        minimum_cards_per_session,
        scheduled_cards,
        due_cards,
    )
}

pub(crate) fn append_status_warning(status: String, warning: Option<String>) -> String {
    match warning {
        Some(warning) => format!("{status} {warning}"),
        None => status,
    }
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum EditCardMenuAction {
    SelectedReadyCard,
    DisabledDuringPrompt,
    RevealedReviewCard,
}

impl EditCardMenuAction {
    pub(crate) fn for_phase(phase: ReviewPhase) -> Self {
        match phase {
            ReviewPhase::ReadyToStart => Self::SelectedReadyCard,
            ReviewPhase::Prompt => Self::DisabledDuringPrompt,
            ReviewPhase::Response { .. } => Self::RevealedReviewCard,
        }
    }

    pub(crate) fn label(self, localizer: Localizer) -> &'static str {
        match self {
            Self::SelectedReadyCard => localizer.edit_selected_card_menu_item(),
            Self::DisabledDuringPrompt | Self::RevealedReviewCard => {
                localizer.edit_current_card_menu_item()
            }
        }
    }

    pub(crate) fn is_enabled(self, has_selected_card: bool) -> bool {
        match self {
            Self::SelectedReadyCard => has_selected_card,
            Self::DisabledDuringPrompt => false,
            Self::RevealedReviewCard => true,
        }
    }
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) struct ReadySchedulePreview {
    pub(crate) scheduled_cards: usize,
    pub(crate) due_cards: usize,
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) enum ReadyScheduleStatus {
    NotReadyPhase,
    Available(ReadySchedulePreview),
    Unavailable(String),
}

impl ReadyScheduleStatus {
    fn for_phase(state: &AppState, phase: ReviewPhase) -> Self {
        if !matches!(phase, ReviewPhase::ReadyToStart) {
            return Self::NotReadyPhase;
        }

        ready_phase_schedule_status(state)
    }

    pub(crate) fn preview(&self) -> Option<ReadySchedulePreview> {
        match self {
            Self::Available(preview) => Some(*preview),
            Self::NotReadyPhase | Self::Unavailable(_) => None,
        }
    }

    pub(crate) fn warning(&self) -> Option<&str> {
        match self {
            Self::Unavailable(warning) => Some(warning),
            Self::NotReadyPhase | Self::Available(_) => None,
        }
    }
}

pub(crate) fn ready_phase_schedule_status(state: &AppState) -> ReadyScheduleStatus {
    match schedule_preview_if_started_now(state) {
        Ok(preview) => ReadyScheduleStatus::Available(preview),
        Err(error) => ReadyScheduleStatus::Unavailable(
            state
                .localizer
                .could_not_preview_scheduled_cards_status(&error),
        ),
    }
}

pub(crate) fn schedule_preview_if_started_now(
    state: &AppState,
) -> Result<ReadySchedulePreview, String> {
    let (now_unix_seconds, _) = current_time_and_seed()?;
    schedule_preview_at(state, now_unix_seconds, 0)
}

pub(crate) fn schedule_preview_at(
    state: &AppState,
    now_unix_seconds: i64,
    filler_seed: u64,
) -> Result<ReadySchedulePreview, String> {
    if state.data.is_empty() {
        return Ok(ReadySchedulePreview {
            scheduled_cards: 0,
            due_cards: 0,
        });
    }

    let config = SchedulerConfig::new(
        state.settings.minimum_cards_per_session,
        now_unix_seconds,
        filler_seed,
    );
    let items = state
        .study_items()
        .map_err(|error| state.localizer.could_not_build_review_queue(&error))?;
    let schedules = state
        .load_study_item_schedules(&items)
        .map_err(|error| state.localizer.could_not_build_review_queue(&error))?;
    let suspended = state
        .load_suspended_study_item_keys(&items)
        .map_err(|error| state.localizer.could_not_build_review_queue(&error))?;
    let deferred = state
        .load_deferred_study_item_keys_at(&items, now_unix_seconds)
        .map_err(|error| state.localizer.could_not_build_review_queue(&error))?;
    let active_data: Vec<(DatumKey, &Datum)> = items
        .iter()
        .filter(|item| {
            !suspended.contains(item.datum_key()) && !deferred.contains(item.datum_key())
        })
        .map(|item| (item.datum_key().clone(), item.datum()))
        .collect();
    let scheduled_cards = schedule_session_for_keys(&active_data, &schedules, config);
    let due_cards = scheduled_cards
        .iter()
        .filter(|card| card.selection_kind() == ScheduledSelectionKind::Due)
        .count();
    Ok(ReadySchedulePreview {
        scheduled_cards: scheduled_cards.len(),
        due_cards,
    })
}

pub(crate) fn main_status_bar_text(
    state: &AppState,
    now_unix_seconds: i64,
) -> Result<String, String> {
    let items = state
        .study_items()
        .map_err(|error| state.localizer.could_not_build_review_queue(&error))?;
    let total_cards = items.len();
    if total_cards == 0 {
        return Ok(state
            .localizer
            .main_status_bar_no_review_scheduled(total_cards));
    }

    let schedules = state
        .load_study_item_schedules(&items)
        .map_err(|error| state.localizer.could_not_build_review_queue(&error))?;
    let suspended = state
        .load_suspended_study_item_keys(&items)
        .map_err(|error| state.localizer.could_not_build_review_queue(&error))?;
    let deferred = state
        .load_deferred_study_item_keys_at(&items, now_unix_seconds)
        .map_err(|error| state.localizer.could_not_build_review_queue(&error))?;
    let mut due_cards = 0;
    let mut next_due_at: Option<i64> = None;

    for item in &items {
        if suspended.contains(item.datum_key()) || deferred.contains(item.datum_key()) {
            continue;
        }
        let scheduled = schedule_datum_for_key(
            item.datum_key().clone(),
            schedules.get(item.datum_key()),
            now_unix_seconds,
        );
        if scheduled.is_due() {
            due_cards += 1;
        } else {
            next_due_at = Some(match next_due_at {
                Some(existing) => existing.min(scheduled.due_at_unix_seconds()),
                None => scheduled.due_at_unix_seconds(),
            });
        }
    }

    if due_cards > 0 {
        return Ok(state
            .localizer
            .main_status_bar_due_now(total_cards, due_cards));
    }

    match next_due_at {
        Some(next_due_at) => Ok(state.localizer.main_status_bar_next_review(
            total_cards,
            &format_relative_duration_rounded(
                state.localizer,
                next_due_at.saturating_sub(now_unix_seconds),
            ),
        )),
        None => Ok(state
            .localizer
            .main_status_bar_no_review_scheduled(total_cards)),
    }
}

fn sync_ready_cards_list(ui: &ReviewUi, state: &AppState) {
    state.ready_list_sync_in_progress.set(true);
    let visible_indices = state.ready_visible_indices_for_current_filter();
    let items = state
        .study_items()
        .unwrap_or_else(|_| state.fallback_study_items());
    let suspended = state
        .load_suspended_study_item_keys(&items)
        .unwrap_or_default();
    let flagged = state
        .load_flagged_study_item_keys(&items)
        .unwrap_or_default();
    let deferred = state
        .load_deferred_study_item_keys(&items)
        .unwrap_or_default();
    let non_reversible = state.load_reversal_excluded_card_keys().unwrap_or_default();
    let expected_labels = ready_study_item_labels_for_indices(
        &items,
        &visible_indices,
        &suspended,
        &flagged,
        &deferred,
        &non_reversible,
        state.localizer,
    );
    *state.ready_visible_indices.borrow_mut() = visible_indices.clone();

    ui.sync_ready_card_labels(&expected_labels);

    let selected_visible_index = state
        .selected_ready_key
        .as_ref()
        .and_then(|selected_key| {
            items
                .iter()
                .position(|item| item.datum_key() == selected_key)
        })
        .and_then(|selected_index| {
            visible_indices
                .iter()
                .position(|index| *index == selected_index)
        });
    if selected_visible_index != ui.ready_card_selection() {
        ui.set_ready_card_selection(selected_visible_index);
    }
    state.ready_list_sync_in_progress.set(false);
}

fn sync_ready_filter_choices(ui: &ReviewUi, state: &AppState) {
    let counts = state.study_items().ok().and_then(|items| {
        let schedules = state.load_study_item_schedules(&items).ok()?;
        let suspended = state.load_suspended_study_item_keys(&items).ok()?;
        let flagged = state.load_flagged_study_item_keys(&items).ok()?;
        let deferred = state.load_deferred_study_item_keys(&items).ok()?;
        let non_reversible = state.load_reversal_excluded_card_keys().ok()?;
        let now_unix_seconds = current_time_and_seed()
            .map(|(now_unix_seconds, _)| now_unix_seconds)
            .unwrap_or(0);
        let filter_state = ReadyStudyItemFilterState {
            schedules: &schedules,
            suspended: &suspended,
            flagged: &flagged,
            deferred: &deferred,
            non_reversible: &non_reversible,
            now_unix_seconds,
        };
        Some(ready_study_item_filter_counts(
            &items,
            &state.ready_card_text_filter,
            &filter_state,
        ))
    });
    set_ready_filter_choices(ui.ready_card_filter_choice, state.localizer, counts);
    ui.ready_card_filter_choice
        .set_selection(state.ready_card_filter.selection_index());
}

fn sync_ready_card_text_filter(ui: &ReviewUi, state: &AppState) {
    if ui.ready_card_text_filter.get_value() != state.ready_card_text_filter {
        ui.ready_card_text_filter
            .set_value(&state.ready_card_text_filter);
    }
}

fn sync_ready_sort_menu(ui: &ReviewUi, state: &AppState) {
    if let Some(menu_bar) = ui.frame.get_menu_bar() {
        menu_bar.check_item(
            CosimoUi::xrc_id("menu_sort_deck_order"),
            state.ready_card_sort == ReadyCardSort::DeckOrder,
        );
        menu_bar.check_item(
            CosimoUi::xrc_id("menu_sort_next_scheduled"),
            state.ready_card_sort == ReadyCardSort::NextScheduledReview,
        );
        menu_bar.check_item(
            CosimoUi::xrc_id("menu_sort_last_reviewed"),
            state.ready_card_sort == ReadyCardSort::LastReviewed,
        );
        menu_bar.check_item(
            CosimoUi::xrc_id("menu_sort_prompt_text"),
            state.ready_card_sort == ReadyCardSort::PromptText,
        );
        menu_bar.check_item(
            CosimoUi::xrc_id("menu_sort_response_text"),
            state.ready_card_sort == ReadyCardSort::ResponseText,
        );
        menu_bar.check_item(
            CosimoUi::xrc_id("menu_sort_estimated_recall"),
            state.ready_card_sort == ReadyCardSort::EstimatedRecall,
        );
        menu_bar.check_item(
            CosimoUi::xrc_id("menu_sort_ascending"),
            state.ready_card_sort_direction == ReadyCardSortDirection::Ascending,
        );
        menu_bar.check_item(
            CosimoUi::xrc_id("menu_sort_descending"),
            state.ready_card_sort_direction == ReadyCardSortDirection::Descending,
        );
    }
}

pub(crate) fn ready_screen_has_cards(state: &AppState) -> bool {
    !state
        .study_items()
        .unwrap_or_else(|_| state.fallback_study_items())
        .is_empty()
}

pub(crate) fn sync_ready_selection_controls(ui: &ReviewUi, state: &AppState) {
    let has_selected_card = selected_ready_item_is_visible(state);
    ui.ready_edit_card_button.enable(has_selected_card);
    ui.ready_remove_card_button.enable(has_selected_card);
    sync_cards_menu_state(ui, state);
}

#[derive(Debug, PartialEq, Eq)]
pub(crate) struct ReadyCardContextMenuLabels {
    pub(crate) edit: &'static str,
    pub(crate) remove: &'static str,
    pub(crate) toggle_suspension: String,
    pub(crate) toggle_flag: &'static str,
    pub(crate) toggle_deferral: &'static str,
    pub(crate) toggle_reversal: Option<&'static str>,
    pub(crate) toggle_reversal_exclusion: Option<&'static str>,
}

pub(crate) fn ready_card_context_menu_labels(
    state: &AppState,
) -> Option<ReadyCardContextMenuLabels> {
    if !state.session.is_ready() || !selected_ready_item_is_visible(state) {
        return None;
    }

    let toggle_suspension = if selected_ready_card_is_suspended(state).unwrap_or(false) {
        state.localizer.unsuspend_selected_card_menu_item()
    } else {
        state.localizer.suspend_selected_card_menu_item()
    };

    Some(ReadyCardContextMenuLabels {
        edit: state.localizer.edit_selected_card_menu_item(),
        remove: state.localizer.remove_selected_card_menu_item(),
        toggle_suspension: format!("{toggle_suspension}\tCtrl-S"),
        toggle_flag: current_or_selected_card_flag_menu_label(state),
        toggle_deferral: selected_ready_card_deferral_label(state),
        toggle_reversal: selected_ready_card_reversal_available(state)
            .then(|| selected_ready_card_reversal_label(state)),
        toggle_reversal_exclusion: selected_ready_card_reversal_exclusion_available(state)
            .then(|| selected_ready_card_reversal_exclusion_label(state)),
    })
}

pub(crate) fn show_ready_card_context_menu(
    ui: &ReviewUi,
    state: &Rc<RefCell<AppState>>,
    position: Option<wxdragon::geometry::Point>,
) {
    let state_ref = state.borrow();
    let Some(labels) = ready_card_context_menu_labels(&state_ref) else {
        return;
    };
    drop(state_ref);

    let mut menu_builder = Menu::builder()
        .append_item(CosimoUi::xrc_id("menu_edit_card"), labels.edit, "")
        .append_item(CosimoUi::xrc_id("menu_remove_card"), labels.remove, "")
        .append_item(
            CosimoUi::xrc_id("menu_toggle_card_suspension"),
            &labels.toggle_suspension,
            "",
        )
        .append_item(
            CosimoUi::xrc_id("menu_toggle_card_flag"),
            labels.toggle_flag,
            "",
        )
        .append_item(
            CosimoUi::xrc_id("menu_toggle_card_deferral"),
            labels.toggle_deferral,
            "",
        );
    if let Some(label) = labels.toggle_reversal {
        menu_builder =
            menu_builder.append_item(CosimoUi::xrc_id("menu_toggle_card_reversal"), label, "");
    }
    if let Some(label) = labels.toggle_reversal_exclusion {
        menu_builder = menu_builder.append_item(
            CosimoUi::xrc_id("menu_toggle_card_reversal_exclusion"),
            label,
            "",
        );
    }
    let mut menu = menu_builder.build();
    ui.ready_cards.popup_menu(&mut menu, position);
}

pub(crate) fn sync_cards_menu_state(ui: &ReviewUi, state: &AppState) {
    let has_selected_card = selected_ready_item_is_visible(state);
    let edit_action = EditCardMenuAction::for_phase(state.session.phase());
    let edit_label = edit_action.label(state.localizer);
    let edit_enabled = edit_action.is_enabled(has_selected_card);
    let suspend_label = if selected_ready_card_is_suspended(state).unwrap_or(false) {
        state.localizer.unsuspend_selected_card_menu_item()
    } else {
        state.localizer.suspend_selected_card_menu_item()
    };
    let flag_label = current_or_selected_card_flag_menu_label(state);
    let deferral_label = selected_ready_card_deferral_label(state);
    let reversal_label = selected_ready_card_reversal_label(state);
    let reversal_exclusion_label = selected_ready_card_reversal_exclusion_label(state);
    if let Some(menu_bar) = ui.frame.get_menu_bar() {
        set_menu_item_label(&menu_bar, CosimoUi::xrc_id("menu_edit_card"), edit_label);
        set_menu_item_label(
            &menu_bar,
            CosimoUi::xrc_id("menu_toggle_card_suspension"),
            suspend_label,
        );
        set_menu_item_label(
            &menu_bar,
            CosimoUi::xrc_id("menu_toggle_card_flag"),
            flag_label,
        );
        set_menu_item_label(
            &menu_bar,
            CosimoUi::xrc_id("menu_toggle_card_deferral"),
            deferral_label,
        );
        set_menu_item_label(
            &menu_bar,
            CosimoUi::xrc_id("menu_toggle_card_reversal"),
            reversal_label,
        );
        set_menu_item_label(
            &menu_bar,
            CosimoUi::xrc_id("menu_toggle_card_reversal_exclusion"),
            reversal_exclusion_label,
        );
    }
    let can_modify_ready_cards = state.session.is_ready();
    let flag_enabled = if state.session.is_ready() {
        has_selected_card
    } else {
        state.current_study_item().is_some()
    };
    let undo_bulk_reversal_enabled = can_modify_ready_cards
        && state
            .latest_bulk_reversal_count()
            .is_ok_and(|count| count > 0);
    let make_eligible_reversal_enabled = eligible_reversal_menu_enabled(state);
    let review_flagged_enabled = can_modify_ready_cards
        && state
            .available_flagged_card_count()
            .is_ok_and(|count| count > 0);
    ui.sync_menu_state(MenuAvailability {
        add: can_modify_ready_cards,
        edit: edit_enabled,
        remove: can_modify_ready_cards && has_selected_card,
        suspend: can_modify_ready_cards && has_selected_card,
        flag: flag_enabled,
        review_flagged: review_flagged_enabled,
        defer: can_modify_ready_cards && has_selected_card,
        reverse: can_modify_ready_cards
            && has_selected_card
            && selected_ready_card_reversal_available(state),
        reversal_exclusion: can_modify_ready_cards
            && has_selected_card
            && selected_ready_card_reversal_exclusion_available(state),
        make_eligible_reversal: make_eligible_reversal_enabled,
        undo_bulk_reversal: undo_bulk_reversal_enabled,
    });
}

pub(crate) fn eligible_reversal_menu_enabled(state: &AppState) -> bool {
    state.session.is_ready()
        && state
            .eligible_reversible_card_count()
            .is_ok_and(|count| count > 0)
}

pub(crate) fn selected_ready_item_is_visible(state: &AppState) -> bool {
    let Some(selected_key) = &state.selected_ready_key else {
        return false;
    };
    let items = state
        .study_items()
        .unwrap_or_else(|_| state.fallback_study_items());
    let Some(selected_index) = items
        .iter()
        .position(|item| item.datum_key() == selected_key)
    else {
        return false;
    };
    state
        .ready_visible_indices
        .borrow()
        .contains(&selected_index)
}

pub(crate) fn render_ready_screen_after_card_change(ui: &ReviewUi, state: &Rc<RefCell<AppState>>) {
    {
        let mut state_ref = state.borrow_mut();
        state_ref.normalize_ready_selection_for_filter();
        state_ref.sync_ready_due_notification_baseline();
    }
    let state_ref = state.borrow();
    render(ui, &state_ref);
    ui.focus_ready_card_list(ready_screen_has_cards(&state_ref));
}

pub(crate) fn render_ready_screen_after_maintenance(
    ui: &ReviewUi,
    state: &Rc<RefCell<AppState>>,
    previous_focus: Option<ReadyScreenFocus>,
) {
    {
        let mut state_ref = state.borrow_mut();
        state_ref.normalize_ready_selection_for_filter();
        state_ref.sync_ready_due_notification_baseline();
    }
    let state_ref = state.borrow();
    render(ui, &state_ref);
    if let Some(previous_focus) = previous_focus {
        ui.restore_ready_screen_focus(previous_focus, ready_screen_has_cards(&state_ref));
    }
}

pub(crate) fn selected_ready_card_is_suspended(state: &AppState) -> Result<bool, String> {
    let Some(item) = state.selected_ready_study_item()? else {
        return Ok(false);
    };
    let suspended = state.load_suspended_card_keys()?;
    Ok(suspended.contains(item.datum_key()))
}

fn selected_ready_card_is_deferred(state: &AppState) -> Result<bool, String> {
    let Some(item) = state.selected_ready_study_item()? else {
        return Ok(false);
    };
    let deferred = state.load_deferred_card_keys()?;
    Ok(deferred.contains(item.datum_key()))
}

fn current_or_selected_card_is_flagged(state: &AppState) -> Result<bool, String> {
    let item = if state.session.is_ready() {
        state.selected_ready_study_item()?
    } else {
        state.current_study_item()
    };
    let Some(item) = item else {
        return Ok(false);
    };
    let flagged = state.load_flagged_card_keys()?;
    Ok(flagged.contains(item.datum_key()))
}

pub(crate) fn current_or_selected_card_flag_menu_label(state: &AppState) -> &'static str {
    let is_flagged = current_or_selected_card_is_flagged(state).unwrap_or(false);
    match (state.session.is_ready(), is_flagged) {
        (true, true) => state.localizer.unflag_selected_card_menu_item(),
        (true, false) => state.localizer.flag_selected_card_menu_item(),
        (false, true) => state.localizer.unflag_current_card_menu_item(),
        (false, false) => state.localizer.flag_current_card_menu_item(),
    }
}

pub(crate) fn current_card_flag_button_label(state: &AppState) -> &'static str {
    if current_or_selected_card_is_flagged(state).unwrap_or(false) {
        state.localizer.unflag_card_button_label()
    } else {
        state.localizer.flag_card_button_label()
    }
}

pub(crate) fn selected_ready_card_deferral_label(state: &AppState) -> &'static str {
    if selected_ready_card_is_deferred(state).unwrap_or(false) {
        state.localizer.clear_selected_card_deferral_menu_item()
    } else {
        state
            .localizer
            .defer_selected_card_until_tomorrow_menu_item()
    }
}

pub(crate) fn selected_ready_card_reversal_label(state: &AppState) -> &'static str {
    let Ok(Some(item)) = state.selected_ready_study_item() else {
        return state.localizer.make_selected_card_reversible_menu_item();
    };
    if item.is_reverse()
        || state
            .load_reversible_card_keys()
            .is_ok_and(|keys| keys.contains(&item.parent_datum_key()))
    {
        state.localizer.remove_reverse_card_menu_item()
    } else {
        state.localizer.make_selected_card_reversible_menu_item()
    }
}

pub(crate) fn selected_ready_card_reversal_available(state: &AppState) -> bool {
    let Ok(Some(item)) = state.selected_ready_study_item() else {
        return false;
    };
    if item.is_reverse() {
        return true;
    }
    let parent_key = item.parent_datum_key();
    if state
        .load_reversible_card_keys()
        .is_ok_and(|keys| keys.contains(&parent_key))
    {
        return true;
    }
    !state
        .load_reversal_excluded_card_keys()
        .is_ok_and(|keys| keys.contains(&parent_key))
}

pub(crate) fn selected_ready_card_reversal_exclusion_label(state: &AppState) -> &'static str {
    let Ok(Some(item)) = state.selected_ready_study_item() else {
        return state
            .localizer
            .mark_selected_card_non_reversible_menu_item();
    };
    if state
        .load_reversal_excluded_card_keys()
        .is_ok_and(|keys| keys.contains(&item.parent_datum_key()))
    {
        state.localizer.allow_selected_card_reversal_menu_item()
    } else {
        state
            .localizer
            .mark_selected_card_non_reversible_menu_item()
    }
}

pub(crate) fn selected_ready_card_reversal_exclusion_available(state: &AppState) -> bool {
    state
        .selected_ready_study_item()
        .is_ok_and(|item| item.is_some_and(|item| !item.is_reverse()))
}

pub(crate) fn install_ready_refresh_timer(
    ui: &ReviewUi,
    state: Rc<RefCell<AppState>>,
    resident_tray: Rc<RefCell<Option<ResidentTray>>>,
) -> Rc<RefCell<Option<Timer<Panel>>>> {
    let timer_store = Rc::new(RefCell::new(None));
    let timer = Timer::new(&ui.root_panel);
    let ui_copy = *ui;
    timer.on_tick(move |_| {
        refresh_ready_screen_after_timed_refresh(&ui_copy, &state, &resident_tray)
    });
    timer.start(READY_REFRESH_INTERVAL_MS, false);
    timer_store.borrow_mut().replace(timer);
    timer_store
}

pub(crate) fn install_ready_refresh_key_handlers(ui: &ReviewUi, state: Rc<RefCell<AppState>>) {
    bind_ready_refresh_key_handler(ui.frame, *ui, Rc::clone(&state));
    bind_ready_refresh_key_handler(ui.root_panel, *ui, Rc::clone(&state));
    bind_ready_refresh_key_handler(ui.start_panel, *ui, Rc::clone(&state));
    bind_ready_refresh_key_handler(ui.cycle_summary, *ui, Rc::clone(&state));
    bind_ready_refresh_key_handler(ui.ready_cards, *ui, Rc::clone(&state));
    bind_ready_refresh_key_handler(ui.ready_card_text_filter, *ui, Rc::clone(&state));
    bind_ready_refresh_key_handler(ui.ready_card_filter_choice, *ui, Rc::clone(&state));
    bind_ready_refresh_key_handler(ui.ready_add_card_button, *ui, Rc::clone(&state));
    bind_ready_refresh_key_handler(ui.ready_edit_card_button, *ui, Rc::clone(&state));
    bind_ready_refresh_key_handler(ui.ready_remove_card_button, *ui, Rc::clone(&state));
    bind_ready_refresh_key_handler(ui.start_cycle_button, *ui, Rc::clone(&state));
    bind_ready_refresh_key_handler(ui.force_start_cycle_button, *ui, Rc::clone(&state));
    bind_ready_refresh_key_handler(ui.review_flagged_cards_button, *ui, Rc::clone(&state));
    bind_ready_refresh_key_handler(ui.start_exam_button, *ui, Rc::clone(&state));
    bind_ready_refresh_key_handler(ui.start_quit_button, *ui, Rc::clone(&state));
}

fn bind_ready_refresh_key_handler<W>(widget: W, ui: ReviewUi, state: Rc<RefCell<AppState>>)
where
    W: WxWidget + WindowEvents + Copy + 'static,
{
    widget.on_key_down(move |event| {
        if let WindowEventData::Keyboard(key_event) = event {
            match ready_refresh_key_disposition(
                state.borrow().session.is_ready(),
                key_event.get_key_code(),
            ) {
                ReadyRefreshKeyDisposition::Refresh => {
                    refresh_ready_screen_without_notification(&ui, &state);
                    key_event.event.skip(false);
                }
                ReadyRefreshKeyDisposition::Skip => key_event.event.skip(true),
            }
        }
    });
}

pub(crate) fn ready_refresh_key_disposition(
    is_ready_screen: bool,
    key_code: Option<i32>,
) -> ReadyRefreshKeyDisposition {
    if is_ready_screen && is_ready_screen_refresh_key(key_code) {
        ReadyRefreshKeyDisposition::Refresh
    } else {
        ReadyRefreshKeyDisposition::Skip
    }
}

fn refresh_ready_screen_after_timed_refresh(
    ui: &ReviewUi,
    state: &Rc<RefCell<AppState>>,
    resident_tray: &Rc<RefCell<Option<ResidentTray>>>,
) {
    if !state.borrow().session.is_ready() {
        return;
    }

    notify_ready_due_change(ui, state, resident_tray);
    state.borrow_mut().normalize_ready_selection_for_filter();
    render(ui, &state.borrow());
    sync_ready_selection_controls(ui, &state.borrow());
}

pub(crate) fn refresh_ready_screen_without_notification(
    ui: &ReviewUi,
    state: &Rc<RefCell<AppState>>,
) {
    if !state.borrow().session.is_ready() {
        return;
    }

    state.borrow_mut().sync_ready_due_notification_baseline();
    state.borrow_mut().normalize_ready_selection_for_filter();
    render(ui, &state.borrow());
    sync_ready_selection_controls(ui, &state.borrow());
}

fn notify_ready_due_change(
    ui: &ReviewUi,
    state: &Rc<RefCell<AppState>>,
    resident_tray: &Rc<RefCell<Option<ResidentTray>>>,
) {
    let current_due_count = {
        let state_ref = state.borrow();
        match schedule_preview_if_started_now(&state_ref) {
            Ok(preview) => preview.due_cards,
            Err(_) => return,
        }
    };

    let notification_kind = {
        let mut state_ref = state.borrow_mut();
        let notification_kind = if sound_notification_policy_allows(
            state_ref.settings.sound_notification_policy,
            ui.frame.is_shown() && !ui.frame.is_iconized(),
            resident_tray_visible(resident_tray),
        ) {
            due_notification_kind(
                state_ref.last_ready_due_notification_count,
                current_due_count,
            )
        } else {
            None
        };
        state_ref.last_ready_due_notification_count = Some(current_due_count);
        notification_kind
    };

    if let Some(notification_kind) = notification_kind {
        play_due_notification(notification_kind);
    }
}

pub(crate) fn sound_notification_policy_allows(
    policy: SoundNotificationPolicy,
    app_visible: bool,
    in_tray: bool,
) -> bool {
    policy.allows_notification(app_visible, in_tray)
}

pub(crate) fn render(ui: &ReviewUi, state: &AppState) {
    let total_cards = state.data.len();
    let scheduled_cards = state.scheduled_card_count();
    let localizer = state.localizer;
    let phase = state.session.phase();
    let ready_schedule_status = ReadyScheduleStatus::for_phase(state, phase);
    let scheduled_cards_if_started = ready_schedule_status.preview();
    let phase_instruction = match phase {
        ReviewPhase::ReadyToStart => localizer.ready_phase_instruction(total_cards),
        ReviewPhase::Prompt => localizer.prompt_phase_instruction(),
        ReviewPhase::Response { .. } => localizer.response_phase_instruction(),
    };

    let status = if state.last_status.is_empty() {
        localizer.loaded_cards_status(total_cards, phase_instruction)
    } else {
        match phase {
            ReviewPhase::ReadyToStart => format!("{} {}", state.last_status, phase_instruction),
            ReviewPhase::Prompt | ReviewPhase::Response { .. } => {
                let card_number = state.session.current_index() + 1;
                localizer.current_card_status(
                    &state.last_status,
                    card_number,
                    scheduled_cards,
                    phase_instruction,
                )
            }
        }
    };
    let status = append_status_warning(status, ready_schedule_status.warning().map(str::to_string));

    ui.status.set_label(&status);
    let status_bar_text = current_time_and_seed()
        .and_then(|(now_unix_seconds, _)| main_status_bar_text(state, now_unix_seconds))
        .unwrap_or_else(|_| localizer.main_status_bar_schedule_unavailable(total_cards));
    ui.status_bar.set_status_text(&status_bar_text, 0);
    sync_cards_menu_state(ui, state);

    match phase {
        ReviewPhase::ReadyToStart => {
            ui.start_panel.show(true);
            ui.ready_cards_heading.show(false);
            sync_cycle_summary_text(
                ui,
                &ready_to_start_summary_text(
                    localizer,
                    total_cards,
                    &state.review_db_path,
                    state.settings.minimum_cards_per_session,
                    scheduled_cards_if_started.map(|preview| preview.scheduled_cards),
                    scheduled_cards_if_started.map(|preview| preview.due_cards),
                ),
            );
            sync_ready_cards_list(ui, state);
            sync_ready_card_text_filter(ui, state);
            sync_ready_filter_choices(ui, state);
            sync_ready_sort_menu(ui, state);
            let has_selected_card = selected_ready_item_is_visible(state);
            ui.ready_edit_card_button.enable(has_selected_card);
            ui.ready_remove_card_button.enable(has_selected_card);
            ui.start_cycle_button
                .enable(match scheduled_cards_if_started {
                    Some(preview) => preview.scheduled_cards > 0,
                    None => total_cards > 0,
                });
            let active_cards = state.active_card_count().unwrap_or(total_cards);
            ui.force_start_cycle_button.enable(active_cards > 0);
            ui.review_flagged_cards_button.enable(
                state
                    .available_flagged_card_count()
                    .is_ok_and(|count| count > 0),
            );
            ui.start_exam_button.enable(active_cards > 0);
            ui.prompt_heading.show(false);
            ui.prompt_heading.set_label(localizer.prompt_heading());
            ui.prompt.show(false);
            ui.prompt.set_value("");
            ui.response_heading.show(false);
            ui.response_heading.set_label(localizer.response_heading());
            ui.response.show(false);
            ui.response.set_value("");
            ui.review_audio_button.show(false);
            ui.review_audio_button.enable(false);
            ui.guess_panel.show(false);
            ui.ready_card_text_filter_label.show(true);
            ui.ready_card_text_filter.show(true);
            ui.ready_card_filter_label.show(true);
            ui.ready_card_filter_choice.show(true);
            ui.feedback_panel.show(false);
            set_buttons_enabled(&ui.guess_buttons, false);
            set_buttons_enabled(&ui.feedback_buttons, false);
            ui.reveal_response_button.enable(false);
            ui.guess_flag_card_button.enable(false);
            ui.guess_defer_card_button.enable(false);
            ui.guess_end_pass_button.enable(false);
            ui.feedback_end_pass_button.enable(false);
            ui.feedback_flag_card_button.enable(false);
            ui.feedback_edit_card_button.enable(false);
        }
        ReviewPhase::Prompt => {
            let datum = state
                .current_datum()
                .expect("current index is kept in range");
            let review_audio = current_review_audio_file(state);
            ui.start_panel.show(false);
            ui.ready_cards_heading.show(false);
            ui.ready_card_text_filter_label.show(false);
            ui.ready_card_text_filter.show(false);
            ui.prompt_heading.set_label(
                if review_audio
                    .as_ref()
                    .is_some_and(|audio| audio.side() == ReviewAudioSide::Prompt)
                {
                    localizer.prompt_heading_with_audio()
                } else {
                    localizer.prompt_heading()
                },
            );
            ui.prompt_heading.show(true);
            ui.prompt.show(true);
            ui.prompt.set_value(datum.prompt());
            ui.prompt.set_insertion_point(0);
            ui.response.set_value("");
            ui.response_heading.set_label(localizer.response_heading());
            ui.response_heading.show(false);
            ui.response.show(false);
            ui.review_audio_button
                .set_label(localizer.play_stop_review_audio_button_label());
            ui.review_audio_button.show(review_audio.is_some());
            ui.review_audio_button.enable(review_audio.is_some());
            ui.guess_panel.show(true);
            ui.feedback_panel.show(false);
            set_buttons_enabled(&ui.guess_buttons, true);
            set_buttons_enabled(&ui.feedback_buttons, false);
            ui.reveal_response_button.enable(true);
            ui.guess_flag_card_button
                .set_label(current_card_flag_button_label(state));
            ui.guess_flag_card_button.enable(true);
            ui.guess_defer_card_button.enable(true);
            ui.guess_end_pass_button.enable(true);
            ui.feedback_end_pass_button.enable(false);
            ui.feedback_flag_card_button.enable(false);
            ui.feedback_edit_card_button.enable(false);
            ui.start_exam_button.enable(false);
            ui.prompt.set_focus();
        }
        ReviewPhase::Response { .. } => {
            let datum = state
                .current_datum()
                .expect("current index is kept in range");
            let review_audio = current_review_audio_file(state);
            ui.start_panel.show(false);
            ui.ready_cards_heading.show(false);
            ui.ready_card_text_filter_label.show(false);
            ui.ready_card_text_filter.show(false);
            ui.prompt_heading.set_label(localizer.prompt_heading());
            ui.prompt_heading.show(true);
            ui.prompt.show(true);
            ui.prompt.set_value(datum.prompt());
            ui.prompt.set_insertion_point(0);
            ui.response_heading.set_label(
                if review_audio
                    .as_ref()
                    .is_some_and(|audio| audio.side() == ReviewAudioSide::Response)
                {
                    localizer.response_heading_with_audio()
                } else {
                    localizer.response_heading()
                },
            );
            ui.response_heading.show(true);
            ui.response.show(true);
            ui.response.set_value(datum.response());
            ui.response.set_insertion_point(0);
            ui.review_audio_button
                .set_label(localizer.play_stop_review_audio_button_label());
            ui.review_audio_button.show(review_audio.is_some());
            ui.review_audio_button.enable(review_audio.is_some());
            ui.guess_panel.show(false);
            ui.feedback_panel.show(true);
            set_buttons_enabled(&ui.guess_buttons, false);
            set_buttons_enabled(&ui.feedback_buttons, true);
            ui.reveal_response_button.enable(false);
            ui.guess_flag_card_button.enable(false);
            ui.guess_defer_card_button.enable(false);
            ui.guess_end_pass_button.enable(false);
            ui.feedback_end_pass_button.enable(true);
            ui.feedback_flag_card_button
                .set_label(current_card_flag_button_label(state));
            ui.feedback_flag_card_button.enable(true);
            ui.feedback_edit_card_button.enable(true);
            ui.start_exam_button.enable(false);
            ui.response.set_focus();
        }
    }

    ui.root_panel.layout();
    state
        .review_audio_player
        .borrow_mut()
        .sync_after_render(ui, state);
}

fn set_buttons_enabled(buttons: &[Button; 4], enabled: bool) {
    for button in buttons {
        button.enable(enabled);
    }
}

fn sync_cycle_summary_text(ui: &ReviewUi, summary: &str) {
    let current_summary = ui.cycle_summary.get_value();
    if text_control_value_matches(&current_summary, summary) {
        return;
    }

    let insertion_point = ui
        .cycle_summary
        .has_focus()
        .then(|| ui.cycle_summary.get_insertion_point());
    ui.cycle_summary.set_value(summary);
    if let Some(insertion_point) = insertion_point {
        ui.cycle_summary
            .set_insertion_point(insertion_point.min(summary.len() as i64));
    } else {
        ui.cycle_summary.set_insertion_point(0);
    }
}

pub(crate) fn text_control_value_matches(current: &str, expected: &str) -> bool {
    current == expected || current.replace("\r\n", "\n") == expected
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn ready_refresh_key_only_refreshes_ready_screen_on_f5() {
        assert_eq!(
            ready_refresh_key_disposition(true, Some(116)),
            ReadyRefreshKeyDisposition::Refresh
        );
        assert_eq!(
            ready_refresh_key_disposition(true, Some(344)),
            ReadyRefreshKeyDisposition::Refresh
        );
        assert_eq!(
            ready_refresh_key_disposition(false, Some(116)),
            ReadyRefreshKeyDisposition::Skip
        );
        assert_eq!(
            ready_refresh_key_disposition(true, Some(115)),
            ReadyRefreshKeyDisposition::Skip
        );
        assert_eq!(
            ready_refresh_key_disposition(true, None),
            ReadyRefreshKeyDisposition::Skip
        );
    }
}
