//! Various extension methods to ast Expr Nodes, which are hard to code-generate.

use rowan::WalkEvent;

use crate::{
    ast::{
        self,
        operators::{ArithOp, BinaryOp, CmpOp, LogicOp, Ordering, RangeOp, UnaryOp},
        support, AstChildren, AstNode,
    },
    AstToken,
    SyntaxKind::*,
    SyntaxToken, T,
};

impl ast::AttrsOwner for ast::Expr {}

impl ast::Expr {
    pub fn is_block_like(&self) -> bool {
        matches!(
            self,
            ast::Expr::IfExpr(_)
                | ast::Expr::LoopExpr(_)
                | ast::Expr::ForExpr(_)
                | ast::Expr::WhileExpr(_)
                | ast::Expr::BlockExpr(_)
                | ast::Expr::MatchExpr(_)
                | ast::Expr::EffectExpr(_)
        )
    }

    pub fn name_ref(&self) -> Option<ast::NameRef> {
        if let ast::Expr::PathExpr(expr) = self {
            let path = expr.path()?;
            let segment = path.segment()?;
            let name_ref = segment.name_ref()?;
            if path.qualifier().is_none() {
                return Some(name_ref);
            }
        }
        None
    }

    /// Preorder walk all the expression's child expressions.
    pub fn walk(&self, cb: &mut dyn FnMut(ast::Expr)) {
        self.preorder(&mut |ev| {
            if let WalkEvent::Enter(expr) = ev {
                cb(expr);
            }
            false
        })
    }

    /// Preorder walk all the expression's child expressions preserving events.
    /// If the callback returns true on an [`WalkEvent::Enter`], the subtree of the expression will be skipped.
    /// Note that the subtree may already be skipped due to the context analysis this function does.
    pub fn preorder(&self, cb: &mut dyn FnMut(WalkEvent<ast::Expr>) -> bool) {
        let mut preorder = self.syntax().preorder();
        while let Some(event) = preorder.next() {
            let node = match event {
                WalkEvent::Enter(node) => node,
                WalkEvent::Leave(node) => {
                    if let Some(expr) = ast::Expr::cast(node) {
                        cb(WalkEvent::Leave(expr));
                    }
                    continue;
                }
            };
            match ast::Stmt::cast(node.clone()) {
                // recursively walk the initializer, skipping potential const pat expressions
                // let statements aren't usually nested too deeply so this is fine to recurse on
                Some(ast::Stmt::LetStmt(l)) => {
                    if let Some(expr) = l.initializer() {
                        expr.preorder(cb);
                    }
                    preorder.skip_subtree();
                }
                // Don't skip subtree since we want to process the expression child next
                Some(ast::Stmt::ExprStmt(_)) => (),
                // This might be an expression
                Some(ast::Stmt::Item(ast::Item::MacroCall(mcall))) => {
                    cb(WalkEvent::Enter(ast::Expr::MacroCall(mcall)));
                    preorder.skip_subtree();
                }
                // skip inner items which might have their own expressions
                Some(ast::Stmt::Item(_)) => preorder.skip_subtree(),
                None => {
                    // skip const args, those expressions are a different context
                    if ast::GenericArg::can_cast(node.kind()) {
                        preorder.skip_subtree();
                    } else if let Some(expr) = ast::Expr::cast(node) {
                        let is_different_context = match &expr {
                            ast::Expr::EffectExpr(effect) => {
                                matches!(
                                    effect.effect(),
                                    ast::Effect::Async(_)
                                        | ast::Effect::Try(_)
                                        | ast::Effect::Const(_)
                                )
                            }
                            ast::Expr::ClosureExpr(_) => true,
                            _ => false,
                        };
                        let skip = cb(WalkEvent::Enter(expr));
                        if skip || is_different_context {
                            preorder.skip_subtree();
                        }
                    }
                }
            }
        }
    }

