use crate::clang::{Clang, Node};
use crate::syntax::attrs::OtherAttrs;
use crate::syntax::cfg::CfgExpr;
use crate::syntax::namespace::Namespace;
use crate::syntax::report::Errors;
use crate::syntax::{Api, Discriminant, Doc, Enum, EnumRepr, ForeignName, Pair, Variant};
use flate2::write::GzDecoder;
use memmap::Mmap;
use proc_macro2::{Delimiter, Group, Ident, TokenStream};
use quote::{format_ident, quote, quote_spanned};
use std::env;
use std::fmt::{self, Display};
use std::fs::File;
use std::io::Write;
use std::path::PathBuf;
use std::str::FromStr;
use syn::{parse_quote, Path};

const CXX_CLANG_AST: &str = "CXX_CLANG_AST";

pub(crate) fn load(cx: &mut Errors, apis: &mut [Api]) {
    let ref mut variants_from_header = Vec::new();
    for api in apis {
        if let Api::Enum(enm) = api {
            if enm.variants_from_header {
                if enm.variants.is_empty() {
                    variants_from_header.push(enm);
                } else {
                    let span = span_for_enum_error(enm);
                    cx.error(
                        span,
                        "enum with #![variants_from_header] must be written with no explicit variants",
                    );
                }
            }
        }
    }

    let span = match variants_from_header.first() {
        None => return,
        Some(enm) => enm.variants_from_header_attr.clone().unwrap(),
    };

    let ast_dump_path = match env::var_os(CXX_CLANG_AST) {
        Some(ast_dump_path) => PathBuf::from(ast_dump_path),
        None => {
            let msg = format!(
                "environment variable ${} has not been provided",
                CXX_CLANG_AST,
            );
            return cx.error(span, msg);
        }
    };

    let memmap = File::open(&ast_dump_path).and_then(|file| unsafe { Mmap::map(&file) });
    let mut gunzipped;
    let ast_dump_bytes = match match memmap {
        Ok(ref memmap) => {
            let is_gzipped = memmap.get(..2) == Some(b"\x1f\x8b");
            if is_gzipped {
                gunzipped = Vec::new();
                let decode_result = GzDecoder::new(&mut gunzipped).write_all(memmap);
                decode_result.map(|()| gunzipped.as_slice())
            } else {
                Ok(memmap as &[u8])
            }
        }
        Err(error) => Err(error),
    } {
        Ok(bytes) => bytes,
        Err(error) => {
            let msg = format!("failed to read {}: {}", ast_dump_path.display(), error);
            return cx.error(span, msg);
        }
    };

    let ref root: Node = match serde_json::from_slice(ast_dump_bytes) {
        Ok(root) => root,
        Err(error) => {
            let msg = format!("failed to read {}: {}", ast_dump_path.display(), error);
            return cx.error(span, msg);
        }
    };

    let ref mut namespace = Vec::new();
    traverse(cx, root, namespace, variants_from_header, None);

    for enm in variants_from_header {
        if enm.variants.is_empty() {
            let span = &enm.variants_from_header_attr;
            let name = CxxName(&enm.name);
            let msg = format!("failed to find any C++ definition of enum {}", name);
            cx.error(span, msg);
        }
    }
}

