use bao_tree::{ChunkNum, ChunkRanges};
use decdn_protocol::{Coverage, discovery_block_bytes, num_blocks};
use std::collections::{HashMap, HashSet};
const BAO_CHUNK_BYTES: u64 = 1024;
#[allow(clippy::missing_const_for_fn)]
fn chunks_per_block() -> u64 {
discovery_block_bytes() / BAO_CHUNK_BYTES
}
#[derive(Debug, Clone)]
pub struct SourceCoverage {
pub source_ix: usize,
pub coverage: Coverage,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct CoveredRun {
pub offset: u64,
pub len: u64,
pub source_ix: usize,
}
fn block_offset(block: u32) -> u64 {
u64::from(block) * discovery_block_bytes()
}
fn gap_extent(gap: &ChunkRanges, lo: u64, hi: u64) -> Option<(u64, u64)> {
if hi <= lo {
return None;
}
let lo_chunk = lo / BAO_CHUNK_BYTES;
let hi_chunk = hi.div_ceil(BAO_CHUNK_BYTES);
let window = ChunkRanges::from(ChunkNum(lo_chunk)..ChunkNum(hi_chunk));
let intersect = gap & &window;
let bounds = intersect.boundaries();
let first = bounds.first()?;
let last = bounds.last()?;
let start = first.0.saturating_mul(BAO_CHUNK_BYTES).max(lo);
let end = last.0.saturating_mul(BAO_CHUNK_BYTES).min(hi);
(end > start).then_some((start, end))
}
fn block_chunks(block: u32) -> ChunkRanges {
let per = chunks_per_block();
let start = u64::from(block) * per;
let end = start + per;
ChunkRanges::from(ChunkNum(start)..ChunkNum(end))
}
fn run_from(
source_ix: usize,
start_block: u32,
last_block: u32,
total_bytes: u64,
gap: &ChunkRanges,
) -> CoveredRun {
let block_lo = block_offset(start_block);
let block_hi = block_offset(last_block)
.saturating_add(discovery_block_bytes())
.min(total_bytes);
let (offset, end) = gap_extent(gap, block_lo, block_hi).unwrap_or((block_lo, block_hi));
CoveredRun {
offset,
len: end.saturating_sub(offset),
source_ix,
}
}
#[must_use]
pub(crate) fn covers_byte_range(
coverage: &Coverage,
start: u64,
len: u64,
total_bytes: u64,
) -> bool {
if len == 0 {
return true;
}
let end = start.saturating_add(len).min(total_bytes);
if end <= start {
return true;
}
let block_bytes = discovery_block_bytes();
let first_block = u32::try_from(start / block_bytes).unwrap_or(u32::MAX);
let last_block = u32::try_from((end - 1) / block_bytes).unwrap_or(u32::MAX);
(first_block..=last_block).all(|b| coverage.covers(b))
}
#[must_use]
pub(crate) fn covered_suffix_start(
coverage: &Coverage,
start: u64,
len: u64,
total_bytes: u64,
) -> Option<u64> {
let end = start.saturating_add(len).min(total_bytes);
if end <= start {
return Some(start);
}
let block_bytes = discovery_block_bytes();
let first_block = u32::try_from(start / block_bytes).unwrap_or(u32::MAX);
let mut block = u32::try_from((end - 1) / block_bytes).unwrap_or(u32::MAX);
if !coverage.covers(block) {
return None;
}
while block > first_block && coverage.covers(block - 1) {
block -= 1;
}
Some(u64::from(block).saturating_mul(block_bytes).max(start))
}
pub(crate) fn covered_part(
coverage: &Coverage,
chunks: &ChunkRanges,
total_bytes: u64,
) -> ChunkRanges {
let mut held = ChunkRanges::empty();
for block in (0..num_blocks(total_bytes)).filter(|&b| coverage.covers(b)) {
held |= block_chunks(block);
}
chunks & &held
}
#[must_use]
pub(crate) fn covering_sources(
block: u32,
sources: &[SourceCoverage],
rank: &[usize],
) -> Option<usize> {
rank.iter().copied().find(|&ix| {
sources
.iter()
.any(|s| s.source_ix == ix && s.coverage.covers(block))
})
}
#[must_use]
pub fn plan_covered_runs(
gap: &ChunkRanges,
total_bytes: u64,
sources: &[SourceCoverage],
rank: &[usize],
) -> (Vec<CoveredRun>, ChunkRanges) {
let n = num_blocks(total_bytes);
let mut runs = Vec::new();
let mut uncovered = ChunkRanges::empty();
let mut current: Option<(usize, u32, u32)> = None;
for block in 0..n {
let chunks = block_chunks(block);
if !gap.intersects(&chunks) {
continue;
}
let sticky = current.and_then(|(src, _, last)| {
(last.checked_add(1) == Some(block)
&& sources
.iter()
.any(|s| s.source_ix == src && s.coverage.covers(block)))
.then_some(src)
});
let chosen = sticky.or_else(|| covering_sources(block, sources, rank));
if let Some(src) = chosen {
current = match current {
Some((cur_src, start, last))
if cur_src == src && last.checked_add(1) == Some(block) =>
{
Some((cur_src, start, block))
}
Some((cur_src, start, last)) => {
runs.push(run_from(cur_src, start, last, total_bytes, gap));
Some((src, block, block))
}
None => Some((src, block, block)),
};
} else {
if let Some((cur_src, start, last)) = current.take() {
runs.push(run_from(cur_src, start, last, total_bytes, gap));
}
uncovered |= &chunks & gap;
}
}
if let Some((cur_src, start, last)) = current.take() {
runs.push(run_from(cur_src, start, last, total_bytes, gap));
}
(runs, uncovered)
}
#[must_use]
pub(crate) fn spread_segments(
gap: &ChunkRanges,
total_bytes: u64,
sources: &[SourceCoverage],
rank: &[usize],
) -> (Vec<CoveredRun>, ChunkRanges) {
let n = num_blocks(total_bytes);
let mut uncovered = ChunkRanges::empty();
let mut candidates_by_block: Vec<(u32, Vec<usize>)> = Vec::new();
let mut is_uncovered: HashSet<u32> = HashSet::new();
for block in 0..n {
let chunks = block_chunks(block);
if !gap.intersects(&chunks) {
continue;
}
let candidates: Vec<usize> = sources
.iter()
.filter(|s| s.coverage.covers(block))
.map(|s| s.source_ix)
.collect();
if candidates.is_empty() {
uncovered |= &chunks & gap;
is_uncovered.insert(block);
} else {
candidates_by_block.push((block, candidates));
}
}
let n_sources = sources.len().max(1);
let target_share = candidates_by_block.len().div_ceil(n_sources).max(1);
let rank_pos_by_source: HashMap<usize, usize> = rank
.iter()
.enumerate()
.map(|(pos, &src)| (src, pos))
.collect();
let mut assigned_count: HashMap<usize, usize> = HashMap::new();
let mut assignment: HashMap<u32, usize> = HashMap::new();
let mut current: Option<usize> = None;
for (block, candidates) in &candidates_by_block {
let stick = current.filter(|src| {
candidates.contains(src) && assigned_count.get(src).copied().unwrap_or(0) < target_share
});
let chosen = stick.or_else(|| {
candidates.iter().copied().min_by_key(|src| {
let load = assigned_count.get(src).copied().unwrap_or(0);
let rank_pos = rank_pos_by_source.get(src).copied().unwrap_or(usize::MAX);
(load, rank_pos)
})
});
if let Some(src) = chosen {
assignment.insert(*block, src);
*assigned_count.entry(src).or_insert(0) += 1;
current = Some(src);
}
}
let mut runs = Vec::new();
let mut current: Option<(usize, u32, u32)> = None;
for block in 0..n {
if let Some(&src) = assignment.get(&block) {
current = match current {
Some((cur_src, start, last))
if cur_src == src && last.checked_add(1) == Some(block) =>
{
Some((cur_src, start, block))
}
Some((cur_src, start, last)) => {
runs.push(run_from(cur_src, start, last, total_bytes, gap));
Some((src, block, block))
}
None => Some((src, block, block)),
};
} else if is_uncovered.contains(&block)
&& let Some((cur_src, start, last)) = current.take()
{
runs.push(run_from(cur_src, start, last, total_bytes, gap));
}
}
if let Some((cur_src, start, last)) = current.take() {
runs.push(run_from(cur_src, start, last, total_bytes, gap));
}
(runs, uncovered)
}
#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::expect_used, clippy::indexing_slicing)]
mod tests;