use super::{
    ADAPTIVE_ROI_SCORE_FRACTION, MAX_ADAPTIVE_ROI_FRACTION, MIN_ADAPTIVE_ROI_PIXELS,
    MIN_GRAY_ROI_SCORE, MIN_RGB_ROI_SCORE, VideoColorFrame,
};

pub(super) fn summarize_gray_frames_with_adaptive_roi<'a>(
    frames: impl Iterator<Item = &'a [u8]>,
    pixel_count: usize,
) -> Vec<u8> {
    let frames = frames.collect::<Vec<_>>();
    let mask = adaptive_gray_roi_mask(&frames, pixel_count);
    frames
        .iter()
        .map(|frame| summarize_frame_brightness(frame, mask.as_deref()))
        .collect()
}

pub(super) fn summarize_rgb_frames_with_adaptive_roi<'a>(
    frames: impl Iterator<Item = &'a [u8]>,
    pixel_count: usize,
) -> Vec<VideoColorFrame> {
    let frames = frames.collect::<Vec<_>>();
    let mask = adaptive_rgb_roi_mask(&frames, pixel_count);
    frames
        .iter()
        .map(|frame| summarize_frame_color(frame, mask.as_deref()))
        .collect()
}

/// Keeps `summarize_frame_brightness` explicit because it sits on sync-probe analysis, where small timestamp or pairing mistakes can hide real A/V skew.
/// Inputs are the typed parameters; output is the return value or side effect.
pub(super) fn summarize_frame_brightness(frame: &[u8], mask: Option<&[bool]>) -> u8 {
    let mut sum = 0u64;
    let mut selected = 0u64;
    for (index, value) in frame.iter().copied().enumerate() {
        if mask.is_none_or(|mask| mask.get(index).copied().unwrap_or(false)) {
            sum += u64::from(value);
            selected += 1;
        }
    }
    if selected == 0 {
        sum = frame.iter().map(|value| u64::from(*value)).sum();
        selected = frame.len().max(1) as u64;
    }
    let mean = sum / selected;
    mean.min(u64::from(u8::MAX)) as u8
}

/// Keeps `summarize_frame_color` explicit because it sits on sync-probe analysis, where small timestamp or pairing mistakes can hide real A/V skew.
/// Inputs are the typed parameters; output is the return value or side effect.
pub(super) fn summarize_frame_color(frame: &[u8], mask: Option<&[bool]>) -> VideoColorFrame {
    let mut r_sum = 0u64;
    let mut g_sum = 0u64;
    let mut b_sum = 0u64;
    let mut selected = 0u64;

    for (index, pixel) in frame.chunks_exact(3).enumerate() {
        if !mask.is_none_or(|mask| mask.get(index).copied().unwrap_or(false)) {
            continue;
        }
        let r = pixel[0];
        let g = pixel[1];
        let b = pixel[2];
        let max = r.max(g).max(b);
        let min = r.min(g).min(b);
        if max >= 60 && max.saturating_sub(min) >= 24 {
            r_sum += u64::from(r);
            g_sum += u64::from(g);
            b_sum += u64::from(b);
            selected += 1;
        }
    }

    if selected == 0 {
        for (index, pixel) in frame.chunks_exact(3).enumerate() {
            if !mask.is_none_or(|mask| mask.get(index).copied().unwrap_or(false)) {
                continue;
            }
            r_sum += u64::from(pixel[0]);
            g_sum += u64::from(pixel[1]);
            b_sum += u64::from(pixel[2]);
            selected += 1;
        }
    }

    if selected == 0 {
        for pixel in frame.chunks_exact(3) {
            r_sum += u64::from(pixel[0]);
            g_sum += u64::from(pixel[1]);
            b_sum += u64::from(pixel[2]);
            selected += 1;
        }
    }
    selected = selected.max(1);

    VideoColorFrame {
        r: (r_sum / selected).min(u64::from(u8::MAX)) as u8,
        g: (g_sum / selected).min(u64::from(u8::MAX)) as u8,
        b: (b_sum / selected).min(u64::from(u8::MAX)) as u8,
    }
}

