use std::io::Write;

use crate::callbacks::IntKind;

use crate::ir::comp::CompKind;
use crate::ir::context::{BindgenContext, TypeId};
use crate::ir::function::{Function, FunctionKind};
use crate::ir::item::Item;
use crate::ir::item::ItemCanonicalName;
use crate::ir::item_kind::ItemKind;
use crate::ir::ty::{FloatKind, Type, TypeKind};

use super::{CodegenError, WrapAsVariadic};

fn get_loc(item: &Item) -> String {
    item.location()
        .map(|x| x.to_string())
        .unwrap_or_else(|| "unknown".to_owned())
}

pub(super) trait CSerialize<'a> {
    type Extra;

    fn serialize<W: Write>(
        &self,
        ctx: &BindgenContext,
        extra: Self::Extra,
        stack: &mut Vec<String>,
        writer: &mut W,
    ) -> Result<(), CodegenError>;
}

impl<'a> CSerialize<'a> for Item {
    type Extra = &'a Option<WrapAsVariadic>;

    fn serialize<W: Write>(
        &self,
        ctx: &BindgenContext,
        extra: Self::Extra,
        stack: &mut Vec<String>,
        writer: &mut W,
    ) -> Result<(), CodegenError> {
        match self.kind() {
            ItemKind::Function(func) => {
                func.serialize(ctx, (self, extra), stack, writer)
            }
            kind => Err(CodegenError::Serialize {
                msg: format!("Cannot serialize item kind {:?}", kind),
                loc: get_loc(self),
            }),
        }
    }
}

impl<'a> CSerialize<'a> for Function {
    type Extra = (&'a Item, &'a Option<WrapAsVariadic>);

    fn serialize<W: Write>(
        &self,
        ctx: &BindgenContext,
        (item, wrap_as_variadic): Self::Extra,
        stack: &mut Vec<String>,
        writer: &mut W,
    ) -> Result<(), CodegenError> {
        if self.kind() != FunctionKind::Function {
            return Err(CodegenError::Serialize {
                msg: format!(
                    "Cannot serialize function kind {:?}",
                    self.kind(),
                ),
                loc: get_loc(item),
            });
        }

        let signature = match ctx.resolve_type(self.signature()).kind() {
            TypeKind::Function(signature) => signature,
            _ => unreachable!(),
        };

        assert!(!signature.is_variadic());

        let name = self.name();

        // Function argoments stored as `(name, type_id)` tuples.
        let args = {
            let mut count = 0;

            let idx_to_prune = wrap_as_variadic.as_ref().map(
                |WrapAsVariadic {
                     idx_of_va_list_arg, ..
                 }| *idx_of_va_list_arg,
            );

            signature
                .argument_types()
                .iter()
                .cloned()
                .enumerate()
                .filter_map(|(idx, (opt_name, type_id))| {
                    if Some(idx) == idx_to_prune {
                        None
                    } else {
                        Some((
                            opt_name.unwrap_or_else(|| {
                                let name = format!("arg_{}", count);
                                count += 1;
                                name
                            }),
                            type_id,
                        ))
                    }
                })
                .collect::<Vec<_>>()
        };

        // The name used for the wrapper self.
        let wrap_name = format!("{}{}", name, ctx.wrap_static_fns_suffix());

        // The function's return type
        let (ret_item, ret_ty) = {
            let type_id = signature.return_type();
            let ret_item = ctx.resolve_item(type_id);
            let ret_ty = ret_item.expect_type();

            // Write `ret_ty`.
            ret_ty.serialize(ctx, ret_item, stack, writer)?;

            (ret_item, ret_ty)
        };

        const INDENT: &str = "    ";

        // Write `wrap_name(args`.
        write!(writer, " {}(", wrap_name)?;
        serialize_args(&args, ctx, writer)?;

        if wrap_as_variadic.is_none() {
            // Write `) { name(` if the function returns void and `) { return name(` if it does not.
            if ret_ty.is_void() {
                write!(writer, ") {{ {}(", name)?;
            } else {
                write!(writer, ") {{ return {}(", name)?;
            }
        } else {
            // Write `, ...) {`
            writeln!(writer, ", ...) {{")?;

            // Declare the return type `RET_TY ret;` if their is a need to do so
            if !ret_ty.is_void() {
                write!(writer, "{INDENT}")?;
                ret_ty.serialize(ctx, ret_item, stack, writer)?;
                writeln!(writer, " ret;")?;
            }

            // Setup va_list
            writeln!(writer, "{INDENT}va_list ap;\n")?;
            writeln!(
                writer,
                "{INDENT}va_start(ap, {});",
                args.last().unwrap().0
            )?;

            write!(writer, "{INDENT}")?;
            // Write `ret = name(` or `name(` depending if the function returns something
            if !ret_ty.is_void() {
                write!(writer, "ret = ")?;
            }
            write!(writer, "{}(", name)?;
        }

        // Get the arguments names and insert at the right place if necessary `ap`
        let mut args: Vec<_> = args.into_iter().map(|(name, _)| name).collect();
        if let Some(WrapAsVariadic {
            idx_of_va_list_arg, ..
        }) = wrap_as_variadic
        {
            args.insert(*idx_of_va_list_arg, "ap".to_owned());
        }

        // Write `arg_names);`.
        serialize_sep(", ", args.iter(), ctx, writer, |name, _, buf| {
            write!(buf, "{}", name).map_err(From::from)
        })?;
        #[rustfmt::skip]
        write!(writer, ");{}", if wrap_as_variadic.is_none() { " " } else { "\n" })?;

        if wrap_as_variadic.is_some() {
            // End va_list and return the result if their is one
            writeln!(writer, "{INDENT}va_end(ap);")?;
            if !ret_ty.is_void() {
                writeln!(writer, "{INDENT}return ret;")?;
            }
        }

        writeln!(writer, "}}")?;

        Ok(())
    }
}

