use super::*;
use crate::winmd::writer;

// TODO: store span in winmd so that errors resolving type references can be traced back to file/line/column
use std::collections::HashMap;
//use syn::spanned::Spanned;

// TODO: this creates a temporary in-memory winmd used to treat the IDL content uniformly as metadata.
// The winmd_to_winmd does the harder job of validating and producing canonical winmd for public consumption.

pub fn rdl_to_winmd(file: &File) -> Result<Vec<u8>> {
    // Local-to-qualified type names found in use declaration - e.g. "IStringable" -> "Windows.Foundation.IStringable"
    // This is just a convenience for the developer to shorten common references like this but would not support globs or renames.
    // Note that none of these are verified to be real until much later when the winmd is validated since we don't
    // know what other metadata may be combined
    let mut _use_map = HashMap::<String, String>::new();

    // TODO: read file and populate use_map

    // Types are collected here in two passes - this allows us to figure out whether a local name points to a relative type
    // or a type from a use declaration...?
    let mut collector = HashMap::<String, HashMap<&str, ModuleMember>>::new();

    file.modules.iter().for_each(|module| collect_module(&mut collector, module));

    // TODO: collect type names into hashmap (phase 1) and just drop clones of the IDL members into the collector

    // TODO: Can we just walk the collector at this point and populate the winmd writer and thus need the read-phase?
    // this second walking of the collector is basically the "define" phase

    let mut writer = winmd::Writer::new("temp.winmd");

    collector.iter().for_each(|(namespace, members)| members.iter().for_each(|(name, member)| write_member(&mut writer, namespace, name, member)));

    Ok(writer.into_stream())
}

fn collect_module<'a>(collector: &mut HashMap<String, HashMap<&'a str, ModuleMember>>, module: &'a Module) {
    module.members.iter().for_each(|member| collect_member(collector, module, member));
}

fn collect_member<'a>(collector: &mut HashMap<String, HashMap<&'a str, ModuleMember>>, module: &'a Module, member: &'a ModuleMember) {
    match member {
        ModuleMember::Module(module) => collect_module(collector, module),
        ModuleMember::Constant(_) | ModuleMember::Function(_) => {
            collector.entry(module.namespace.to_string()).or_default().entry("Apis").or_insert(member.clone());
        }
        _ => {
            collector.entry(module.namespace.to_string()).or_default().entry(member.name()).or_insert(member.clone());
        }
    }
}

fn write_member(writer: &mut winmd::Writer, namespace: &str, name: &str, member: &ModuleMember) {
    match member {
        ModuleMember::Interface(member) => write_interface(writer, namespace, name, member),
        ModuleMember::Struct(member) => write_struct(writer, namespace, name, member),
        ModuleMember::Enum(member) => write_enum(writer, namespace, name, member),
        ModuleMember::Class(member) => write_class(writer, namespace, name, member),
        rest => unimplemented!("{rest:?}"),
    }
}

fn write_interface(writer: &mut winmd::Writer, namespace: &str, name: &str, member: &Interface) {
    let mut flags = metadata::TypeAttributes::Public | metadata::TypeAttributes::Interface | metadata::TypeAttributes::Abstract;

    if member.winrt {
        flags |= metadata::TypeAttributes::WindowsRuntime
    }

    writer.tables.TypeDef.push(winmd::TypeDef {
        Extends: winmd::TypeDefOrRef::none(),
        FieldList: writer.tables.Field.len() as u32,
        MethodList: writer.tables.MethodDef.len() as u32,
        Flags: flags.0,
        TypeName: writer.strings.insert(name),
        TypeNamespace: writer.strings.insert(namespace),
    });

    for (number, generic) in member.generics.iter().enumerate() {
        writer.tables.GenericParam.push(writer::GenericParam {
            Number: number as u16,
            Flags: 0,
            Owner: writer::TypeOrMethodDef::TypeDef(writer.tables.TypeDef.len() as u32 - 1),
            Name: writer.strings.insert(generic),
        });
    }

    for type_path in &member.extends {
        let ty = syn_type_path(namespace, &member.generics, type_path);

        let reference = match &ty {
            winmd::Type::TypeRef(type_name) if type_name.generics.is_empty() => writer.insert_type_ref(&type_name.namespace, &type_name.name),
            winmd::Type::TypeRef(_) => writer.insert_type_spec(ty),
            rest => unimplemented!("{rest:?}"),
        };

        writer.tables.InterfaceImpl.push(writer::InterfaceImpl { Class: writer.tables.TypeDef.len() as u32 - 1, Interface: reference });
    }

    for method in &member.methods {
        let signature = syn_signature(namespace, &member.generics, &method.sig);

        let params: Vec<winmd::Type> = signature.params.iter().map(|param| param.ty.clone()).collect();

        let signature_blob = writer.insert_method_sig(metadata::MethodCallAttributes(0), &signature.return_type, &params);

        let flags = metadata::MethodAttributes::Abstract | metadata::MethodAttributes::HideBySig | metadata::MethodAttributes::HideBySig | metadata::MethodAttributes::NewSlot | metadata::MethodAttributes::Public | metadata::MethodAttributes::Virtual;

        writer.tables.MethodDef.push(winmd::MethodDef {
            RVA: 0,
            ImplFlags: 0,
            Flags: flags.0,
            Name: writer.strings.insert(&method.sig.ident.to_string()),
            Signature: signature_blob,
            ParamList: writer.tables.Param.len() as u32,
        });

        for (sequence, param) in signature.params.iter().enumerate() {
            writer.tables.Param.push(winmd::Param { Flags: 0, Sequence: (sequence + 1) as u16, Name: writer.strings.insert(&param.name) });
        }
    }
}

