use crate::metadata::{
    ir::{self},
    MemoryRegion, NvStruct, METADATA,
};

use super::types::Info;

#[derive(Debug, Clone, Copy)]
pub struct Descriptor {
    pub(super) region: &'static MemoryRegion,
    pub(super) nv: &'static NvStruct,
}

impl Descriptor {
    pub fn iter() -> impl Iterator<Item = Descriptor> {
        METADATA.nv_structs.iter().flat_map(|binding| {
            let region = METADATA
                .memory
                .iter()
                .find(|r| r.name == binding.region)
                .expect("nv_struct binding must reference an existing memory region");
            binding
                .structs
                .iter()
                .map(move |nv| Descriptor { region, nv })
        })
    }

    pub fn find(name: &str) -> Option<Descriptor> {
        Self::iter().find(|d| d.nv.kind.eq_ignore_ascii_case(name))
    }

    pub fn name(&self) -> &'static str {
        self.nv.name
    }

    pub fn kind(&self) -> &'static str {
        self.nv.kind
    }

    pub(super) fn block(&self) -> Option<&'static ir::Block> {
        self.nv.ir.blocks.iter().find(|b| b.name == self.nv.block)
    }

    pub(super) fn item(&self, entry: &str) -> Option<&'static ir::BlockItem> {
        self.block()?
            .items
            .iter()
            .find(|i| i.name.eq_ignore_ascii_case(entry))
    }

    pub(super) fn register_of(item: &ir::BlockItem) -> Option<&ir::Register> {
        match &item.inner {
            ir::BlockItemInner::Register(r) => Some(r),
            _ => None,
        }
    }

    pub(super) fn fieldset_of(&self, item: &ir::BlockItem) -> Option<&'static ir::FieldSet> {
        let reg = Self::register_of(item)?;
        let name = reg.fieldset?;
        self.nv.ir.fieldsets.iter().find(|fs| fs.name == name)
    }

    pub(super) fn field_in(fs: &'static ir::FieldSet, name: &str) -> Option<&'static ir::Field> {
        fs.fields.iter().find(|f| f.name.eq_ignore_ascii_case(name))
    }

    pub(super) fn enumm_named(&self, name: &str) -> Option<&'static ir::Enum> {
        self.nv.ir.enums.iter().find(|e| e.name == name)
    }

    fn default_for(&self, entry: &str) -> Option<u64> {
        self.nv
            .defaults
            .iter()
            .find(|(n, _)| n.eq_ignore_ascii_case(entry))
            .map(|(_, v)| *v as u64)
    }

    pub fn entries(&self) -> impl Iterator<Item = &'static str> {
        self.block()
            .into_iter()
            .flat_map(|b| b.items.iter().map(|i| i.name))
    }

    pub fn fields(&self, entry: &str) -> Option<impl Iterator<Item = &'static str>> {
        let item = self.item(entry)?;
        let fs = self.fieldset_of(item)?;
        Some(fs.fields.iter().map(|f| f.name))
    }

    pub fn describe(&self, path: &str) -> Option<Info> {
        let (entry_name, field_name) = match path.split_once('.') {
            Some((e, f)) => (e, Some(f)),
            None => (path, None),
        };
        let item = self.item(entry_name)?;
        let reg = Self::register_of(item)?;

        match field_name {
            None => Some(Info {
                description: item.description,
                byte_offset: item.byte_offset,
                bit_offset: 0,
                bit_size: reg.bit_size,
                access: reg.access.clone(),
                enumm: None,
                default: self.default_for(entry_name),
            }),
            Some(fname) => {
                let fs = self.fieldset_of(item)?;
                let field = Self::field_in(fs, fname)?;
                let bit_offset = match &field.bit_offset {
                    ir::BitOffset::Regular(r) => r.offset,
                    ir::BitOffset::Cursed(_) => return None,
                };
                let enumm = field.enumm.and_then(|n| self.enumm_named(n));
                Some(Info {
                    description: field.description,
                    byte_offset: item.byte_offset,
                    bit_offset,
                    bit_size: field.bit_size,
                    access: reg.access.clone(),
                    enumm,
                    default: None,
                })
            }
        }
    }
}