    /// Preorder walk all the expression's child patterns.
    pub fn walk_patterns(&self, cb: &mut dyn FnMut(ast::Pat)) {
        let mut preorder = self.syntax().preorder();
        while let Some(event) = preorder.next() {
            let node = match event {
                WalkEvent::Enter(node) => node,
                WalkEvent::Leave(_) => continue,
            };
            match ast::Stmt::cast(node.clone()) {
                Some(ast::Stmt::LetStmt(l)) => {
                    if let Some(pat) = l.pat() {
                        pat.walk(cb);
                    }
                    if let Some(expr) = l.initializer() {
                        expr.walk_patterns(cb);
                    }
                    preorder.skip_subtree();
                }
                // Don't skip subtree since we want to process the expression child next
                Some(ast::Stmt::ExprStmt(_)) => (),
                // skip inner items which might have their own patterns
                Some(ast::Stmt::Item(_)) => preorder.skip_subtree(),
                None => {
                    // skip const args, those are a different context
                    if ast::GenericArg::can_cast(node.kind()) {
                        preorder.skip_subtree();
                    } else if let Some(expr) = ast::Expr::cast(node.clone()) {
                        let is_different_context = match &expr {
                            ast::Expr::EffectExpr(effect) => {
                                matches!(
                                    effect.effect(),
                                    ast::Effect::Async(_)
                                        | ast::Effect::Try(_)
                                        | ast::Effect::Const(_)
                                )
                            }
                            ast::Expr::ClosureExpr(_) => true,
                            _ => false,
                        };
                        if is_different_context {
                            preorder.skip_subtree();
                        }
                    } else if let Some(pat) = ast::Pat::cast(node) {
                        preorder.skip_subtree();
                        pat.walk(cb);
                    }
                }
            }
        }
    }
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ElseBranch {
    Block(ast::BlockExpr),
    IfExpr(ast::IfExpr),
}

impl From<ast::BlockExpr> for ElseBranch {
    fn from(block_expr: ast::BlockExpr) -> Self {
        Self::Block(block_expr)
    }
}

impl From<ast::IfExpr> for ElseBranch {
    fn from(if_expr: ast::IfExpr) -> Self {
        Self::IfExpr(if_expr)
    }
}

impl ast::IfExpr {
    pub fn then_branch(&self) -> Option<ast::BlockExpr> {
        self.blocks().next()
    }

    pub fn else_branch(&self) -> Option<ElseBranch> {
        let res = match self.blocks().nth(1) {
            Some(block) => ElseBranch::Block(block),
            None => {
                let elif: ast::IfExpr = support::child(self.syntax())?;
                ElseBranch::IfExpr(elif)
            }
        };
        Some(res)
    }

    pub fn blocks(&self) -> AstChildren<ast::BlockExpr> {
        support::children(self.syntax())
    }
}

impl ast::PrefixExpr {
    pub fn op_kind(&self) -> Option<UnaryOp> {
        let res = match self.op_token()?.kind() {
            T![*] => UnaryOp::Deref,
            T![!] => UnaryOp::Not,
            T![-] => UnaryOp::Neg,
            _ => return None,
        };
        Some(res)
    }

