/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

use syn::ext::IdentExt;

use crate::bindgen::ir::ty::{IntKind, PrimitiveType};

#[derive(Debug, Copy, Clone, PartialEq, Eq, Default)]
pub enum ReprStyle {
    #[default]
    Rust,
    C,
    Transparent,
}

#[derive(Debug, Copy, Clone, PartialEq, Eq)]
pub struct ReprType {
    kind: IntKind,
    signed: bool,
}

impl ReprType {
    pub(crate) fn to_primitive(self) -> PrimitiveType {
        PrimitiveType::Integer {
            kind: self.kind,
            signed: self.signed,
            zeroable: true,
        }
    }
}

#[derive(Debug, Copy, Clone, PartialEq, Eq)]
pub enum ReprAlign {
    Packed,
    Align(u64),
}

#[derive(Debug, Copy, Clone, PartialEq, Eq, Default)]
pub struct Repr {
    pub style: ReprStyle,
    pub ty: Option<ReprType>,
    pub align: Option<ReprAlign>,
}

impl Repr {
    pub fn load(attrs: &[syn::Attribute]) -> Result<Repr, String> {
        let ids = attrs
            .iter()
            .filter_map(|attr| {
                if let syn::Meta::List(syn::MetaList { path, nested, .. }) =
                    attr.parse_meta().ok()?
                {
                    if path.is_ident("repr") {
                        return Some(nested.into_iter().collect::<Vec<_>>());
                    }
                }
                None
            })
            .flatten()
            .filter_map(|meta| match meta {
                syn::NestedMeta::Meta(syn::Meta::Path(path)) => Some((
                    path.segments.first().unwrap().ident.unraw().to_string(),
                    None,
                )),
                syn::NestedMeta::Meta(syn::Meta::List(syn::MetaList { path, nested, .. })) => {
                    Some((
                        path.segments.first().unwrap().ident.unraw().to_string(),
                        Some(
                            nested
                                .iter()
                                .filter_map(|meta| match meta {
                                    // Only used for #[repr(align(...))].
                                    syn::NestedMeta::Lit(syn::Lit::Int(literal)) => {
                                        Some(literal.base10_digits().to_string())
                                    }
                                    // Only single levels of nesting supported at the moment.
                                    _ => None,
                                })
                                .collect::<Vec<_>>(),
                        ),
                    ))
                }
                _ => None,
            });

        let mut repr = Repr::default();
        for id in ids {
            let (int_kind, signed) = match (id.0.as_ref(), id.1) {
                ("u8", None) => (IntKind::B8, false),
                ("u16", None) => (IntKind::B16, false),
                ("u32", None) => (IntKind::B32, false),
                ("u64", None) => (IntKind::B64, false),
                ("usize", None) => (IntKind::Size, false),
                ("i8", None) => (IntKind::B8, true),
                ("i16", None) => (IntKind::B16, true),
                ("i32", None) => (IntKind::B32, true),
                ("i64", None) => (IntKind::B64, true),
                ("isize", None) => (IntKind::Size, true),
                ("C", None) => {
                    repr.style = ReprStyle::C;
                    continue;
                }
                ("transparent", None) => {
                    repr.style = ReprStyle::Transparent;
                    continue;
                }
                ("packed", args) => {
                    // #[repr(packed(n))] not supported because of some open questions about how
                    // to calculate the native alignment of types. See mozilla/cbindgen#433.
                    if args.is_some() {
                        return Err(
                            "Not-yet-implemented #[repr(packed(...))] encountered.".to_string()
                        );
                    }
                    let align = ReprAlign::Packed;
                    // Only permit a single alignment-setting repr.
                    if let Some(old_align) = repr.align {
                        return Err(format!(
                            "Conflicting #[repr(align(...))] type hints {:?} and {:?}.",
                            old_align, align
                        ));
                    }
                    repr.align = Some(align);
                    continue;
                }
                ("align", Some(args)) => {
                    // #[repr(align(...))] only allows a single argument.
                    if args.len() != 1 {
                        return Err(format!(
                            "Unsupported #[repr(align({}))], align must have exactly one argument.",
                            args.join(", ")
                        ));
                    }
                    // Must be a positive integer.
                    let align = match args.first().unwrap().parse::<u64>() {
                        Ok(align) => align,
                        Err(_) => {
                            return Err(format!("Non-numeric #[repr(align({}))].", args.join(", ")))
                        }
                    };
                    // Must be a power of 2.
                    if !align.is_power_of_two() || align == 0 {
                        return Err(format!("Invalid alignment to #[repr(align({}))].", align));
                    }
                    // Only permit a single alignment-setting repr.
                    if let Some(old_align) = repr.align {
                        return Err(format!(
                            "Conflicting #[repr(align(...))] type hints {:?} and {:?}.",
                            old_align,
                            ReprAlign::Align(align)
                        ));
                    }
                    repr.align = Some(ReprAlign::Align(align));
                    continue;
                }
                (path, args) => match args {
                    None => return Err(format!("Unsupported #[repr({})].", path)),
                    Some(args) => {
                        return Err(format!(
                            "Unsupported #[repr({}({}))].",
                            path,
                            args.join(", ")
                        ));
                    }
                },
            };
            let ty = ReprType {
                kind: int_kind,
                signed,
            };
            if let Some(old_ty) = repr.ty {
                return Err(format!(
                    "Conflicting #[repr(...)] type hints {:?} and {:?}.",
                    old_ty, ty
                ));
            }
            repr.ty = Some(ty);
        }
        Ok(repr)
    }
}
