use super::super::conflict_resolver;
use super::canvas::keyed_canvas;
use super::diff_text::{whitespace_visible_line_styled_text_for_raw, whitespace_visible_line_text};
use super::*;
use gpui::{
    App, Bounds, ContentMask, DispatchPhase, HighlightStyle, Pixels, Styled, TextRun, TextStyle,
    Window, fill, point, px, size,
};
use palette::IntoColor;
use rustc_hash::FxHasher;
use std::cell::RefCell;
use std::hash::{Hash, Hasher};
use std::ops::Range;
use std::sync::Arc;
use std::sync::OnceLock;

const DIFF_FONT_SCALE: f32 = 0.80;
const GUTTER_TEXT_LAYOUT_CACHE_MAX_ENTRIES: usize = 16_384;
const CONFLICT_TEXT_LAYOUT_CACHE_MAX_ENTRIES: usize = 32_768;

type HighlightSpans = Arc<[(Range<usize>, HighlightStyle)]>;
thread_local! {
    static GUTTER_TEXT_LAYOUT_CACHE: RefCell<FxLruCache<u64, gpui::ShapedLine>> =
        RefCell::new(new_fx_lru_cache(GUTTER_TEXT_LAYOUT_CACHE_MAX_ENTRIES));
    static CONFLICT_TEXT_LAYOUT_CACHE: RefCell<FxLruCache<u64, gpui::ShapedLine>> =
        RefCell::new(new_fx_lru_cache(CONFLICT_TEXT_LAYOUT_CACHE_MAX_ENTRIES));
}

#[derive(Clone, Debug)]
pub(super) struct ConflictChunkContext {
    pub(super) conflict_ix: usize,
    pub(super) has_base: bool,
    pub(super) selected_choices: Vec<conflict_resolver::ConflictChoice>,
}

/// KDiff3 manual diff help: what one source-column row offers to a manual
/// alignment. Alt+click marks the line; Alt+Shift+click extends the mark.
///
/// Marking works on context rows too, not just inside conflict blocks — the
/// whole point of a manual alignment is to pin lines the automatic alignment
/// placed apart.
#[derive(Clone, Copy, Debug)]
pub(super) struct AlignmentMarkContext {
    pub(super) column: ThreeWayColumn,
    /// Line in this column's own file. Padding rows have none and cannot be
    /// marked, since there is no line there to pin.
    pub(super) side_line: Option<usize>,
    pub(super) marked: bool,
}

