extern crate proc_macro;

use crate::proc_macro::TokenStream;
use quote::{quote, ToTokens};
use syn::{
    self, 
    Data,
    DataStruct,  
    DeriveInput,
    Error,
    Field,
    Fields,
    export::Span,
    LitStr,
    punctuated::Punctuated,
    spanned::Spanned,
    token::Comma
};


fn extract_struct_fields<'a>(data: &'a Data, span: &Span) -> Result<&'a DataStruct, Error> {
    match data {
        Data::Struct(struct_fields) => Ok(struct_fields),
        _ => Err( Error::new(span.clone(), "cannot derive json type for non structs"))
    }
}

fn extract_named_fields<'a>(data_struct: &'a DataStruct, 
                            span: &Span) -> Result<Option<&'a Punctuated<Field, Comma>>, Error> {
    match data_struct.fields {
        Fields::Named(ref named_fields) => Ok(Some(&named_fields.named)),
        Fields::Unnamed(_) => Err( Error::new(span.clone(), "cannot derive json type for a tuple struct")),
        Fields::Unit => Ok(None)
    }
}


fn fields_to_json(data: &Data, span: &Span) -> Result<String, Error> {
    let data_struct = extract_struct_fields(data, span)?;
    if let Some(named_fields) = extract_named_fields(data_struct, span)? {

        let mut fields = Vec::new();
        for field in named_fields {
            let ident = field.ident
                .as_ref()
                .ok_or(Error::new(span.clone(), "cannot derive json type for unnamed field"))?
                .to_string();

            let ty = field.ty
                .clone()
                .into_token_stream()
                .to_string()
                .replace(" ", "");

            fields.push(format!("{{\"{}\": \"{}\"}}", ident, ty));
            fields.push(String::from(", "));        
        }

        let _ = fields.pop();

        let mut json = String::from("[");
        for field in fields.iter() {
            json.push_str(&*field);
        }
        json.push(']');

        Ok(json)
    }
    else {
        Ok("[]".to_owned())
    }
}


fn impl_json_type_macro(ast: &DeriveInput) -> Result<TokenStream, Error> {
    let name = &ast.ident;
    let json_fields = fields_to_json(&ast.data, &ast.span())?;
    
    let format_json = format!("{{\"name\": {}, fields: {}}}", name.to_string(), json_fields);
    let json = LitStr::new(&*format_json, Span::call_site());

    let gen = quote! {
        impl json_type::JsonType for #name {
            fn json_type() -> &'static str {
                #json
            }
        }
    };
    Ok(gen.into())
}


#[proc_macro_derive(JsonType)]
pub fn json_type_derive(input: TokenStream) -> TokenStream { 
    match syn::parse(input) {
        Ok(ast) => {            
            impl_json_type_macro(&ast)
                .unwrap_or_else(|e| e.to_compile_error().into())
        },
        Err(e) => {
            e.to_compile_error().into()
        }
    }
}


