use std::borrow::Cow;

use super::encoded::*;
use scroll::{
    ctx::{TryFromCtx, TryIntoCtx},
    Pread, Pwrite,
};

pub struct SerString<'a>(pub Option<Cow<'a, str>>);
impl<'a> TryFromCtx<'a> for SerString<'a> {
    type Error = scroll::Error;

    fn try_from_ctx(from: &'a [u8], (): ()) -> Result<(Self, usize), Self::Error> {
        let offset = &mut 0;
        let val = match from.gread_with(offset, scroll::LE)? {
            0xFF_u8 => None,
            0x00_u8 => Some("".into()),
            len => Some(
                from.gread_with::<&str>(offset, scroll::ctx::StrCtx::Length(len as usize))?
                    .into(),
            ),
        };

        Ok((SerString(val), *offset))
    }
}
try_into_ctx!(SerString<'_>, |self, into| {
    let offset = &mut 0;

    match &self.0 {
        Some(str) => {
            into.gwrite_with(str.len() as u8, offset, scroll::LE)?;
            into.gwrite(str.as_bytes(), offset)?;
        }
        None => {
            into.gwrite_with(0xFF_u8, offset, scroll::LE)?;
        }
    }

    Ok(*offset)
});

#[derive(Debug, Clone)]
pub enum FieldOrPropType<'a> {
    Boolean,
    Char,
    Int8,
    UInt8,
    Int16,
    UInt16,
    Int32,
    UInt32,
    Int64,
    UInt64,
    Float32,
    Float64,
    String,
    Type,
    Object,
    Vector(Box<FieldOrPropType<'a>>),
    Enum(Cow<'a, str>),
}

impl<'a> TryFromCtx<'a> for FieldOrPropType<'a> {
    type Error = scroll::Error;

    fn try_from_ctx(from: &'a [u8], (): ()) -> Result<(Self, usize), Self::Error> {
        let offset = &mut 0;

        use FieldOrPropType::*;

        let val = match from.gread_with::<u8>(offset, scroll::LE)? {
            ELEMENT_TYPE_BOOLEAN => Boolean,
            ELEMENT_TYPE_CHAR => Char,
            ELEMENT_TYPE_I1 => Int8,
            ELEMENT_TYPE_U1 => UInt8,
            ELEMENT_TYPE_I2 => Int16,
            ELEMENT_TYPE_U2 => UInt16,
            ELEMENT_TYPE_I4 => Int32,
            ELEMENT_TYPE_U4 => UInt32,
            ELEMENT_TYPE_I8 => Int64,
            ELEMENT_TYPE_U8 => UInt64,
            ELEMENT_TYPE_R4 => Float32,
            ELEMENT_TYPE_R8 => Float64,
            ELEMENT_TYPE_STRING => String,
            ELEMENT_TYPE_SZARRAY => Vector(Box::new(from.gread(offset)?)),
            0x50 => Type,
            0x51 => Object,
            0x55 => Enum(from.gread::<SerString>(offset)?.0.ok_or_else(|| {
                scroll::Error::Custom("null enum name encountered when parsing custom attribute".to_string())
            })?),
            bad => throw!("bad custom attribute type tag {:#04x}", bad),
        };

        Ok((val, *offset))
    }
}
try_into_ctx!(FieldOrPropType<'_>, |self, into| {
    let offset = &mut 0;

    use FieldOrPropType::*;
    match self {
        Boolean => {
            into.gwrite_with(ELEMENT_TYPE_BOOLEAN, offset, scroll::LE)?;
        }
        Char => {
            into.gwrite_with(ELEMENT_TYPE_CHAR, offset, scroll::LE)?;
        }
        Int8 => {
            into.gwrite_with(ELEMENT_TYPE_I1, offset, scroll::LE)?;
        }
        UInt8 => {
            into.gwrite_with(ELEMENT_TYPE_U1, offset, scroll::LE)?;
        }
        Int16 => {
            into.gwrite_with(ELEMENT_TYPE_I2, offset, scroll::LE)?;
        }
        UInt16 => {
            into.gwrite_with(ELEMENT_TYPE_U2, offset, scroll::LE)?;
        }
        Int32 => {
            into.gwrite_with(ELEMENT_TYPE_I4, offset, scroll::LE)?;
        }
        UInt32 => {
            into.gwrite_with(ELEMENT_TYPE_U4, offset, scroll::LE)?;
        }
        Int64 => {
            into.gwrite_with(ELEMENT_TYPE_I8, offset, scroll::LE)?;
        }
        UInt64 => {
            into.gwrite_with(ELEMENT_TYPE_U8, offset, scroll::LE)?;
        }
        Float32 => {
            into.gwrite_with(ELEMENT_TYPE_R4, offset, scroll::LE)?;
        }
        Float64 => {
            into.gwrite_with(ELEMENT_TYPE_R8, offset, scroll::LE)?;
        }
        String => {
            into.gwrite_with(ELEMENT_TYPE_STRING, offset, scroll::LE)?;
        }
        Type => {
            into.gwrite_with(0x50_u8, offset, scroll::LE)?;
        }
        Object => {
            into.gwrite_with(0x51_u8, offset, scroll::LE)?;
        }
        Vector(t) => {
            into.gwrite_with(ELEMENT_TYPE_SZARRAY, offset, scroll::LE)?;
            into.gwrite(*t, offset)?;
        }
        Enum(n) => {
            into.gwrite_with(0x55_u8, offset, scroll::LE)?;
            into.gwrite(SerString(Some(n)), offset)?;
        }
    }

    Ok(*offset)
});