#[allow(clippy::too_many_arguments)]
pub(super) fn split_conflict_row_canvas(
    theme: AppTheme,
    view: Entity<MainPaneView>,
    visible_row_ix: usize,
    row_ix: usize,
    min_width: Pixels,
    left_target_width: Pixels,
    right_target_width: Pixels,
    show_line_numbers: bool,
    left_line_no: SharedString,
    right_line_no: SharedString,
    left_bg: gpui::Rgba,
    right_bg: gpui::Rgba,
    left_fg: gpui::Rgba,
    right_fg: gpui::Rgba,
    left_text: SharedString,
    right_text: SharedString,
    left_styled: Option<&CachedDiffStyledText>,
    right_styled: Option<&CachedDiffStyledText>,
    reveal_whitespace_chars: bool,
    chunk_context: Option<ConflictChunkContext>,
    ui_scale_percent: u32,
) -> AnyElement {
    let left_prepared =
        prepare_conflict_text_for_canvas(left_text, left_styled, reveal_whitespace_chars);
    let right_prepared =
        prepare_conflict_text_for_canvas(right_text, right_styled, reveal_whitespace_chars);

    keyed_canvas(
        ("conflict_resolver_split_row_canvas", visible_row_ix),
        move |bounds, _window, _cx| {
            let handle_width = conflict_scaled_px(PANE_RESIZE_HANDLE_PX, ui_scale_percent);
            let (left_col, handle_bounds, right_col) = split_columns_with_widths(
                bounds,
                left_target_width,
                right_target_width,
                handle_width,
            );
            SplitRowPrepaintState {
                left_col,
                handle_bounds,
                right_col,
            }
        },
        move |bounds, prepaint, window, cx| {
            let line_metrics = line_metrics(window);
            let y = center_text_y(bounds, line_metrics.line_height);
            let pad = px_2(window);
            let gap = pad;
            let line_no_w = conflict_line_no_width(ui_scale_percent);
            let clip_bounds = window.content_mask().bounds;
            let left_gutter =
                sticky_gutter_bounds(prepaint.left_col, clip_bounds, pad, gap, line_no_w);
            let right_gutter =
                sticky_gutter_bounds(prepaint.right_col, clip_bounds, pad, gap, line_no_w);

            window.paint_quad(fill(prepaint.left_col, left_bg));
            window.paint_quad(fill(prepaint.right_col, right_bg));

            let divider_x = prepaint.handle_bounds.left()
                + ((prepaint.handle_bounds.size.width - px(1.0)).max(px(0.0)) * 0.5).floor();
            window.paint_quad(fill(
                Bounds::new(
                    point(divider_x, prepaint.handle_bounds.top()),
                    size(px(1.0), prepaint.handle_bounds.size.height),
                ),
                theme.colors.stroke.default,
            ));

            if show_line_numbers {
                window.with_content_mask(
                    Some(ContentMask {
                        bounds: left_gutter,
                    }),
                    |window| {
                        paint_gutter_text(
                            &left_line_no,
                            left_gutter.left() + pad,
                            y,
                            theme.colors.foreground.secondary,
                            line_metrics,
                            window,
                            cx,
                        );
                    },
                );
                window.with_content_mask(
                    Some(ContentMask {
                        bounds: right_gutter,
                    }),
                    |window| {
                        paint_gutter_text(
                            &right_line_no,
                            right_gutter.left() + pad,
                            y,
                            theme.colors.foreground.secondary,
                            line_metrics,
                            window,
                            cx,
                        );
                    },
                );
                paint_gutter_divider(
                    left_gutter,
                    pad,
                    line_no_w,
                    theme.colors.stroke.default,
                    window,
                );
                paint_gutter_divider(
                    right_gutter,
                    pad,
                    line_no_w,
                    theme.colors.stroke.default,
                    window,
                );
            }

            let left_text_bounds =
                split_column_text_bounds(prepaint.left_col, pad, gap, show_line_numbers, line_no_w);
            let right_text_bounds = split_column_text_bounds(
                prepaint.right_col,
                pad,
                gap,
                show_line_numbers,
                line_no_w,
            );
            let left_text_clip = if show_line_numbers {
                text_clip_bounds_behind_gutter(left_text_bounds, left_gutter)
            } else {
                left_text_bounds
            };
            let right_text_clip = if show_line_numbers {
                text_clip_bounds_behind_gutter(right_text_bounds, right_gutter)
            } else {
                right_text_bounds
            };

            window.with_content_mask(
                Some(ContentMask {
                    bounds: left_text_clip,
                }),
                |window| {
                    paint_conflict_text(
                        left_text_bounds,
                        left_fg,
                        y,
                        line_metrics,
                        &left_prepared,
                        window,
                        cx,
                    );
                },
            );
            window.with_content_mask(
                Some(ContentMask {
                    bounds: right_text_clip,
                }),
                |window| {
                    paint_conflict_text(
                        right_text_bounds,
                        right_fg,
                        y,
                        line_metrics,
                        &right_prepared,
                        window,
                        cx,
                    );
                },
            );

            if let Some(chunk_context) = chunk_context.clone() {
                let visible_left = prepaint.left_col.intersect(&clip_bounds);
                let visible_right = prepaint.right_col.intersect(&clip_bounds);
                window.on_mouse_event({
                    let view = view.clone();
                    move |event: &gpui::MouseDownEvent, phase, window, cx| {
                        if phase != DispatchPhase::Bubble {
                            return;
                        }
                        if event.button == gpui::MouseButton::Left {
                            if visible_left.contains(&event.position)
                                || visible_right.contains(&event.position)
                            {
                                // section 30: clicking a conflict block body selects it.
                                let conflict_ix = chunk_context.conflict_ix;
                                view.update(cx, |this, cx| {
                                    this.conflict_resolver_select_conflict(conflict_ix, cx);
                                });
                            }
                            return;
                        }
                        if event.button != gpui::MouseButton::Right {
                            return;
                        }

                        let invoker = if visible_left.contains(&event.position) {
                            Some::<SharedString>(
                                format!(
                                    "resolver_two_way_split_ours_chunk_menu_{}_{}",
                                    chunk_context.conflict_ix, row_ix
                                )
                                .into(),
                            )
                        } else if visible_right.contains(&event.position) {
                            Some::<SharedString>(
                                format!(
                                    "resolver_two_way_split_theirs_chunk_menu_{}_{}",
                                    chunk_context.conflict_ix, row_ix
                                )
                                .into(),
                            )
                        } else {
                            None
                        };

                        let Some(invoker) = invoker else {
                            return;
                        };

                        let conflict_ix = chunk_context.conflict_ix;
                        let has_base = chunk_context.has_base;
                        let selected_choices = chunk_context.selected_choices.clone();
                        let anchor = event.position;
                        view.update(cx, |this, cx| {
                            this.open_conflict_resolver_chunk_context_menu(
                                invoker,
                                conflict_ix,
                                has_base,
                                false,
                                selected_choices,
                                None,
                                anchor,
                                window,
                                cx,
                            );
                            cx.notify();
                        });
                    }
                });
            }
        },
    )
    .h(conflict_row_height(ui_scale_percent))
    .min_w(min_width)
    .w_full()
    .text_xs()
    .whitespace_nowrap()
    .into_any_element()
}