fn write_struct(writer: &mut winmd::Writer, namespace: &str, name: &str, member: &Struct) {
    let mut flags = metadata::TypeAttributes::Public | metadata::TypeAttributes::Sealed | metadata::TypeAttributes::SequentialLayout;

    if member.winrt {
        flags |= metadata::TypeAttributes::WindowsRuntime
    }

    let extends = writer.insert_type_ref("System", "ValueType");

    writer.tables.TypeDef.push(winmd::TypeDef {
        Extends: extends,
        FieldList: writer.tables.Field.len() as u32,
        MethodList: writer.tables.MethodDef.len() as u32,
        Flags: flags.0,
        TypeName: writer.strings.insert(name),
        TypeNamespace: writer.strings.insert(namespace),
    });

    for field in &member.fields {
        let flags = metadata::FieldAttributes::Public;
        let ty = syn_type(namespace, &[], &field.ty);
        let signature = writer.insert_field_sig(&ty);

        writer.tables.Field.push(winmd::Field { Flags: flags.0, Name: writer.strings.insert(&field.name), Signature: signature });
    }
}

fn write_enum(_writer: &mut winmd::Writer, _namespace: &str, _name: &str, _member: &Enum) {}

fn write_class(writer: &mut winmd::Writer, namespace: &str, name: &str, member: &Class) {
    let flags = metadata::TypeAttributes::Public | metadata::TypeAttributes::Sealed | metadata::TypeAttributes::WindowsRuntime;

    let extends = if let Some(base) = &member.base {
        match syn_type_path(namespace, &[], base) {
            winmd::Type::TypeRef(base) => writer.insert_type_ref(&base.namespace, &base.name),
            rest => unimplemented!("{rest:?}"),
        }
    } else {
        writer.insert_type_ref("System", "Object")
    };

    writer.tables.TypeDef.push(winmd::TypeDef {
        Extends: extends,
        // Even though ECMA-335 says these can be "null", bugs in ILDASM necessitate this to avoid "misreading" the list terminators.
        FieldList: writer.tables.Field.len() as u32,
        MethodList: writer.tables.MethodDef.len() as u32,
        Flags: flags.0,
        TypeName: writer.strings.insert(name),
        TypeNamespace: writer.strings.insert(namespace),
    });

    for (index, extends) in member.extends.iter().enumerate() {
        let ty = syn_type_path(namespace, &[], extends);

        let reference = match &ty {
            winmd::Type::TypeRef(type_name) if type_name.generics.is_empty() => writer.insert_type_ref(&type_name.namespace, &type_name.name),
            winmd::Type::TypeRef(_) => writer.insert_type_spec(ty),
            winmd::Type::IUnknown => writer.insert_type_ref("Windows.Win32.System.Com", "IUnknown"),
            winmd::Type::IInspectable => writer.insert_type_ref("Windows.Win32.System.WinRT", "IInspectable"),
            rest => unimplemented!("{rest:?}"),
        };

        writer.tables.InterfaceImpl.push(writer::InterfaceImpl { Class: writer.tables.TypeDef.len() as u32 - 1, Interface: reference });

        if index == 0 {
            // TODO: add the DefaultAttribute to the first interface
        }
    }
}

fn syn_signature(namespace: &str, generics: &[String], sig: &syn::Signature) -> winmd::Signature {
    let params = sig
        .inputs
        .iter()
        .map(|param| match param {
            syn::FnArg::Typed(pat_type) => {
                let name = match &*pat_type.pat {
                    syn::Pat::Ident(pat_ident) => pat_ident.ident.to_string(),
                    rest => unimplemented!("{rest:?}"),
                };
                let ty = syn_type(namespace, generics, &pat_type.ty);
                winmd::SignatureParam { name, ty }
            }
            rest => unimplemented!("{rest:?}"),
        })
        .collect();

    let return_type = if let syn::ReturnType::Type(_, ty) = &sig.output { syn_type(namespace, generics, ty) } else { winmd::Type::Void };

    winmd::Signature { params, return_type, call_flags: 0 }
}