/// Keeps `adaptive_gray_roi_mask` explicit because it sits on sync-probe analysis, where small timestamp or pairing mistakes can hide real A/V skew.
/// Inputs are the typed parameters; output is the return value or side effect.
pub(super) fn adaptive_gray_roi_mask(frames: &[&[u8]], pixel_count: usize) -> Option<Vec<bool>> {
    if frames.len() < 2 || pixel_count == 0 {
        return None;
    }
    let mut scores = vec![0.0; pixel_count];
    for (pixel_index, score) in scores.iter_mut().enumerate() {
        let mut min = u8::MAX;
        let mut max = u8::MIN;
        for frame in frames {
            let value = frame[pixel_index];
            min = min.min(value);
            max = max.max(value);
        }
        *score = f64::from(max.saturating_sub(min)) * dark_roi_factor(min);
    }
    adaptive_roi_mask_from_scores(&scores, MIN_GRAY_ROI_SCORE)
}

/// Keeps `adaptive_rgb_roi_mask` explicit because it sits on sync-probe analysis, where small timestamp or pairing mistakes can hide real A/V skew.
/// Inputs are the typed parameters; output is the return value or side effect.
pub(super) fn adaptive_rgb_roi_mask(frames: &[&[u8]], pixel_count: usize) -> Option<Vec<bool>> {
    if frames.len() < 2 || pixel_count == 0 {
        return None;
    }
    let mut scores = vec![0.0; pixel_count];
    for (pixel_index, score) in scores.iter_mut().enumerate() {
        let mut min_r = u8::MAX;
        let mut min_g = u8::MAX;
        let mut min_b = u8::MAX;
        let mut max_r = u8::MIN;
        let mut max_g = u8::MIN;
        let mut max_b = u8::MIN;
        let mut min_luma = u8::MAX;
        let mut max_luma = u8::MIN;
        let mut best_palette_score = 0.0_f64;

        for frame in frames {
            let offset = pixel_index * 3;
            let r = frame[offset];
            let g = frame[offset + 1];
            let b = frame[offset + 2];
            min_r = min_r.min(r);
            min_g = min_g.min(g);
            min_b = min_b.min(b);
            max_r = max_r.max(r);
            max_g = max_g.max(g);
            max_b = max_b.max(b);
            let luma = luma_u8(r, g, b);
            min_luma = min_luma.min(luma);
            max_luma = max_luma.max(luma);
            best_palette_score = best_palette_score.max(palette_match_score(r, g, b));
        }

        let rgb_span = f64::from(max_r.saturating_sub(min_r))
            + f64::from(max_g.saturating_sub(min_g))
            + f64::from(max_b.saturating_sub(min_b));
        let luma_span = f64::from(max_luma.saturating_sub(min_luma));
        *score =
            (rgb_span + (2.0 * luma_span)) * (1.0 + best_palette_score) * dark_roi_factor(min_luma);
    }
    adaptive_roi_mask_from_scores(&scores, MIN_RGB_ROI_SCORE)
}

/// Keeps `adaptive_roi_mask_from_scores` explicit because it sits on sync-probe analysis, where small timestamp or pairing mistakes can hide real A/V skew.
/// Inputs are the typed parameters; output is the return value or side effect.
pub(super) fn adaptive_roi_mask_from_scores(scores: &[f64], min_score: f64) -> Option<Vec<bool>> {
    let max_score = scores.iter().copied().fold(0.0_f64, f64::max);
    if max_score < min_score {
        return None;
    }

    let mut ranked = scores
        .iter()
        .copied()
        .enumerate()
        .filter(|(_, score)| score.is_finite() && *score > 0.0)
        .collect::<Vec<_>>();
    ranked.sort_by(|left, right| right.1.total_cmp(&left.1));

    let max_selected = ((scores.len() as f64 * MAX_ADAPTIVE_ROI_FRACTION).round() as usize)
        .max(MIN_ADAPTIVE_ROI_PIXELS)
        .min(scores.len());
    let score_floor = (max_score * ADAPTIVE_ROI_SCORE_FRACTION).max(min_score);
    let mut mask = vec![false; scores.len()];
    for (selected, (index, score)) in ranked.into_iter().take(max_selected).enumerate() {
        if score < score_floor && selected >= MIN_ADAPTIVE_ROI_PIXELS {
            break;
        }
        mask[index] = true;
    }

    let mask = retain_largest_connected_roi(mask);
    let selected = mask.iter().filter(|selected| **selected).count();
    (selected >= MIN_ADAPTIVE_ROI_PIXELS).then_some(mask)
}