/// Canvas renderer for a single conflict column (used when per-column lists are active).
#[allow(clippy::too_many_arguments)]
pub(super) fn single_column_conflict_canvas(
    theme: AppTheme,
    view: Entity<MainPaneView>,
    id_prefix: &'static str,
    visible_row_ix: usize,
    row_ix: usize,
    min_width: Pixels,
    show_line_numbers: bool,
    line_no: SharedString,
    bg: gpui::Rgba,
    fg: gpui::Rgba,
    text: SharedString,
    styled: Option<&CachedDiffStyledText>,
    reveal_whitespace_chars: bool,
    chunk_context: Option<ConflictChunkContext>,
    chunk_menu_prefix: &'static str,
    is_three_way: bool,
    semantic_nav_target: Option<usize>,
    active_conflict_marker: bool,
    // section 30 split: `Some(selected)` enables drag selection on this row
    // (`selected` paints the highlight); `None` disables it.
    row_selection: Option<bool>,
    // kdiff3 manual diff help: `Some` enables Alt+click marking on this row.
    alignment_mark: Option<AlignmentMarkContext>,
    // Which column this row belongs to, so quick search can find where it
    // painted its text and scroll sideways to a match. `None` for rows outside
    // the three shared columns.
    hitbox_column: Option<ThreeWayColumn>,
    ui_scale_percent: u32,
) -> AnyElement {
    let prepared = prepare_conflict_text_for_canvas(text, styled, reveal_whitespace_chars);
    let row_selected = row_selection == Some(true);
    let alignment_marked = alignment_mark.is_some_and(|mark| mark.marked);

    keyed_canvas(
        (id_prefix, visible_row_ix),
        move |bounds, _window, _cx| bounds,
        move |bounds, _prepaint, window, cx| {
            let line_metrics = line_metrics(window);
            let y = center_text_y(bounds, line_metrics.line_height);
            let pad = px_2(window);
            let gap = pad;
            let line_no_w = conflict_line_no_width(ui_scale_percent);
            let clip_bounds = window.content_mask().bounds;
            let gutter_bounds = sticky_gutter_bounds(bounds, clip_bounds, pad, gap, line_no_w);

            window.paint_quad(fill(bounds, bg));

            // section 30 split: highlight rows in the drag selection.
            if row_selected {
                window.paint_quad(fill(
                    bounds,
                    with_alpha(
                        theme.colors.accent.foreground,
                        if theme.is_dark { 0.20 } else { 0.14 },
                    ),
                ));
            }

            // kdiff3 manual diff help: marked lines use the warning hue so they
            // stay distinguishable from an accent-tinted split selection, which
            // can be active in the same columns at the same time.
            if alignment_marked {
                window.paint_quad(fill(
                    bounds,
                    with_alpha(
                        theme.colors.status.warning.foreground,
                        if theme.is_dark { 0.22 } else { 0.16 },
                    ),
                ));
            }

            // section 30: mark the active conflict's rows with an accent bar so a
            // click/keyboard selection is visible in the source columns.
            if active_conflict_marker {
                let bar = gpui::Bounds::new(
                    point(
                        if show_line_numbers {
                            gutter_bounds.left()
                        } else {
                            bounds.left()
                        },
                        bounds.top(),
                    ),
                    gpui::size(
                        conflict_scaled_px(CONFLICT_ROW_ACCENT_BAR_WIDTH_PX, ui_scale_percent),
                        bounds.size.height,
                    ),
                );
                window.paint_quad(fill(bar, theme.colors.accent.foreground));
            }

            if show_line_numbers {
                window.with_content_mask(
                    Some(ContentMask {
                        bounds: gutter_bounds,
                    }),
                    |window| {
                        paint_gutter_text(
                            &line_no,
                            gutter_bounds.left() + pad,
                            y,
                            theme.colors.foreground.secondary,
                            line_metrics,
                            window,
                            cx,
                        );
                    },
                );
                paint_gutter_divider(
                    gutter_bounds,
                    pad,
                    line_no_w,
                    theme.colors.stroke.default,
                    window,
                );
            }

            let text_bounds =
                split_column_text_bounds(bounds, pad, gap, show_line_numbers, line_no_w);
            let text_clip_bounds = if show_line_numbers {
                text_clip_bounds_behind_gutter(text_bounds, gutter_bounds)
            } else {
                text_bounds
            };
            window.with_content_mask(
                Some(ContentMask {
                    bounds: text_clip_bounds,
                }),
                |window| {
                    if let Some(layout) =
                        paint_conflict_text(text_bounds, fg, y, line_metrics, &prepared, window, cx)
                        && let Some(column) = hitbox_column
                    {
                        view.update(cx, |this, _cx| {
                            this.set_conflict_text_hitbox(
                                visible_row_ix,
                                column,
                                crate::view::mod_helpers::ConflictTextHitbox {
                                    bounds: text_bounds,
                                    layout,
                                },
                            );
                        });
                    }
                },
            );

            // kdiff3 manual diff help: Alt+click marks this line for the next
            // Ctrl+Y. Registered outside the conflict-block handler below so
            // context rows can be marked too.
            if let Some(mark) = alignment_mark
                && let Some(side_line) = mark.side_line
            {
                let visible = bounds.intersect(&clip_bounds);
                let view = view.clone();
                window.on_mouse_event(move |event: &gpui::MouseDownEvent, phase, _window, cx| {
                    if phase != DispatchPhase::Bubble
                        || event.button != gpui::MouseButton::Left
                        || !event.modifiers.alt
                        || !visible.contains(&event.position)
                    {
                        return;
                    }
                    view.update(cx, |this, cx| {
                        this.conflict_resolver_mark_alignment_line(
                            mark.column,
                            side_line,
                            event.modifiers.shift,
                            cx,
                        );
                    });
                });
            }

            if chunk_context.is_none()
                && let Some(target_index) = semantic_nav_target
            {
                let visible = bounds.intersect(&clip_bounds);
                let view = view.clone();
                window.on_mouse_event(move |event: &gpui::MouseDownEvent, phase, _window, cx| {
                    if phase != DispatchPhase::Bubble
                        || event.button != gpui::MouseButton::Left
                        || event.modifiers.alt
                        || !visible.contains(&event.position)
                    {
                        return;
                    }
                    view.update(cx, |this, cx| {
                        this.conflict_jump_to_nav_target(target_index, cx);
                    });
                });
            }

            if let Some(chunk_context) = chunk_context.clone() {
                let visible = bounds.intersect(&clip_bounds);
                if row_selection.is_some() {
                    // section 30 split: extend the drag as the cursor passes over
                    // this row (begin happens on left-down below).
                    let view = view.clone();
                    let conflict_ix = chunk_context.conflict_ix;
                    window.on_mouse_event(
                        move |event: &gpui::MouseMoveEvent, phase, _window, cx| {
                            if phase != DispatchPhase::Bubble {
                                return;
                            }
                            if !visible.contains(&event.position) {
                                return;
                            }
                            view.update(cx, |this, cx| {
                                this.conflict_resolver_extend_row_selection(
                                    conflict_ix,
                                    row_ix,
                                    cx,
                                );
                            });
                        },
                    );
                }
                window.on_mouse_event({
                    let view = view.clone();
                    move |event: &gpui::MouseDownEvent, phase, window, cx| {
                        if phase != DispatchPhase::Bubble {
                            return;
                        }
                        if !visible.contains(&event.position) {
                            return;
                        }
                        if event.button == gpui::MouseButton::Left {
                            // Alt+click belongs to the manual-alignment handler
                            // above; it must not also start a split drag.
                            if event.modifiers.alt {
                                return;
                            }
                            let conflict_ix = chunk_context.conflict_ix;
                            view.update(cx, |this, cx| {
                                if row_selection.is_some() {
                                    if event.modifiers.shift || event.modifiers.control {
                                        this.conflict_resolver_click_row_selection(
                                            conflict_ix,
                                            row_ix,
                                            event.modifiers,
                                            cx,
                                        );
                                    } else {
                                        // section 30 split: begin a drag selection (also
                                        // selects the block).
                                        this.conflict_resolver_begin_row_selection(
                                            conflict_ix,
                                            row_ix,
                                            cx,
                                        );
                                    }
                                } else {
                                    // section 30: clicking a conflict block body selects it.
                                    this.conflict_resolver_select_conflict(conflict_ix, cx);
                                }
                            });
                            return;
                        }
                        if event.button != gpui::MouseButton::Right {
                            return;
                        }
                        let invoker: SharedString = format!(
                            "{}_{}_{}",
                            chunk_menu_prefix, chunk_context.conflict_ix, row_ix
                        )
                        .into();
                        let anchor = event.position;
                        view.update(cx, |this, cx| {
                            this.open_conflict_resolver_chunk_context_menu(
                                invoker,
                                chunk_context.conflict_ix,
                                chunk_context.has_base,
                                is_three_way,
                                chunk_context.selected_choices.clone(),
                                None,
                                anchor,
                                window,
                                cx,
                            );
                            cx.notify();
                        });
                    }
                });
            }
        },
    )
    .h(conflict_row_height(ui_scale_percent))
    .min_w(min_width)
    .w_full()
    .text_xs()
    .whitespace_nowrap()
    .into_any_element()
}

