use text_size::{TextRange, TextSize};

/// Maps byte offsets to 0-based (line, col) positions for one source file.
pub struct LineIndex {
    /// Byte offset of the first character of each line.
    line_starts: Vec<u32>,
}

impl LineIndex {
    /// Build from file text.
    #[must_use]
    pub fn new(text: &str) -> Self {
        let mut line_starts = vec![0u32];
        for (i, b) in text.bytes().enumerate() {
            if b == b'\n' {
                // SAFETY: byte offset won't reach 4 GiB in any realistic SQL file.
                #[allow(clippy::cast_possible_truncation)]
                line_starts.push((i + 1) as u32);
            }
        }
        Self { line_starts }
    }

    /// Convert a byte offset to `(line, col)`, both 0-based.
    #[must_use]
    pub fn line_col(&self, offset: TextSize) -> (u32, u32) {
        let offset = u32::from(offset);
        let line = self
            .line_starts
            .partition_point(|&s| s <= offset)
            .saturating_sub(1);
        let col = offset - self.line_starts[line];
        // Line index fits u32: files with 4B+ lines are not a realistic concern.
        #[allow(clippy::cast_possible_truncation)]
        let line_u32 = line as u32;
        (line_u32, col)
    }

    /// Convert a `TextRange` to a SCIP range vec.
    ///
    /// Same-line: `[start_line, start_char, end_char]` (3 elements).
    /// Multi-line: `[start_line, start_char, end_line, end_char]` (4 elements).
    #[must_use]
    pub fn to_scip_range(&self, range: TextRange) -> Vec<i32> {
        let (sl, sc) = self.line_col(range.start());
        let (el, ec) = self.line_col(range.end());
        #[allow(clippy::cast_possible_wrap)]
        if sl == el {
            vec![sl as i32, sc as i32, ec as i32]
        } else {
            vec![sl as i32, sc as i32, el as i32, ec as i32]
        }
    }
}

#[cfg(test)]
mod tests {
    use text_size::{TextRange, TextSize};

    use super::*;

    #[test]
    fn first_line_offset() {
        let idx = LineIndex::new("SELECT 1;");
        let (line, col) = idx.line_col(TextSize::from(0u32));
        assert_eq!((line, col), (0, 0));
    }

    #[test]
    fn second_line() {
        let idx = LineIndex::new("SELECT 1;\nSELECT 2;");
        let (line, _col) = idx.line_col(TextSize::from(10u32));
        assert_eq!(line, 1);
    }

    #[test]
    fn col_on_second_line() {
        let idx = LineIndex::new("SELECT 1;\nSELECT 2;");
        // "SELECT 1;\n" is 10 bytes; "SE" on second line → col 2
        let (_line, col) = idx.line_col(TextSize::from(12u32));
        assert_eq!(col, 2);
    }

    #[test]
    fn scip_range_same_line() {
        let idx = LineIndex::new("SELECT id FROM items;");
        let range = TextRange::new(TextSize::from(7u32), TextSize::from(9u32));
        let scip_range = idx.to_scip_range(range);
        assert_eq!(scip_range.len(), 3, "same-line range must have 3 elements");
        assert!(scip_range.iter().all(|&v| v >= 0));
    }

    #[test]
    fn scip_range_multi_line() {
        let idx = LineIndex::new("SELECT\nid\nFROM items;");
        // start on line 0, end on line 2
        let range = TextRange::new(TextSize::from(0u32), TextSize::from(10u32));
        let scip_range = idx.to_scip_range(range);
        assert_eq!(scip_range.len(), 4, "multi-line range must have 4 elements");
        assert!(scip_range.iter().all(|&v| v >= 0));
    }
}
