use zyn::ToTokens;
use zyn::ext::AttrExt;
use zyn::ext::FieldsExt;
use zyn::ext::TypeExt;
use zyn::quote::quote;
use zyn::syn::Field;
use zyn::syn::GenericParam;
use zyn::syn::parse_quote;

use crate::SyncBlockConfig;

// Sync block

#[zyn::element]
pub fn sync_block_impl(item: zyn::syn::ItemStruct, config: SyncBlockConfig) -> zyn::TokenStream
{
    zyn::zyn!(
        {{ item }}

        // module to keep everything clean
        mod {{ item.ident | snake | ident: "{}_synchronous_block" }}
        {
            use super::*;

            @sync_block_view_struct(item = item.clone())
        }

        @sync_block_syncio_impl(item = item.clone(), config = *config)
        @sync_block_impl_block(item = item.clone(), tagged = config.tagged)
    )
}

// Block implementation for sync block
#[zyn::element]
fn sync_block_syncio_impl(item: zyn::syn::ItemStruct, config: SyncBlockConfig) -> zyn::TokenStream
{
    let view_lifetime: GenericParam = parse_quote!('view);
    let mut view_generics = item.generics.clone();
    view_generics.params.iter_mut().for_each(|x| match x
    {
        GenericParam::Lifetime(_) =>
        {}
        GenericParam::Type(type_param) => type_param
            .bounds
            .push(zyn::syn::TypeParamBound::Lifetime(parse_quote!('view))),
        GenericParam::Const(_) =>
        {}
    });
    view_generics.params.insert(0, view_lifetime);

    let (view_impl_generics, view_type_generics, view_where_clause) =
        view_generics.split_for_impl();
    let (_impl_generics, type_generics, _where_clause) = item.generics.split_for_impl();

    zyn::zyn!(
        impl {{ view_impl_generics }} oxydsp_flowgraph::block::SyncBlockIO<'view> for {{ item.ident }} {{ type_generics }}
            {{ view_where_clause }}
        {
            // Path within module
            type StateView = {{ item.ident | snake | ident: "{}_synchronous_block" }}::{{ item.ident | ident:"{}View" }} {{ view_type_generics }};
            type Input = {{ sync_block_io_types(item.clone(), "input", config.tagged) }};
            type Output = {{ sync_block_io_types(item.clone(), "output", config.tagged) }};
        }
    )
}

// Input/Output types for block
fn sync_block_io_types(
    item: zyn::syn::ItemStruct,
    io: &'static str,
    tagged: bool,
) -> zyn::TokenStream
{
    let field_types = item
        .fields
        .as_named()
        .unwrap()
        .named
        .iter()
        .filter(|f| f.attrs.iter().any(|attr| attr.is(io)))
        .map(|x| x.ty.clone())
        .map(|ty| {
            match ty
                .as_path()
                .unwrap()
                .segments
                .last()
                .unwrap()
                .arguments
                .clone()
            {
                zyn::syn::PathArguments::AngleBracketed(args) =>
                {
                    let args = args.args.to_token_stream();
                    if tagged
                    {
                        quote!(oxydsp_flowgraph::tag::Tagged<#args>).into_token_stream()
                    }
                    else
                    {
                        args
                    }
                }
                zyn::syn::PathArguments::None => panic!(),
                zyn::syn::PathArguments::Parenthesized(_) => panic!(),
            }
        })
        .collect::<Vec<_>>();

    zyn::zyn!(
        @if (field_types.is_empty())
        {
            ()
        } @else if (field_types.len() == 1)
        {
            {{ field_types[0].clone() }}
        } @else
        {
            (
                @for (x in field_types.iter())
                {
                    {{ x }},
                }
            )
        }
    )
    .into_token_stream()
}

// View struct for sync block
#[zyn::element]
fn sync_block_view_struct(item: zyn::syn::ItemStruct) -> zyn::TokenStream
{
    // Create view liftime to add to struct definition
    let lifetime: GenericParam = parse_quote!('view);
    let mut generics = item.generics.clone();
    generics.params.iter_mut().for_each(|x| match x
    {
        GenericParam::Lifetime(_) =>
        {}
        GenericParam::Type(type_param) => type_param
            .bounds
            .push(zyn::syn::TypeParamBound::Lifetime(parse_quote!('view))),
        GenericParam::Const(_) =>
        {}
    });
    generics.params.insert(0, lifetime);

    let (impl_generics, _type_generics, where_clause) = generics.split_for_impl();
    let fields = &item.fields.as_named().unwrap().named;

    let mut state_fields = vec![];
    for field in fields.iter()
    {
        let mut f = field.clone();
        if f.attrs.iter().any(|x| x.is("input") || x.is("output"))
        {
            continue;
        }

        let tk = field.ty.clone().into_token_stream();
        f.ty = parse_quote!(&'view mut #tk);
        state_fields.push(f);
    }

    let phantom_types = generics
        .params
        .iter()
        .map(|param| {
            match param
            {
                GenericParam::Type(t) =>
                {
                    let ident = &t.ident;
                    quote!(#ident)
                }
                GenericParam::Lifetime(l) =>
                {
                    let lifetime = &l.lifetime;
                    // Lifetimes need to be wrapped in a reference or similar
                    quote!(& #lifetime ())
                }
                GenericParam::Const(_) =>
                {
                    // Const generics generally don't need PhantomData
                    // as they don't affect variance or drop-check.
                    quote!()
                }
            }
        })
        .filter(|tokens| !tokens.is_empty());

    zyn::zyn!(
        pub struct {{ item.ident | ident:"{}View" }} {{ impl_generics }}
            {{ where_clause }}
        {
            @for (field in state_fields.iter())
            {
                pub {{ field }},
            }
            pub _sync_block_phantom: std::marker::PhantomData<(
                                    @for (ty in phantom_types)
                                    {
                                        {{ ty }},
                                    }
                                 )>,
        }
    )
}

// Instantiates the view struct
fn sync_block_make_view_struct(item: zyn::syn::ItemStruct) -> zyn::TokenStream
{
    let fields = &item.fields.as_named().unwrap().named;

    let mut state_fields = vec![];
    for field in fields.iter()
    {
        let mut f = field.clone();
        if f.attrs.iter().any(|x| x.is("input") || x.is("output"))
        {
            continue;
        }

        let tk = field.ty.clone().into_token_stream();
        f.ty = parse_quote!(&'view mut #tk);
        state_fields.push(f);
    }

    zyn::zyn!(
        {{item.ident | snake | ident:"{}_synchronous_block" }}::{{ item.ident | ident:"{}View" }} {
            @for (field in state_fields)
            {
                {{field.ident}}: &mut self.{{ field.ident }},
            }
            _sync_block_phantom: Default::default(),
        }
    )
    .into_token_stream()
}

// Impl Block for syncio
#[zyn::element]
fn sync_block_impl_block(item: zyn::syn::ItemStruct, tagged: bool) -> zyn::TokenStream
{
    let item2 = item.clone();
    let (impl_generics, type_generics, where_clause) = item2.generics.split_for_impl();
    let fields = &item.fields.as_named().unwrap().named;

    // Retrieve fields
    let input_fields = fields
        .iter()
        .filter(|f| f.attrs.iter().any(|attr| attr.is("input")))
        .cloned()
        .collect::<Vec<_>>();
    let output_fields = fields
        .iter()
        .filter(|f| f.attrs.iter().any(|attr| attr.is("output")))
        .cloned()
        .collect::<Vec<_>>();

    zyn::zyn!(
        impl {{ impl_generics }} oxydsp_flowgraph::block::Block for {{ item.ident }} {{ type_generics }}
            {{ where_clause }}
        {
            fn work(&mut self) -> oxydsp_flowgraph::block::BlockResult
            {
                use oxydsp_flowgraph::tag::TagMergable;
                // Get writers from outputs
                let mut max_len = usize::MAX;
                @for (out_field in output_fields.iter())
                {
                    let {{ out_field.ident.clone().unwrap() | ident: "{}_writer"}} = self.{{ out_field.ident }}.write();
                }

                // Compute max_len
                let max_len = *([
                    usize::MAX,
                    @for (out_field in output_fields.iter())
                    {
                        {{ out_field.ident.clone().unwrap() | ident: "{}_writer"}}.len(),
                    }
                ].iter().min().unwrap());


                @if (!input_fields.is_empty())
                {
                    @sync_block_block_impl_with_inputs(item = item.clone(), input_fields = input_fields.clone(), output_fields = output_fields.clone(), tagged = *tagged)
                }
                @else
                {
                    @sync_block_block_impl_without_inputs(item = item.clone(), output_fields = output_fields.clone(), tagged = *tagged)
                }

                oxydsp_flowgraph::block::BlockResult::Ok
            }
        }
    )
}

// Impl Block for syncio (no inputs)
#[zyn::element]
fn sync_block_block_impl_without_inputs(
    item: zyn::syn::ItemStruct,
    output_fields: Vec<Field>,
    tagged: bool,
) -> zyn::TokenStream
{
    zyn::zyn!(
    for _ in 0..max_len
    {
        // Get outputs
        let state = {{ sync_block_make_view_struct(item.clone()) }};
        let @sync_block_output_descons(output_fields = output_fields.clone(), tagged = *tagged) =
            <Self as oxydsp_flowgraph::block::SyncBlock>::sync_work(state, ()).unwrap();

        // Now the output samples must be sent to their resepective outputs
        @for (out_field in output_fields.iter())
        {
            @if (*tagged)
            {
            let _ = {{ out_field.ident.clone().unwrap() | ident: "{}_writer"}}.push(
                oxydsp_flowgraph::tag::Tagged(
                    {{ out_field.ident.clone().unwrap() | ident: "{}_element"}},
                    {{ out_field.ident.clone().unwrap() | ident: "{}_tag_opt"}},
                )
                );
            } @else
            {
            let _ = {{ out_field.ident.clone().unwrap() | ident: "{}_writer"}}.push(
                oxydsp_flowgraph::tag::Tagged(
                    {{ out_field.ident.clone().unwrap() | ident: "{}_element"}},
                    None,
                )
                );
            }
        }
    }
    )
}

// Impl Block for syncio (with inputs)
#[zyn::element]
fn sync_block_block_impl_with_inputs(
    item: zyn::syn::ItemStruct,
    input_fields: Vec<Field>,
    output_fields: Vec<Field>,
    tagged: bool,
) -> zyn::TokenStream
{
    zyn::zyn!(

    use oxydsp_flowgraph::io::PopIterable;
    // Iterate on inputs
    (
        @for (in_field in input_fields.iter())
        {
            &mut self.{{ in_field.ident }},
        }
    ).pop_iter()
    .take(max_len)
    .for_each(
    // Deconstruct foreach arguments
    |
        (@for (in_field in input_fields.iter())
        {
            oxydsp_flowgraph::tag::Tagged({{in_field.ident.clone().unwrap() | ident:"{}_element"}},
             {{in_field.ident.clone().unwrap() | ident:"{}_tag_opt"}}),
        })
    |
    {
        // Create output tag
        let common_tag = oxydsp_flowgraph::tag::Tag::from_tag_opts([
            @for (in_field in input_fields.iter())
            {
                 &{{in_field.ident.clone().unwrap() | ident:"{}_tag_opt"}},
            }
        ]);

        let state = {{ sync_block_make_view_struct(item.clone()) }};
        // Compute output sample
        let @sync_block_output_descons(output_fields = output_fields.clone(), tagged = *tagged)
            = <Self as oxydsp_flowgraph::block::SyncBlock>::sync_work(state, @sync_block_input_cons(input_fields = input_fields.clone(), tagged = *tagged)).unwrap();

        // Now the output samples must be sent to their resepective outputs
        @for (out_field in output_fields.iter())
        {
            @if (*tagged)
            {
            let _ = {{ out_field.ident.clone().unwrap() | ident: "{}_writer"}}.push(
                oxydsp_flowgraph::tag::Tagged(
                    {{ out_field.ident.clone().unwrap() | ident: "{}_element"}},
                    {{ out_field.ident.clone().unwrap() | ident: "{}_tag_opt"}}.merge(&common_tag),
                )
                );
            } @else
            {
            let _ = {{ out_field.ident.clone().unwrap() | ident: "{}_writer"}}.push(
                oxydsp_flowgraph::tag::Tagged(
                    {{ out_field.ident.clone().unwrap() | ident: "{}_element"}},
                    common_tag,
                )
                );
            }
        }
        //

    }
    );
    )
}

#[zyn::element]
fn sync_block_output_descons(output_fields: Vec<Field>, tagged: bool) -> zyn::TokenStream
{
    #[allow(clippy::collapsible_else_if)]
    if *tagged
    {
        // If tagged : deconstruct tags
        if output_fields.is_empty()
        {
            zyn::zyn!(_)
        }
        else if output_fields.len() == 1
        {
            zyn::zyn!(
                oxydsp_flowgraph::tag::Tagged({{output_fields[0].ident.clone().unwrap() | ident:"{}_element"}}, {{output_fields[0].ident.clone().unwrap() | ident:"{}_tag_opt"}})
            )
        }
        else
        {
            zyn::zyn!(
                (@for (in_field in output_fields.iter())
                 {
                     oxydsp_flowgraph::tag::Tagged({{in_field.ident.clone().unwrap() | ident:"{}_element"}}, {{in_field.ident.clone().unwrap() | ident:"{}_tag_opt"}}.clone()),
                 }
                )
            )
        }
    }
    else
    {
        // Otherwise just get the output element
        if output_fields.is_empty()
        {
            zyn::zyn!(_)
        }
        else if output_fields.len() == 1
        {
            zyn::zyn!(
                {{output_fields[0].ident.clone().unwrap() | ident:"{}_element"}}
            )
        }
        else
        {
            zyn::zyn!(
                (@for (in_field in output_fields.iter())
                 {
                     {{in_field.ident.clone().unwrap() | ident:"{}_element"}},
                 }
                )
            )
        }
    }
}

#[zyn::element]
fn sync_block_input_cons(input_fields: Vec<Field>, tagged: bool) -> zyn::TokenStream
{
    #[allow(clippy::collapsible_else_if)]
    if *tagged
    {
        // If tagged : deconstruct tags
        if input_fields.is_empty()
        {
            zyn::zyn!(())
        }
        else if input_fields.len() == 1
        {
            zyn::zyn!(
                oxydsp_flowgraph::tag::Tagged({{input_fields[0].ident.clone().unwrap() | ident:"{}_element"}}, {{input_fields[0].ident.clone().unwrap() | ident:"{}_tag_opt"}})
            )
        }
        else
        {
            zyn::zyn!(
                (@for (in_field in input_fields.iter())
                 {
                     oxydsp_flowgraph::tag::Tagged({{in_field.ident.clone().unwrap() | ident:"{}_element"}}, {{in_field.ident.clone().unwrap() | ident:"{}_tag_opt"}}.clone()),
                 }
                )
            )
        }
    }
    else
    {
        // Otherwise just get the output element
        if input_fields.is_empty()
        {
            zyn::zyn!(_)
        }
        else if input_fields.len() == 1
        {
            zyn::zyn!(
                {{input_fields[0].ident.clone().unwrap() | ident:"{}_element"}}
            )
        }
        else
        {
            zyn::zyn!(
                (@for (in_field in input_fields.iter())
                 {
                     {{in_field.ident.clone().unwrap() | ident:"{}_element"}},
                 }
                )
            )
        }
    }
}
