// This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/.
//! Named tuples, or named structs with certain tuple-like functions

use proc_macro::TokenStream;
use quote::ToTokens;
use syn::{
    parse::{Parse, ParseStream},
    Result,
};

struct NamedTupleAttrs(syn::punctuated::Punctuated<NamedTupleAttr, syn::Token![,]>);

impl Parse for NamedTupleAttrs {
    fn parse(input: ParseStream) -> Result<Self> {
        input
            .parse_terminated::<NamedTupleAttr, syn::Token![,]>(NamedTupleAttr::parse)
            .map(NamedTupleAttrs)
    }
}

struct NamedTupleAttr {
    ident: syn::Ident,
    _eq_token: syn::Token![=],
    value: syn::Lit,
}

impl Parse for NamedTupleAttr {
    fn parse(input: ParseStream) -> Result<Self> {
        Ok(NamedTupleAttr {
            ident: input.parse()?,
            _eq_token: input.parse()?,
            value: input.parse()?,
        })
    }
}

struct NamedTupleConfig {
    constructor: bool,
    tuple_methods: bool,
}

impl Default for NamedTupleConfig {
    fn default() -> Self {
        Self {
            constructor: true,
            tuple_methods: true,
        }
    }
}

impl TryFrom<NamedTupleAttrs> for NamedTupleConfig {
    type Error = syn::Error;
    fn try_from(attrs: NamedTupleAttrs) -> syn::Result<Self> {
        attrs.0.iter().try_fold(Self::default(), |mut acc, attr| {
            match (attr.ident.to_string().as_str(), &attr.value) {
                ("constructor", syn::Lit::Bool(val)) => acc.constructor = val.value,
                ("tuple_methods", syn::Lit::Bool(val)) => acc.tuple_methods = val.value,
                _ => {
                    return Err(syn::Error::new(
                        attr.value.span(),
                        "invalid or unknown parameter",
                    ))
                }
            }
            Ok(acc)
        })
    }
}

pub fn named_tuple(attrs: TokenStream, item: TokenStream) -> TokenStream {
    let attrs = syn::parse_macro_input!(attrs as NamedTupleAttrs);
    let config = NamedTupleConfig::try_from(attrs)
        .map_err(syn::Error::into_compile_error)
        .unwrap();

    let strct = syn::parse_macro_input!(item as syn::ItemStruct);
    if let syn::Fields::Named(ref fields) = strct.fields {
        let name = &strct.ident;
        let (impl_generics, type_generics, where_clause) = &strct.generics.split_for_impl();
        let (names, types): (Vec<_>, Vec<_>) = fields
            .named
            .iter()
            .cloned()
            .map(|it| (it.ident.unwrap(), it.ty))
            .unzip();
        let mut stream = proc_macro2::TokenStream::new();
        strct.to_tokens(&mut stream);

        if config.constructor {
            quote::quote! {
                impl #impl_generics #name #type_generics #where_clause {
                    pub fn new(#(#names: #types),*) -> Self {
                        Self {
                            #(#names),*
                        }
                    }
                }
            }
            .to_tokens(&mut stream);
        }

        if config.tuple_methods {
            quote::quote! {
                impl #impl_generics From<(#(#types),*)> for #name #type_generics #where_clause {
                    fn from((#(#names),*): (#(#types),*)) -> Self {
                        Self { #(#names),* }
                    }
                }

                impl #impl_generics From<#name #type_generics> for (#(#types),*) #where_clause {
                    fn from(it: #name #type_generics) -> (#(#types),*) {
                        (#(it.#names),*)
                    }
                }

                impl #impl_generics #name #type_generics #where_clause {
                    pub fn as_tuple(self) -> (#(#types),*) {
                        <(#(#types),*)>::from(self)
                    }
                }
            }
            .to_tokens(&mut stream);
        }

        stream.into()
    } else {
        syn::Error::new(
            strct.struct_token.span,
            "Named tuples must have named fields!",
        )
        .into_compile_error();
        unreachable!();
    }
}