    pub fn op_token(&self) -> Option<SyntaxToken> {
        self.syntax().first_child_or_token()?.into_token()
    }
}

impl ast::BinExpr {
    pub fn op_details(&self) -> Option<(SyntaxToken, BinaryOp)> {
        self.syntax().children_with_tokens().filter_map(|it| it.into_token()).find_map(|c| {
            #[rustfmt::skip]
            let bin_op = match c.kind() {
                T![||] => BinaryOp::LogicOp(LogicOp::Or),
                T![&&] => BinaryOp::LogicOp(LogicOp::And),

                T![==] => BinaryOp::CmpOp(CmpOp::Eq { negated: false }),
                T![!=] => BinaryOp::CmpOp(CmpOp::Eq { negated: true }),
                T![<=] => BinaryOp::CmpOp(CmpOp::Ord { ordering: Ordering::Less,    strict: false }),
                T![>=] => BinaryOp::CmpOp(CmpOp::Ord { ordering: Ordering::Greater, strict: false }),
                T![<]  => BinaryOp::CmpOp(CmpOp::Ord { ordering: Ordering::Less,    strict: true }),
                T![>]  => BinaryOp::CmpOp(CmpOp::Ord { ordering: Ordering::Greater, strict: true }),

                T![+]  => BinaryOp::ArithOp(ArithOp::Add),
                T![*]  => BinaryOp::ArithOp(ArithOp::Mul),
                T![-]  => BinaryOp::ArithOp(ArithOp::Sub),
                T![/]  => BinaryOp::ArithOp(ArithOp::Div),
                T![%]  => BinaryOp::ArithOp(ArithOp::Rem),
                T![<<] => BinaryOp::ArithOp(ArithOp::Shl),
                T![>>] => BinaryOp::ArithOp(ArithOp::Shr),
                T![^]  => BinaryOp::ArithOp(ArithOp::BitXor),
                T![|]  => BinaryOp::ArithOp(ArithOp::BitOr),
                T![&]  => BinaryOp::ArithOp(ArithOp::BitAnd),

                T![=]   => BinaryOp::Assignment { op: None },
                T![+=]  => BinaryOp::Assignment { op: Some(ArithOp::Add) },
                T![*=]  => BinaryOp::Assignment { op: Some(ArithOp::Mul) },
                T![-=]  => BinaryOp::Assignment { op: Some(ArithOp::Sub) },
                T![/=]  => BinaryOp::Assignment { op: Some(ArithOp::Div) },
                T![%=]  => BinaryOp::Assignment { op: Some(ArithOp::Rem) },
                T![<<=] => BinaryOp::Assignment { op: Some(ArithOp::Shl) },
                T![>>=] => BinaryOp::Assignment { op: Some(ArithOp::Shr) },
                T![^=]  => BinaryOp::Assignment { op: Some(ArithOp::BitXor) },
                T![|=]  => BinaryOp::Assignment { op: Some(ArithOp::BitOr) },
                T![&=]  => BinaryOp::Assignment { op: Some(ArithOp::BitAnd) },

                _ => return None,
            };
            Some((c, bin_op))
        })
    }

    pub fn op_kind(&self) -> Option<BinaryOp> {
        self.op_details().map(|t| t.1)
    }

    pub fn op_token(&self) -> Option<SyntaxToken> {
        self.op_details().map(|t| t.0)
    }

    pub fn lhs(&self) -> Option<ast::Expr> {
        support::children(self.syntax()).next()
    }

    pub fn rhs(&self) -> Option<ast::Expr> {
        support::children(self.syntax()).nth(1)
    }

    pub fn sub_exprs(&self) -> (Option<ast::Expr>, Option<ast::Expr>) {
        let mut children = support::children(self.syntax());
        let first = children.next();
        let second = children.next();
        (first, second)
    }
}

impl ast::RangeExpr {
    fn op_details(&self) -> Option<(usize, SyntaxToken, RangeOp)> {
        self.syntax().children_with_tokens().enumerate().find_map(|(ix, child)| {
            let token = child.into_token()?;
            let bin_op = match token.kind() {
                T![..] => RangeOp::Exclusive,
                T![..=] => RangeOp::Inclusive,
                _ => return None,
            };
            Some((ix, token, bin_op))
        })
    }

    pub fn op_kind(&self) -> Option<RangeOp> {
        self.op_details().map(|t| t.2)
    }

    pub fn op_token(&self) -> Option<SyntaxToken> {
        self.op_details().map(|t| t.1)
    }

    pub fn start(&self) -> Option<ast::Expr> {
        let op_ix = self.op_details()?.0;
        self.syntax()
            .children_with_tokens()
            .take(op_ix)
            .find_map(|it| ast::Expr::cast(it.into_node()?))
    }

