// Copyright 2015-2018 Benjamin Fry <benjaminfry@me.com>
//
// Licensed under the Apache License, Version 2.0, <LICENSE-APACHE or
// http://apache.org/licenses/LICENSE-2.0> or the MIT license <LICENSE-MIT or
// http://opensource.org/licenses/MIT>, at your option. This file may not be
// copied, modified, or distributed except according to those terms.

//! # enum-as-inner
//!
//! A deriving proc-macro for generating functions to automatically give access to the inner members of enum.
//!
//! ## Basic unnamed field case
//!
//! The basic case is meant for single item enums, like:
//!
//! ```rust
//! # #[macro_use] extern crate enum_as_inner;
//! # fn main() {
//!
//! #[derive(Debug, EnumAsInner)]
//! enum OneEnum {
//!     One(u32),
//! }
//!
//! let one = OneEnum::One(1);
//!
//! assert_eq!(*one.as_one().unwrap(), 1);
//! assert_eq!(one.into_one().unwrap(), 1);
//! # }
//! ```
//!
//! where the result is either a reference for inner items or a tuple containing the inner items.
//!
//! ## Unit case
//!
//! This will return copy's of the value of the unit variant, as `isize`:
//!
//! ```rust
//! # #[macro_use] extern crate enum_as_inner;
//! # fn main() {
//!
//! #[derive(EnumAsInner)]
//! enum UnitVariants {
//!     Zero,
//!     One,
//!     Two,
//! }
//!
//! let unit = UnitVariants::Two;
//!
//! assert_eq!(unit.as_two().unwrap(), ());
//! # }
//! ```
//!
//! Note that for unit enums there is no `into_*()` function generated.
//!
//! ## Mutliple, unnamed field case
//!
//! This will return a tuple of the inner types:
//!
//! ```rust
//! # #[macro_use] extern crate enum_as_inner;
//! # fn main() {
//!
//! #[derive(Debug, EnumAsInner)]
//! enum ManyVariants {
//!     One(u32),
//!     Two(u32, i32),
//!     Three(bool, u32, i64),
//! }
//!
//! let many = ManyVariants::Three(true, 1, 2);
//!
//! assert_eq!(many.as_three().unwrap(), (&true, &1_u32, &2_i64));
//! assert_eq!(many.into_three().unwrap(), (true, 1_u32, 2_i64));
//! # }
//! ```
//!
//! ## Multiple, named field case
//!
//! This will return a tuple of the inner types, like the unnamed option:
//!
//! ```rust
//! # #[macro_use] extern crate enum_as_inner;
//! # fn main() {
//!
//! #[derive(Debug, EnumAsInner)]
//! enum ManyVariants {
//!     One { one: u32 },
//!     Two { one: u32, two: i32 },
//!     Three { one: bool, two: u32, three: i64 },
//! }
//!
//! let many = ManyVariants::Three { one: true, two: 1, three: 2 };
//!
//! assert_eq!(many.as_three().unwrap(), (&true, &1_u32, &2_i64));
//! assert_eq!(many.into_three().unwrap(), (true, 1_u32, 2_i64));
//! # }
//! ```

extern crate proc_macro;
extern crate proc_macro2;
#[macro_use]
extern crate syn;
#[macro_use]
extern crate quote;

use heck::SnakeCase;
use proc_macro2::{Ident, Span, TokenStream};
use syn::DeriveInput;

/// returns first the types to return, the match names, and then tokens to the field accesses
fn unit_fields_return(
    name: &syn::Ident,
    variant_name: &syn::Ident,
    function_name: &Ident,
    doc: &str,
) -> TokenStream {
    quote!(
        #[doc = #doc ]
        pub fn #function_name(&self) -> Option<()> {
            match self {
                #name::#variant_name => {
                    Some(())
                }
                _ => None
            }
        }
    )
}