#[derive(Clone, Debug)]
struct SplitRowPrepaintState {
    left_col: Bounds<Pixels>,
    handle_bounds: Bounds<Pixels>,
    right_col: Bounds<Pixels>,
}

#[derive(Clone, Debug)]
struct PreparedConflictText {
    text: SharedString,
    highlights: HighlightSpans,
    text_hash: u64,
    highlights_hash: u64,
}

fn prepare_conflict_text_for_canvas(
    text: SharedString,
    styled: Option<&CachedDiffStyledText>,
    reveal_whitespace_chars: bool,
) -> PreparedConflictText {
    let Some(styled) = styled else {
        let display = if reveal_whitespace_chars {
            whitespace_visible_line_text(text.as_ref())
        } else {
            text
        };
        return PreparedConflictText {
            text_hash: hash_text(display.as_ref()),
            text: display,
            highlights: empty_highlights(),
            highlights_hash: 0,
        };
    };

    if styled.highlights.is_empty() {
        let display = if reveal_whitespace_chars {
            whitespace_visible_line_text(text.as_ref())
        } else {
            styled.text.clone()
        };
        return PreparedConflictText {
            text_hash: hash_text(display.as_ref()),
            text: display,
            highlights: empty_highlights(),
            highlights_hash: 0,
        };
    }

    if reveal_whitespace_chars {
        let visible = whitespace_visible_line_styled_text_for_raw(styled, text.as_ref());
        return PreparedConflictText {
            text: visible.text,
            highlights: visible.highlights,
            text_hash: visible.text_hash,
            highlights_hash: visible.highlights_hash,
        };
    }

    PreparedConflictText {
        text: styled.text.clone(),
        highlights: Arc::clone(&styled.highlights),
        text_hash: styled.text_hash,
        highlights_hash: styled.highlights_hash,
    }
}