#[derive(Debug, Copy, Clone)]
pub enum IntegralParam {
    Int8(i8),
    Int16(i16),
    Int32(i32),
    Int64(i64),
    UInt8(u8),
    UInt16(u16),
    UInt32(u32),
    UInt64(u64),
}
impl IntegralParam {
    pub fn argument_type(&self) -> FieldOrPropType<'_> {
        macro_rules! build_match {
            ($($variant:ident),*) => {
                match self {
                    $(
                        IntegralParam::$variant(_) => FieldOrPropType::$variant,
                    )*
                }
            }
        }

        build_match!(Int8, Int16, Int32, Int64, UInt8, UInt16, UInt32, UInt64)
    }
}
try_into_ctx!(IntegralParam, |self, into| {
    let offset = &mut 0;

    macro_rules! build_match {
        ($($variant:ident),*) => {
            match self {
                $(
                    IntegralParam::$variant(val) => into.gwrite_with(val, offset, scroll::LE)?,
                )*
            }
        }
    }

    build_match!(Int8, Int16, Int32, Int64, UInt8, UInt16, UInt32, UInt64);

    Ok(*offset)
});

#[derive(Debug, Clone)]
pub enum FixedArg<'a> {
    Boolean(bool),
    Char(char),
    Float32(f32),
    Float64(f64),
    String(Option<Cow<'a, str>>),
    Integral(IntegralParam),
    Enum(Cow<'a, str>, IntegralParam),
    Type(Cow<'a, str>),
    Array(FieldOrPropType<'a>, Option<Vec<FixedArg<'a>>>),
    Object(Box<FixedArg<'a>>),
}
impl FixedArg<'_> {
    pub fn argument_type(&self) -> FieldOrPropType<'_> {
        use FixedArg::*;
        match self {
            Boolean(_) => FieldOrPropType::Boolean,
            Char(_) => FieldOrPropType::Char,
            Float32(_) => FieldOrPropType::Float32,
            Float64(_) => FieldOrPropType::Float64,
            String(_) => FieldOrPropType::String,
            Integral(i) => i.argument_type(),
            Enum(name, _) => FieldOrPropType::Enum(name.clone()),
            Type(_name) => FieldOrPropType::Type,
            Array(t, _) => FieldOrPropType::Vector(Box::new(t.clone())),
            Object(_) => FieldOrPropType::Object,
        }
    }
}

try_into_ctx!(FixedArg<'_>, |self, into| {
    let offset = &mut 0;

    use FixedArg::*;
    match self {
        Boolean(b) => {
            into.gwrite_with(b as u8, offset, scroll::LE)?;
        }
        Char(c) => {
            into.gwrite_with(c as u16, offset, scroll::LE)?;
        }
        Float32(f) => {
            into.gwrite_with(f, offset, scroll::LE)?;
        }
        Float64(f) => {
            into.gwrite_with(f, offset, scroll::LE)?;
        }
        String(s) => {
            into.gwrite(SerString(s), offset)?;
        }
        Integral(i) => {
            into.gwrite(i, offset)?;
        }
        Enum(_, val) => {
            into.gwrite(val, offset)?;
        }
        Type(t) => {
            into.gwrite(SerString(Some(t)), offset)?;
        }
        Array(_, v) => match v {
            Some(vector) => {
                into.gwrite_with(vector.len() as u32, offset, scroll::LE)?;
                for value in vector {
                    into.gwrite(value, offset)?;
                }
            }
            None => {
                into.gwrite_with(u32::MAX, offset, scroll::LE)?;
            }
        },
        Object(b) => {
            into.gwrite(b.argument_type(), offset)?;
            into.gwrite(*b, offset)?;
        }
    }

    Ok(*offset)
});

