//! Defines attribute helpers for name resolution.
//!
//! Notice we don't preserve all attributes for name resolution, to save space:
//! for example, we skip doc comments (desugared to `#[doc = "..."]` attributes)
//! and `#[inline]`. The filtered attributes are listed in [`hir_expand::attrs`].

use std::{
    borrow::Cow,
    convert::Infallible,
    ops::{self, ControlFlow},
};

use base_db::SourceDatabase;
use cfg::{CfgExpr, CfgOptions};
use either::Either;
use hir_expand::{
    attrs::{Attr, AttrId, AttrInput, collect_item_tree_attrs},
    mod_path::ModPath,
    name::Name,
};
use intern::{Interned, Symbol, sym};
use syntax::{AstNode, ast};
use syntax_bridge::DocCommentDesugarMode;
use tt::token_to_literal;

use crate::item_tree::lower::Ctx;

#[derive(Debug, PartialEq, Eq)]
pub(crate) enum AttrsOrCfg {
    Enabled {
        attrs: AttrsOwned,
    },
    /// This only collects the attributes up to the disabled `cfg` (this is what needed for crate-level attributes.)
    CfgDisabled(Box<(CfgExpr, AttrsOwned)>),
}

impl Default for AttrsOrCfg {
    #[inline]
    fn default() -> Self {
        AttrsOrCfg::Enabled { attrs: AttrsOwned(Box::new([])) }
    }
}

impl AttrsOrCfg {
    pub(crate) fn lower<'a, S>(
        db: &dyn SourceDatabase,
        owner: &dyn ast::HasAttrs,
        cfg_options: &dyn Fn() -> &'a CfgOptions,
        span_map: S,
    ) -> AttrsOrCfg
    where
        S: syntax_bridge::SpanMapper + Copy,
    {
        let mut attrs = Vec::new();
        let result = collect_item_tree_attrs::<Infallible>(owner, cfg_options, |meta, _| {
            // NOTE: We cannot early return from this function, *every* attribute must be pushed, otherwise we'll mess the `AttrId`
            // tracking.
            let path = meta.path();
            let path_range = path
                .as_ref()
                .map(|path| path.syntax().text_range())
                .unwrap_or_else(|| meta.syntax().text_range());
            let (span, input) = match &meta {
                ast::Meta::KeyValueMeta(meta) => {
                    let span = span_map.span_for(path_range);
                    let input = meta.expr().and_then(|value| {
                        if let ast::Expr::Literal(value) = value {
                            Some(Box::new(AttrInput::Literal(token_to_literal(
                                value.token().text(),
                                span_map.span_for(value.syntax().text_range()),
                            ))))
                        } else {
                            None
                        }
                    });
                    (span, input)
                }
                ast::Meta::TokenTreeMeta(meta) => {
                    let span = span_map.span_for(path_range);
                    let tt = syntax_bridge::syntax_node_to_token_tree(
                        &meta
                            .token_tree()
                            .map(|it| it.syntax().clone())
                            .unwrap_or_else(|| meta.syntax().clone()),
                        span_map,
                        span,
                        DocCommentDesugarMode::ProcMacro,
                    );
                    let input = Some(Box::new(AttrInput::TokenTree(tt)));
                    (span, input)
                }
                ast::Meta::PathMeta(_) => {
                    let span = span_map.span_for(path_range);
                    (span, None)
                }
                ast::Meta::CfgMeta(_) | ast::Meta::CfgAttrMeta(_) | ast::Meta::UnsafeMeta(_) => {
                    unreachable!(
                        "`cfg`, `cfg_attr` and `unsafe(...)` are handled in `collect_item_tree_attrs()`"
                    )
                }
            };

            let path = path.and_then(|path| {
                ModPath::from_src(db, path, &mut |range| span_map.span_for(range).ctx)
            });
            let path = path.unwrap_or_else(|| Name::missing().into());

            attrs.push(Attr { path: Interned::new(path), input, ctxt: span.ctx });
            ControlFlow::Continue(())
        });
        let attrs = AttrsOwned(attrs.into_boxed_slice());
        match result {
            Some(Either::Right(cfg)) => AttrsOrCfg::CfgDisabled(Box::new((cfg, attrs))),
            None => AttrsOrCfg::Enabled { attrs },
        }
    }

    // Merges two `AttrsOrCfg`s, assuming `self` is placed before `other` in the source code.
    // The operation follows these rules:
    //
    //   - If `self` and `other` are both `AttrsOrCfg::Enabled`, the result is a new
    //     `AttrsOrCfg::Enabled`. It contains the concatenation of `self`'s attributes followed by
    //     `other`'s.
    //   - If `self` is `AttrsOrCfg::Enabled` but `other` is `AttrsOrCfg::CfgDisabled`, the result
    //     is a new `AttrsOrCfg::CfgDisabled`. It contains the concatenation of `self`'s attributes
    //     followed by `other`'s.
    //   - If `self` is `AttrsOrCfg::CfgDisabled`, return `self` as-is.
    //
    // The rationale is that attribute collection is sequential and order-sensitive. This operation
    // preserves those semantics when combining attributes from two different sources.
    // `AttrsOrCfg::CfgDisabled` marks a point where collection stops due to a false `#![cfg(...)]`
    // condition. It acts as a "breakpoint": attributes beyond it are not collected. Therefore,
    // when merging, an `AttrsOrCfg::CfgDisabled` on the left-hand side short-circuits the
    // operation, while an `AttrsOrCfg::CfgDisabled` on the right-hand side preserves all
    // attributes collected up to that point.
    //
    // Note that this operation is neither commutative nor associative.
    pub(crate) fn merge(self, other: AttrsOrCfg) -> AttrsOrCfg {
        match (self, other) {
            (AttrsOrCfg::Enabled { attrs }, AttrsOrCfg::Enabled { attrs: other_attrs }) => {
                let mut v = attrs.0.into_vec();
                v.extend(other_attrs.0);
                AttrsOrCfg::Enabled { attrs: AttrsOwned(v.into_boxed_slice()) }
            }
            (AttrsOrCfg::Enabled { attrs }, AttrsOrCfg::CfgDisabled(mut other)) => {
                let other_attrs = &mut other.1;
                let mut v = attrs.0.into_vec();
                v.extend(std::mem::take(&mut other_attrs.0));
                other_attrs.0 = v.into_boxed_slice();
                AttrsOrCfg::CfgDisabled(other)
            }
            (this @ AttrsOrCfg::CfgDisabled(_), _) => this,
        }
    }
}

