pub fn sync_feed_source_combo(
    combo: &gtk::ComboBoxText,
    options: Vec<FeedSourceChoice>,
    selected: FeedSourcePreset,
) {
    combo.remove_all();
    for option in options {
        combo.append(Some(option.preset.as_id()), option.label);
    }
    combo.set_active_id(Some(selected.as_id()));
    combo.set_sensitive(true);
}

pub fn sync_capture_resolution_combo(
    combo: &gtk::ComboBoxText,
    options: Vec<CaptureSizeChoice>,
    selected: CaptureSizePreset,
) {
    combo.remove_all();
    let option_count = options.len();
    for option in options {
        let label = compact_capture_mode_label(option.width, option.height, option.fps);
        combo.append(Some(option.preset.as_id()), &label);
    }
    combo.set_active_id(Some(selected.as_id()));
    combo.set_sensitive(option_count > 1);
}

pub fn sync_capture_resolution_locked(
    combo: &gtk::ComboBoxText,
    options: Vec<CaptureSizeChoice>,
    selected: CaptureSizePreset,
) {
    sync_capture_resolution_combo(combo, options, selected);
    combo.set_sensitive(false);
}

pub fn sync_capture_resolution_disabled(combo: &gtk::ComboBoxText) {
    combo.remove_all();
    combo.append(Some("off"), "Feed disabled");
    combo.set_active_id(Some("off"));
    combo.set_sensitive(false);
}

pub fn sync_breakout_size_combo(
    combo: &gtk::ComboBoxText,
    options: Vec<BreakoutSizeChoice>,
    selected: BreakoutSizePreset,
) {
    combo.remove_all();
    for option in options {
        let label = match option.preset {
            BreakoutSizePreset::Source => format!("Source {}", compact_size_label(option.height)),
            BreakoutSizePreset::FillDisplay => {
                format!("Display {}", compact_size_label(option.height))
            }
            _ => format!("{} {}x{}", option.preset.label(), option.width, option.height),
        };
        combo.append(Some(option.preset.as_id()), &label);
    }
    combo.set_active_id(Some(selected.as_id()));
}

pub fn sync_stage_device_combo(
    combo: &gtk::ComboBoxText,
    values: &[String],
    selected: Option<&str>,
) {
    combo.remove_all();
    for value in values {
        append_stage_choice(combo, value);
    }
    set_stage_combo_active_text(combo, selected);
    refresh_device_choice_combo_child(combo);
}

pub fn sync_camera_quality_combo(
    combo: &gtk::ComboBoxText,
    options: &[CameraMode],
    selected: Option<CameraMode>,
) {
    combo.remove_all();
    if options.is_empty() {
        combo.append(Some("none"), "Quality");
        combo.set_active_id(Some("none"));
        combo.set_sensitive(false);
        return;
    }

    for option in options {
        combo.append(Some(&option.id()), &option.short_label());
    }
    let active = selected
        .filter(|mode| options.contains(mode))
        .or_else(|| options.first().copied())
        .map(CameraMode::id);
    combo.set_active_id(active.as_deref());
}

pub fn sync_input_device_combo(
    combo: &gtk::ComboBoxText,
    values: &[String],
    selected: Option<&str>,
    all_label: &str,
) {
    combo.remove_all();
    combo.append(Some("all"), all_label);
    for value in values {
        append_input_choice(combo, value);
    }
    super::ui_runtime::set_combo_active_text(combo, selected);
    refresh_device_choice_combo_child(combo);
}

fn install_device_choice_combo_behavior(combo: &gtk::ComboBoxText) {
    combo.add_css_class("device-choice-combo");
    combo.set_popup_fixed_width(false);
    combo.set_tooltip_text(Some(
        "Open the dropdown for full device names; hover clipped popup choices to scroll them.",
    ));

    combo.connect_changed(move |combo| {
        refresh_device_choice_combo_child(combo);
    });
    refresh_device_choice_combo_child(combo);
}

fn refresh_device_choice_combo_child(combo: &gtk::ComboBoxText) {
    let full = combo_active_full_label(combo);
    combo.set_tooltip_text(Some(&format!(
        "Selected: {full}. Open the dropdown for compact choices; hover clipped popup choices to scroll them."
    )));
}

fn combo_active_full_label(combo: &gtk::ComboBoxText) -> String {
    let Some(active_id) = combo.active_id() else {
        return "No device".to_string();
    };
    if active_id.as_str() == "all" {
        return combo
            .active_text()
            .map(|text| text.to_string())
            .filter(|text| !text.trim().is_empty())
            .unwrap_or_else(|| "all devices".to_string());
    }
    Device::open(active_id.as_str())
        .ok()
        .and_then(|device| device.name().map(ToString::to_string))
        .unwrap_or_else(|| stage_choice_label(active_id.as_str()))
}