/// Keeps `retain_largest_connected_roi` explicit because it sits on sync-probe analysis, where small timestamp or pairing mistakes can hide real A/V skew.
/// Inputs are the typed parameters; output is the return value or side effect.
pub(super) fn retain_largest_connected_roi(mask: Vec<bool>) -> Vec<bool> {
    let side = (mask.len() as f64).sqrt().round() as usize;
    if side == 0 || side * side != mask.len() {
        return mask;
    }

    let mut visited = vec![false; mask.len()];
    let mut best_component = Vec::<usize>::new();
    for start in 0..mask.len() {
        if !mask[start] || visited[start] {
            continue;
        }
        let mut stack = vec![start];
        let mut component = Vec::new();
        visited[start] = true;
        while let Some(index) = stack.pop() {
            component.push(index);
            let x = index % side;
            let y = index / side;
            let mut push_neighbor = |neighbor: usize| {
                if mask[neighbor] && !visited[neighbor] {
                    visited[neighbor] = true;
                    stack.push(neighbor);
                }
            };
            if x > 0 {
                push_neighbor(index - 1);
            }
            if x + 1 < side {
                push_neighbor(index + 1);
            }
            if y > 0 {
                push_neighbor(index - side);
            }
            if y + 1 < side {
                push_neighbor(index + side);
            }
        }
        if component.len() > best_component.len() {
            best_component = component;
        }
    }

    if best_component.len() < MIN_ADAPTIVE_ROI_PIXELS {
        return mask;
    }
    let mut retained = vec![false; mask.len()];
    for index in best_component {
        retained[index] = true;
    }
    retained
}

fn luma_u8(r: u8, g: u8, b: u8) -> u8 {
    ((u16::from(r) * 77 + u16::from(g) * 150 + u16::from(b) * 29) / 256) as u8
}

/// Keeps `dark_roi_factor` explicit because it sits on sync-probe analysis, where small timestamp or pairing mistakes can hide real A/V skew.
/// Inputs are the typed parameters; output is the return value or side effect.
pub(super) fn dark_roi_factor(min_luma: u8) -> f64 {
    match min_luma {
        0..=80 => 1.0,
        81..=120 => 0.55,
        121..=160 => 0.25,
        _ => 0.10,
    }
}

/// Keeps `palette_match_score` explicit because it sits on sync-probe analysis, where small timestamp or pairing mistakes can hide real A/V skew.
/// Inputs are the typed parameters; output is the return value or side effect.
pub(super) fn palette_match_score(r: u8, g: u8, b: u8) -> f64 {
    let max = r.max(g).max(b);
    let min = r.min(g).min(b);
    if max < 50 || max.saturating_sub(min) < 20 {
        return 0.0;
    }

    const PALETTE: [(u8, u8, u8); 16] = [
        (255, 45, 45),
        (0, 230, 118),
        (41, 121, 255),
        (255, 179, 0),
        (216, 27, 96),
        (0, 188, 212),
        (205, 220, 57),
        (126, 87, 194),
        (255, 112, 67),
        (38, 166, 154),
        (255, 64, 129),
        (92, 107, 192),
        (255, 235, 59),
        (105, 240, 174),
        (171, 71, 188),
        (3, 169, 244),
    ];
    let best_distance = PALETTE
        .into_iter()
        .map(|(pr, pg, pb)| {
            let dr = f64::from(r) - f64::from(pr);
            let dg = f64::from(g) - f64::from(pg);
            let db = f64::from(b) - f64::from(pb);
            dr * dr + dg * dg + db * db
        })
        .fold(f64::INFINITY, f64::min);
    (1.0 - (best_distance / 65_025.0)).clamp(0.0, 1.0)
}
