pub fn normalize_audio_gain_percent(percent: u32) -> u32 {
    percent.min(MAX_AUDIO_GAIN_PERCENT)
}

pub fn format_audio_gain_percent(percent: u32) -> String {
    format!("{}%", normalize_audio_gain_percent(percent))
}

pub fn normalize_mic_gain_percent(percent: u32) -> u32 {
    percent.min(MAX_MIC_GAIN_PERCENT)
}

pub fn format_mic_gain_percent(percent: u32) -> String {
    format!("{}%", normalize_mic_gain_percent(percent))
}

fn breakout_size_choice(
    physical_limit: PreviewSourceSize,
    display_fill: PreviewSourceSize,
    source: PreviewSourceSize,
    preset: BreakoutSizePreset,
) -> BreakoutSizeChoice {
    let physical_width = physical_limit.width.max(1) as i32;
    let physical_height = physical_limit.height.max(1) as i32;
    let display_width = display_fill.width.max(1) as i32;
    let display_height = display_fill.height.max(1) as i32;
    let (width, height) = match preset {
        BreakoutSizePreset::P360 => {
            fit_standard_dimensions(physical_width, physical_height, 640, 360)
        }
        BreakoutSizePreset::P540 => {
            fit_standard_dimensions(physical_width, physical_height, 960, 540)
        }
        BreakoutSizePreset::P720 => {
            fit_standard_dimensions(physical_width, physical_height, 1280, 720)
        }
        BreakoutSizePreset::P900 => {
            fit_standard_dimensions(physical_width, physical_height, 1600, 900)
        }
        BreakoutSizePreset::P1080 => {
            fit_standard_dimensions(physical_width, physical_height, 1920, 1080)
        }
        BreakoutSizePreset::P1440 => {
            fit_standard_dimensions(physical_width, physical_height, 2560, 1440)
        }
        BreakoutSizePreset::Source => fit_standard_dimensions(
            physical_width,
            physical_height,
            source.width.max(1) as i32,
            source.height.max(1) as i32,
        ),
        BreakoutSizePreset::FillDisplay => (display_width, display_height),
    };
    BreakoutSizeChoice {
        preset,
        width,
        height,
    }
}

fn breakout_size_options(
    physical_limit: PreviewSourceSize,
    display_fill: PreviewSourceSize,
    source: PreviewSourceSize,
) -> Vec<BreakoutSizeChoice> {
    let mut options = Vec::new();
    for preset in [
        BreakoutSizePreset::Source,
        BreakoutSizePreset::P360,
        BreakoutSizePreset::P540,
        BreakoutSizePreset::P720,
        BreakoutSizePreset::P900,
        BreakoutSizePreset::P1080,
        BreakoutSizePreset::P1440,
        BreakoutSizePreset::FillDisplay,
    ] {
        let choice = breakout_size_choice(physical_limit, display_fill, source, preset);
        let allow_duplicate_label = matches!(
            preset,
            BreakoutSizePreset::Source | BreakoutSizePreset::FillDisplay
        );
        if !allow_duplicate_label
            && options.iter().any(|existing: &BreakoutSizeChoice| {
                existing.width == choice.width && existing.height == choice.height
            })
        {
            continue;
        }
        options.push(choice);
    }
    options
}

fn capture_size_choice(
    _source: PreviewSourceSize,
    preset: CaptureSizePreset,
    selected_fps: u32,
    selected_bitrate_kbit: u32,
) -> CaptureSizeChoice {
    let preset = normalize_capture_size_preset(preset);
    let mode = preset.source_mode();
    let _ = (selected_fps, selected_bitrate_kbit);
    let (width, height, fps, max_bitrate_kbit) = (
        mode.width as i32,
        mode.height as i32,
        mode.fps,
        estimate_source_bitrate_kbit(mode.width as i32, mode.height as i32, mode.fps),
    );
    CaptureSizeChoice {
        preset,
        width,
        height,
        fps,
        max_bitrate_kbit,
    }
}