fn syn_type(namespace: &str, generics: &[String], ty: &syn::Type) -> winmd::Type {
    match ty {
        syn::Type::Path(ty) => syn_type_path(namespace, generics, ty),
        syn::Type::Ptr(ptr) => syn_type_ptr(namespace, ptr),
        syn::Type::Array(array) => syn_type_array(namespace, array),
        rest => unimplemented!("{rest:?}"),
    }
}

fn syn_type_array(namespace: &str, array: &syn::TypeArray) -> winmd::Type {
    let ty = syn_type(namespace, &[], &array.elem);

    if let syn::Expr::Lit(lit) = &array.len {
        if let syn::Lit::Int(lit) = &lit.lit {
            if let Ok(len) = lit.base10_parse() {
                return ty.into_array(len);
            }
        }
    }

    unimplemented!()
}

fn syn_type_ptr(namespace: &str, ptr: &syn::TypePtr) -> winmd::Type {
    let ty = syn_type(namespace, &[], &ptr.elem);
    if ptr.mutability.is_some() {
        ty.into_mut_ptr()
    } else {
        ty.into_const_ptr()
    }
}

fn syn_type_path(namespace: &str, generics: &[String], ty: &syn::TypePath) -> winmd::Type {
    if ty.qself.is_none() {
        return syn_path(namespace, generics, &ty.path);
    }

    unimplemented!()
}

fn syn_path(namespace: &str, generics: &[String], path: &syn::Path) -> winmd::Type {
    if let Some(segment) = path.segments.first() {
        if path.segments.len() == 1 && segment.arguments.is_empty() {
            let name = segment.ident.to_string();

            if let Some(number) = generics.iter().position(|generic| generic == &name) {
                return winmd::Type::GenericParam(number as u16);
            }

            match name.as_str() {
                "void" => return winmd::Type::Void,
                "bool" => return winmd::Type::Bool,
                "char" => return winmd::Type::Char,
                "i8" => return winmd::Type::I8,
                "u8" => return winmd::Type::U8,
                "i16" => return winmd::Type::I16,
                "u16" => return winmd::Type::U16,
                "i32" => return winmd::Type::I32,
                "u32" => return winmd::Type::U32,
                "i64" => return winmd::Type::I64,
                "u64" => return winmd::Type::U64,
                "f32" => return winmd::Type::F32,
                "f64" => return winmd::Type::F64,
                "isize" => return winmd::Type::ISize,
                "usize" => return winmd::Type::USize,
                "HSTRING" => return winmd::Type::String,
                "GUID" => return winmd::Type::GUID,
                "IUnknown" => return winmd::Type::IUnknown,
                "IInspectable" => return winmd::Type::IInspectable,
                "HRESULT" => return winmd::Type::HRESULT,
                "PSTR" => return winmd::Type::PSTR,
                "PWSTR" => return winmd::Type::PWSTR,
                "PCSTR" => return winmd::Type::PCSTR,
                "PCWSTR" => return winmd::Type::PCWSTR,
                "BSTR" => return winmd::Type::BSTR,
                _ => {}
            };
        }
    }

    // TODO: Here we assume that paths are absolute since there's no way to disambiguate between nested and absolute paths
    // The canonicalize function (should maybe) preprocesses the IDL to make this work

    let mut builder = vec![];

    for segment in &path.segments {
        let segment = segment.ident.to_string();

        if segment == "super" {
            if builder.is_empty() {
                for segment in namespace.split('.') {
                    builder.push(segment.to_string());
                }
            }
            builder.pop();
        } else {
            builder.push(segment);
        }
    }

    // Unwrapping is fine as there should always be at least one segment.
    let (name, type_namespace) = builder.split_last().unwrap();
    let type_namespace = if type_namespace.is_empty() { namespace.to_string() } else { type_namespace.join(".") };
    let mut type_generics = vec![];

    if let Some(segment) = path.segments.last() {
        if let syn::PathArguments::AngleBracketed(args) = &segment.arguments {
            for arg in &args.args {
                match arg {
                    syn::GenericArgument::Type(ty) => type_generics.push(syn_type(namespace, generics, ty)),
                    rest => unimplemented!("{rest:?}"),
                }
            }
        }
    }

    winmd::Type::TypeRef(winmd::TypeName { namespace: type_namespace, name: name.to_string(), generics: type_generics })
}