fn hash_text(text: &str) -> u64 {
    let mut hasher = FxHasher::default();
    text.hash(&mut hasher);
    hasher.finish()
}

#[cfg(test)]
fn whitespace_visible_text(text: &str) -> SharedString {
    whitespace_visible_text_and_highlights(text, &[]).0
}

#[cfg(test)]
fn whitespace_visible_text_and_highlights(
    text: &str,
    highlights: &[(Range<usize>, HighlightStyle)],
) -> (SharedString, Vec<(Range<usize>, HighlightStyle)>) {
    let mut out = String::with_capacity(text.len());
    let mut byte_map = vec![0usize; text.len() + 1];

    for (start, ch) in text.char_indices() {
        byte_map[start] = out.len();
        match ch {
            ' ' => out.push('\u{00B7}'),
            '\t' => out.push('\u{2192}'),
            '\r' => out.push('\u{240D}'),
            '\n' => out.push('\u{21B5}'),
            _ if ch.is_whitespace() => out.push('\u{2420}'),
            _ => out.push(ch),
        }
        let end = start + ch.len_utf8();
        let mapped_end = out.len();
        for mapped in byte_map.iter_mut().take(end + 1).skip(start + 1) {
            *mapped = mapped_end;
        }
    }

    let mut remapped = Vec::with_capacity(highlights.len());
    for (range, style) in highlights {
        let start = *byte_map.get(range.start).unwrap_or(&out.len());
        let end = *byte_map.get(range.end).unwrap_or(&out.len());
        if start < end {
            remapped.push((start..end, *style));
        }
    }

    (out.into(), remapped)
}

#[derive(Clone, Copy, Debug)]
struct LineMetrics {
    font_size: Pixels,
    line_height: Pixels,
}

fn diff_text_style(window: &Window) -> TextStyle {
    let mut style = window.text_style();
    style.font_weight = FontWeight::NORMAL;
    style
}

fn line_metrics(window: &Window) -> LineMetrics {
    let style = diff_text_style(window);
    let font_size = style.font_size.to_pixels(window.rem_size()) * DIFF_FONT_SCALE;
    let line_height = style
        .line_height
        .to_pixels(font_size.into(), window.rem_size());
    LineMetrics {
        font_size,
        line_height,
    }
}

fn center_text_y(bounds: Bounds<Pixels>, line_height: Pixels) -> Pixels {
    let extra = (bounds.size.height - line_height).max(px(0.0));
    bounds.top() + extra * 0.5
}

fn px_2(window: &Window) -> Pixels {
    window.rem_size() * 0.5
}

fn split_columns_with_widths(
    bounds: Bounds<Pixels>,
    left_target_width: Pixels,
    right_target_width: Pixels,
    handle_width: Pixels,
) -> (Bounds<Pixels>, Bounds<Pixels>, Bounds<Pixels>) {
    let width = bounds.size.width.max(px(0.0));
    let mut left_w = left_target_width
        .min((width - handle_width).max(px(0.0)))
        .max(px(0.0));

    let mut right_w = (width - left_w - handle_width).max(px(0.0));
    if right_w < right_target_width {
        let deficit = right_target_width - right_w;
        let left_shrink = left_w.min(deficit);
        left_w -= left_shrink;
        right_w = (right_w + left_shrink).max(px(0.0));
    }

    let left = Bounds::new(bounds.origin, size(left_w, bounds.size.height));
    let handle = Bounds::new(
        point(bounds.left() + left_w, bounds.top()),
        size(handle_width, bounds.size.height),
    );
    let right = Bounds::new(
        point(handle.right(), bounds.top()),
        size(right_w, bounds.size.height),
    );
    (left, handle, right)
}

#[cfg(test)]
type ThreeWayColumnBounds = (
    Bounds<Pixels>,
    Bounds<Pixels>,
    Bounds<Pixels>,
    Bounds<Pixels>,
    Bounds<Pixels>,
);