fn estimate_source_bitrate_kbit(width: i32, height: i32, fps: u32) -> u32 {
    let pixels_per_second = width.max(1) as u64 * height.max(1) as u64 * fps.max(1) as u64;
    match pixels_per_second {
        p if p >= 1920_u64 * 1080 * 50 => 18_000,
        p if p >= 1920_u64 * 1080 * 24 => 12_000,
        p if p >= 1280_u64 * 720 * 24 => 6_000,
        _ => 2_500,
    }
}

fn capture_size_options(source: PreviewSourceSize) -> Vec<CaptureSizeChoice> {
    native_eye_source_modes()
        .iter()
        .copied()
        .filter(|mode| mode.width <= source.width && mode.height <= source.height)
        .map(CaptureSizePreset::from_source_mode)
        .map(|preset| {
            let defaults = default_profile_for_preset(source, preset);
            capture_size_choice(source, preset, defaults.fps, defaults.max_bitrate_kbit)
        })
        .collect()
}

fn capture_fps_options(source: PreviewSourceSize) -> Vec<CaptureFpsChoice> {
    vec![CaptureFpsChoice {
        fps: source.fps.max(1),
    }]
}

fn capture_bitrate_options(source: PreviewSourceSize) -> Vec<CaptureBitrateChoice> {
    vec![CaptureBitrateChoice {
        max_bitrate_kbit: estimate_source_bitrate_kbit(
            source.width as i32,
            source.height as i32,
            source.fps,
        ),
    }]
}

fn default_profile_for_preset(
    _source: PreviewSourceSize,
    preset: CaptureSizePreset,
) -> CaptureSizeChoice {
    let preset = normalize_capture_size_preset(preset);
    let mode = preset.source_mode();
    let (width, height, fps, max_bitrate_kbit) = (
        mode.width as i32,
        mode.height as i32,
        mode.fps,
        estimate_source_bitrate_kbit(mode.width as i32, mode.height as i32, mode.fps),
    );
    CaptureSizeChoice {
        preset,
        width,
        height,
        fps,
        max_bitrate_kbit,
    }
}

fn normalize_capture_size_preset(preset: CaptureSizePreset) -> CaptureSizePreset {
    match preset {
        CaptureSizePreset::Vga | CaptureSizePreset::P480 | CaptureSizePreset::P576 => {
            CaptureSizePreset::P720
        }
        other => other,
    }
}

fn fit_standard_dimensions(
    limit_width: i32,
    limit_height: i32,
    wanted_width: i32,
    wanted_height: i32,
) -> (i32, i32) {
    let width = wanted_width.min(limit_width).max(2);
    let height = wanted_height.min(limit_height).max(2);
    if width == limit_width && height == limit_height {
        return (width, height);
    }
    let width_from_height = round_down_even((height * 16) / 9);
    if width_from_height <= limit_width {
        (round_down_even(width_from_height), round_down_even(height))
    } else {
        let height_from_width = round_down_even((width * 9) / 16);
        (round_down_even(width), round_down_even(height_from_width))
    }
}

fn round_down_even(value: i32) -> i32 {
    let rounded = value.max(2);
    rounded - (rounded % 2)
}

fn normalize_selection(value: Option<String>) -> Option<String> {
    value.and_then(|v| {
        let trimmed = v.trim();
        if trimmed.is_empty() || trimmed.eq_ignore_ascii_case("auto") {
            None
        } else {
            Some(trimmed.to_string())
        }
    })
}

fn keep_or_select_first(selection: &mut Option<String>, values: &[String]) {
    if selection.is_none() {
        *selection = values.first().cloned();
    }
}

fn media_status_label(enabled: bool, selected: Option<&str>) -> &str {
    if !enabled {
        "off"
    } else {
        selected.unwrap_or("none")
    }
}

fn normalize_swap_key(value: String) -> String {
    let trimmed = value.trim();
    if trimmed.is_empty() {
        "off".to_string()
    } else {
        trimmed.to_ascii_lowercase()
    }
}