#[derive(Debug, Clone)]
pub enum NamedArg<'a> {
    Field(Cow<'a, str>, FixedArg<'a>),
    Property(Cow<'a, str>, FixedArg<'a>),
}
try_into_ctx!(NamedArg<'_>, |self, into| {
    let offset = &mut 0;

    use NamedArg::*;
    let (tag, name, arg) = match self {
        Field(n, a) => (0x53_u8, n, a),
        Property(n, a) => (0x54_u8, n, a),
    };

    into.gwrite_with(tag, offset, scroll::LE)?;
    into.gwrite(arg.argument_type(), offset)?;
    into.gwrite(SerString(Some(name.clone())), offset)?;
    into.gwrite(arg, offset)?;

    Ok(*offset)
});

#[derive(Debug)]
pub struct CustomAttributeData<'a> {
    pub constructor_args: Vec<FixedArg<'a>>,
    pub named_args: Vec<NamedArg<'a>>,
}
try_into_ctx!(CustomAttributeData<'_>, |self, into| {
    let offset = &mut 0;

    into.gwrite_with(0x0001_u16, offset, scroll::LE)?;

    for a in self.constructor_args {
        into.gwrite(a, offset)?;
    }

    into.gwrite_with(self.named_args.len() as u16, offset, scroll::LE)?;

    for a in self.named_args {
        into.gwrite(a, offset)?;
    }

    Ok(*offset)
});

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

    #[test]
    fn attr_args_write() -> Result<(), Box<dyn std::error::Error>> {
        use scroll::Pwrite;
        use FixedArg::*;
        use IntegralParam::*;
        use NamedArg::*;

        const SIZE: usize = 119;
        // retrieved from ildasm
        const DATA: [u8; SIZE] = [
            0x01, 0x00, 0x01, 0x61, 0x00, 0x04, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0x03, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00,
            0x00, 0x04, 0x00, 0x00, 0x00, 0x05, 0x00, 0x00, 0x00, 0x08, 0x09, 0x00, 0x00, 0x00, 0x1D, 0x51, 0x04, 0x00,
            0x00, 0x00, 0x08, 0x02, 0x00, 0x00, 0x00, 0x08, 0x05, 0x00, 0x00, 0x00, 0x1D, 0x51, 0x01, 0x00, 0x00, 0x00,
            0x08, 0x02, 0x00, 0x00, 0x00, 0x0E, 0xFF, 0x03, 0x00, 0x00, 0x00, 0x08, 0x03, 0x00, 0x00, 0x00, 0x55, 0x09,
            0x74, 0x65, 0x73, 0x74, 0x2E, 0x41, 0x73, 0x64, 0x66, 0x04, 0x00, 0x0E, 0x04, 0x6F, 0x6F, 0x70, 0x73, 0x02,
            0x00, 0x53, 0x08, 0x03, 0x59, 0x65, 0x73, 0x03, 0x00, 0x00, 0x00, 0x53, 0x51, 0x02, 0x4E, 0x6F, 0x55, 0x09,
            0x74, 0x65, 0x73, 0x74, 0x2E, 0x41, 0x73, 0x64, 0x66, 0x04, 0x00,
        ];

        let mut buf = [0_u8; SIZE];
        buf.pwrite(
            // Into<Box<_>> is a little less noisy here
            CustomAttributeData {
                constructor_args: vec![
                    Boolean(true),
                    Char('a'),
                    Integral(UInt16(4)),
                    Array(FieldOrPropType::Int32, None),
                    Array(
                        FieldOrPropType::Int32,
                        Some(vec![Integral(Int32(2)), Integral(Int32(4)), Integral(Int32(5))]),
                    ),
                    Object(Integral(Int32(9)).into()),
                    Object(
                        Array(
                            FieldOrPropType::Object,
                            Some(vec![
                                Object(Integral(Int32(2)).into()),
                                Object(Integral(Int32(5)).into()),
                                Object(
                                    Array(FieldOrPropType::Object, Some(vec![Object(Integral(Int32(2)).into())]))
                                        .into(),
                                ),
                                Object(String(None).into()),
                            ]),
                        )
                        .into(),
                    ),
                    Array(
                        FieldOrPropType::Object,
                        Some(vec![
                            Object(Integral(Int32(3)).into()),
                            Object(Enum("test.Asdf".into(), UInt16(4)).into()),
                            Object(String(Some("oops".into())).into()),
                        ]),
                    ),
                ],
                named_args: vec![
                    Field("Yes".into(), Integral(Int32(3))),
                    Field("No".into(), Object(Enum("test.Asdf".into(), UInt16(4)).into())),
                ],
            },
            0,
        )?;

        assert_eq!(buf, DATA);

        Ok(())
    }
}