fn traverse<'a>(
    cx: &mut Errors,
    node: &'a Node,
    namespace: &mut Vec<&'a str>,
    variants_from_header: &mut [&mut Enum],
    mut idx: Option<usize>,
) {
    match &node.kind {
        Clang::NamespaceDecl(decl) => {
            let Some(name) = &decl.name else {
                // Can ignore enums inside an anonymous namespace.
                return;
            };
            namespace.push(name);
            idx = None;
        }
        Clang::EnumDecl(decl) => {
            let Some(name) = &decl.name else {
                return;
            };
            idx = None;
            for (i, enm) in variants_from_header.iter_mut().enumerate() {
                if enm.name.cxx == **name && enm.name.namespace.iter().eq(&*namespace) {
                    if !enm.variants.is_empty() {
                        let span = &enm.variants_from_header_attr;
                        let qual_name = CxxName(&enm.name);
                        let msg = format!("found multiple C++ definitions of enum {}", qual_name);
                        cx.error(span, msg);
                        return;
                    }
                    let Some(fixed_underlying_type) = &decl.fixed_underlying_type else {
                        let span = &enm.variants_from_header_attr;
                        let name = &enm.name.cxx;
                        let qual_name = CxxName(&enm.name);
                        let msg = format!(
                            "implicit implementation-defined repr for enum {} is not supported yet; consider changing its C++ definition to `enum {}: int {{...}}",
                            qual_name, name,
                        );
                        cx.error(span, msg);
                        return;
                    };
                    let repr = translate_qual_type(
                        cx,
                        enm,
                        fixed_underlying_type
                            .desugared_qual_type
                            .as_ref()
                            .unwrap_or(&fixed_underlying_type.qual_type),
                    );
                    enm.repr = EnumRepr::Foreign { rust_type: repr };
                    idx = Some(i);
                    break;
                }
            }
            if idx.is_none() {
                return;
            }
        }
        Clang::EnumConstantDecl(decl) => {
            if let Some(idx) = idx {
                let enm = &mut *variants_from_header[idx];
                let span = enm
                    .variants_from_header_attr
                    .as_ref()
                    .unwrap()
                    .path()
                    .get_ident()
                    .unwrap()
                    .span();
                let Ok(cxx_name) = ForeignName::parse(&decl.name, span) else {
                    let span = &enm.variants_from_header_attr;
                    let msg = format!("unsupported C++ variant name: {}", decl.name);
                    return cx.error(span, msg);
                };
                let rust_name: Ident = match syn::parse_str(&decl.name) {
                    Ok(ident) => ident,
                    Err(_) => format_ident!("__Variant{}", enm.variants.len()),
                };
                let discriminant = match discriminant_value(&node.inner) {
                    ParsedDiscriminant::Constant(discriminant) => discriminant,
                    ParsedDiscriminant::Successor => match enm.variants.last() {
                        None => Discriminant::zero(),
                        Some(last) => match last.discriminant.checked_succ() {
                            Some(discriminant) => discriminant,
                            None => {
                                let span = &enm.variants_from_header_attr;
                                let msg = format!(
                                    "overflow processing discriminant value for variant: {}",
                                    decl.name,
                                );
                                return cx.error(span, msg);
                            }
                        },
                    },
                    ParsedDiscriminant::Fail => {
                        let span = &enm.variants_from_header_attr;
                        let msg = format!(
                            "failed to obtain discriminant value for variant: {}",
                            decl.name,
                        );
                        cx.error(span, msg);
                        Discriminant::zero()
                    }
                };
                enm.variants.push(Variant {
                    cfg: CfgExpr::Unconditional,
                    doc: Doc::new(),
                    attrs: OtherAttrs::none(),
                    name: Pair {
                        namespace: Namespace::ROOT,
                        cxx: cxx_name,
                        rust: rust_name,
                    },
                    discriminant,
                    expr: None,
                });
            }
        }
        _ => {}
    }
    for inner in &node.inner {
        traverse(cx, inner, namespace, variants_from_header, idx);
    }
    if let Clang::NamespaceDecl(_) = &node.kind {
        let _ = namespace.pop().unwrap();
    }
}

fn translate_qual_type(cx: &mut Errors, enm: &Enum, qual_type: &str) -> Path {
    let rust_std_name = match qual_type {
        "char" => "c_char",
        "int" => "c_int",
        "long" => "c_long",
        "long long" => "c_longlong",
        "signed char" => "c_schar",
        "short" => "c_short",
        "unsigned char" => "c_uchar",
        "unsigned int" => "c_uint",
        "unsigned long" => "c_ulong",
        "unsigned long long" => "c_ulonglong",
        "unsigned short" => "c_ushort",
        unsupported => {
            let span = &enm.variants_from_header_attr;
            let qual_name = CxxName(&enm.name);
            let msg = format!(
                "unsupported underlying type for {}: {}",
                qual_name, unsupported,
            );
            cx.error(span, msg);
            "c_int"
        }
    };
    let span = enm
        .variants_from_header_attr
        .as_ref()
        .unwrap()
        .path()
        .get_ident()
        .unwrap()
        .span();
    let ident = Ident::new(rust_std_name, span);
    let path = quote_spanned!(span=> ::cxx::core::ffi::#ident);
    parse_quote!(#path)
}

enum ParsedDiscriminant {
    Constant(Discriminant),
    Successor,
    Fail,
}

fn discriminant_value(mut clang: &[Node]) -> ParsedDiscriminant {
    if clang.is_empty() {
        // No discriminant expression provided; use successor of previous
        // discriminant.
        return ParsedDiscriminant::Successor;
    }

    loop {
        if clang.len() != 1 {
            return ParsedDiscriminant::Fail;
        }

        let node = &clang[0];
        match &node.kind {
            Clang::ImplicitCastExpr => clang = &node.inner,
            Clang::ConstantExpr(expr) => match Discriminant::from_str(&expr.value) {
                Ok(discriminant) => return ParsedDiscriminant::Constant(discriminant),
                Err(_) => return ParsedDiscriminant::Fail,
            },
            _ => return ParsedDiscriminant::Fail,
        }
    }
}

fn span_for_enum_error(enm: &Enum) -> TokenStream {
    let enum_token = enm.enum_token;
    let mut brace_token = Group::new(Delimiter::Brace, TokenStream::new());
    brace_token.set_span(enm.brace_token.span.join());
    quote!(#enum_token #brace_token)
}

struct CxxName<'a>(&'a Pair);

impl<'a> Display for CxxName<'a> {
    fn fmt(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
        for namespace in &self.0.namespace {
            write!(formatter, "{}::", namespace)?;
        }
        write!(formatter, "{}", self.0.cxx)
    }
}