#[cfg(test)]
fn three_way_columns_with_widths(
    bounds: Bounds<Pixels>,
    base_target_width: Pixels,
    ours_target_width: Pixels,
    theirs_target_width: Pixels,
    handle_width: Pixels,
) -> ThreeWayColumnBounds {
    let width = bounds.size.width.max(px(0.0));
    let handles_total = handle_width * 2.0;
    let available = (width - handles_total).max(px(0.0));

    let min_total = base_target_width + ours_target_width + theirs_target_width;
    let (base_w, ours_w, theirs_w) = if available >= min_total {
        (
            base_target_width.max(px(0.0)),
            ours_target_width.max(px(0.0)),
            (available - base_target_width - ours_target_width).max(px(0.0)),
        )
    } else if available <= px(0.0) {
        (px(0.0), px(0.0), px(0.0))
    } else {
        let scale = available / min_total.max(px(1.0));
        let mut base = (base_target_width * scale).max(px(0.0));
        let mut ours = (ours_target_width * scale).max(px(0.0));
        let mut theirs = (available - base - ours).max(px(0.0));

        let used = base + ours + theirs;
        let slack = (available - used).max(px(0.0));
        theirs += slack;

        if theirs < px(0.0) {
            theirs = px(0.0);
        }

        base = base.max(px(0.0));
        ours = ours.max(px(0.0));
        (base, ours, theirs)
    };

    let base_col = Bounds::new(bounds.origin, size(base_w, bounds.size.height));
    let first_handle = Bounds::new(
        point(bounds.left() + base_w, bounds.top()),
        size(handle_width, bounds.size.height),
    );
    let ours_col = Bounds::new(
        point(first_handle.right(), bounds.top()),
        size(ours_w, bounds.size.height),
    );
    let second_handle = Bounds::new(
        point(ours_col.right(), bounds.top()),
        size(handle_width, bounds.size.height),
    );
    let theirs_col = Bounds::new(
        point(second_handle.right(), bounds.top()),
        size(theirs_w, bounds.size.height),
    );

    (base_col, first_handle, ours_col, second_handle, theirs_col)
}

fn split_column_text_bounds(
    col: Bounds<Pixels>,
    pad: Pixels,
    gap: Pixels,
    show_line_numbers: bool,
    line_no_width: Pixels,
) -> Bounds<Pixels> {
    let gutter_width = if show_line_numbers {
        line_no_width + gap
    } else {
        px(0.0)
    };
    let left = col.left() + pad + gutter_width;
    let width = (col.size.width - pad * 2.0 - gutter_width).max(px(0.0));
    Bounds::new(point(left, col.top()), size(width, col.size.height))
}

/// Paint the vertical divider between the line-number gutter and the code,
/// matching the div path's `conflict_diff_line_number_cell` right border. Sits
/// at the right edge of the number cell (before the gap), so it stays pinned
/// with the sticky gutter as the column scrolls horizontally.
fn paint_gutter_divider(
    gutter_bounds: Bounds<Pixels>,
    pad: Pixels,
    line_no_width: Pixels,
    color: gpui::Rgba,
    window: &mut Window,
) {
    let x = gutter_bounds.left() + pad + line_no_width;
    window.paint_quad(fill(
        Bounds::new(
            point(x, gutter_bounds.top()),
            size(px(1.0), gutter_bounds.size.height),
        ),
        color,
    ));
}

/// Keep the line-number gutter at the visible edge of a horizontally scrolled
/// column. The row itself still moves so its measured width and scrollbar range
/// remain unchanged.
fn sticky_gutter_bounds(
    column_bounds: Bounds<Pixels>,
    clip_bounds: Bounds<Pixels>,
    pad: Pixels,
    gap: Pixels,
    line_no_width: Pixels,
) -> Bounds<Pixels> {
    let visible_column = column_bounds.intersect(&clip_bounds);
    let width = (pad + line_no_width + gap).min(visible_column.size.width);
    Bounds::new(
        point(visible_column.left(), column_bounds.top()),
        size(width.max(px(0.0)), column_bounds.size.height),
    )
}

/// Clip the moving source text at the pinned gutter without changing the
/// text's paint origin. This makes content pass behind the gutter instead of
/// shifting as the horizontal offset changes.
fn text_clip_bounds_behind_gutter(
    text_bounds: Bounds<Pixels>,
    gutter_bounds: Bounds<Pixels>,
) -> Bounds<Pixels> {
    let left = text_bounds.left().max(gutter_bounds.right());
    Bounds::new(
        point(left, text_bounds.top()),
        size(
            (text_bounds.right() - left).max(px(0.0)),
            text_bounds.size.height,
        ),
    )
}