    pub fn end(&self) -> Option<ast::Expr> {
        let op_ix = self.op_details()?.0;
        self.syntax()
            .children_with_tokens()
            .skip(op_ix + 1)
            .find_map(|it| ast::Expr::cast(it.into_node()?))
    }
}

impl ast::IndexExpr {
    pub fn base(&self) -> Option<ast::Expr> {
        support::children(self.syntax()).next()
    }
    pub fn index(&self) -> Option<ast::Expr> {
        support::children(self.syntax()).nth(1)
    }
}

pub enum ArrayExprKind {
    Repeat { initializer: Option<ast::Expr>, repeat: Option<ast::Expr> },
    ElementList(AstChildren<ast::Expr>),
}

impl ast::ArrayExpr {
    pub fn kind(&self) -> ArrayExprKind {
        if self.is_repeat() {
            ArrayExprKind::Repeat {
                initializer: support::children(self.syntax()).next(),
                repeat: support::children(self.syntax()).nth(1),
            }
        } else {
            ArrayExprKind::ElementList(support::children(self.syntax()))
        }
    }

    fn is_repeat(&self) -> bool {
        self.syntax().children_with_tokens().any(|it| it.kind() == T![;])
    }
}

#[derive(Clone, Debug, PartialEq, Eq, Hash)]
pub enum LiteralKind {
    String(ast::String),
    ByteString(ast::ByteString),
    IntNumber(ast::IntNumber),
    FloatNumber(ast::FloatNumber),
    Char,
    Byte,
    Bool(bool),
}

impl ast::Literal {
    pub fn token(&self) -> SyntaxToken {
        self.syntax()
            .children_with_tokens()
            .find(|e| e.kind() != ATTR && !e.kind().is_trivia())
            .and_then(|e| e.into_token())
            .unwrap()
    }
    pub fn kind(&self) -> LiteralKind {
        let token = self.token();

        if let Some(t) = ast::IntNumber::cast(token.clone()) {
            return LiteralKind::IntNumber(t);
        }
        if let Some(t) = ast::FloatNumber::cast(token.clone()) {
            return LiteralKind::FloatNumber(t);
        }
        if let Some(t) = ast::String::cast(token.clone()) {
            return LiteralKind::String(t);
        }
        if let Some(t) = ast::ByteString::cast(token.clone()) {
            return LiteralKind::ByteString(t);
        }

        match token.kind() {
            T![true] => LiteralKind::Bool(true),
            T![false] => LiteralKind::Bool(false),
            CHAR => LiteralKind::Char,
            BYTE => LiteralKind::Byte,
            _ => unreachable!(),
        }
    }
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Effect {
    Async(SyntaxToken),
    Unsafe(SyntaxToken),
    Try(SyntaxToken),
    Const(SyntaxToken),
    // Very much not an effect, but we stuff it into this node anyway
    Label(ast::Label),
}

impl ast::EffectExpr {
    pub fn effect(&self) -> Effect {
        if let Some(token) = self.async_token() {
            return Effect::Async(token);
        }
        if let Some(token) = self.unsafe_token() {
            return Effect::Unsafe(token);
        }
        if let Some(token) = self.try_token() {
            return Effect::Try(token);
        }
        if let Some(token) = self.const_token() {
            return Effect::Const(token);
        }
        if let Some(label) = self.label() {
            return Effect::Label(label);
        }
        unreachable!("ast::EffectExpr without Effect")
    }
}

impl ast::BlockExpr {
    /// false if the block is an intrinsic part of the syntax and can't be
    /// replaced with arbitrary expression.
    ///
    /// ```not_rust
    /// fn foo() { not_stand_alone }
    /// const FOO: () = { stand_alone };
    /// ```
    pub fn is_standalone(&self) -> bool {
        let parent = match self.syntax().parent() {
            Some(it) => it,
            None => return true,
        };
        !matches!(parent.kind(), FN | IF_EXPR | WHILE_EXPR | LOOP_EXPR | EFFECT_EXPR)
    }
}

#[test]
fn test_literal_with_attr() {
    let parse = ast::SourceFile::parse(r#"const _: &str = { #[attr] "Hello" };"#);
    let lit = parse.tree().syntax().descendants().find_map(ast::Literal::cast).unwrap();
    assert_eq!(lit.token().text(), r#""Hello""#);
}

impl ast::RecordExprField {
    pub fn parent_record_lit(&self) -> ast::RecordExpr {
        self.syntax().ancestors().find_map(ast::RecordExpr::cast).unwrap()
    }
}
