// Copyright 2021-2022 Ian Jackson and contributors to Hippotat
// SPDX-License-Identifier: GPL-3.0-or-later WITH LicenseRef-Hippotat-OpenSSL-Exception
// There is NO WARRANTY.

//! Hippotat - proc macros
//!
//! This crate is an internal detail of hippotat.
//! It does not adhere to semver.

#![allow(clippy::style)]

#![allow(clippy::expect_fun_call)]
#![allow(clippy::map_flatten)]
#![allow(clippy::single_char_pattern)]

use syn::{parse_macro_input, parse_quote};
use syn::{Data, DataStruct, DeriveInput, LitStr, Meta, NestedMeta};
use quote::{quote, quote_spanned, ToTokens};
use proc_macro2::{Literal, TokenStream};

use std::cell::RefCell;

use itertools::Itertools;

/// Generates config resolver method
/// 
/// Each field ends up having an SKL and a method.
/// The method actually looks up the value in a particular link context.
/// SKL is passed to the method, which usually uses it to decide which
/// sections to look in.  But it is also used by general validation,
/// unconditionally, to reject settings in the wrong section.
///
/// Atrributes:
///
///  * `limited`, `server`, `client`: cooked sets of settings;
///    default `SKL` is `PerClient` except for `limited`
///  * `global` and `per_client`: set the SKL.
///  * `special(method, SKL)`
///
/// Generated code
///
/// ```rust,ignore
/// impl<'c> ResolveContext<'c> {
///
///   // SKL here is used by SectionKindList::contains()
///   const FIELDS: &'static [(&'static str, SectionKindList)] = &[ ... ];
///
///   #[throws(AE)]
///   fn resolve_instance(&self) -> InstanceConfig {
///     InstanceConfig {
///       ...
///        // SKL here is usually passed to first_of, but the method
///        // can do something more special.
///        max_batch_down: self.limited("max_batch_down", SKL::PerClient)?,
///        ...
///      }
///   }
/// }
///
/// pub struct InstanceConfigCommon { ... }
/// impl InstanceConfigCommon {
///   pub fn from(l: &[InstanceConfig]) { InstanceConfigCommon {
///     field: <Type as ResolveGlobal>::resolve(l.iter().map(|e| &e.field)),
///     ...
///   } }
/// }
/// ```
#[proc_macro_derive(ResolveConfig, attributes(
  limited, server, client, computed, special,
  per_client, global,
))]
pub fn resolve(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
  let input = parse_macro_input!(input as DeriveInput);

  let (fields, top_ident) = match input {
    DeriveInput {
      ref ident,
      data: Data::Struct(DataStruct {
        fields: syn::Fields::Named(ref f),
        ..
      }),
      ..
    } => (f, ident),
    _ => panic!(),
  };

  let target = &input.ident;

  let mut names = vec![];
  let mut output = vec![];
  let mut global_fields = vec![];
  let mut global_assignments = vec![];
  let mut inspects = vec![];
  for field in &fields.named {
    //dbg!(field);
    let fname = &field.ident.as_ref().unwrap();
    let fname_string = fname.to_string();
    let fname_lit = Literal::string( &fname_string );
    let ty = &field.ty;
    let fname_span = fname.span();
    let skl = RefCell::new(None);
    let set_skl = |new| {
      let mut skl = skl.borrow_mut();
      if let Some(old) = &*skl { panic!("dup SKL {} and {} for field {}",
                                        old, new, &fname); }
      *skl = Some(new);
    };
    let mut method = quote_spanned!{fname_span=> ordinary };
    for attr in &field.attrs {
      let atspan = attr.path.segments.last().unwrap().ident.span();
      if attr.tokens.is_empty() {
        if attr.path == parse_quote!{ per_client } {
          set_skl(quote_spanned!{fname_span=> SectionKindList::PerClient });
          continue;
        } else if attr.path == parse_quote!{ global } {
          set_skl(quote_spanned!{fname_span=> SectionKindList::Global });
          global_fields.push(syn::Field {
            attrs: vec![],
            ..field.clone()
          });
          global_assignments.push(quote_spanned!(fname_span=>
            #fname: <#ty as ResolveGlobal>::resolve
                    (l.iter().map(|e| &e.#fname)),
          ));
          inspects.push(quote!{
            #fname_lit => &self.#fname,
          });
          continue;
        }
        method = attr.path.to_token_stream();
        if attr.path == parse_quote!{ limited } {
          set_skl(quote_spanned!{atspan=> SectionKindList::Limited });
        } else if attr.path == parse_quote!{ client } {
          set_skl(quote_spanned!{atspan=> SectionKindList::PerClient });
        } else if attr.path == parse_quote!{ computed } {
          set_skl(quote_spanned!{atspan=> SectionKindList::None });
        }
      } else if attr.path == parse_quote!{ special } {
        let meta = match attr.parse_meta().unwrap() {
          Meta::List(list) => list,
          _ => panic!(),
        };
        let (tmethod, tskl) = meta.nested.iter().collect_tuple().unwrap();
        fn get_path(meta: &NestedMeta) -> TokenStream {
          match meta {
            NestedMeta::Meta(Meta::Path(ref path)) => path.to_token_stream(),
            _ => panic!(),
          }
        }
        method = get_path(tmethod);
        *skl.borrow_mut() = Some(get_path(tskl));
      }
    }
    let skl = skl.into_inner()
      .expect(&format!("SKL not specified! (field {})!", fname));

    names.push(quote!{
      (#fname_lit, #skl),
    });
    //dbg!(&method);
    output.push(quote!{
      #fname: rctx. #method ( #fname_lit, #skl )?,
    });
    //eprintln!("{:?} method={:?} skl={:?}", field.ident, method, skl);
  }
  //dbg!(&output);

  let global = syn::Ident::new(&format!("{}Global", top_ident),
                               top_ident.span());

  let output = quote! {
    impl #target {
      const FIELDS: &'static [(&'static str, SectionKindList)]
        = &[ #( #names )* ];

      fn resolve_instance(rctx: &ResolveContext)
          -> ::std::result::Result<#target, anyhow::Error>
      {
        ::std::result::Result::Ok(#target {
          #( #output )*
        })
      }
    }

    #[derive(Debug)]
    pub struct #global {
      #( #global_fields ),*
    }

    impl #global {
      pub fn from(l: &[#top_ident]) -> #global { #global {
        #( #global_assignments )*
      } }

      pub fn inspect_key(&self, field: &'_ str)
                         -> Option<&dyn InspectableConfigValue> {
        Some(match field {
          #( #inspects )*
          _ => return None,
        })
      }
    }
  };

  //eprintln!("{}", &output);
  output.into()
}

#[proc_macro]
pub fn into_crlfs(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
  let input: proc_macro2::TokenStream = input.into();
  let token: LitStr = syn::parse2(input).expect("expected literal");
  let input = token.value();
  let output = input.split_inclusive('\n')
    .map(|s| s.trim_start_matches(&[' ','\t'][..]))
    .map(|s| match s.strip_suffix("\n") {
      None => [s, ""],
      Some(l) => [l, "\r\n"],
    })
    .flatten()
    .collect::<String>();
  //dbg!(&output);
  let output = LitStr::new(&output, token.span());
  let output = quote!(#output);
  output.into()
}
