//! See [`Name`].

use std::fmt;

use syntax::{ast, utils::is_raw_identifier, SmolStr};

/// `Name` is a wrapper around string, which is used in hir for both references
/// and declarations. In theory, names should also carry hygiene info, but we are
/// not there yet!
///
/// Note that `Name` holds and prints escaped name i.e. prefixed with "r#" when it
/// is a raw identifier. Use [`unescaped()`][Name::unescaped] when you need the
/// name without "r#".
#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct Name(Repr);

/// Wrapper of `Name` to print the name without "r#" even when it is a raw identifier.
#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct UnescapedName<'a>(&'a Name);

#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
enum Repr {
    Text(SmolStr),
    TupleField(usize),
}

impl UnescapedName<'_> {
    /// Returns the textual representation of this name as a [`SmolStr`]. Prefer using this over
    /// [`ToString::to_string`] if possible as this conversion is cheaper in the general case.
    pub fn to_smol_str(&self) -> SmolStr {
        match &self.0 .0 {
            Repr::Text(it) => {
                if let Some(stripped) = it.strip_prefix("r#") {
                    SmolStr::new(stripped)
                } else {
                    it.clone()
                }
            }
            Repr::TupleField(it) => SmolStr::new(it.to_string()),
        }
    }

    pub fn display(&self, db: &dyn crate::db::ExpandDatabase) -> impl fmt::Display + '_ {
        _ = db;
        UnescapedDisplay { name: self }
    }
}

impl Name {
    /// Note: this is private to make creating name from random string hard.
    /// Hopefully, this should allow us to integrate hygiene cleaner in the
    /// future, and to switch to interned representation of names.
    const fn new_text(text: SmolStr) -> Name {
        Name(Repr::Text(text))
    }

    // FIXME: See above, unfortunately some places really need this right now
    #[doc(hidden)]
    pub const fn new_text_dont_use(text: SmolStr) -> Name {
        Name(Repr::Text(text))
    }

    pub fn new_tuple_field(idx: usize) -> Name {
        Name(Repr::TupleField(idx))
    }

    pub fn new_lifetime(lt: &ast::Lifetime) -> Name {
        Self::new_text(lt.text().into())
    }

    /// Shortcut to create inline plain text name. Panics if `text.len() > 22`
    const fn new_inline(text: &str) -> Name {
        Name::new_text(SmolStr::new_inline(text))
    }

    /// Resolve a name from the text of token.
    fn resolve(raw_text: &str) -> Name {
        match raw_text.strip_prefix("r#") {
            // When `raw_text` starts with "r#" but the name does not coincide with any
            // keyword, we never need the prefix so we strip it.
            Some(text) if !is_raw_identifier(text) => Name::new_text(SmolStr::new(text)),
            // Keywords (in the current edition) *can* be used as a name in earlier editions of
            // Rust, e.g. "try" in Rust 2015. Even in such cases, we keep track of them in their
            // escaped form.
            None if is_raw_identifier(raw_text) => {
                Name::new_text(SmolStr::from_iter(["r#", raw_text]))
            }
            _ => Name::new_text(raw_text.into()),
        }
    }

    /// A fake name for things missing in the source code.
    ///
    /// For example, `impl Foo for {}` should be treated as a trait impl for a
    /// type with a missing name. Similarly, `struct S { : u32 }` should have a
    /// single field with a missing name.
    ///
    /// Ideally, we want a `gensym` semantics for missing names -- each missing
    /// name is equal only to itself. It's not clear how to implement this in
    /// salsa though, so we punt on that bit for a moment.
    pub const fn missing() -> Name {
        Name::new_inline("[missing name]")
    }

    /// Returns true if this is a fake name for things missing in the source code. See
    /// [`missing()`][Self::missing] for details.
    ///
    /// Use this method instead of comparing with `Self::missing()` as missing names
    /// (ideally should) have a `gensym` semantics.
    pub fn is_missing(&self) -> bool {
        self == &Name::missing()
    }