impl<'a> CSerialize<'a> for TypeId {
    type Extra = ();

    fn serialize<W: Write>(
        &self,
        ctx: &BindgenContext,
        (): Self::Extra,
        stack: &mut Vec<String>,
        writer: &mut W,
    ) -> Result<(), CodegenError> {
        let item = ctx.resolve_item(*self);
        item.expect_type().serialize(ctx, item, stack, writer)
    }
}

impl<'a> CSerialize<'a> for Type {
    type Extra = &'a Item;

    fn serialize<W: Write>(
        &self,
        ctx: &BindgenContext,
        item: Self::Extra,
        stack: &mut Vec<String>,
        writer: &mut W,
    ) -> Result<(), CodegenError> {
        match self.kind() {
            TypeKind::Void => {
                if self.is_const() {
                    write!(writer, "const ")?;
                }
                write!(writer, "void")?
            }
            TypeKind::NullPtr => {
                if self.is_const() {
                    write!(writer, "const ")?;
                }
                write!(writer, "nullptr_t")?
            }
            TypeKind::Int(int_kind) => {
                if self.is_const() {
                    write!(writer, "const ")?;
                }
                match int_kind {
                    IntKind::Bool => write!(writer, "bool")?,
                    IntKind::SChar => write!(writer, "signed char")?,
                    IntKind::UChar => write!(writer, "unsigned char")?,
                    IntKind::WChar => write!(writer, "wchar_t")?,
                    IntKind::Short => write!(writer, "short")?,
                    IntKind::UShort => write!(writer, "unsigned short")?,
                    IntKind::Int => write!(writer, "int")?,
                    IntKind::UInt => write!(writer, "unsigned int")?,
                    IntKind::Long => write!(writer, "long")?,
                    IntKind::ULong => write!(writer, "unsigned long")?,
                    IntKind::LongLong => write!(writer, "long long")?,
                    IntKind::ULongLong => write!(writer, "unsigned long long")?,
                    IntKind::Char { .. } => write!(writer, "char")?,
                    int_kind => {
                        return Err(CodegenError::Serialize {
                            msg: format!(
                                "Cannot serialize integer kind {:?}",
                                int_kind
                            ),
                            loc: get_loc(item),
                        })
                    }
                }
            }
            TypeKind::Float(float_kind) => {
                if self.is_const() {
                    write!(writer, "const ")?;
                }
                match float_kind {
                    FloatKind::Float16 => write!(writer, "_Float16")?,
                    FloatKind::Float => write!(writer, "float")?,
                    FloatKind::Double => write!(writer, "double")?,
                    FloatKind::LongDouble => write!(writer, "long double")?,
                    FloatKind::Float128 => write!(writer, "__float128")?,
                }
            }
            TypeKind::Complex(float_kind) => {
                if self.is_const() {
                    write!(writer, "const ")?;
                }
                match float_kind {
                    FloatKind::Float16 => write!(writer, "_Float16 complex")?,
                    FloatKind::Float => write!(writer, "float complex")?,
                    FloatKind::Double => write!(writer, "double complex")?,
                    FloatKind::LongDouble => {
                        write!(writer, "long double complex")?
                    }
                    FloatKind::Float128 => write!(writer, "__complex128")?,
                }
            }
            TypeKind::Alias(type_id) => {
                if let Some(name) = self.name() {
                    if self.is_const() {
                        write!(writer, "const {}", name)?;
                    } else {
                        write!(writer, "{}", name)?;
                    }
                } else {
                    type_id.serialize(ctx, (), stack, writer)?;
                }
            }
            TypeKind::Array(type_id, length) => {
                type_id.serialize(ctx, (), stack, writer)?;
                write!(writer, " [{}]", length)?
            }
            TypeKind::Function(signature) => {
                if self.is_const() {
                    stack.push("const ".to_string());
                }

                signature.return_type().serialize(
                    ctx,
                    (),
                    &mut vec![],
                    writer,
                )?;

                write!(writer, " (")?;
                while let Some(item) = stack.pop() {
                    write!(writer, "{}", item)?;
                }
                write!(writer, ")")?;

                let args = signature.argument_types();
                if args.is_empty() {
                    write!(writer, " (void)")?;
                } else {
                    write!(writer, " (")?;
                    serialize_sep(
                        ", ",
                        args.iter(),
                        ctx,
                        writer,
                        |(name, type_id), ctx, buf| {
                            let mut stack = vec![];
                            if let Some(name) = name {
                                stack.push(name.clone());
                            }
                            type_id.serialize(ctx, (), &mut stack, buf)
                        },
                    )?;
                    write!(writer, ")")?
                }
            }
            TypeKind::ResolvedTypeRef(type_id) => {
                if self.is_const() {
                    write!(writer, "const ")?;
                }
                type_id.serialize(ctx, (), stack, writer)?
            }
            TypeKind::Pointer(type_id) => {
                if self.is_const() {
                    stack.push("*const ".to_owned());
                } else {
                    stack.push("*".to_owned());
                }
                type_id.serialize(ctx, (), stack, writer)?
            }
            TypeKind::Comp(comp_info) => {
                if self.is_const() {
                    write!(writer, "const ")?;
                }

                let name = item.canonical_name(ctx);

                match comp_info.kind() {
                    CompKind::Struct => write!(writer, "struct {}", name)?,
                    CompKind::Union => write!(writer, "union {}", name)?,
                };
            }
            TypeKind::Enum(_enum_ty) => {
                if self.is_const() {
                    write!(writer, "const ")?;
                }

                let name = item.canonical_name(ctx);
                write!(writer, "enum {}", name)?;
            }
            ty => {
                return Err(CodegenError::Serialize {
                    msg: format!("Cannot serialize type kind {:?}", ty),
                    loc: get_loc(item),
                })
            }
        };

        if !stack.is_empty() {
            write!(writer, " ")?;
            while let Some(item) = stack.pop() {
                write!(writer, "{}", item)?;
            }
        }

        Ok(())
    }
}

fn serialize_args<W: Write>(
    args: &[(String, TypeId)],
    ctx: &BindgenContext,
    writer: &mut W,
) -> Result<(), CodegenError> {
    if args.is_empty() {
        write!(writer, "void")?;
    } else {
        serialize_sep(
            ", ",
            args.iter(),
            ctx,
            writer,
            |(name, type_id), ctx, buf| {
                type_id.serialize(ctx, (), &mut vec![name.clone()], buf)
            },
        )?;
    }

    Ok(())
}

fn serialize_sep<
    W: Write,
    F: FnMut(I::Item, &BindgenContext, &mut W) -> Result<(), CodegenError>,
    I: Iterator,
>(
    sep: &str,
    mut iter: I,
    ctx: &BindgenContext,
    buf: &mut W,
    mut f: F,
) -> Result<(), CodegenError> {
    if let Some(item) = iter.next() {
        f(item, ctx, buf)?;
        let sep = sep.as_bytes();
        for item in iter {
            buf.write_all(sep)?;
            f(item, ctx, buf)?;
        }
    }

    Ok(())
}
