//! Routines the parser and pretty-printer use to classify AST nodes.

use crate::ast::ExprKind::*;
use crate::ast::{self, MatchKind};
use crate::token::Delimiter;

/// This classification determines whether various syntactic positions break out
/// of parsing the current expression (true) or continue parsing more of the
/// same expression (false).
///
/// For example, it's relevant in the parsing of match arms:
///
/// ```ignore (illustrative)
/// match ... {
///     // Is this calling $e as a function, or is it the start of a new arm
///     // with a tuple pattern?
///     _ => $e (
///             ^                                                          )
///
///     // Is this an Index operation, or new arm with a slice pattern?
///     _ => $e [
///             ^                                                          ]
///
///     // Is this a binary operator, or leading vert in a new arm? Same for
///     // other punctuation which can either be a binary operator in
///     // expression or unary operator in pattern, such as `&` and `-`.
///     _ => $e |
///             ^
/// }
/// ```
///
/// If $e is something like `{}` or `if … {}`, then terminate the current
/// arm and parse a new arm.
///
/// If $e is something like `path::to` or `(…)`, continue parsing the same
/// arm.
///
/// *Almost* the same classification is used as an early bail-out for parsing
/// statements. See `expr_requires_semi_to_be_stmt`.
pub fn expr_is_complete(e: &ast::Expr) -> bool {
    matches!(
        e.kind,
        If(..)
            | Match(..)
            | Block(..)
            | While(..)
            | Loop(..)
            | ForLoop { .. }
            | TryBlock(..)
            | ConstBlock(..)
    )
}

/// Does this expression require a semicolon to be treated as a statement?
///
/// The negation of this: "can this expression be used as a statement without a
/// semicolon" -- is used as an early bail-out when parsing statements so that,
/// for instance,
///
/// ```ignore (illustrative)
/// if true {...} else {...}
/// |x| 5
/// ```
///
/// isn't parsed as `(if true {...} else {...} | x) | 5`.
///
/// Surprising special case: even though braced macro calls like `m! {}`
/// normally do not introduce a boundary when found at the head of a match arm,
/// they do terminate the parsing of a statement.
///
/// ```ignore (illustrative)
/// match ... {
///     _ => m! {} (),  // macro that expands to a function, which is then called
/// }
///
/// let _ = { m! {} () };  // macro call followed by unit
/// ```
pub fn expr_requires_semi_to_be_stmt(e: &ast::Expr) -> bool {
    match &e.kind {
        MacCall(mac_call) => mac_call.args.delim != Delimiter::Brace,
        _ => !expr_is_complete(e),
    }
}

/// Returns whether the leftmost token of the given expression is the label of a
/// labeled loop or block, such as in `'inner: loop { break 'inner 1 } + 1`.
///
/// Such expressions are not allowed as the value of an unlabeled break.
///
/// ```ignore (illustrative)
/// 'outer: {
///     break 'inner: loop { break 'inner 1 } + 1;  // invalid syntax
///
///     break 'outer 'inner: loop { break 'inner 1 } + 1;  // okay
///
///     break ('inner: loop { break 'inner 1 } + 1);  // okay
///
///     break ('inner: loop { break 'inner 1 }) + 1;  // okay
/// }
/// ```
pub fn leading_labeled_expr(mut expr: &ast::Expr) -> bool {
    loop {
        match &expr.kind {
            Block(_, label) | ForLoop { label, .. } | Loop(_, label, _) | While(_, _, label) => {
                return label.is_some();
            }

            Assign(e, _, _)
            | AssignOp(_, e, _)
            | Await(e, _)
            | Move(e, _)
            | Use(e, _)
            | Binary(_, e, _)
            | Call(e, _)
            | Cast(e, _)
            | Field(e, _)
            | Index(e, _, _)
            | Match(e, _, MatchKind::Postfix)
            | Range(Some(e), _, _)
            | Try(e) => {
                expr = e;
            }
            MethodCall(method_call) => {
                expr = &method_call.receiver;
            }

            AddrOf(..)
            | Array(..)
            | Become(..)
            | Break(..)
            | Closure(..)
            | ConstBlock(..)
            | Continue(..)
            | FormatArgs(..)
            | Gen(..)
            | If(..)
            | IncludedBytes(..)
            | InlineAsm(..)
            | Let(..)
            | Lit(..)
            | MacCall(..)
            | Match(_, _, MatchKind::Prefix)
            | OffsetOf(..)
            | Paren(..)
            | Path(..)
            | Range(None, _, _)
            | Repeat(..)
            | Ret(..)
            | Struct(..)
            | TryBlock(..)
            | Tup(..)
            | Type(..)
            | Unary(..)
            | Underscore
            | Yeet(..)
            | Yield(..)
            | UnsafeBinderCast(..)
            | Err(..)
            | Dummy => return false,
        }
    }
}

