use crate::Result;

use super::{
    Moc3CountInfo, Moc3Header, Moc3Ids, Moc3SectionOffsets, Moc3Version,
    parse::{read_i32_section, to_usize},
};

const PART_PARENT_PART_INDICES_SLOT: usize = 9;
const DRAWABLE_PARENT_PART_INDICES_SLOT: usize = 39;
const PART_OFFSCREEN_INDICES_SLOT: usize = 149;
const OFFSCREEN_OWNER_PART_INDICES_SLOT: usize = 155;
const EFFECT_PART_ID: &str = "PartEffect";

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Moc3OffscreenInfo {
    part_parent_indices: Vec<i32>,
    drawable_parent_part_indices: Vec<i32>,
    part_offscreen_indices: Vec<i32>,
    offscreen_owner_part_indices: Vec<i32>,
}

impl Moc3OffscreenInfo {
    #[cfg(test)]
    pub(crate) fn from_parts(
        part_parent_indices: Vec<i32>,
        drawable_parent_part_indices: Vec<i32>,
        part_offscreen_indices: Vec<i32>,
        offscreen_owner_part_indices: Vec<i32>,
    ) -> Self {
        Self {
            part_parent_indices,
            drawable_parent_part_indices,
            part_offscreen_indices,
            offscreen_owner_part_indices,
        }
    }

    pub fn parse(bytes: &[u8]) -> Result<Self> {
        let header = Moc3Header::parse(bytes)?;
        let offsets = Moc3SectionOffsets::parse(bytes)?;
        let counts = Moc3CountInfo::parse(bytes)?;
        let endianness = header.endianness();
        let part_count = to_usize(counts.parts(), "part count")?;
        let drawable_count = to_usize(counts.art_meshes(), "art mesh count")?;

        let part_parent_indices = read_i32_section(
            bytes,
            &offsets,
            PART_PARENT_PART_INDICES_SLOT,
            part_count,
            endianness,
        )?;
        let drawable_parent_part_indices = read_i32_section(
            bytes,
            &offsets,
            DRAWABLE_PARENT_PART_INDICES_SLOT,
            drawable_count,
            endianness,
        )?;
        let part_offscreen_indices = if header.version() == Moc3Version::V5_3_0 {
            read_i32_section(
                bytes,
                &offsets,
                PART_OFFSCREEN_INDICES_SLOT,
                part_count,
                endianness,
            )?
        } else {
            vec![-1; part_count]
        };
        let offscreen_count = match part_offscreen_indices
            .iter()
            .copied()
            .filter(|index| *index >= 0)
            .max()
        {
            Some(index) => usize::try_from(index).map_or(0, |index| index + 1),
            None => 0,
        };
        let offscreen_owner_part_indices = if offscreen_count == 0 {
            Vec::new()
        } else {
            read_i32_section(
                bytes,
                &offsets,
                OFFSCREEN_OWNER_PART_INDICES_SLOT,
                offscreen_count,
                endianness,
            )?
        };

        Ok(Self {
            part_parent_indices,
            drawable_parent_part_indices,
            part_offscreen_indices,
            offscreen_owner_part_indices,
        })
    }

    pub fn offscreen_count(&self) -> usize {
        self.offscreen_owner_part_indices.len()
    }

    pub fn part_offscreen_index(&self, part_index: usize) -> Option<i32> {
        self.part_offscreen_indices.get(part_index).copied()
    }

    pub fn part_offscreen_indices(&self) -> &[i32] {
        &self.part_offscreen_indices
    }

    pub fn offscreen_owner_part_index(&self, offscreen_index: usize) -> Option<i32> {
        self.offscreen_owner_part_indices
            .get(offscreen_index)
            .copied()
    }

    pub fn drawable_parent_part_index(&self, drawable_index: usize) -> Option<i32> {
        self.drawable_parent_part_indices
            .get(drawable_index)
            .copied()
    }

    pub(crate) fn effect_source_drawable_indices(&self, ids: &Moc3Ids) -> Vec<usize> {
        if self.offscreen_count() == 0 {
            return Vec::new();
        }

        let Some(effect_part_index) = ids.parts().iter().position(|id| id == EFFECT_PART_ID) else {
            return Vec::new();
        };

        self.drawable_parent_part_indices
            .iter()
            .copied()
            .enumerate()
            .filter_map(|(drawable_index, parent_part_index)| {
                self.is_part_descendant_of(parent_part_index, effect_part_index)
                    .then_some(drawable_index)
            })
            .collect()
    }

    fn is_part_descendant_of(&self, part_index: i32, ancestor_index: usize) -> bool {
        let mut part_index = part_index;
        let mut guard = 0usize;

        while part_index >= 0 {
            let Ok(index) = usize::try_from(part_index) else {
                return false;
            };
            if index == ancestor_index {
                return true;
            }
            let Some(parent_index) = self.part_parent_indices.get(index).copied() else {
                return false;
            };
            part_index = parent_index;
            guard += 1;
            if guard > self.part_parent_indices.len() {
                return false;
            }
        }

        false
    }
}
