// Copyright Two Neutron Stars Incorporated and contributors
// SPDX-License-Identifier: BlueOak-1.0.0

use super::*;

struct FieldMeta {
  name: syn::Ident,
  node_name: syn::TypePath,
  value: syn::TypePath,
}

/// Each field may require a new type or enum to represent the valid values
/// when walking the AST. This function generates those types.
pub(super) fn map_valid_field_type(
  f: &crate::ast::parse::Field,
) -> Result<(TokenStream, Option<syn::Ident>), crate::error::Error> {
  let field_name_str = f.name.to_upper_camel_case();
  let field_name: syn::Ident = match syn::parse_str(&field_name_str) {
    | Ok(ident) => ident,
    | Err(_) => {
      return Err(crate::error::Error::ReservedKeywordFieldName(
        proc_macro2::Span::call_site(),
        f.name.clone(),
        field_name_str,
      ));
    },
  };

  // let field_doc_comment: syn::LitStr = syn::LitStr::new(
  //   &format!("Returns the `{}` field", field_name),
  //   proc_macro2::Span::call_site(),
  // );

  if f.is_field {
    return Ok((quote!(), None));
  }

  match &f.ty {
    | FieldType::MultipleNode(items) => {
      let (field_meta, field_impls): (Vec<FieldMeta>, Vec<TokenStream>) = items
        .iter()
        .map(|opt| {
          let field_option_value: syn::TypePath =
            syn::parse_str(&opt.clone()).unwrap();

          let name = opt
              .replace("crate::", "")
              .replace("::", "_")
              // we first convert to valid snake case, so heck can then convert
              // to upper camel
              // .to_ascii_lowercase()
            .to_snake_case()
              .to_upper_camel_case();

          let field_option_name: syn::Ident = syn::parse_str(&name).unwrap();
          let node_name: syn::TypePath =
            syn::parse_str(&format!("crate::Node::{name}")).unwrap();

          (
            FieldMeta {
              name: field_option_name.clone(),
              node_name: node_name.clone(),
              value: field_option_value.clone(),
            },
            quote!(
              impl<'ast> From<&'ast #field_option_value> for #field_name<'ast> {
                fn from(v: &'ast #field_option_value) -> Self {
                  Self::#field_option_name(v)
                }
              }
            ),
          )
        })
        .unzip();

      let field_options = field_meta
        .iter()
        .map(|f| {
          let field_option_name = &f.name;
          let field_option_value = &f.value;

          quote!(#field_option_name(&'ast #field_option_value))
        })
        .collect::<Vec<TokenStream>>();

      let field_option_impls = field_meta
        .iter()
        .map(|f| {
          let field_option_name = &f.name;
          let field_node_name = &f.node_name;

          quote!( #field_node_name(val) => { #field_name::#field_option_name(val) })
        })
        .collect::<Vec<TokenStream>>();

      Ok((
        quote! {
          #[derive(Debug)]
          pub enum #field_name<'ast> {
            #(#field_options),*,
            Error
          }

          impl<'ast> #field_name<'ast> {
            pub fn validate(
              &self,
              ast: &'ast crate::AST,
            ) -> crate::Result<()> {

              match self {
                Self::Error => Err(crate::invalid_node_error()),
                _ => Ok(()),
              }
            }

            pub fn from_node(node: &'ast crate::Node) -> Option<#field_name<'ast>> {
              Some(Self::from(node))
            }
          }

          impl<'ast> From<&'ast crate::Node> for #field_name<'ast> {
            fn from(n: &'ast crate::Node) -> Self {
              match n {
                #(#field_option_impls),*,
                _ => #field_name::Error,
              }
            }
          }

          #(#field_impls)*
        },
        Some(field_name),
      ))
    },
    // | FieldType::EnumNodeId(ty) => (quote!(#ty), None),
    | _ => {
      // We don't need a custom enum for this field
      Ok((quote!(), None))
    },
  }
}