fn paint_gutter_text(
    text: &SharedString,
    x: Pixels,
    y: Pixels,
    color: gpui::Rgba,
    metrics: LineMetrics,
    window: &mut Window,
    cx: &mut App,
) {
    if text.is_empty() {
        return;
    }
    let mut style = diff_text_style(window);
    style.color = color.into_color();
    let key = {
        let mut hasher = FxHasher::default();
        text.as_ref().hash(&mut hasher);
        metrics.font_size.hash(&mut hasher);
        style.font_family.hash(&mut hasher);
        style.font_weight.hash(&mut hasher);
        color.red.to_bits().hash(&mut hasher);
        color.green.to_bits().hash(&mut hasher);
        color.blue.to_bits().hash(&mut hasher);
        color.alpha.to_bits().hash(&mut hasher);
        hasher.finish()
    };

    let shaped = GUTTER_TEXT_LAYOUT_CACHE.with(|cache| cache.borrow_mut().get(&key).cloned());
    let shaped = shaped.unwrap_or_else(|| {
        let run = style.to_run(text.len());
        let shaped = window
            .text_system()
            .shape_line(text.clone(), metrics.font_size, &[run], None);

        GUTTER_TEXT_LAYOUT_CACHE.with(|cache| {
            cache.borrow_mut().put(key, shaped.clone());
        });

        shaped
    });
    let _ = shaped.paint(
        point(x, y),
        metrics.line_height,
        gpui::TextAlign::Left,
        None,
        window,
        cx,
    );
}

/// Paints one conflict row's text and hands back the line it shaped.
///
/// The caller keeps that line so quick search can measure where a match sits
/// along the row — the columns scroll sideways, and a hit past the right edge
/// has to be scrolled to like any other.
fn paint_conflict_text(
    bounds: Bounds<Pixels>,
    fg: gpui::Rgba,
    y: Pixels,
    metrics: LineMetrics,
    prepared: &PreparedConflictText,
    window: &mut Window,
    cx: &mut App,
) -> Option<gpui::ShapedLine> {
    if prepared.text.is_empty() {
        return None;
    }

    let mut base_style = diff_text_style(window);
    base_style.color = fg.into_color();
    base_style.white_space = gpui::WhiteSpace::Nowrap;
    base_style.text_overflow = None;

    let layout = ensure_layout_cached(prepared, &base_style, fg, metrics, window);

    if prepared.highlights.is_empty() {
        let _ = layout.paint(
            point(bounds.left(), y),
            metrics.line_height,
            gpui::TextAlign::Left,
            None,
            window,
            cx,
        );
        return Some(layout);
    }

    let _ = layout.paint_background(
        point(bounds.left(), y),
        metrics.line_height,
        gpui::TextAlign::Left,
        None,
        window,
        cx,
    );
    let _ = layout.paint(
        point(bounds.left(), y),
        metrics.line_height,
        gpui::TextAlign::Left,
        None,
        window,
        cx,
    );
    Some(layout)
}

fn ensure_layout_cached(
    prepared: &PreparedConflictText,
    base_style: &TextStyle,
    fg: gpui::Rgba,
    metrics: LineMetrics,
    window: &mut Window,
) -> gpui::ShapedLine {
    let key = conflict_layout_key(prepared, base_style, fg, metrics);
    if let Some(layout) =
        CONFLICT_TEXT_LAYOUT_CACHE.with(|cache| cache.borrow_mut().get(&key).cloned())
    {
        return layout;
    }

    let shaped = if prepared.highlights.is_empty() {
        let run = base_style.to_run(prepared.text.len());
        window
            .text_system()
            .shape_line(prepared.text.clone(), metrics.font_size, &[run], None)
    } else {
        let runs = compute_runs(
            prepared.text.as_ref(),
            base_style,
            prepared.highlights.as_ref(),
        );
        window
            .text_system()
            .shape_line(prepared.text.clone(), metrics.font_size, &runs, None)
    };

    CONFLICT_TEXT_LAYOUT_CACHE.with(|cache| {
        cache.borrow_mut().put(key, shaped.clone());
    });

    shaped
}

fn conflict_layout_key(
    prepared: &PreparedConflictText,
    base_style: &TextStyle,
    fg: gpui::Rgba,
    metrics: LineMetrics,
) -> u64 {
    let mut hasher = FxHasher::default();
    prepared.text_hash.hash(&mut hasher);
    prepared.highlights_hash.hash(&mut hasher);
    metrics.font_size.hash(&mut hasher);
    base_style.font_family.hash(&mut hasher);
    base_style.font_weight.hash(&mut hasher);
    fg.red.to_bits().hash(&mut hasher);
    fg.green.to_bits().hash(&mut hasher);
    fg.blue.to_bits().hash(&mut hasher);
    fg.alpha.to_bits().hash(&mut hasher);
    hasher.finish()
}

fn compute_runs(
    text: &str,
    default_style: &TextStyle,
    highlights: &[(Range<usize>, HighlightStyle)],
) -> Vec<TextRun> {
    crate::text_runs::text_runs_for_highlights(text, default_style, highlights)
}

