use super::map::{CommentMap, HasSpan};
use crate::{Span, TriviaEvent, TriviaTable};

#[derive(Clone, Copy, Debug)]
pub struct AttachOptions {
    pub trailing_same_line: bool,
}

impl Default for AttachOptions {
    fn default() -> Self {
        Self {
            trailing_same_line: true,
        }
    }
}

pub fn attach<N: HasSpan, K: Clone + Default>(
    source: &str,
    table: &TriviaTable<K>,
    nodes: impl IntoIterator<Item = N>,
    opts: AttachOptions,
) -> CommentMap<K> {
    let mut spans: Vec<Span> = nodes.into_iter().map(|n| n.span()).collect();
    spans.sort_unstable();
    spans.dedup();

    let mut map = CommentMap::new();
    for event in table.events() {
        place_event(source, event, &spans, &mut map, opts);
    }
    map
}

fn place_event<K: Clone + Default>(
    source: &str,
    event: &TriviaEvent<K>,
    spans: &[Span],
    map: &mut CommentMap<K>,
    opts: AttachOptions,
) {
    let lo = event.span.start;
    let prev = preceding_node(spans, lo);
    let next = following_node(spans, lo);

    if event.trivia.is_blank() {
        if let Some(n) = next {
            map.entry(n).leading.push(event.trivia.clone());
        }
        return;
    }

    if opts.trailing_same_line {
        if let Some(p) = prev {
            if same_line(source, p.end, lo) {
                map.entry(p).trailing.push(event.trivia.clone());
                return;
            }
        }
    }

    if let Some(n) = next {
        map.entry(n).leading.push(event.trivia.clone());
    } else {
        map.push_dangling(event.trivia.clone());
    }
}

fn preceding_node(spans: &[Span], pos: usize) -> Option<Span> {
    spans
        .iter()
        .filter(|s| s.end <= pos)
        .max_by_key(|s| (s.end, std::cmp::Reverse(s.start)))
        .copied()
}

fn following_node(spans: &[Span], pos: usize) -> Option<Span> {
    spans
        .iter()
        .filter(|s| s.start >= pos)
        .min_by_key(|s| (s.start, std::cmp::Reverse(s.end)))
        .copied()
}

fn same_line(source: &str, from: usize, to: usize) -> bool {
    source
        .get(from..to)
        .is_some_and(|s| !s.bytes().any(|b| b == b'\n'))
}
