//! Reader for Fortran sequential unformatted records (the "record marker" convention
//! used by MCNP RSSA files: each record is prefixed and suffixed with a 4-byte
//! little-endian integer giving the record's length in bytes).

use crate::error::{Result, RssaError};

pub struct RecordCursor<'a> {
    data: &'a [u8],
    pos: usize,
}

impl<'a> RecordCursor<'a> {
    pub fn new(data: &'a [u8]) -> Self {
        Self { data, pos: 0 }
    }

    /// Current byte offset into the underlying buffer.
    pub fn position(&self) -> usize {
        self.pos
    }

    /// Reads one Fortran record and returns a slice over its payload (excluding the
    /// leading/trailing 4-byte markers).
    pub fn read_record(&mut self) -> Result<&'a [u8]> {
        let record_offset = self.pos;
        let before = self.read_marker()?;
        let len = usize::try_from(before).map_err(|_| RssaError::RecordMarkerMismatch {
            offset: record_offset,
            before,
            after: before,
        })?;

        let start = self.pos;
        let end = start
            .checked_add(len)
            .filter(|&end| end <= self.data.len())
            .ok_or(RssaError::UnexpectedEof {
                offset: record_offset,
            })?;
        let payload = &self.data[start..end];
        self.pos = end;

        let after = self.read_marker()?;
        if after != before {
            return Err(RssaError::RecordMarkerMismatch {
                offset: record_offset,
                before,
                after,
            });
        }

        Ok(payload)
    }

    fn read_marker(&mut self) -> Result<i32> {
        let start = self.pos;
        let end = start
            .checked_add(4)
            .filter(|&end| end <= self.data.len())
            .ok_or(RssaError::UnexpectedEof { offset: start })?;
        let bytes: [u8; 4] = self.data[start..end].try_into().unwrap();
        self.pos = end;
        Ok(i32::from_le_bytes(bytes))
    }
}

/// Reads a little-endian `i32` at `offset` within `record`.
pub fn read_i32(record: &[u8], offset: usize) -> Result<i32> {
    let slice = record
        .get(offset..offset + 4)
        .ok_or(RssaError::UnexpectedEof { offset })?;
    Ok(i32::from_le_bytes(slice.try_into().unwrap()))
}

/// Reads a little-endian `f64` at `offset` within `record`.
pub fn read_f64(record: &[u8], offset: usize) -> Result<f64> {
    let slice = record
        .get(offset..offset + 8)
        .ok_or(RssaError::UnexpectedEof { offset })?;
    Ok(f64::from_le_bytes(slice.try_into().unwrap()))
}

/// Reads a little-endian `i64` at `offset` within `record`.
pub fn read_i64(record: &[u8], offset: usize) -> Result<i64> {
    let slice = record
        .get(offset..offset + 8)
        .ok_or(RssaError::UnexpectedEof { offset })?;
    Ok(i64::from_le_bytes(slice.try_into().unwrap()))
}

/// Test-only helpers for building synthetic Fortran-record byte streams, shared with
/// other modules' tests (e.g. to build synthetic RSSA headers).
#[cfg(test)]
pub(crate) mod test_support {
    /// Encodes `payload` as a Fortran sequential-unformatted record.
    pub fn write_record(payload: &[u8]) -> Vec<u8> {
        let mut out = Vec::with_capacity(payload.len() + 8);
        let marker = (payload.len() as i32).to_le_bytes();
        out.extend_from_slice(&marker);
        out.extend_from_slice(payload);
        out.extend_from_slice(&marker);
        out
    }
}

#[cfg(test)]
mod tests {
    use super::test_support::write_record;
    use super::*;

    #[test]
    fn round_trips_a_single_record() {
        let bytes = write_record(b"hello");
        let mut cursor = RecordCursor::new(&bytes);
        assert_eq!(cursor.read_record().unwrap(), b"hello");
        assert_eq!(cursor.position(), bytes.len());
    }

    #[test]
    fn round_trips_multiple_records() {
        let mut bytes = write_record(b"first");
        bytes.extend(write_record(b"second!!"));
        let mut cursor = RecordCursor::new(&bytes);
        assert_eq!(cursor.read_record().unwrap(), b"first");
        assert_eq!(cursor.read_record().unwrap(), b"second!!");
    }

    #[test]
    fn empty_record_is_valid() {
        let bytes = write_record(b"");
        let mut cursor = RecordCursor::new(&bytes);
        assert_eq!(cursor.read_record().unwrap(), b"");
    }

    #[test]
    fn mismatched_markers_produce_an_error() {
        let mut bytes = write_record(b"hello");
        // Corrupt the trailing marker.
        let last = bytes.len() - 1;
        bytes[last] = 0xFF;
        let mut cursor = RecordCursor::new(&bytes);
        let err = cursor.read_record().unwrap_err();
        assert!(matches!(err, RssaError::RecordMarkerMismatch { .. }));
    }

    #[test]
    fn truncated_file_produces_eof_error() {
        let mut bytes = write_record(b"hello world");
        bytes.truncate(6);
        let mut cursor = RecordCursor::new(&bytes);
        let err = cursor.read_record().unwrap_err();
        assert!(matches!(err, RssaError::UnexpectedEof { .. }));
    }

    #[test]
    fn negative_length_marker_is_rejected() {
        let bytes = (-1i32).to_le_bytes().to_vec();
        let mut cursor = RecordCursor::new(&bytes);
        let err = cursor.read_record().unwrap_err();
        assert!(matches!(err, RssaError::RecordMarkerMismatch { .. }));
    }

    #[test]
    fn read_i32_and_i64_helpers() {
        let mut payload = Vec::new();
        payload.extend_from_slice(&42i64.to_le_bytes());
        payload.extend_from_slice(&7i32.to_le_bytes());
        assert_eq!(read_i64(&payload, 0).unwrap(), 42);
        assert_eq!(read_i32(&payload, 8).unwrap(), 7);
        assert!(read_i32(&payload, 20).is_err());
    }
}