pub enum TrailingBrace<'a> {
    /// Trailing brace in a macro call, like the one in `x as *const brace! {}`.
    /// We will suggest changing the macro call to a different delimiter.
    MacCall(&'a ast::MacCall),
    /// Trailing brace in any other expression, such as `a + B {}`. We will
    /// suggest wrapping the innermost expression in parentheses: `a + (B {})`.
    Expr(&'a ast::Expr),
}

/// If an expression ends with `}`, returns the innermost expression ending in the `}`
pub fn expr_trailing_brace(mut expr: &ast::Expr) -> Option<TrailingBrace<'_>> {
    loop {
        match &expr.kind {
            AddrOf(_, _, e)
            | Assign(_, e, _)
            | AssignOp(_, _, e)
            | Binary(_, _, e)
            | Break(_, Some(e))
            | Let(_, e, _, _)
            | Range(_, Some(e), _)
            | Ret(Some(e))
            | Unary(_, e)
            | Yeet(Some(e))
            | Move(e, _)
            | Become(e) => {
                expr = e;
            }
            Yield(kind) => match kind.expr() {
                Some(e) => expr = e,
                None => break None,
            },
            Closure(closure) => {
                expr = &closure.body;
            }
            Gen(..)
            | Block(..)
            | ForLoop { .. }
            | If(..)
            | Loop(..)
            | Match(..)
            | Struct(..)
            | TryBlock(..)
            | While(..)
            | ConstBlock(_) => break Some(TrailingBrace::Expr(expr)),

            Cast(_, ty) => {
                break type_trailing_braced_mac_call(ty).map(TrailingBrace::MacCall);
            }

            MacCall(mac) => {
                break (mac.args.delim == Delimiter::Brace).then_some(TrailingBrace::MacCall(mac));
            }

            InlineAsm(_) | OffsetOf(_, _) | IncludedBytes(_) | FormatArgs(_) => {
                // These should have been denied pre-expansion.
                break None;
            }

            Break(_, None)
            | Range(_, None, _)
            | Ret(None)
            | Array(_)
            | Call(_, _)
            | MethodCall(_)
            | Tup(_)
            | Lit(_)
            | Type(_, _)
            | Await(_, _)
            | Use(_, _)
            | Field(_, _)
            | Index(_, _, _)
            | Underscore
            | Path(_, _)
            | Continue(_)
            | Repeat(_, _)
            | Paren(_)
            | Try(_)
            | Yeet(None)
            | UnsafeBinderCast(..)
            | Err(_)
            | Dummy => {
                break None;
            }
        }
    }
}

/// If the type's last token is `}`, it must be due to a braced macro call, such
/// as in `*const brace! { ... }`. Returns that trailing macro call.
fn type_trailing_braced_mac_call(mut ty: &ast::Ty) -> Option<&ast::MacCall> {
    loop {
        match &ty.kind {
            ast::TyKind::MacCall(mac) => {
                break (mac.args.delim == Delimiter::Brace).then_some(mac);
            }

            ast::TyKind::Ptr(mut_ty)
            | ast::TyKind::Ref(_, mut_ty)
            | ast::TyKind::PinnedRef(_, mut_ty) => {
                ty = &mut_ty.ty;
            }

            ast::TyKind::UnsafeBinder(binder) => {
                ty = &binder.inner_ty;
            }

            ast::TyKind::FnPtr(fn_ty) => match &fn_ty.decl.output {
                ast::FnRetTy::Default(_) => break None,
                ast::FnRetTy::Ty(ret) => ty = ret,
            },

            ast::TyKind::Path(_, path) => match path_return_type(path) {
                Some(trailing_ty) => ty = trailing_ty,
                None => break None,
            },

            ast::TyKind::TraitObject(bounds, _) | ast::TyKind::ImplTrait(_, bounds) => {
                match bounds.last() {
                    Some(ast::GenericBound::Trait(bound)) => {
                        match path_return_type(&bound.trait_ref.path) {
                            Some(trailing_ty) => ty = trailing_ty,
                            None => break None,
                        }
                    }
                    Some(ast::GenericBound::Outlives(_) | ast::GenericBound::Use(..)) | None => {
                        break None;
                    }
                }
            }

            ast::TyKind::Slice(..)
            | ast::TyKind::Array(..)
            | ast::TyKind::Never
            | ast::TyKind::Tup(..)
            | ast::TyKind::Paren(..)
            | ast::TyKind::Infer
            | ast::TyKind::ImplicitSelf
            | ast::TyKind::CVarArgs
            | ast::TyKind::Pat(..)
            | ast::TyKind::FieldOf(..)
            | ast::TyKind::Dummy
            | ast::TyKind::Err(..) => break None,
        }
    }
}

/// Returns the trailing return type in the given path, if it has one.
///
/// ```ignore (illustrative)
/// ::std::ops::FnOnce(&str) -> fn() -> *const c_void
///                             ^^^^^^^^^^^^^^^^^^^^^
/// ```
fn path_return_type(path: &ast::Path) -> Option<&ast::Ty> {
    let last_segment = path.segments.last()?;
    let args = last_segment.args.as_ref()?;
    match &**args {
        ast::GenericArgs::Parenthesized(args) => match &args.output {
            ast::FnRetTy::Default(_) => None,
            ast::FnRetTy::Ty(ret) => Some(ret),
        },
        ast::GenericArgs::AngleBracketed(_) | ast::GenericArgs::ParenthesizedElided(_) => None,
    }
}
