//! Backward boundary detection for FASTQ chunks.
//!
//! FASTQ records have a rigid 4-line structure:
//!   @header
//!   sequence
//!   +
//!   quality
//!
//! The `@` in quality lines (Phred 31 = ASCII 64) makes forward heuristics
//! unreliable. The backward scanner identifies a genuine record start by
//! verifying the structural context around each `@` candidate.

use obikrope::{Rope, RopeCursor, SeekMode};

#[inline]
fn is_eol(c: u8) -> bool {
    c == b'\n' || c == b'\r'
}

#[inline]
fn is_sep(c: u8) -> bool {
    c == b' ' || c == b'\t' || is_eol(c)
}

#[inline]
fn is_seq_char(c: u8) -> bool {
    c.is_ascii_alphabetic() || c == b'-' || c == b'.' || c == b'[' || c == b']'
}

/// Scan the rope backward for the start of the last complete FASTQ record.
///
/// Returns the absolute byte offset of the `@` that begins the last complete
/// record, so that `rope[..offset]` is a self-contained chunk and
/// `rope[offset..]` is the remainder for the next chunk.
/// Returns `None` if no valid boundary is found (need more data).
pub fn end_of_last_fastq_entry(rope: &Rope) -> Option<usize> {
    let cursor = rope.bw_cursor();
    let mut state: u8 = 0;
    let mut restart: usize = 0;
    let mut cut: usize = rope.len();

    for c in cursor.iter() {
        match state {
            0 => {
                if c == b'+' {
                    state = 1;
                    restart = cursor.tell()?;
                }
            }
            1 => {
                if is_eol(c) {
                    state = 2;
                } else {
                    state = 0;
                    cursor.seek(restart as isize, SeekMode::Absolute).ok();
                }
            }
            2 => {
                if is_sep(c) {
                    // stay
                } else if is_seq_char(c) {
                    state = 3;
                } else {
                    state = 0;
                    cursor.seek(restart as isize, SeekMode::Absolute).ok();
                }
            }
            3 => {
                if is_eol(c) {
                    state = 4;
                } else if is_seq_char(c) {
                    // stay
                } else {
                    state = 0;
                    cursor.seek(restart as isize, SeekMode::Absolute).ok();
                }
            }
            4 => {
                if !is_eol(c) {
                    state = 5;
                }
            }
            5 => {
                if is_eol(c) {
                    state = 0;
                    cursor.seek(restart as isize, SeekMode::Absolute).ok();
                } else if c == b'@' {
                    cut = cursor.tell()?;
                    state = 6;
                }
                // else stay
            }
            6 => {
                if is_eol(c) {
                    if cut > 0 {
                        return Some(cut);
                    }
                    return None;
                } else {
                    state = 5;
                }
            }
            _ => unreachable!(),
        }
    }
    None
}

#[cfg(test)]
#[path = "tests/fastq.rs"]
mod tests;