    /// Generates a new name which is only equal to itself, by incrementing a counter. Due
    /// its implementation, it should not be used in things that salsa considers, like
    /// type names or field names, and it should be only used in names of local variables
    /// and labels and similar things.
    pub fn generate_new_name() -> Name {
        use std::sync::atomic::{AtomicUsize, Ordering};
        static CNT: AtomicUsize = AtomicUsize::new(0);
        let c = CNT.fetch_add(1, Ordering::Relaxed);
        Name::new_text(format!("<ra@gennew>{c}").into())
    }

    /// Returns the tuple index this name represents if it is a tuple field.
    pub fn as_tuple_index(&self) -> Option<usize> {
        match self.0 {
            Repr::TupleField(idx) => Some(idx),
            _ => None,
        }
    }

    /// Returns the text this name represents if it isn't a tuple field.
    pub fn as_text(&self) -> Option<SmolStr> {
        match &self.0 {
            Repr::Text(it) => Some(it.clone()),
            _ => None,
        }
    }

    /// Returns the text this name represents if it isn't a tuple field.
    pub fn as_str(&self) -> Option<&str> {
        match &self.0 {
            Repr::Text(it) => Some(it),
            _ => None,
        }
    }

    /// Returns the textual representation of this name as a [`SmolStr`].
    /// Prefer using this over [`ToString::to_string`] if possible as this conversion is cheaper in
    /// the general case.
    pub fn to_smol_str(&self) -> SmolStr {
        match &self.0 {
            Repr::Text(it) => it.clone(),
            Repr::TupleField(it) => SmolStr::new(it.to_string()),
        }
    }

    pub fn unescaped(&self) -> UnescapedName<'_> {
        UnescapedName(self)
    }

    pub fn is_escaped(&self) -> bool {
        match &self.0 {
            Repr::Text(it) => it.starts_with("r#"),
            Repr::TupleField(_) => false,
        }
    }

    pub fn display<'a>(&'a self, db: &dyn crate::db::ExpandDatabase) -> impl fmt::Display + 'a {
        _ = db;
        Display { name: self }
    }
}

struct Display<'a> {
    name: &'a Name,
}

impl fmt::Display for Display<'_> {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match &self.name.0 {
            Repr::Text(text) => fmt::Display::fmt(&text, f),
            Repr::TupleField(idx) => fmt::Display::fmt(&idx, f),
        }
    }
}

struct UnescapedDisplay<'a> {
    name: &'a UnescapedName<'a>,
}

impl fmt::Display for UnescapedDisplay<'_> {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match &self.name.0 .0 {
            Repr::Text(text) => {
                let text = text.strip_prefix("r#").unwrap_or(text);
                fmt::Display::fmt(&text, f)
            }
            Repr::TupleField(idx) => fmt::Display::fmt(&idx, f),
        }
    }
}

pub trait AsName {
    fn as_name(&self) -> Name;
}

impl AsName for ast::NameRef {
    fn as_name(&self) -> Name {
        match self.as_tuple_field() {
            Some(idx) => Name::new_tuple_field(idx),
            None => Name::resolve(&self.text()),
        }
    }
}

impl AsName for ast::Name {
    fn as_name(&self) -> Name {
        Name::resolve(&self.text())
    }
}

impl AsName for ast::NameOrNameRef {
    fn as_name(&self) -> Name {
        match self {
            ast::NameOrNameRef::Name(it) => it.as_name(),
            ast::NameOrNameRef::NameRef(it) => it.as_name(),
        }
    }
}

impl<Span> AsName for tt::Ident<Span> {
    fn as_name(&self) -> Name {
        Name::resolve(&self.text)
    }
}

impl AsName for ast::FieldKind {
    fn as_name(&self) -> Name {
        match self {
            ast::FieldKind::Name(nr) => nr.as_name(),
            ast::FieldKind::Index(idx) => {
                let idx = idx.text().parse::<usize>().unwrap_or(0);
                Name::new_tuple_field(idx)
            }
        }
    }
}

impl AsName for base_db::Dependency {
    fn as_name(&self) -> Name {
        Name::new_text(SmolStr::new(&*self.name))
    }
}