/// returns first the types to return, the match names, and then tokens to the field accesses
fn unnamed_fields_return(
    name: &syn::Ident,
    variant_name: &syn::Ident,
    (function_name_ref, doc_ref): (&Ident, &str),
    (function_name_val, doc_val): (&Ident, &str),
    fields: &syn::FieldsUnnamed,
) -> TokenStream {
    let (returns_ref, returns_val, matches, accesses_ref, accesses_val) = match fields.unnamed.len()
    {
        1 => {
            let field = fields.unnamed.first().expect("no fields on type");

            let returns = &field.ty;
            let returns_ref = quote!(&#returns);
            let returns_val = quote!(#returns);
            let matches = quote!(inner);
            let accesses_ref = quote!(&inner);
            let accesses_val = quote!(inner);

            (
                returns_ref,
                returns_val,
                matches,
                accesses_ref,
                accesses_val,
            )
        }
        0 => (quote!(()), quote!(()), quote!(), quote!(()), quote!(())),
        _ => {
            let mut returns_ref = TokenStream::new();
            let mut returns_val = TokenStream::new();
            let mut matches = TokenStream::new();
            let mut accesses_ref = TokenStream::new();
            let mut accesses_val = TokenStream::new();

            for (i, field) in fields.unnamed.iter().enumerate() {
                let rt = &field.ty;
                let match_name = Ident::new(&format!("match_{}", i), Span::call_site());
                returns_ref.extend(quote!(&#rt,));
                returns_val.extend(quote!(#rt,));
                matches.extend(quote!(#match_name,));
                accesses_ref.extend(quote!(&#match_name,));
                accesses_val.extend(quote!(#match_name,));
            }

            (
                quote!((#returns_ref)),
                quote!((#returns_val)),
                quote!(#matches),
                quote!((#accesses_ref)),
                quote!((#accesses_val)),
            )
        }
    };

    quote!(
        #[doc = #doc_ref ]
        pub fn #function_name_ref(&self) -> Option<#returns_ref> {
            match self {
                #name::#variant_name(#matches) => {
                    Some(#accesses_ref)
                }
                _ => None
            }
        }

        #[doc = #doc_val ]
        pub fn #function_name_val(self) -> ::core::result::Result<#returns_val, Self> {
            match self {
                #name::#variant_name(#matches) => {
                    Ok(#accesses_val)
                },
                _ => Err(self)
            }
        }
    )
}

/// returns first the types to return, the match names, and then tokens to the field accesses
fn named_fields_return(
    name: &syn::Ident,
    variant_name: &syn::Ident,
    (function_name_ref, doc_ref): (&Ident, &str),
    (function_name_val, doc_val): (&Ident, &str),
    fields: &syn::FieldsNamed,
) -> TokenStream {
    let (returns_ref, returns_val, matches, accesses_ref, accesses_val) = match fields.named.len() {
        1 => {
            let field = fields.named.first().expect("no fields on type");
            let match_name = field.ident.as_ref().expect("expected a named field");

            let returns = &field.ty;
            let returns_ref = quote!(&#returns);
            let returns_val = quote!(#returns);
            let matches = quote!(#match_name);
            let accesses_ref = quote!(&#match_name);
            let accesses_val = quote!(#match_name);

            (
                returns_ref,
                returns_val,
                matches,
                accesses_ref,
                accesses_val,
            )
        }
        0 => (quote!(()), quote!(()), quote!(), quote!(()), quote!(())),
        _ => {
            let mut returns_ref = TokenStream::new();
            let mut returns_val = TokenStream::new();
            let mut matches = TokenStream::new();
            let mut accesses_ref = TokenStream::new();
            let mut accesses_val = TokenStream::new();

            for field in fields.named.iter() {
                let rt = &field.ty;
                let match_name = field.ident.as_ref().expect("expected a named field");

                returns_ref.extend(quote!(&#rt,));
                returns_val.extend(quote!(#rt,));
                matches.extend(quote!(#match_name,));
                accesses_ref.extend(quote!(&#match_name,));
                accesses_val.extend(quote!(#match_name,));
            }

            (
                quote!((#returns_ref)),
                quote!((#returns_val)),
                quote!(#matches),
                quote!((#accesses_ref)),
                quote!((#accesses_val)),
            )
        }
    };

    quote!(
        #[doc = #doc_ref ]
        pub fn #function_name_ref(&self) -> Option<#returns_ref> {
            match self {
                #name::#variant_name{ #matches } => {
                    Some(#accesses_ref)
                }
                _ => None
            }
        }

        #[doc = #doc_val ]
        pub fn #function_name_val(self) -> ::core::result::Result<#returns_val, Self> {
            match self {
                #name::#variant_name{ #matches } => {
                    Ok(#accesses_val)
                }
                _ => Err(self)
            }
        }
    )
}

fn impl_all_as_fns(ast: &DeriveInput) -> TokenStream {
    let name = &ast.ident;

    let enum_data = if let syn::Data::Enum(data) = &ast.data {
        data
    } else {
        panic!("{} is not an enum", name);
    };

    let mut stream = TokenStream::new();

    for variant_data in &enum_data.variants {
        let variant_name = &variant_data.ident;
        let function_name_ref = Ident::new(
            &format!("as_{}", variant_name).to_snake_case(),
            Span::call_site(),
        );
        let doc_ref = format!(
            "Optionally returns references to the inner fields if this is a `{}::{}`, otherwise `None`",
            name,
            variant_name,
        );
        let function_name_val = Ident::new(
            &format!("into_{}", variant_name).to_snake_case(),
            Span::call_site(),
        );
        let doc_val = format!(
            "Returns the inner fields if this is a `{}::{}`, otherwise returns back the enum in the `Err` case of the result",
            name,
            variant_name,
        );

        let tokens = match &variant_data.fields {
            syn::Fields::Unit => {
                unit_fields_return(name, variant_name, &function_name_ref, &doc_ref)
            }
            syn::Fields::Unnamed(unnamed) => unnamed_fields_return(
                name,
                variant_name,
                (&function_name_ref, &doc_ref),
                (&function_name_val, &doc_val),
                &unnamed,
            ),
            syn::Fields::Named(named) => named_fields_return(
                name,
                variant_name,
                (&function_name_ref, &doc_ref),
                (&function_name_val, &doc_val),
                &named,
            ),
        };

        stream.extend(tokens);
    }

    quote!(
        impl #name {
            #stream
        }
    )
}

#[proc_macro_derive(EnumAsInner)]
pub fn enum_as_inner(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
    // get a usable token stream
    let ast: DeriveInput = parse_macro_input!(input as DeriveInput);

    // Build the impl
    let expanded: TokenStream = impl_all_as_fns(&ast);

    // Return the generated impl
    proc_macro::TokenStream::from(expanded)
}
