use doc_data::*;
use once_cell::sync::Lazy;
use proc_macro::TokenStream;
use proc_macro2::{Span, TokenTree};
use quote::quote;
use std::sync::atomic::{AtomicUsize, Ordering};
use strum::IntoEnumIterator;
use syn::{
  parse::{Parse, ParseStream},
  parse_macro_input, parse_str, Attribute, AttributeArgs, Error, Field, Fields, FieldsNamed,
  FieldsUnnamed, Ident, Item, ItemEnum, ItemStruct, LitStr, Path, Result, Variant, Visibility,
};
static INVOCATIONS: AtomicUsize = AtomicUsize::new(0);
fn count_invocation() -> usize {
  INVOCATIONS.fetch_add(1, Ordering::SeqCst)
}
fn paths_eq(
  p_0: &Path,
  p_1: &Path,
) -> bool {
  format!("{}", quote! {#p_0}) == format!("{}", quote! {#p_1})
}
#[derive(Clone, Debug)]
#[allow(dead_code)]
struct ParsedOuterAttrs {
    pub vis_opt: Option<Visibility>,
    pub outer_attrs: Vec<Attribute>,
    pub item_opt: Option<Item>,
}
impl ParsedOuterAttrs {
    #[allow(clippy::declare_interior_mutable_const)]
  pub const DOC_PATH: Lazy<Path> =
    Lazy::new(|| parse_str::<Path>("doc").expect("must parse doc path"));
    #[allow(dead_code)]
  pub const DOC_ATTR_LINE_START: &'static str = "= \"";
    pub const DOC_ATTR_LINE_END: &'static str = "\"";
    pub const LINE_JOINER: &'static str = "\n";
    pub fn extract_doc_data(
    &self,
    _count: usize,
  ) -> anyhow::Result<()> {
    type AttrsCollectionType = Vec<(Option<Ident>, Vec<(HelperAttr, Attribute)>, Vec<Attribute>)>;
        let extract_attrs_and_ident_into_map_fold_fn = |attrs: &Vec<Attribute>,
                                                    ident: &Option<Ident>,
                                                    m: &mut AttrsCollectionType|
     -> anyhow::Result<()> {
      let mut helper_attrs_and_attributes: Vec<(HelperAttr, Attribute)> =
        Vec::with_capacity(attrs.len());
      let mut other_attrs: Vec<Attribute> = Vec::with_capacity(attrs.len());
      for attr in attrs.iter() {
        if let Some(_helper_attr) =
          HelperAttr::iter().find(|helper_attr| paths_eq(&attr.path, &helper_attr.into()))
        {
          helper_attrs_and_attributes.push((HelperAttr::from_attribute(attr)?, attr.clone()));
        } else {
          other_attrs.push(attr.clone());
        }
      }
      if !helper_attrs_and_attributes.is_empty() {
        m.push((ident.clone(), helper_attrs_and_attributes, other_attrs));
      }
      Ok(())
    };
    let record_doc_from_attrs_collection =
      |attrs_collection: AttrsCollectionType| -> anyhow::Result<()> {
        let _attrs_collection_len = attrs_collection.len();
                for (_i, (_ident_opt, helper_attrs_and_attributes, other_attributes)) in
          attrs_collection.iter().enumerate()
        {
                    let helper_attrs = helper_attrs_and_attributes
            .iter()
            .map(|(h, _)| h.clone())
            .collect();
                    let content: String = Self::get_outer_doc_comments_string(other_attributes);
          record_doc_from_helper_attributes_and_str(
            true,             &content,
            &helper_attrs,
          )?;
        }
        Ok(())
      };
        if let Some(ref item) = self.item_opt {
      match item {
        Item::Enum(ItemEnum { ref variants, .. }) => {
                    let mut attrs_collection: AttrsCollectionType = Vec::with_capacity(variants.len());
          for Variant {
            ref attrs,
            ref ident,
            ..
          } in variants.iter()
          {
            extract_attrs_and_ident_into_map_fold_fn(
              attrs,
              &Some(ident.clone()),
              &mut attrs_collection,
            )?
          }
          
          record_doc_from_attrs_collection(attrs_collection)
        }
        Item::Struct(ItemStruct {
           ref fields,
          ..
        }) => {
                    match fields {
            Fields::Named(FieldsNamed { ref named, .. }) => {
              let mut attrs_collection: AttrsCollectionType = Vec::with_capacity(named.len());
              for Field {
                ref attrs,
                ref ident,
                ..
              } in named.iter()
              {
                extract_attrs_and_ident_into_map_fold_fn(attrs, ident, &mut attrs_collection)?
              }
              record_doc_from_attrs_collection(attrs_collection)
            }
            Fields::Unnamed(FieldsUnnamed { ref unnamed, .. }) => {
              let mut attrs_collection: AttrsCollectionType = Vec::with_capacity(unnamed.len());
              for Field {
                ref attrs,
                ref ident,
                ..
              } in unnamed.iter()
              {
                extract_attrs_and_ident_into_map_fold_fn(attrs, ident, &mut attrs_collection)?
              }
              record_doc_from_attrs_collection(attrs_collection)
            }
            _ => Ok(()),
          }
        }
        _ => Ok(()),
      }
    } else {
      Ok(())
    }
  }
  #[allow(dead_code)]
    pub fn extract_comment_text(s: &str) -> String {
    if s.starts_with(Self::DOC_ATTR_LINE_START) && s.ends_with(Self::DOC_ATTR_LINE_END) {
      s[Self::DOC_ATTR_LINE_START.len()..(s.len() - Self::DOC_ATTR_LINE_END.len())].to_string()
    } else {
      String::new()
    }
  }
  #[allow(clippy::borrow_interior_mutable_const)]
    pub fn get_doc_comments_lines(outer_attrs: &[Attribute]) -> Vec<String> {
    outer_attrs
      .iter()
      .filter_map(
        |Attribute {
           path, ref tokens, ..
         }| {
          if paths_eq(path, &Self::DOC_PATH) {
            match tokens.clone().into_iter().nth(1) {
              Some(TokenTree::Literal(literal)) => Some(
                literal
                  .to_string()
                  .trim_end_matches(Self::DOC_ATTR_LINE_END)
                  .trim_start_matches(Self::DOC_ATTR_LINE_END)
                  .to_string(),
              ),
              _ => None,
            }
          } else {
            None
          }
        },
      )
      .collect()
  }
    pub fn get_outer_doc_comments_string(attrs: &[Attribute]) -> String {
    Self::get_doc_comments_lines(attrs).join(ParsedOuterAttrs::LINE_JOINER)
  }
}
impl Parse for ParsedOuterAttrs {
  fn parse(input: ParseStream) -> Result<Self> {
        Ok(Self {
      outer_attrs: input.call(Attribute::parse_outer)?,
      vis_opt: input.parse().ok(),
      item_opt: input.parse().ok(),
    })
  }
}
#[allow(clippy::ptr_arg)]
fn record_doc_from_helper_attributes_and_str(
  do_save: bool,
  doc_comment_string: &str,
  helper_attributes: &Vec<HelperAttr>,
) -> Result<()> {
      let mut chapter_blurb_opt = None;
  let mut name_opt = None;
  let mut number_opt = None;
  let mut name_path_opt = None;
  let mut number_path_opt = None;
  for helper_attribute in helper_attributes.iter() {
    match helper_attribute {
      HelperAttr::ChapterName(ref chapter_name) => {
        name_opt = Some(chapter_name.to_string());
      }
      HelperAttr::ChapterBlurb(ref chapter_blurb) => {
        chapter_blurb_opt = Some(chapter_blurb.to_string());
      }
      HelperAttr::ChapterNameSlug(ref chapter_names) => {
        name_path_opt = Some(chapter_names.to_vec());
      }
      HelperAttr::ChapterNum(ref chapter_number) => {
        number_opt = Some(*chapter_number);
      }
      HelperAttr::ChapterNumSlug(ref chapter_numbers) => {
        number_path_opt = Some(chapter_numbers.to_vec());
      }
    }
  }
      let generate_documentable = |name_opt: &Option<String>| -> Documentable {
    let documentable = Documentable::Doc(
      name_opt.as_ref().cloned().unwrap_or_default(),
      doc_comment_string.to_string(),
    );
        documentable
  };
    let docs = &*doc_data::DOCS;
  let mut docs_write_lock = docs.write().expect("Must get write lock on global docs");
  let write_res = match (number_path_opt, name_path_opt) {
    (Some(ref path_numbers), Some(ref path_names)) => {
            if name_opt.is_none() {
        name_opt = path_names.get(path_numbers.len() - 1).cloned();
      }
            let documentable = generate_documentable(&name_opt);
                  docs_write_lock.add_path(
        &chapter_blurb_opt,
        &name_opt,
        Some(documentable),
        Some(true),
        path_names,
        path_numbers,
      )
    }
    (Some(ref path_numbers), None) => {
      let documentable = generate_documentable(&name_opt);
                  docs_write_lock.add_path(
        &chapter_blurb_opt,
        &name_opt,
        Some(documentable),
        Some(true),
        &[],
        path_numbers,
      )
    }
    (None, Some(ref path_names)) => {
            if name_opt.is_none() {
        name_opt = path_names.last().cloned();
      }
      let documentable = generate_documentable(&name_opt);
                  docs_write_lock.add_path(
        &chapter_blurb_opt,
        &name_opt,
        Some(documentable),
        Some(true),
        path_names,
        &[],
      )
    }
    (None, None) => {
            let documentable = generate_documentable(&name_opt);
            docs_write_lock.add_entry(documentable, name_opt, number_opt, Some(true))
    }
  };
    drop(docs_write_lock);
  match write_res {
    Ok(()) => {
      if do_save {
                match doc_data::persist_docs() {
          Ok(()) => Ok(()),
          Err(doc_save_error) => Err(Error::new(
            Span::mixed_site(),
            format!("{:#?}", doc_save_error),
          )),
        }
      } else {
        Ok(())
      }
    }
    Err(error) => Err(Error::new(Span::mixed_site(), format!("{:#?}", error))),
  }
}
#[proc_macro_derive(
  user_doc_item,
  attributes(
    chapter_blurb,
    chapter_name,
    chapter_name_slug,
    chapter_num,
    chapter_num_slug,
  )
)]
pub fn user_doc_item(item: TokenStream) -> TokenStream {
  let count = count_invocation();
    let parsed_outer_attrs = parse_macro_input!(item as ParsedOuterAttrs);
      match parsed_outer_attrs.extract_doc_data(count) {
    Ok(()) => TokenStream::new(),
    Err(extraction_error) => Error::new(
      Span::call_site(),
      format!(
        "Could not extract doc data during derive macro invocation:\n{:#?}",
        extraction_error
      ),
    )
    .into_compile_error()
    .into(),
  }
}
#[proc_macro_attribute]
pub fn user_doc_fn(
  own_attr: TokenStream,
  item: TokenStream,
) -> TokenStream {
  let _count: usize = count_invocation();
    let it = item.clone();
    let own_attribute_args = parse_macro_input!(own_attr as AttributeArgs);
  let helper_attributes_res: Result<Vec<HelperAttr>> =
    HelperAttr::from_attribute_args(&own_attribute_args);
  let parsed_outer_attrs = parse_macro_input!(item as ParsedOuterAttrs);
        match helper_attributes_res {
    Ok(helper_attributes) => {
      match record_doc_from_helper_attributes_and_str(
        true,         &ParsedOuterAttrs::get_outer_doc_comments_string(&parsed_outer_attrs.outer_attrs),
        &helper_attributes,
      ) {
        Ok(()) => it,
        Err(err) => err.into_compile_error().into(),
      }
    }
    Err(err) => err.into_compile_error().into(),
  }
}
#[proc_macro]
pub fn set_persistence_file_name(input: TokenStream) -> TokenStream {
  let lit_str = parse_macro_input!(input as LitStr);
  let value = lit_str.value();
  if !value.is_empty() {
    if let Ok(mut custom_output_file_name_write) = CUSTOM_OUTPUT_FILE_NAME.try_write() {
      let _ = custom_output_file_name_write.get_or_insert(value);
          }
  }
  TokenStream::new()
}