use crate::error::AxmlError;

pub(crate) const RESOURCE_MAP_TYPE: u16 = 0x0180;
pub(crate) const RESOURCE_MAP_HEADER_SIZE: u16 = 8;

/// Maps attribute indices to system resource IDs.
#[derive(Debug, Clone)]
pub struct ResourceMap {
    pub ids: Vec<u32>,
    pub header_size: u16,
    pub chunk_size: u32,
}

impl Default for ResourceMap {
    fn default() -> Self {
        Self::new()
    }
}

impl ResourceMap {
    pub fn new() -> Self {
        ResourceMap {
            ids: Vec::new(),
            header_size: RESOURCE_MAP_HEADER_SIZE,
            chunk_size: 0,
        }
    }

    pub fn new_with_ids(ids: Vec<u32>) -> Self {
        ResourceMap {
            ids,
            header_size: RESOURCE_MAP_HEADER_SIZE,
            chunk_size: 0,
        }
    }

    /// Parse resource map from binary data. Returns (map, bytes_consumed).
    pub fn parse(data: &[u8]) -> Result<(Option<Self>, usize), AxmlError> {
        if data.len() < 8 {
            return Ok((None, 0));
        }
        let chunk_type = u16::from_le_bytes([data[0], data[1]]);
        if chunk_type != RESOURCE_MAP_TYPE {
            return Ok((None, 0));
        }
        let header_size = u16::from_le_bytes([data[2], data[3]]);
        let chunk_size = u32::from_le_bytes([data[4], data[5], data[6], data[7]]);

        if chunk_size < 8 || chunk_size as usize > data.len() {
            return Err(AxmlError::SliceError(format!(
                "Resource map chunk_size {} invalid",
                chunk_size
            )));
        }

        let data_size = chunk_size - 8;
        if !data_size.is_multiple_of(4) {
            return Err(AxmlError::SliceError(
                "Resource map data not 4-byte aligned".to_string(),
            ));
        }
        let count = data_size / 4;
        let mut ids = Vec::with_capacity(count as usize);
        for i in 0..count as usize {
            let pos = 8 + i * 4;
            ids.push(u32::from_le_bytes([
                data[pos],
                data[pos + 1],
                data[pos + 2],
                data[pos + 3],
            ]));
        }
        Ok((
            Some(ResourceMap {
                ids,
                header_size,
                chunk_size,
            }),
            chunk_size as usize,
        ))
    }

    /// Serialize resource map to bytes.
    pub fn pack(&self) -> Vec<u8> {
        let chunk_size = if self.chunk_size == 0 {
            (8 + self.ids.len() * 4) as u32
        } else {
            self.chunk_size
        };
        let mut out = Vec::with_capacity(chunk_size as usize);
        out.extend_from_slice(&RESOURCE_MAP_TYPE.to_le_bytes());
        out.extend_from_slice(&self.header_size.to_le_bytes());
        out.extend_from_slice(&chunk_size.to_le_bytes());
        for id in &self.ids {
            out.extend_from_slice(&id.to_le_bytes());
        }
        out
    }

    #[expect(dead_code)]
    pub fn header_size(&self) -> u16 {
        self.header_size
    }

    #[expect(dead_code)]
    pub fn chunk_size(&self) -> u32 {
        self.chunk_size
    }

    pub fn set_chunk_size(&mut self, chunk_size: u32) {
        self.chunk_size = chunk_size;
    }

    /// Get the resource ID for attribute at the given string pool index.
    pub fn get_id(&self, idx: u32) -> Option<u32> {
        let idx = idx as usize;
        if idx < self.ids.len() {
            let id = self.ids[idx];
            if id != 0 {
                Some(id)
            } else {
                None
            }
        } else {
            None
        }
    }
}

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

    fn build_chunk(ids: &[u32]) -> Vec<u8> {
        let chunk_size = (8 + ids.len() * 4) as u32;
        let mut v = Vec::new();
        v.extend_from_slice(&RESOURCE_MAP_TYPE.to_le_bytes());
        v.extend_from_slice(&8u16.to_le_bytes());
        v.extend_from_slice(&chunk_size.to_le_bytes());
        for id in ids {
            v.extend_from_slice(&id.to_le_bytes());
        }
        v
    }

    #[test]
    fn test_parse_empty_ids() {
        let data = build_chunk(&[]);
        let (map, consumed) = ResourceMap::parse(&data).unwrap();
        let map = map.unwrap();
        assert_eq!(consumed, 8);
        assert!(map.ids.is_empty());
    }

    #[test]
    fn test_parse_ids() {
        let data = build_chunk(&[0x7f04_0001, 0x7f04_0002]);
        let (map, consumed) = ResourceMap::parse(&data).unwrap();
        let map = map.unwrap();
        assert_eq!(consumed, 16);
        assert_eq!(map.ids, vec![0x7f04_0001, 0x7f04_0002]);
    }

    #[test]
    fn test_parse_wrong_type_returns_none() {
        let mut data = build_chunk(&[0x1234_5678]);
        // overwrite chunk type with a non-resource-map type
        data[0] = 0x01;
        data[1] = 0x01;
        let (map, consumed) = ResourceMap::parse(&data).unwrap();
        assert!(map.is_none());
        assert_eq!(consumed, 0);
    }

    #[test]
    fn test_parse_too_short_returns_none() {
        let (map, consumed) = ResourceMap::parse(&[0x00, 0x01]).unwrap();
        assert!(map.is_none());
        assert_eq!(consumed, 0);
    }

    #[test]
    fn test_parse_invalid_chunk_size_errors() {
        let mut data = build_chunk(&[0x1234]);
        // set chunk_size to 999 (larger than data)
        let bad_size: u32 = 999;
        data[4..8].copy_from_slice(&bad_size.to_le_bytes());
        assert!(ResourceMap::parse(&data).is_err());
    }

    #[test]
    fn test_pack_roundtrip() {
        let ids: Vec<u32> = vec![0x7f04_0001, 0x0101_021b, 0];
        let map: ResourceMap = ResourceMap::new_with_ids(ids.clone());
        let packed: Vec<u8> = map.pack();
        let (parsed, consumed) = ResourceMap::parse(&packed).unwrap();
        let parsed: ResourceMap = parsed.unwrap();
        assert_eq!(consumed, packed.len());
        assert_eq!(parsed.ids, ids);
    }

    #[test]
    fn test_get_id_present() {
        let map: ResourceMap = ResourceMap::new_with_ids(vec![0x7f04_0001, 0x7f04_0002]);
        assert_eq!(map.get_id(0), Some(0x7f04_0001));
        assert_eq!(map.get_id(1), Some(0x7f04_0002));
    }

    #[test]
    fn test_get_id_zero_is_none() {
        let map: ResourceMap = ResourceMap::new_with_ids(vec![0, 0x7f04_0002]);
        assert_eq!(map.get_id(0), None);
    }

    #[test]
    fn test_get_id_out_of_bounds_is_none() {
        let map: ResourceMap = ResourceMap::new_with_ids(vec![0x7f04_0001]);
        assert_eq!(map.get_id(5), None);
    }
}
