use syn::{spanned::Spanned, Field, Ident, Path};

use crate::{
    attr,
    index::{quote_index, quote_optional_index, Index, IndexAllocator},
    label::{optional_label_or_ident, quote_label, quote_optional_label, Label, LabelValue},
    tag::quote_optional_tag,
    value_trait::{field_codegen, ImplStrategy},
};

#[derive(Clone, Copy)]
pub(crate) enum RecordTupleTarget {
    RecordTuple,
    Record,
    Tuple,
    Seq,
}

impl RecordTupleTarget {
    pub(crate) fn named_fields() -> Self {
        RecordTupleTarget::RecordTuple
    }

    pub(crate) fn unnamed_fields() -> Self {
        RecordTupleTarget::Tuple
    }
}

pub(crate) fn stream_record_tuple<'a, I, B>(
    path: proc_macro2::TokenStream,
    fields: I,
    impl_block: &B,
    mut target: RecordTupleTarget,
    tag: Option<&Path>,
    label: Option<Label>,
    index: Option<Index>,
    unlabeled_fields: bool,
    unindexed_fields: bool,
) -> syn::Result<proc_macro2::TokenStream>
where
    I: Iterator<Item = &'a Field>,
    B: ImplStrategy + ?Sized,
{
    let tag = quote_optional_tag(tag);
    let label = quote_optional_label(label);
    let index = quote_optional_index(index);

    let mut field_binding = Vec::new();
    let mut stream_field = Vec::new();

    let mut field_count = 0usize;
    let mut labeled_field_count = 0;
    let mut indexed_field_count = 0;
    let mut const_size = true;

    let index_ident = Ident::new("__sval_index", proc_macro2::Span::call_site());
    let label_ident = Ident::new("__sval_label", proc_macro2::Span::call_site());

    let mut index_allocator = IndexAllocator::new();

    for (i, field) in fields.enumerate() {
        attr::check(
            "struct field",
            &[
                &attr::TagAttr,
                &attr::DataTagAttr,
                &attr::IndexAttr,
                &attr::LabelAttr,
                &attr::SkipAttr,
                &attr::FlattenAttr,
                &attr::OuterRefAttr,
                &attr::InnerRefAttr,
                &attr::ComputedAttr,
            ],
            &field.attrs,
        )?;

        let i = syn::Index::from(i);

        if attr::get("struct field", attr::SkipAttr, &field.attrs)?.unwrap_or(false) {
            field_binding.push(quote_field_skip(&i, field));
            continue;
        }

        let (ident, binding) = get_field(&i, field);

        let field_tag =
            quote_optional_tag(attr::get("struct field", attr::TagAttr, &field.attrs)?.as_ref());

        let label = if unlabeled_fields {
            attr::ensure_missing("struct field", attr::LabelAttr, &field.attrs)?;

            None
        } else {
            get_label(
                attr::get("struct field", attr::LabelAttr, &field.attrs)?,
                field.ident.as_ref(),
            )
        };

        let index = if unindexed_fields {
            attr::ensure_missing("struct field", attr::IndexAttr, &field.attrs)?;

            None
        } else {
            Some(quote_index(index_allocator.next_computed_index(
                &index_ident,
                attr::get("struct field", attr::IndexAttr, &field.attrs)?,
            )))
        };

        let flatten = attr::get("struct field", attr::FlattenAttr, &field.attrs)?.unwrap_or(false);

        const_size = const_size && !flatten;

        let field_value_tokens = if flatten {
            impl_block.quote_stream_value(
                quote!(&mut stream),
                &ident,
                field_codegen(&field.attrs)?,
            )?
        } else {
            impl_block.quote_stream_value(
                quote!(&mut *stream),
                &ident,
                field_codegen(&field.attrs)?,
            )?
        };

        let value =
            if let Some(data_tag) = attr::get("struct field", attr::DataTagAttr, &field.attrs)? {
                let data_tag = quote_optional_tag(Some(&data_tag));
                let data_label = quote_optional_label(None);
                let data_index = quote_optional_index(None);

                quote!({
                    stream.tagged_begin(#data_tag, #data_label, #data_index)?;
                    #field_value_tokens?;
                    stream.tagged_end(#data_tag, #data_label, #data_index)?
                })
            } else {
                quote!(#field_value_tokens?)
            };

        match (&label, &index) {
            (Some(label), Some(index)) => {
                if flatten {
                    stream_field.push(quote!({
                        let mut stream = sval_derive::extensions::flatten::FlattenToRecordTuple::new(&mut *stream, #index_ident);
                        #field_value_tokens?;
                        #index_ident = stream.end().0;
                    }));
                } else {
                    stream_field.push(quote!({
                        let #index_ident = #index;
                        let #label_ident = #label;

                        stream.record_tuple_value_begin(#field_tag, #label_ident, #index_ident)?;
                        #value;
                        stream.record_tuple_value_end(#field_tag, #label_ident, #index_ident)?;
                    }));
                }

                target = RecordTupleTarget::RecordTuple;
                labeled_field_count += 1;
                indexed_field_count += 1;
            }
            (None, Some(index)) => {
                if flatten {
                    stream_field.push(quote!({
                        let mut stream = sval_derive::extensions::flatten::FlattenToTuple::new(&mut *stream, #index_ident);
                        #field_value_tokens?;
                        #index_ident = stream.end().0;
                    }));
                } else {
                    stream_field.push(quote!({
                        let #index_ident = #index;

                        stream.tuple_value_begin(#field_tag, #index_ident)?;
                        #value;
                        stream.tuple_value_end(#field_tag, #index_ident)?;
                    }));
                }

                target = RecordTupleTarget::Tuple;
                indexed_field_count += 1;
            }
            (Some(label), None) => {
                if flatten {
                    stream_field.push(quote!({
                        let mut stream = sval_derive::extensions::flatten::FlattenToRecord::new(&mut *stream, #index_ident);
                        #field_value_tokens?;
                        #index_ident = stream.end().0;
                    }));
                } else {
                    stream_field.push(quote!({
                        let #label_ident = #label;

                        stream.record_value_begin(#field_tag, #label_ident)?;
                        #value;
                        stream.record_value_end(#field_tag, #label_ident)?;
                    }));
                }

                target = RecordTupleTarget::Record;
                labeled_field_count += 1;
            }
            (None, None) => {
                if flatten {
                    stream_field.push(quote!({
                        let mut stream = sval_derive::extensions::flatten::FlattenToSeq::new(&mut *stream, 0);
                        #field_value_tokens?;
                        stream.end();
                    }));
                } else {
                    stream_field.push(quote!({
                        stream.seq_value_begin()?;
                        #value;
                        stream.seq_value_end()?;
                    }));
                }

                target = RecordTupleTarget::Seq;
            }
        }

        field_binding.push(binding);
        field_count += 1;
    }

    if labeled_field_count != 0 && labeled_field_count != field_count {
        return Err(syn::Error::new(
            proc_macro2::Span::call_site(),
            "if any fields have a label then all fields need one",
        ));
    }
    if indexed_field_count != 0 && indexed_field_count != field_count {
        return Err(syn::Error::new(
            proc_macro2::Span::call_site(),
            "if any fields have an index then all fields need one",
        ));
    }

    let field_count = if const_size {
        quote!(sval::__private::option::Option::Some(#field_count))
    } else {
        quote!(sval::__private::option::Option::None)
    };

    Ok(match target {
        RecordTupleTarget::RecordTuple => {
            quote!(#path { #(#field_binding,)* } => {
                stream.record_tuple_begin(#tag, #label, #index, #field_count)?;

                let mut #index_ident = 0;

                #(
                    #stream_field
                )*

                stream.record_tuple_end(#tag, #label, #index)?;
            })
        }
        RecordTupleTarget::Tuple => {
            quote!(#path { #(#field_binding,)* } => {
                stream.tuple_begin(#tag, #label, #index, #field_count)?;

                let mut #index_ident = 0;

                #(
                    #stream_field
                )*

                stream.tuple_end(#tag, #label, #index)?;
            })
        }
        RecordTupleTarget::Record => {
            quote!(#path { #(#field_binding,)* } => {
                stream.record_begin(#tag, #label, #index, #field_count)?;

                let mut #index_ident = 0;

                #(
                    #stream_field
                )*

                stream.record_end(#tag, #label, #index)?;
            })
        }
        RecordTupleTarget::Seq => {
            quote!(#path { #(#field_binding,)* } => {
                stream.tagged_begin(#tag, #label, #index)?;
                stream.seq_begin(#field_count)?;

                #(
                    #stream_field
                )*

                stream.seq_end()?;
                stream.tagged_end(#tag, #label, #index)?;
            })
        }
    })
}

fn get_field(index: &syn::Index, field: &Field) -> (Ident, proc_macro2::TokenStream) {
    let ident = Ident::new(&format!("field{}", index.index), field.span());

    if let Some(ref field) = field.ident {
        let tokens = quote!(#field: ref #ident);

        (ident, tokens)
    } else {
        let tokens = quote!(#index: ref #ident);

        (ident, tokens)
    }
}

fn get_label(
    explicit: Option<LabelValue>,
    ident: Option<&Ident>,
) -> Option<proc_macro2::TokenStream> {
    optional_label_or_ident(explicit, ident).map(|label| quote_label(label))
}

fn quote_field_skip(index: &syn::Index, field: &Field) -> proc_macro2::TokenStream {
    if let Some(ref field) = field.ident {
        quote!(#field: _)
    } else {
        quote!(#index: _)
    }
}
