/// Location of stored bytes within a [`crate::FileArena`].
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
pub struct Location {
    file_index: u16,
    offset: u32,
    len: u32,
}

impl Location {
    /// Creates a new `Location`, panicking if offset or len exceed `u32::MAX`.
    ///
    /// # Panics
    ///
    /// Panics if `offset` or `len` exceed `u32::MAX` (4GB file limit).
    pub fn new(file_index: u16, offset: usize, len: usize) -> Self {
        Self {
            file_index,
            offset: u32::try_from(offset)
                .expect("Location: offset exceeds u32::MAX (4GB file limit)"),
            len: u32::try_from(len).expect("Location: len exceeds u32::MAX (4GB file limit)"),
        }
    }

    /// The file index this location refers to within the arena.
    #[must_use]
    pub fn file_index(&self) -> u16 {
        self.file_index
    }

    /// The byte offset within the file.
    #[must_use]
    pub fn offset(&self) -> u32 {
        self.offset
    }

    /// The length of the stored data in bytes.
    #[must_use]
    pub fn len(&self) -> u32 {
        self.len
    }

    /// Returns true if this location points to zero bytes.
    #[must_use]
    pub fn is_empty(&self) -> bool {
        self.len == 0
    }
}

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

    #[test]
    #[should_panic(expected = "offset exceeds u32::MAX")]
    fn new_panics_on_large_offset() {
        Location::new(0, u32::MAX as usize + 1, 10);
    }

    #[test]
    #[should_panic(expected = "len exceeds u32::MAX")]
    fn new_panics_on_large_len() {
        Location::new(0, 10, u32::MAX as usize + 1);
    }

    #[test]
    fn test_location_eq() {
        let loc1 = Location::new(0, 100, 50);
        let loc2 = Location::new(0, 100, 50);
        let loc3 = Location::new(1, 100, 50);
        assert_eq!(loc1, loc2);
        assert_ne!(loc1, loc3);
    }

    #[test]
    fn location_is_empty() {
        let loc = Location::new(0, 0, 0);
        assert!(loc.is_empty());
        assert!(!Location::new(0, 100, 50).is_empty());
        assert!(Location::default().is_empty());
    }

    #[test]
    fn test_location_hash() {
        use std::collections::HashSet;
        let loc1 = Location::new(0, 100, 50);
        let loc2 = Location::new(0, 100, 50);
        let loc3 = Location::new(1, 100, 50);
        let mut set = HashSet::new();
        set.insert(loc1);
        assert!(set.contains(&loc2));
        assert!(!set.contains(&loc3));
    }
}