fn empty_highlights() -> HighlightSpans {
    static EMPTY: OnceLock<HighlightSpans> = OnceLock::new();
    Arc::clone(EMPTY.get_or_init(|| Arc::from(Vec::new())))
}

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

    #[test]
    fn prepare_text_cell_applies_whitespace_when_no_styled_text() {
        let prepared = prepare_conflict_text_for_canvas("a b\t".into(), None, true);
        assert_eq!(prepared.text.as_ref(), "a·b→↵");
        assert!(prepared.highlights.is_empty());
    }

    #[test]
    fn three_way_layout_grows_last_column_when_space_allows() {
        let bounds = Bounds::new(point(px(0.0), px(0.0)), size(px(300.0), px(20.0)));
        let (base, _, ours, _, theirs) =
            three_way_columns_with_widths(bounds, px(70.0), px(70.0), px(70.0), px(10.0));

        assert_eq!(base.size.width, px(70.0));
        assert_eq!(ours.size.width, px(70.0));
        assert_eq!(theirs.size.width, px(140.0));
    }

    #[test]
    fn three_way_layout_scales_columns_when_space_is_tight() {
        let bounds = Bounds::new(point(px(0.0), px(0.0)), size(px(120.0), px(20.0)));
        let (base, _, ours, _, theirs) =
            three_way_columns_with_widths(bounds, px(70.0), px(70.0), px(70.0), px(10.0));
        let available = px(100.0);

        assert!(
            (base.size.width + ours.size.width + theirs.size.width - available).abs() < px(0.01)
        );
        assert!(base.size.width > px(0.0));
        assert!(ours.size.width > px(0.0));
        assert!(theirs.size.width > px(0.0));
    }

    #[test]
    fn split_layout_preserves_right_target_by_shrinking_left() {
        let bounds = Bounds::new(point(px(0.0), px(0.0)), size(px(200.0), px(20.0)));
        let handle_width = px(10.0);
        let (left, handle, right) =
            split_columns_with_widths(bounds, px(120.0), px(120.0), handle_width);

        assert_eq!(left.size.width, px(70.0));
        assert_eq!(handle.size.width, handle_width);
        assert_eq!(right.size.width, px(120.0));
        assert_eq!(
            left.size.width + handle.size.width + right.size.width,
            bounds.size.width
        );
    }

    #[test]
    fn line_number_gutter_stays_at_clip_edge_during_horizontal_scroll() {
        let column = Bounds::new(point(px(-120.0), px(10.0)), size(px(500.0), px(20.0)));
        let clip = Bounds::new(point(px(0.0), px(0.0)), size(px(300.0), px(200.0)));

        let gutter = sticky_gutter_bounds(column, clip, px(8.0), px(8.0), px(38.0));

        assert_eq!(gutter.left(), clip.left());
        assert_eq!(gutter.size.width, px(54.0));
        assert_eq!(gutter.top(), column.top());
    }

    #[test]
    fn moving_text_is_clipped_behind_sticky_line_number_gutter() {
        let text = Bounds::new(point(px(-66.0), px(10.0)), size(px(430.0), px(20.0)));
        let gutter = Bounds::new(point(px(0.0), px(10.0)), size(px(54.0), px(20.0)));

        let text_clip = text_clip_bounds_behind_gutter(text, gutter);

        assert_eq!(text_clip.left(), gutter.right());
        assert_eq!(text_clip.right(), text.right());
    }

    #[test]
    fn prepare_text_cell_remaps_highlighted_styled_text_for_whitespace() {
        let style = gpui::HighlightStyle::default();
        let styled = CachedDiffStyledText {
            text: "a b".into(),
            highlights: Arc::from(vec![(1..3, style)]),
            highlights_hash: 11,
            text_hash: 7,
        };

        let prepared = prepare_conflict_text_for_canvas("a b".into(), Some(&styled), true);
        assert_eq!(prepared.text.as_ref(), "a·b↵");
        assert_eq!(prepared.highlights.len(), 1);
        assert_eq!(prepared.highlights[0].0, 1..4);
        assert_eq!(prepared.text_hash, hash_text("a·b↵"));
        assert_ne!(prepared.highlights_hash, 0);
    }

    #[test]
    fn prepare_text_cell_applies_whitespace_for_unhighlighted_styled_text() {
        let styled = CachedDiffStyledText {
            text: "a b\t".into(),
            highlights: empty_highlights(),
            highlights_hash: 0,
            text_hash: 1,
        };

        let prepared = prepare_conflict_text_for_canvas("a b\t".into(), Some(&styled), true);
        assert_eq!(prepared.text.as_ref(), "a·b→↵");
        assert!(prepared.highlights.is_empty());
        assert_eq!(prepared.highlights_hash, 0);
    }

    #[test]
    fn whitespace_visible_text_marks_all_whitespace_kinds() {
        let display = whitespace_visible_text(" \t\r\n");
        assert_eq!(display.as_ref(), "·→␍↵");
    }
}
