use crate::git::pack::PackIndex;
use ratatui::style::{Color, Modifier, Style};
use ratatui::text::Line;
pub struct FanoutFormatter<'a> {
pack_index: &'a PackIndex,
}
impl<'a> FanoutFormatter<'a> {
pub fn new(pack_index: &'a PackIndex) -> Self {
Self { pack_index }
}
fn calculate_fanout_byte_position(&self, fanout_index: usize) -> u64 {
let header_size = 8u64; let fanout_start = header_size;
fanout_start + (fanout_index as u64 * 4) + 1
}
pub fn format_fanout_table(&self, lines: &mut Vec<Line<'static>>) {
lines.push(Line::styled(
"FAN-OUT TABLE",
Style::default().add_modifier(Modifier::BOLD),
));
lines.push(Line::from("─".repeat(20)));
lines.push(Line::from(""));
lines.push(Line::from(
"Helps to minimize the number of objects to search through.",
));
lines.push(Line::from(""));
lines.push(Line::from(
"Structure: 256 entries (one per possible first byte 0x00-0xFF)",
));
lines.push(Line::from(
"Each entry: Cumulative count of objects with first byte ≤ index",
));
lines.push(Line::from(""));
self.add_distribution_stats(lines);
self.add_sample_entries(lines);
self.add_search_explanation(lines);
}
fn add_distribution_stats(&self, lines: &mut Vec<Line<'static>>) {
lines.push(Line::styled(
"Distribution Analysis:",
Style::default().add_modifier(Modifier::BOLD),
));
let mut non_empty_buckets = 0;
let mut max_bucket_size = 0;
let mut prev_count = 0;
for &count in &self.pack_index.fan_out {
let bucket_size = count - prev_count;
if bucket_size > 0 {
non_empty_buckets += 1;
max_bucket_size = max_bucket_size.max(bucket_size);
}
prev_count = count;
}
lines.push(Line::from(format!(
"• Non-empty buckets: {} / 256",
non_empty_buckets
)));
lines.push(Line::from(format!(
"• Largest bucket: {} objects",
max_bucket_size
)));
lines.push(Line::from(""));
}
fn add_sample_entries(&self, lines: &mut Vec<Line<'static>>) {
lines.push(Line::styled(
"Complete Fanout Table:",
Style::default().add_modifier(Modifier::BOLD),
));
lines.push(Line::from(""));
lines.push(Line::from(
"┌──────┬──────┬─────────────┬─────────┬────────┐",
));
lines.push(Line::styled(
"│ Byte │ Index│ Hex Value │ Decimal │ Bucket │",
Style::default().add_modifier(Modifier::BOLD),
));
lines.push(Line::from(
"├──────┼──────┼─────────────┼─────────┼────────┤",
));
let entries_to_show = self.determine_entries_to_show();
let mut prev_count = 0;
let mut last_shown_index = None;
for (i, show_entry) in entries_to_show.iter().enumerate() {
let count = self.pack_index.fan_out[i];
if *show_entry {
if let Some(last_idx) = last_shown_index
&& i > last_idx + 1
{
let line_parts = vec![
("│ ".to_string(), Style::default()),
("...".to_string(), Style::default().fg(Color::Gray)),
(" │ ".to_string(), Style::default()),
("...".to_string(), Style::default().fg(Color::Gray)),
(" │ ".to_string(), Style::default()),
("....".to_string(), Style::default().fg(Color::Gray)),
(" │ ".to_string(), Style::default()),
("...".to_string(), Style::default().fg(Color::Gray)),
(" │ ".to_string(), Style::default()),
("...".to_string(), Style::default().fg(Color::Gray)),
(" │".to_string(), Style::default()),
];
let mut line = Line::default();
for (text, style) in line_parts {
line.spans.push(ratatui::text::Span::styled(text, style));
}
lines.push(line);
}
let bucket_size = count - prev_count;
let byte_pos = self.calculate_fanout_byte_position(i);
let hex_value = self.format_hex_value(count);
if bucket_size > 0 {
let line_parts = vec![
("│ ".to_string(), Style::default()),
(format!("{:4}", byte_pos), Style::default().fg(Color::Cyan)),
(" │ ".to_string(), Style::default()),
(format!("0x{:02x}", i), Style::default().fg(Color::Cyan)),
(" │ ".to_string(), Style::default()),
(hex_value, Style::default().fg(Color::Blue)),
(" │ ".to_string(), Style::default()),
(format!("{:7}", count), Style::default().fg(Color::Cyan)),
(" │ ".to_string(), Style::default()),
(
format!("{:6}", bucket_size),
Style::default().fg(Color::Cyan),
),
(" │".to_string(), Style::default()),
];
let mut line = Line::default();
for (text, style) in line_parts {
line.spans.push(ratatui::text::Span::styled(text, style));
}
lines.push(line);
} else {
let line_parts = vec![
("│ ".to_string(), Style::default()),
(format!("{:4}", byte_pos), Style::default().fg(Color::Gray)),
(" │ ".to_string(), Style::default()),
(format!("0x{:02x}", i), Style::default().fg(Color::Gray)),
(" │ ".to_string(), Style::default()),
(hex_value, Style::default().fg(Color::Gray)),
(" │ ".to_string(), Style::default()),
(format!("{:7}", count), Style::default().fg(Color::Gray)),
(" │ ".to_string(), Style::default()),
(
format!("{:6}", bucket_size),
Style::default().fg(Color::Gray),
),
(" │".to_string(), Style::default()),
];
let mut line = Line::default();
for (text, style) in line_parts {
line.spans.push(ratatui::text::Span::styled(text, style));
}
lines.push(line);
}
last_shown_index = Some(i);
}
prev_count = count;
}
lines.push(Line::from(
"└──────┴──────┴─────────────┴─────────┴────────┘",
));
lines.push(Line::from(""));
self.add_table_legend(lines);
}
fn add_table_legend(&self, lines: &mut Vec<Line<'static>>) {
lines.push(Line::styled(
"Column Legend:",
Style::default().add_modifier(Modifier::BOLD),
));
lines.push(Line::from(""));
lines.push(Line::styled(
"• Byte:",
Style::default()
.add_modifier(Modifier::BOLD)
.fg(Color::Yellow),
));
lines.push(Line::from(
" Position in the .idx file of the first byte out of 4 for the entry",
));
lines.push(Line::from(""));
lines.push(Line::styled(
"• Index:",
Style::default()
.add_modifier(Modifier::BOLD)
.fg(Color::Yellow),
));
lines.push(Line::from(
" Corresponds to the first byte value (0x00-0xFF) of the object",
));
lines.push(Line::from(""));
lines.push(Line::styled(
"• Hex Value:",
Style::default()
.add_modifier(Modifier::BOLD)
.fg(Color::Yellow),
));
lines.push(Line::from(" raw value of the fanout table entry."));
lines.push(Line::from(
" represents the number of objects with first byte ≤ this index.",
));
lines.push(Line::from(""));
lines.push(Line::styled(
"• Decimal:",
Style::default()
.add_modifier(Modifier::BOLD)
.fg(Color::Yellow),
));
lines.push(Line::from(
" Converted cumulative count of objects with first byte ≤ this index",
));
lines.push(Line::from(""));
lines.push(Line::styled(
"• Bucket:",
Style::default()
.add_modifier(Modifier::BOLD)
.fg(Color::Yellow),
));
lines.push(Line::from(
" Number of objects with first byte exactly equal to this index",
));
lines.push(Line::from(
" (calculated as: current_count - previous_count)",
));
lines.push(Line::from(""));
lines.push(Line::styled(
"Color Coding:",
Style::default().add_modifier(Modifier::BOLD),
));
lines.push(Line::styled(
"• Blue: Raw data from the fanout table (hex values)",
Style::default().fg(Color::Blue),
));
lines.push(Line::styled(
"• Cyan: Calculated values for non-empty buckets",
Style::default().fg(Color::Cyan),
));
lines.push(Line::styled(
"• Gray: Empty buckets (no objects)",
Style::default().fg(Color::Gray),
));
lines.push(Line::from(""));
}
fn determine_entries_to_show(&self) -> Vec<bool> {
let mut show_entry = vec![false; 256];
let mut prev_count = 0;
let mut active_entries = vec![false; 256];
for (i, &count) in self.pack_index.fan_out.iter().enumerate() {
let bucket_size = count - prev_count;
if bucket_size > 0 {
active_entries[i] = true;
}
prev_count = count;
}
show_entry[0] = true;
show_entry[1] = true;
show_entry[254] = true;
show_entry[255] = true;
for i in 0..256 {
if active_entries[i] {
show_entry[i] = true;
if i > 0 {
show_entry[i - 1] = true;
}
if i < 255 {
show_entry[i + 1] = true;
}
}
}
show_entry
}
fn format_hex_value(&self, value: u32) -> String {
format!(
"{:02x} {:02x} {:02x} {:02x}",
(value >> 24) & 0xff,
(value >> 16) & 0xff,
(value >> 8) & 0xff,
value & 0xff
)
}
fn add_search_explanation(&self, lines: &mut Vec<Line<'static>>) {
lines.push(Line::styled(
"Binary Search Optimization:",
Style::default().add_modifier(Modifier::BOLD),
));
lines.push(Line::from(""));
lines.push(Line::from("To find object with SHA-1 starting with 0x42:"));
lines.push(Line::from(""));
let first_byte = 0x42;
let start_idx = if first_byte == 0 {
0
} else {
self.pack_index.fan_out[first_byte - 1]
};
let end_idx = self.pack_index.fan_out[first_byte];
let range_size = end_idx - start_idx;
lines.push(Line::styled(
format!("1. Check fan_out[0x41] = {} (start of range)", start_idx),
Style::default().fg(Color::Yellow),
));
lines.push(Line::styled(
format!("2. Check fan_out[0x42] = {} (end of range)", end_idx),
Style::default().fg(Color::Yellow),
));
lines.push(Line::styled(
format!(
"3. Binary search within {} objects (indices {}-{})",
range_size,
start_idx,
end_idx - 1
),
Style::default().fg(Color::Green),
));
lines.push(Line::from(""));
let total_objects = self.pack_index.object_count();
let search_reduction = if total_objects > 0 {
(range_size as f64 / total_objects as f64) * 100.0
} else {
0.0
};
lines.push(Line::styled(
format!(
"Search space reduced to {:.1}% of total objects!",
search_reduction
),
Style::default()
.fg(Color::Green)
.add_modifier(Modifier::BOLD),
));
lines.push(Line::from(""));
}
}