use crate::Spanned;
use proc_macro2::{Ident, Span};
use quote::ToTokens;
use std::fmt;
use syn::{
    ext::IdentExt,
    parse::{Parse, ParseStream},
    Result, Token,
};
mod path;
pub use path::SolPath;
static KW_DIFFERENCE: &[&str] = &include!("./difference.expr");
#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[repr(transparent)]
pub struct SolIdent(pub Ident);
impl quote::IdentFragment for SolIdent {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        self.0.fmt(f)
    }
    fn span(&self) -> Option<Span> {
        Some(self.0.span())
    }
}
impl fmt::Display for SolIdent {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        self.0.fmt(f)
    }
}
impl fmt::Debug for SolIdent {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        f.debug_tuple("SolIdent").field(&self.to_string()).finish()
    }
}
impl<T: ?Sized + AsRef<str>> PartialEq<T> for SolIdent {
    fn eq(&self, other: &T) -> bool {
        self.0 == other
    }
}
impl From<Ident> for SolIdent {
    fn from(value: Ident) -> Self {
        Self::new_spanned(&value.to_string(), value.span())
    }
}
impl From<SolIdent> for Ident {
    fn from(value: SolIdent) -> Self {
        value.0
    }
}
impl From<&str> for SolIdent {
    fn from(value: &str) -> Self {
        Self::new(value)
    }
}
impl Parse for SolIdent {
    fn parse(input: ParseStream<'_>) -> Result<Self> {
        check_dollar(input)?;
        let id = Ident::parse_any(input)?;
        Ok(Self::from(id))
    }
}
impl ToTokens for SolIdent {
    fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) {
        self.0.to_tokens(tokens);
    }
}
impl Spanned for SolIdent {
    fn span(&self) -> Span {
        self.0.span()
    }
    fn set_span(&mut self, span: Span) {
        self.0.set_span(span);
    }
}
impl SolIdent {
    pub fn new(s: &str) -> Self {
        Self::new_spanned(s, Span::call_site())
    }
    pub fn new_spanned(mut s: &str, span: Span) -> Self {
        let mut new_raw = KW_DIFFERENCE.contains(&s);
        if s.starts_with("r#") {
            new_raw = true;
            s = &s[2..];
        }
        if matches!(s, "_" | "self" | "Self" | "super" | "crate") {
            new_raw = false;
        }
        if new_raw {
            Self(Ident::new_raw(s, span))
        } else {
            Self(Ident::new(s, span))
        }
    }
    pub fn as_string(&self) -> String {
        let mut s = self.0.to_string();
        if s.starts_with("r#") {
            s = s[2..].to_string();
        }
        s
    }
    pub fn parse_any(input: ParseStream<'_>) -> Result<Self> {
        check_dollar(input)?;
        input.call(Ident::parse_any).map(Into::into)
    }
    pub fn peek_any(input: ParseStream<'_>) -> bool {
        input.peek(Ident::peek_any)
    }
    pub fn parse_opt(input: ParseStream<'_>) -> Result<Option<Self>> {
        if Self::peek_any(input) {
            input.parse().map(Some)
        } else {
            Ok(None)
        }
    }
}
fn check_dollar(input: ParseStream<'_>) -> Result<()> {
    if input.peek(Token![$]) {
        Err(input.error("Solidity identifiers starting with `$` are unsupported. This is a known limitation of syn-solidity."))
    } else {
        Ok(())
    }
}
#[cfg(test)]
mod tests {
    use super::*;
    use crate::sol_path;
    #[test]
    fn ident() {
        let id: SolIdent = syn::parse_str("a").unwrap();
        assert_eq!(id, SolIdent::new("a"));
    }
    #[test]
    fn keywords() {
        let difference: &[&str] = &include!("./difference.expr");
        for &s in difference {
            let id: SolIdent = syn::parse_str(s).unwrap();
            assert_eq!(id, SolIdent::new(s));
            assert_eq!(id.to_string(), format!("r#{s}"));
            assert_eq!(id.as_string(), s);
        }
        let intersection: &[&str] = &include!("./intersection.expr");
        for &s in intersection {
            let id: SolIdent = syn::parse_str(s).unwrap();
            assert_eq!(id, SolIdent::new(s));
            assert_eq!(id.to_string(), s);
            assert_eq!(id.as_string(), s);
        }
    }
    #[test]
    fn ident_path() {
        let path: SolPath = syn::parse_str("a.b.c").unwrap();
        assert_eq!(path, sol_path!["a", "b", "c"]);
    }
    #[test]
    fn ident_path_trailing() {
        let _e = syn::parse_str::<SolPath>("a.b.").unwrap_err();
    }
    #[test]
    fn ident_dollar() {
        assert!(syn::parse_str::<SolIdent>("$hello")
            .unwrap_err()
            .to_string()
            .contains("Solidity identifiers starting with `$` are unsupported."));
    }
}