fn attach_device_control_row(
    grid: &gtk::Grid,
    row: i32,
    stream_toggle: &impl IsA<gtk::Widget>,
    selector: &impl IsA<gtk::Widget>,
    test_button: &gtk::Button,
) {
    stream_toggle.set_halign(gtk::Align::Start);
    selector.set_hexpand(true);
    grid.attach(stream_toggle, 0, row, 1, 1);
    grid.attach(selector, 1, row, 1, 1);
    grid.attach(test_button, 2, row, 1, 1);
}

fn build_inline_selector_row(label: &str, combo: &gtk::ComboBoxText) -> gtk::Box {
    let block = gtk::Box::new(gtk::Orientation::Horizontal, 8);
    let label_widget = gtk::Label::new(Some(label));
    label_widget.set_halign(gtk::Align::Start);
    label_widget.set_width_chars(7);
    label_widget.set_xalign(0.0);
    combo.set_hexpand(true);
    combo.set_size_request(0, -1);
    block.append(&label_widget);
    block.append(combo);
    block
}

fn build_inline_combo_row(
    label: &str,
    combo: &impl IsA<gtk::Widget>,
    min_label_chars: i32,
) -> gtk::Box {
    let row = gtk::Box::new(gtk::Orientation::Horizontal, 8);
    let label_widget = gtk::Label::new(Some(label));
    label_widget.add_css_class("dim-label");
    label_widget.set_width_chars(min_label_chars);
    label_widget.set_xalign(0.0);
    label_widget.set_halign(gtk::Align::Start);
    row.append(&label_widget);
    row.append(combo);
    row
}

fn append_input_choice(combo: &gtk::ComboBoxText, value: &str) {
    let short = value.rsplit('/').next().unwrap_or(value);
    let label = Device::open(value)
        .ok()
        .and_then(|device| device.name().map(ToString::to_string))
        .unwrap_or_else(|| short.to_string());
    combo.append(Some(value), &shorten_label_with_limit(&label, DEVICE_COMBO_BUTTON_LABEL_LIMIT));
}

fn append_stage_choice(combo: &gtk::ComboBoxText, value: &str) {
    combo.append(
        Some(value),
        &shorten_label_with_limit(&stage_choice_label(value), DEVICE_COMBO_BUTTON_LABEL_LIMIT),
    );
}

/// Keeps eye capture labels short so GTK does not widen the whole launcher.
fn compact_capture_mode_label(width: i32, height: i32, fps: u32) -> String {
    let size = compact_size_label(height);
    if width >= 1280 {
        format!("{size}@{fps}")
    } else {
        format!("{width}x{height}@{fps}")
    }
}

/// Formats common video heights in the same terse style used by meeting apps.
fn compact_size_label(height: i32) -> String {
    match height {
        2160 => "4K".to_string(),
        1080 | 720 | 576 | 480 => format!("{height}p"),
        other => format!("{other}p"),
    }
}

fn set_stage_combo_active_text(combo: &gtk::ComboBoxText, selected: Option<&str>) {
    if selected
        .filter(|value| !value.trim().is_empty())
        .is_some_and(|value| combo.set_active_id(Some(value)))
    {
        return;
    }
    combo.set_active(Some(0));
}

fn stage_choice_label(value: &str) -> String {
    let trimmed = value.trim();
    if trimmed.is_empty() {
        return "No device".to_string();
    }
    let camera = trimmed
        .strip_prefix("usb-")
        .unwrap_or(trimmed)
        .split("-video-index")
        .next()
        .unwrap_or(trimmed);
    if camera != trimmed {
        return camera.to_string();
    }
    if let Some(rest) = trimmed
        .strip_prefix("alsa_input.")
        .or_else(|| trimmed.strip_prefix("alsa_output."))
    {
        return human_audio_node_label(rest);
    }
    if let Some(rest) = trimmed
        .strip_prefix("bluez_input.")
        .or_else(|| trimmed.strip_prefix("bluez_output."))
    {
        return format!(
            "Bluetooth {}",
            rest.split('.').next().unwrap_or(rest).replace('_', ":")
        );
    }
    if let Some(short) = trimmed.rsplit('/').next()
        && short != trimmed
    {
        return short.to_string();
    }
    trimmed.to_string()
}

fn human_audio_node_label(value: &str) -> String {
    let compact = value
        .trim()
        .strip_prefix("usb-")
        .unwrap_or(value.trim())
        .replace(".analog-stereo", " analog stereo")
        .replace(".mono-fallback", " mono")
        .replace(".stereo-fallback", " stereo")
        .replace(['-', '_'], " ");
    if compact.starts_with("pci ") || compact.starts_with("pci-") {
        if compact.contains("analog stereo") {
            "Analog stereo".to_string()
        } else {
            "System audio".to_string()
        }
    } else {
        compact
    }
}

const DEVICE_COMBO_BUTTON_LABEL_LIMIT: usize = 18;

fn shorten_label_with_limit(value: &str, max: usize) -> String {
    let compact = value.replace('_', " ");
    let mut chars = compact.chars();
    let preview: String = chars.by_ref().take(max).collect();
    if chars.next().is_some() {
        format!("{preview}…")
    } else {
        preview
    }
}
