use heck::{ToKebabCase, ToTitleCase};
use proc_macro2::TokenStream;
use quote::{ToTokens, quote};
use syn::parse::{Parse, ParseStream};
use syn::token::Token;
use syn::{Attribute, Ident, Result, Token};

/// Return an error at the given item.
macro_rules! bail {
    (callsite, $($tts:tt)*) => {
        return Err(syn::Error::new(
            proc_macro2::Span::call_site(),
            format!("typst: {}", format!($($tts)*))
        ))
    };
    ($item:expr, $($tts:tt)*) => {
        return Err(syn::Error::new_spanned(
            &$item,
            format!("typst: {}", format!($($tts)*))
        ))
    };
}

/// Extract documentation comments from an attribute list.
pub fn documentation(attrs: &[syn::Attribute]) -> String {
    let mut doc = String::new();

    // Parse doc comments.
    for attr in attrs {
        if let syn::Meta::NameValue(meta) = &attr.meta
            && meta.path.is_ident("doc")
        {
            if let syn::Expr::Lit(lit) = &meta.value
                && let syn::Lit::Str(string) = &lit.lit
            {
                let full = string.value();
                let line = full.strip_prefix(' ').unwrap_or(&full);
                doc.push_str(line);
                doc.push('\n');
            } else if let syn::Expr::Macro(expr) = &meta.value
                // The `stringify!` macro does not expand eagerly so we have
                // some very basic support for int and float expressions here.
                // This is e.g. used for paper sizes.
                && expr.mac.path.is_ident("stringify")
                && let Ok(lit) = syn::parse2::<syn::Lit>(expr.mac.tokens.clone())
                && let Some(value) = match &lit {
                    syn::Lit::Int(int) => Some(int.base10_digits()),
                    syn::Lit::Float(float) => Some(float.base10_digits()),
                    _ => None,
                }
            {
                doc.push_str(value);
                doc.push('\n');
            }
        }
    }

    doc.trim().into()
}

/// Whether an attribute list has a specified attribute.
pub fn has_attr(attrs: &mut Vec<syn::Attribute>, target: &str) -> bool {
    take_attr(attrs, target).is_some()
}

/// Whether an attribute list has a specified attribute.
pub fn parse_attr<T: Parse>(
    attrs: &mut Vec<syn::Attribute>,
    target: &str,
) -> Result<Option<Option<T>>> {
    take_attr(attrs, target)
        .map(|attr| {
            Ok(match attr.meta {
                syn::Meta::Path(_) => None,
                syn::Meta::List(list) => Some(list.parse_args()?),
                syn::Meta::NameValue(meta) => bail!(meta, "not valid here"),
            })
        })
        .transpose()
}

/// Whether an attribute list has a specified attribute.
pub fn take_attr(
    attrs: &mut Vec<syn::Attribute>,
    target: &str,
) -> Option<syn::Attribute> {
    attrs
        .iter()
        .position(|attr| attr.path().is_ident(target))
        .map(|i| attrs.remove(i))
}

/// Ensure that no unrecognized attributes remain.
pub fn validate_attrs(attrs: &[syn::Attribute]) -> Result<()> {
    for attr in attrs {
        if !attr.path().is_ident("doc") && !attr.path().is_ident("derive") {
            let ident = attr.path().get_ident().unwrap();
            bail!(ident, "unrecognized attribute: {ident}");
        }
    }
    Ok(())
}