#[derive(Debug, PartialEq, Eq)]
pub(crate) struct AttrsOwned(Box<[Attr]>);

#[derive(Debug, Clone, Copy)]
pub(crate) struct Attrs<'a>(&'a [Attr]);

impl ops::Deref for Attrs<'_> {
    type Target = [Attr];

    #[inline]
    fn deref(&self) -> &Self::Target {
        self.0
    }
}

impl Ctx<'_> {
    #[inline]
    pub(super) fn lower_attrs(&self, owner: &dyn ast::HasAttrs) -> AttrsOrCfg {
        AttrsOrCfg::lower(self.db, owner, &|| self.cfg_options(), self.span_map())
    }
}

impl AttrsOwned {
    #[inline]
    pub(crate) fn as_ref(&self) -> Attrs<'_> {
        Attrs(&self.0)
    }
}

impl<'a> Attrs<'a> {
    pub(crate) const EMPTY: Self = Attrs(&[]);

    #[inline]
    pub(crate) fn by_key(self, key: Symbol) -> AttrQuery<'a> {
        AttrQuery { attrs: self, key }
    }

    #[inline]
    pub(crate) fn iter(self) -> impl Iterator<Item = (AttrId, &'a Attr)> {
        self.0.iter().enumerate().map(|(id, attr)| (AttrId::from_item_tree_index(id as u32), attr))
    }

    #[inline]
    pub(crate) fn iter_after(
        self,
        after: Option<AttrId>,
    ) -> impl Iterator<Item = (AttrId, &'a Attr)> {
        let skip = after.map_or(0, |after| after.item_tree_index() + 1);
        self.0[skip as usize..]
            .iter()
            .enumerate()
            .map(move |(id, attr)| (AttrId::from_item_tree_index(id as u32 + skip), attr))
    }

    #[inline]
    pub(crate) fn is_proc_macro(&self) -> bool {
        self.by_key(sym::proc_macro).exists()
    }

    #[inline]
    pub(crate) fn is_proc_macro_attribute(&self) -> bool {
        self.by_key(sym::proc_macro_attribute).exists()
    }
}
#[derive(Debug, Clone)]
pub(crate) struct AttrQuery<'attr> {
    attrs: Attrs<'attr>,
    key: Symbol,
}

impl<'attr> AttrQuery<'attr> {
    #[inline]
    pub(crate) fn tt_values(self) -> impl Iterator<Item = &'attr crate::tt::TopSubtree> {
        self.attrs().filter_map(|attr| attr.token_tree_value())
    }

    #[inline]
    pub(crate) fn string_value_with_span(self) -> Option<(&'attr str, span::Span)> {
        self.attrs().find_map(|attr| attr.string_value_with_span())
    }

    #[inline]
    pub(crate) fn string_value_unescape(self) -> Option<Cow<'attr, str>> {
        self.attrs().find_map(|attr| attr.string_value_unescape())
    }

    #[inline]
    pub(crate) fn exists(self) -> bool {
        self.attrs().next().is_some()
    }

    #[inline]
    pub(crate) fn attrs(self) -> impl Iterator<Item = &'attr Attr> + Clone {
        let key = self.key;
        self.attrs.0.iter().filter(move |attr| attr.path.as_ident().is_some_and(|s| *s == key))
    }
}

impl AttrsOrCfg {
    #[inline]
    pub(super) fn is_empty(&self) -> bool {
        matches!(self, AttrsOrCfg::Enabled { attrs } if attrs.0.is_empty())
    }
}