/// Function to map fields in an AST struct to a getter method on that struct
pub(super) fn map_impl_walk(
  f: &crate::ast::parse::Field,
  maybe_field_ty: Option<TokenStream>,
) -> (TokenStream, syn::Ident) {
  let field_name: syn::Ident = syn::parse_str(&f.name).unwrap();
  // Prefix the field method to make it easier to find in autocomplete
  let field_method_name: syn::Ident =
    syn::parse_str(&format!("get_{}", f.name)).unwrap();

  let field_doc_comment: syn::LitStr = syn::LitStr::new(
    &format!("Returns the `{field_name}` field"),
    proc_macro2::Span::call_site(),
  );

  let return_ty = {
    if f.is_field {
      let ret_ty = match &f.ty {
        | FieldType::Path(s) => {
          let field_ty: syn::TypePath = syn::parse_str(s).unwrap();

          quote!(#field_ty)
        },
        | FieldType::Enum(s) => {
          let ty: syn::Path = syn::parse_str(s).unwrap();
          quote!(&#ty)
        },
        | FieldType::Literal(s) => {
          let ty: syn::Path = syn::parse_str(s).unwrap();
          quote!(#ty)
        },
        | FieldType::String => quote!(std::string::String),
        | FieldType::Span => quote!(laburnum::Span),
        | FieldType::Ident => quote!(laburnum::Ident),
        | FieldType::SpannedIdent => quote!(laburnum::Spanned<laburnum::Ident>),
        | _ => quote!(&'ast crate::Node),
      };
      match f.is_optional {
        | true => {
          quote! {
            Option<#ret_ty>
          }
        },
        | false => ret_ty,
      }
    } else {
      match &f.ty {
        | FieldType::Literal(s) => {
          let ty: syn::Path = syn::parse_str(s).unwrap();

          quote!(#ty)
        },
        | FieldType::String => quote!(std::string::String),
        | FieldType::Span => quote!(laburnum::Span),
        | FieldType::Ident => quote!(laburnum::Ident),
        | FieldType::SpannedIdent => quote!(laburnum::Spanned<laburnum::Ident>),
        | FieldType::Enum(s) => {
          let ty: syn::Path = syn::parse_str(s).unwrap();
          quote!(&#ty)
        },

        | _ => {
          let ret = match &f.ty {
            | FieldType::Path(s) => {
              let field_ty: syn::TypePath = syn::parse_str(s).unwrap();

              quote!(#field_ty)
            },
            | FieldType::SingleNode(s) => {
              let field_ty: syn::TypePath = syn::parse_str(s).unwrap();

              quote!(&'ast #field_ty)
            },
            | FieldType::EnumNodeId(s) => {
              let field_ty: syn::TypePath = syn::parse_str(s).unwrap();

              quote!(#field_ty<'ast>)
            },
            | FieldType::MultipleNode(_items) => maybe_field_ty
              .clone()
              .map(|ty| quote!(#ty<'ast>))
              .unwrap_or_else(|| quote!(&'ast crate::Node)),
            | FieldType::Literal(s) => {
              let ty: syn::Path = syn::parse_str(s).unwrap();
              quote!(#ty)
            },
            | _ => quote!(()),
          };

          if f.is_vec {
            quote! {Vec<#ret>}
          } else {
            quote! {Option<#ret>}
          }
        },
      }
    }
  };

  let body = {
    let field = if f.is_field {
      let body = match &f.ty {
        | FieldType::Path(_) => {
          quote! {
          #field_name
          }
        },
        | FieldType::Literal(_s) => {
          quote!(*#field_name)
        },
        | FieldType::String => {
          quote!(ast.intern.resolve(#field_name).to_string())
        },
        | FieldType::Span => {
          quote!(*#field_name)
        },
        | FieldType::Ident => {
          quote!(*#field_name)
        },
        | FieldType::SpannedIdent => {
          quote!(*#field_name)
        },
        | FieldType::Enum(_) => {
          quote!(#field_name)
        },
        | _ => quote!(None),
      };

      match f.is_optional {
        | true => {
          quote! {
            Some(#body)
          }
        },
        | false => body,
      }
    } else {
      match &f.ty {
        | FieldType::Path(_) => {
          quote! {
           ast.node(*#field_name)
          }
        },
        | FieldType::SingleNode(s) => {
          let field_ty: syn::TypePath = syn::parse_str(s).unwrap();

          if f.is_vec {
            quote! {
              #field_name
                .iter()
                .filter_map(|n: &crate::NodeId| {
                  ast.node(*n).and_then(|node: &crate::Node| {
                    #field_ty::from_node(node)
                  })
                })
                .collect::<Vec<_>>()
            }
          } else {
            quote! {
             ast.node(*#field_name).and_then(|node| #field_ty::from_node(node))
            }
          }
        },
        | FieldType::EnumNodeId(s) => {
          let field_ty: syn::TypePath = syn::parse_str(s).unwrap();

          if f.is_vec {
            quote! {
              #field_name
                .iter()
                .filter_map(|n: &crate::NodeId| {
                  ast.node(*n).and_then(|node: &crate::Node| {
                    #field_ty::from_node(node)
                  })
                })
                .collect::<Vec<_>>()
            }
          } else {
            quote! {
             ast.node(*#field_name).and_then(|node| #field_ty::from_node(node))
            }
          }
        },
        | FieldType::MultipleNode(_) => match maybe_field_ty {
          | Some(field_ty) => {
            if f.is_vec {
              quote! {
                #field_name
                  .iter()
                  .filter_map(|n| ast.node(*n).and_then(|node| #field_ty::from_node(node)))
                  .collect::<Vec<_>>()
              }
            } else {
              quote! { ast.node(*#field_name).and_then(|node| #field_ty::from_node(node)) }
            }
          },
          | None => {
            quote! { ast.node(*#field_name) }
          },
        },
        | FieldType::Literal(_s) => {
          quote!(*#field_name)
        },
        | FieldType::String => {
          quote!(ast.intern.resolve(#field_name).to_string())
        },
        | FieldType::Span => {
          quote!(*#field_name)
        },
        | FieldType::Ident => {
          quote!(*#field_name)
        },
        | FieldType::SpannedIdent => {
          quote!(*#field_name)
        },
        | FieldType::Enum(_) => {
          quote!(#field_name)
        },
        | _ => quote!(None),
      }
    };

    if f.is_optional {
      quote! {
        {
          if let Some(ref #field_name) = self.#field_name {
            #field
          } else {
            None
          }
        }
      }
    } else {
      quote! {
        {
          let #field_name = &self.#field_name;
          #field
        }
      }
    }
  };

  let (body_ids, return_ty_ids, should_generate_ids) = if !f.is_field {
    match &f.ty {
      | FieldType::Path(_)
      | FieldType::EnumNodeId(_)
      | FieldType::SingleNode(_) => {
        let body = if f.is_vec {
          if f.is_optional {
            quote! {
              {
                if let Some(ref #field_name) = self.#field_name {
                  #field_name.clone()
                } else {
                  Vec::new()
                }
              }
            }
          } else {
            quote! {
              {
                let #field_name = &self.#field_name;
                #field_name.clone()
              }
            }
          }
        } else if f.is_optional {
          quote! {
            {
              if let Some(ref #field_name) = self.#field_name {
                Some(*#field_name)
              } else {
                None
              }
            }
          }
        } else {
          quote! {
            {
              let #field_name = &self.#field_name;
              *#field_name
            }
          }
        };
        let ret_ty = if f.is_vec {
          quote!(Vec<crate::NodeId>)
        } else if f.is_optional {
          quote!(Option<crate::NodeId>)
        } else {
          quote!(crate::NodeId)
        };
        (Some(body), Some(ret_ty), true)
      },
      | _ => (None, None, false),
    }
  } else {
    (None, None, false)
  };

  let extra = {
    if f.ty == FieldType::String {
      let field_method_name: syn::Ident =
        syn::parse_str(&format!("get_{}_spur", f.name)).unwrap();

      let field_doc_comment: syn::LitStr = syn::LitStr::new(
        &format!("Returns the `laburnum::Span` for the `{field_name}` field"),
        proc_macro2::Span::call_site(),
      );

      quote! {
        #[doc = #field_doc_comment]
        pub fn #field_method_name<'ast>(
          &self,
          ast: &'ast crate::AST,
        ) -> laburnum::Span {
          self.#field_name
        }
      }
    } else {
      quote! {}
    }
  };

  let ids_method = if should_generate_ids {
    let method_suffix = if f.is_vec { "ids" } else { "id" };

    let field_method_name: syn::Ident =
      syn::parse_str(&format!("get_{}_{}", f.name, method_suffix)).unwrap();

    let doc_text = if f.is_vec {
      format!("Returns the NodeIds for the `{field_name}` field")
    } else {
      format!("Returns the NodeId for the `{field_name}` field")
    };

    let field_doc_comment_ids: syn::LitStr =
      syn::LitStr::new(&doc_text, proc_macro2::Span::call_site());

    match (body_ids, return_ty_ids) {
      | (Some(body_ids), Some(return_ty_ids)) => {
        quote! {
          #[doc = #field_doc_comment_ids]
          pub fn #field_method_name<'ast>(
            &self,
            ast: &'ast crate::AST,
          ) -> #return_ty_ids {
            #body_ids
          }
        }
      },
      | _ => quote! {},
    }
  } else {
    quote! {}
  };

  (
    quote!(
      #extra

      #[doc = #field_doc_comment]
      pub fn #field_method_name<'ast>(
        &self,
        ast: &'ast crate::AST,
      ) -> #return_ty {
        #body
      }

      #ids_method

    ),
    field_method_name,
  )
}
