//! Specially optimized functions which are implemented separately from `Parser`

use super::replay_command;
use crate::reader::ReplayReadError;
use crate::ReplayResult;
use byteorder::{ByteOrder, LittleEndian};

/// A hand optimized function for finding the offset of where the body starts.
pub fn body_offset(data: &[u8]) -> ReplayResult<usize> {
    unsafe {
        let mut ptr = data.as_ptr();
        let end = ptr.add(data.len());
        let start: usize = ptr as usize;

        ptr = ptr.add(skip_string(ptr, end)?); // Skip scfa_version
        ptr = ptr.add(skip_string(ptr, end)?); // Skip the string "\r\n\x00"
        ptr = ptr.add(skip_string(ptr, end)?); // Skip version_and_mapname
        ptr = ptr.add(skip_string(ptr, end)?); // Skip the string "\r\n\x1a\x00"

        ptr = ptr.add(read_u32(ptr, end)? as usize + 4); // Skip mods
        ptr = ptr.add(read_u32(ptr, end)? as usize + 4); // Skip scenario

        // Need 1 byte for num_sources and 6 for the remaining data which must also appear in any
        // valid replay header.
        verify_len(ptr, end, 1 + 6)?;
        let num_sources = *ptr as usize;
        ptr = ptr.add(1);

        for _ in 0..num_sources {
            // Skip name
            ptr = ptr.add(skip_string(ptr, end)?);
            // Skip player_id
            verify_len(ptr, end, 4 + 6)?;
            ptr = ptr.add(4);
        }

        verify_len(ptr, end, 1 + 5)?;
        ptr = ptr.add(1); // Skip cheats_enabled
        verify_len(ptr, end, 1 + 4)?;
        let army_count = *ptr as usize;
        ptr = ptr.add(1);

        for _ in 0..army_count {
            ptr = ptr.add(read_u32(ptr, end)? as usize + 4); // Skip player_data

            verify_len(ptr, end, 1)?;
            let player_source = *ptr as usize;
            ptr = ptr.add(1);

            if player_source != 255 {
                ptr = ptr.add(1);
            }
        }

        verify_len(ptr, end, 4)?;
        ptr = ptr.add(4); // Skip seed

        Ok(ptr as usize - start)
    }
}

/// A hand optimized function for extracting the tick count as quickly and with as little
/// overhead as possible. Does not check for desyncs.
///
/// This is currently the most common use case for this library so it might as well be completely
/// optimized.
pub fn body_ticks(data: &[u8]) -> ReplayResult<u32> {
    let mut ticks = 0;
    let mut curr = 0;
    let end = data.len();

    // Will not throw an error on missing data
    while curr + 2 < end {
        // First byte is the command type
        let command = unsafe { *data.get_unchecked(curr) };
        if command > replay_command::MAX {
            return Err(ReplayReadError::Malformed("invalid command"));
        }
        // bytes 2 and 3 are the command size
        let size =
            unsafe { LittleEndian::read_u16(data.get_unchecked(curr + 1..curr + 3)) as usize };

        // Advance contains exactly 4 more bytes for the number of ticks
        if command == replay_command::ADVANCE && !(end < curr + size) {
            if size != 7 {
                return Err(ReplayReadError::Malformed("invalid command size"));
            }
            ticks += unsafe { LittleEndian::read_u32(data.get_unchecked(curr + 3..curr + 7)) };
        }
        curr += size;
    }

    Ok(ticks)
}

/// Check if a buffer starts with a full frame.
///
/// Note, that this only checks the frame header which includes the command ID and the data size.
/// The data itself is not checked and should be parsed separately to verify validity.
pub fn has_frame(data: &[u8]) -> ReplayResult<bool> {
    if data.is_empty() {
        return Ok(false);
    }

    let command = unsafe { *data.get_unchecked(0) };
    if command > replay_command::MAX {
        return Err(ReplayReadError::Malformed("invalid command"));
    }

    if data.len() < 3 {
        return Ok(false);
    }

    let size = unsafe { LittleEndian::read_u16(data.get_unchecked(1..3)) as usize };
    if size < 3 {
        return Err(ReplayReadError::Malformed("invalid command size"));
    }

    Ok(!(data.len() < size))
}

unsafe fn skip_string(mut data: *const u8, end: *const u8) -> ReplayResult<usize> {
    let mut count = 1;

    while data < end {
        if *data == b'\x00' {
            return Ok(count);
        }
        data = data.add(1);
        count += 1;
    }

    Err(ReplayReadError::Malformed("missing header data"))
}

#[inline]
unsafe fn read_u32(ptr: *const u8, end: *const u8) -> ReplayResult<u32> {
    verify_len(ptr, end, 4)?;
    Ok(LittleEndian::read_u32(std::slice::from_raw_parts(ptr, 4)))
}

#[inline]
unsafe fn verify_len(ptr: *const u8, end: *const u8, needed: usize) -> ReplayResult<()> {
    return match ptr.add(needed) < end {
        true => Ok(()),
        false => Err(ReplayReadError::Malformed("missing header data")),
    };
}

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

    #[test]
    fn test_body_ticks_empty() {
        assert_eq!(body_ticks(&[]).unwrap(), 0);
    }

    #[test]
    fn test_body_ticks_invalid_command() {
        body_ticks(&[0xff, 0x03, 0x00]).unwrap_err();
    }

    #[test]
    fn test_body_ticks_invalid_command_size() {
        body_ticks(&[0x0, 0x03, 0x00]).unwrap_err();
    }

    #[test]
    fn test_body_ticks_exactly_one() {
        assert_eq!(
            body_ticks(&[0x00, 0x07, 0x00, 0x01, 0x00, 0x00, 0x00]).unwrap(),
            1
        );
    }

    #[test]
    fn test_body_ticks_short_by_one() {
        assert_eq!(
            body_ticks(&[0x00, 0x07, 0x00, 0x01, 0x00, 0x00]).unwrap(),
            0
        );
    }

    #[test]
    fn test_body_ticks_missing_data() {
        assert_eq!(body_ticks(&[0x00, 0x07, 0x00, 0x01]).unwrap(), 0);
    }

    #[test]
    fn test_body_ticks_excessive_data() {
        assert_eq!(
            body_ticks(&[0x00, 0x07, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x07, 0x00, 0x01,])
                .unwrap(),
            1
        );
    }

    #[test]
    fn test_has_frame_empty() {
        assert_eq!(has_frame(&[]).unwrap(), false);
    }

    #[test]
    fn test_has_frame_exact() {
        let data = [0x00, 0x04, 0x00, 0xff];
        assert_eq!(has_frame(&data).unwrap(), true);
    }

    #[test]
    fn test_has_frame_exact_empty_contents() {
        let data = [0x00, 0x03, 0x00];
        assert_eq!(has_frame(&data).unwrap(), true);
    }

    #[test]
    fn test_has_frame_missing_data() {
        let data = [0x00, 0x05, 0x00, 0xff];
        assert_eq!(has_frame(&data).unwrap(), false);
    }

    #[test]
    fn test_has_frame_excessive_data() {
        let data = [0x00, 0x04, 0x00, 0xff, 0xff, 0xff, 0xff, 0xff];
        assert_eq!(has_frame(&data).unwrap(), true);
    }
}
