use proc_macro2::TokenStream;
use quote::{ToTokens, quote};
use syn::Path;

use crate::parse::{
    oneof::utility::{get_variant_extract, get_variant_source_ident},
    options::{ParseDerive, ParseOptions, ParseTaggedUnion, ParseVariant},
    write_utility::{expand_field_inject, expand_write_field_type},
};

pub fn expand(options: &ParseOptions, variants: &[ParseVariant]) -> syn::Result<TokenStream> {
    options.tagged_union.as_ref().map_or_else(
        || expand_write(options, variants),
        |tagged_union| expand_write_tagged_union(options, variants, tagged_union),
    )
}

fn expand_write(options: &ParseOptions, variants: &[ParseVariant]) -> syn::Result<TokenStream> {
    let source = &options.source;
    let ident = &options.ident;

    let (write_derived_borrowed, write_variants) =
        expand_write_variants(options, variants, source)?;

    let (impl_generics, type_generics, where_clause) = options.generics.split_for_impl();

    Ok(quote! {
        #[automatically_derived]
        impl #impl_generics From<#ident #type_generics> for #source #where_clause {
            fn from(target: #ident #type_generics) -> Self {
                #write_derived_borrowed
                match target {
                    #write_variants
                    _ => panic!("unknown oneof variant"),
                }
            }
        }
    })
}

fn expand_write_tagged_union(
    options: &ParseOptions,
    variants: &[ParseVariant],
    tagged_union: &ParseTaggedUnion,
) -> syn::Result<TokenStream> {
    let (write_derived_borrowed, write_variants) =
        expand_write_variants(options, variants, &tagged_union.oneof)?;

    let source = &options.source;
    let ident = &options.ident;

    let field_ident = &tagged_union.field;

    let (impl_generics, type_generics, where_clause) = options.generics.split_for_impl();

    Ok(quote! {
        #[automatically_derived]
        impl #impl_generics From<#ident #type_generics> for #source #where_clause {
            fn from(target: #ident #type_generics) -> Self {
                #write_derived_borrowed
                #source {
                    #field_ident: Some(match target {
                        #write_variants
                        _ => panic!("unknown oneof variant"),
                    }),
                }
            }
        }
    })
}

fn expand_write_variants(
    options: &ParseOptions,
    variants: &[ParseVariant],
    source: &Path,
) -> syn::Result<(TokenStream, TokenStream)> {
    let ident = &options.ident;

    let mut write_derived_borrowed = quote!();
    let mut write_variants = quote!();

    for variant in variants.iter().filter(|variant| !variant.options.skip) {
        let target_variant_ident = &variant.ident;
        let source_variant_ident = get_variant_source_ident(variant)?;

        if variant.fields.is_empty() {
            write_variants.extend(if variant.source_unit {
                quote! {
                    #ident::#target_variant_ident => {
                        #source::#source_variant_ident(Default::default())
                    }
                }
            } else {
                quote! {
                    #ident::#target_variant_ident => {
                        #source::#source_variant_ident
                    }
                }
            });
        } else if matches!(
            variant.options.derive.as_ref(),
            Some(ParseDerive { target_borrow, .. }) if *target_borrow,
        ) {
            let write_variant = expand_write_variant(variant)?;
            write_derived_borrowed.extend(quote! {
                #write_variant
            });
        } else {
            let write_variant = expand_write_variant(variant)?;
            write_variants.extend(quote! {
                #ident::#target_variant_ident(target) => {
                    let mut source = #source::#source_variant_ident(Default::default());
                    if let #source::#source_variant_ident(source) = &mut source {
                        #write_variant
                    }
                    source
                }
            });
        }
    }

    Ok((write_derived_borrowed, write_variants))
}

fn expand_write_variant(variant: &ParseVariant) -> syn::Result<TokenStream> {
    let target_ident = &variant.ident;

    let extract = get_variant_extract(variant)?;

    if let Some(ParseDerive {
        write,
        module,
        target_borrow,
        ..
    }) = variant.options.derive.as_ref()
    {
        let write_impl = write
            .as_ref()
            .map(ToTokens::to_token_stream)
            .or_else(|| module.as_ref().map(|module| quote!(#module::write)))
            .ok_or_else(|| {
                syn::Error::new_spanned(target_ident, "missing derive write implementation")
            })?;

        return Ok(if *target_borrow {
            quote! {
                if let Some(source) = #write_impl(&target) {
                    return source;
                }
            }
        } else {
            quote! {
                *source = {
                    #write_impl(target)
                };
            }
        });
    }

    let field_type_info = variant.type_info.as_ref().unwrap();
    let inject_impl = expand_field_inject(&extract, &variant.options, Some(field_type_info));
    let write_field_impl = expand_write_field_type(&variant.options, field_type_info, inject_impl);

    Ok(quote! {
        let source_field = target;
        #write_field_impl
    })
}