pub mod known {
    macro_rules! known_names {
        ($($ident:ident),* $(,)?) => {
            $(
                #[allow(bad_style)]
                pub const $ident: super::Name =
                    super::Name::new_inline(stringify!($ident));
            )*
        };
    }

    known_names!(
        // Primitives
        isize,
        i8,
        i16,
        i32,
        i64,
        i128,
        usize,
        u8,
        u16,
        u32,
        u64,
        u128,
        f32,
        f64,
        bool,
        char,
        str,
        // Special names
        macro_rules,
        doc,
        cfg,
        cfg_attr,
        register_attr,
        register_tool,
        // Components of known path (value or mod name)
        std,
        core,
        alloc,
        iter,
        ops,
        fmt,
        future,
        result,
        string,
        boxed,
        option,
        prelude,
        rust_2015,
        rust_2018,
        rust_2021,
        v1,
        new_display,
        new_debug,
        new_lower_exp,
        new_upper_exp,
        new_octal,
        new_pointer,
        new_binary,
        new_lower_hex,
        new_upper_hex,
        from_usize,
        // Components of known path (type name)
        Iterator,
        IntoIterator,
        Item,
        IntoIter,
        Try,
        Ok,
        Future,
        IntoFuture,
        Result,
        Option,
        Output,
        Target,
        Box,
        RangeFrom,
        RangeFull,
        RangeInclusive,
        RangeToInclusive,
        RangeTo,
        Range,
        String,
        Neg,
        Not,
        None,
        Index,
        Left,
        Right,
        Center,
        Unknown,
        Is,
        Param,
        Implied,
        // Components of known path (function name)
        filter_map,
        next,
        iter_mut,
        len,
        is_empty,
        as_str,
        new,
        new_v1_formatted,
        none,
        // Builtin macros
        asm,
        assert,
        column,
        compile_error,
        concat_idents,
        concat_bytes,
        concat,
        const_format_args,
        core_panic,
        env,
        file,
        format,
        format_args_nl,
        format_args,
        global_asm,
        include_bytes,
        include_str,
        include,
        line,
        llvm_asm,
        log_syntax,
        module_path,
        option_env,
        std_panic,
        stringify,
        trace_macros,
        unreachable,
        // Builtin derives
        Copy,
        Clone,
        Default,
        Debug,
        Hash,
        Ord,
        PartialOrd,
        Eq,
        PartialEq,
        // Builtin attributes
        bench,
        cfg_accessible,
        cfg_eval,
        crate_type,
        derive,
        derive_const,
        global_allocator,
        no_core,
        no_std,
        test,
        test_case,
        recursion_limit,
        feature,
        // known methods of lang items
        call_once,
        call_mut,
        call,
        eq,
        ne,
        ge,
        gt,
        le,
        lt,
        // known fields of lang items
        pieces,
        // lang items
        add_assign,
        add,
        bitand_assign,
        bitand,
        bitor_assign,
        bitor,
        bitxor_assign,
        bitxor,
        branch,
        deref_mut,
        deref,
        div_assign,
        div,
        drop,
        fn_mut,
        fn_once,
        future_trait,
        index,
        index_mut,
        into_future,
        mul_assign,
        mul,
        neg,
        not,
        owned_box,
        partial_ord,
        poll,
        r#fn,
        rem_assign,
        rem,
        shl_assign,
        shl,
        shr_assign,
        shr,
        sub_assign,
        sub,
        unsafe_cell,
        va_list
    );

    // self/Self cannot be used as an identifier
    pub const SELF_PARAM: super::Name = super::Name::new_inline("self");
    pub const SELF_TYPE: super::Name = super::Name::new_inline("Self");

    pub const STATIC_LIFETIME: super::Name = super::Name::new_inline("'static");

    #[macro_export]
    macro_rules! name {
        (self) => {
            $crate::name::known::SELF_PARAM
        };
        (Self) => {
            $crate::name::known::SELF_TYPE
        };
        ('static) => {
            $crate::name::known::STATIC_LIFETIME
        };
        ($ident:ident) => {
            $crate::name::known::$ident
        };
    }
}

pub use crate::name;