/// Quotes an option literally.
pub fn quote_option<T: ToTokens>(option: &Option<T>) -> TokenStream {
    if let Some(value) = option {
        quote! { Some(#value) }
    } else {
        quote! { None }
    }
}

/// Parse a metadata key-value pair, separated by `=`.
pub fn parse_key_value<K: Token + Default + Parse, V: Parse>(
    input: ParseStream,
) -> Result<Option<V>> {
    if !input.peek(|_| K::default()) {
        return Ok(None);
    }

    let _: K = input.parse()?;
    let _: Token![=] = input.parse()?;
    let value: V = input.parse::<V>()?;
    eat_comma(input);
    Ok(Some(value))
}

/// Parse a metadata key-array pair, separated by `=`.
pub fn parse_key_value_array<K: Token + Default + Parse, V: Parse>(
    input: ParseStream,
) -> Result<Vec<V>> {
    Ok(parse_key_value::<K, Array<V>>(input)?.map_or(vec![], |array| array.0))
}

/// Parse a metadata key-string pair, separated by `=`.
pub fn parse_string<K: Token + Default + Parse>(
    input: ParseStream,
) -> Result<Option<String>> {
    Ok(parse_key_value::<K, syn::LitStr>(input)?.map(|s| s.value()))
}

/// Parse a metadata key-string pair, separated by `=`.
pub fn parse_string_array<K: Token + Default + Parse>(
    input: ParseStream,
) -> Result<Vec<String>> {
    Ok(parse_key_value_array::<K, syn::LitStr>(input)?
        .into_iter()
        .map(|lit| lit.value())
        .collect())
}

/// Parse a metadata flag that can be present or not.
pub fn parse_flag<K: Token + Default + Parse>(input: ParseStream) -> Result<bool> {
    if input.peek(|_| K::default()) {
        let _: K = input.parse()?;
        eat_comma(input);
        return Ok(true);
    }
    Ok(false)
}

/// Parse a comma if there is one.
pub fn eat_comma(input: ParseStream) {
    if input.peek(Token![,]) {
        let _: Token![,] = input.parse().unwrap();
    }
}

/// Determine the normal and title case name of a function, type, or element.
pub fn determine_name_and_title(
    specified_name: Option<String>,
    specified_title: Option<String>,
    ident: &syn::Ident,
    trim: Option<fn(&str) -> &str>,
) -> Result<(String, String)> {
    let name = {
        let trim = trim.unwrap_or(|s| s);
        let default = trim(&ident.to_string()).to_kebab_case();
        if specified_name.as_ref() == Some(&default) {
            bail!(ident, "name was specified unnecessarily");
        }
        specified_name.unwrap_or(default)
    };

    let title = {
        let default = name.to_title_case();
        if specified_title.as_ref() == Some(&default) {
            bail!(ident, "title was specified unnecessarily");
        }
        specified_title.unwrap_or(default)
    };

    Ok((name, title))
}

/// A generic parseable array.
struct Array<T>(Vec<T>);

impl<T: Parse> Parse for Array<T> {
    fn parse(input: ParseStream) -> Result<Self> {
        let content;
        syn::bracketed!(content in input);

        let mut elems = Vec::new();
        while !content.is_empty() {
            let first: T = content.parse()?;
            elems.push(first);
            if !content.is_empty() {
                let _: Token![,] = content.parse()?;
            }
        }

        Ok(Self(elems))
    }
}

/// Shorthand for `::typst_library::foundations`.
#[allow(non_camel_case_types)]
pub struct foundations;

impl quote::ToTokens for foundations {
    fn to_tokens(&self, tokens: &mut TokenStream) {
        quote! { ::typst_library::foundations }.to_tokens(tokens);
    }
}

/// For parsing attributes of the form:
/// #[attr(
///   statement;
///   statement;
///   returned_expression
/// )]
pub struct BlockWithReturn {
    pub prefix: Vec<syn::Stmt>,
    pub expr: syn::Stmt,
}

impl Parse for BlockWithReturn {
    fn parse(input: ParseStream) -> Result<Self> {
        let mut stmts = syn::Block::parse_within(input)?;
        let Some(expr) = stmts.pop() else {
            return Err(input.error("expected at least one expression"));
        };
        Ok(Self { prefix: stmts, expr })
    }
}

/// Parse a bare `type Name;` item.
#[allow(dead_code)]
pub struct BareType {
    pub attrs: Vec<Attribute>,
    pub type_token: Token![type],
    pub ident: Ident,
    pub semi_token: Token![;],
}

impl Parse for BareType {
    fn parse(input: ParseStream) -> Result<Self> {
        Ok(Self {
            attrs: input.call(Attribute::parse_outer)?,
            type_token: input.parse()?,
            ident: input.parse()?,
            semi_token: input.parse()?,
        })
    }
}

pub mod kw {
    syn::custom_keyword!(name);
    syn::custom_keyword!(span);
    syn::custom_keyword!(title);
    syn::custom_keyword!(scope);
    syn::custom_keyword!(contextual);
    syn::custom_keyword!(cast);
    syn::custom_keyword!(constructor);
    syn::custom_keyword!(keywords);
    syn::custom_keyword!(parent);
    syn::custom_keyword!(ext);
}

/// Extract the first line of documentation.
pub fn oneliner(docs: &str) -> String {
    let paragraph = docs.split("\n\n").next().unwrap_or_default();
    let mut depth = 0;
    let mut period = false;
    let mut end = paragraph.len();
    for (i, c) in paragraph.char_indices() {
        match c {
            '(' | '[' | '{' => depth += 1,
            ')' | ']' | '}' => depth -= 1,
            '.' if depth == 0 => period = true,
            c if period && c.is_whitespace() && !docs[..i].ends_with("e.g.") => {
                end = i;
                break;
            }
            _ => period = false,
        }
    }
    String::from(&docs[..end]).replace("\r\n", " ").replace("\n", " ")
}
