use crate::ast::{self, BinOpKind};
use crate::token::{self, BinOpToken, Token};
use rustc_span::symbol::kw;

/// Associative operator with precedence.
///
/// This is the enum which specifies operator precedence and fixity to the parser.
#[derive(Copy, Clone, PartialEq, Debug)]
pub enum AssocOp {
    /// `+`
    Add,
    /// `-`
    Subtract,
    /// `*`
    Multiply,
    /// `/`
    Divide,
    /// `%`
    Modulus,
    /// `&&`
    LAnd,
    /// `||`
    LOr,
    /// `^`
    BitXor,
    /// `&`
    BitAnd,
    /// `|`
    BitOr,
    /// `<<`
    ShiftLeft,
    /// `>>`
    ShiftRight,
    /// `==`
    Equal,
    /// `<`
    Less,
    /// `<=`
    LessEqual,
    /// `!=`
    NotEqual,
    /// `>`
    Greater,
    /// `>=`
    GreaterEqual,
    /// `=`
    Assign,
    /// `?=` where ? is one of the BinOpToken
    AssignOp(BinOpToken),
    /// `as`
    As,
    /// `..` range
    DotDot,
    /// `..=` range
    DotDotEq,
    /// `:`
    Colon,
}

#[derive(PartialEq, Debug)]
pub enum Fixity {
    /// The operator is left-associative
    Left,
    /// The operator is right-associative
    Right,
    /// The operator is not associative
    None,
}

impl AssocOp {
    /// Creates a new AssocOP from a token
    pub fn from_token(t: &Token) -> Option<AssocOp> {
        use AssocOp::*;
        match t.kind {
            token::BinOpEq(k) => Some(AssignOp(k)),
            token::Eq => Some(Assign),
            token::BinOp(BinOpToken::Star) => Some(Multiply),
            token::BinOp(BinOpToken::Slash) => Some(Divide),
            token::BinOp(BinOpToken::Percent) => Some(Modulus),
            token::BinOp(BinOpToken::Plus) => Some(Add),
            token::BinOp(BinOpToken::Minus) => Some(Subtract),
            token::BinOp(BinOpToken::Shl) => Some(ShiftLeft),
            token::BinOp(BinOpToken::Shr) => Some(ShiftRight),
            token::BinOp(BinOpToken::And) => Some(BitAnd),
            token::BinOp(BinOpToken::Caret) => Some(BitXor),
            token::BinOp(BinOpToken::Or) => Some(BitOr),
            token::Lt => Some(Less),
            token::Le => Some(LessEqual),
            token::Ge => Some(GreaterEqual),
            token::Gt => Some(Greater),
            token::EqEq => Some(Equal),
            token::Ne => Some(NotEqual),
            token::AndAnd => Some(LAnd),
            token::OrOr => Some(LOr),
            token::DotDot => Some(DotDot),
            token::DotDotEq => Some(DotDotEq),
            // DotDotDot is no longer supported, but we need some way to display the error
            token::DotDotDot => Some(DotDotEq),
            token::Colon => Some(Colon),
            // `<-` should probably be `< -`
            token::LArrow => Some(Less),
            _ if t.is_keyword(kw::As) => Some(As),
            _ => None,
        }
    }

    /// Creates a new AssocOp from ast::BinOpKind.
    pub fn from_ast_binop(op: BinOpKind) -> Self {
        use AssocOp::*;
        match op {
            BinOpKind::Lt => Less,
            BinOpKind::Gt => Greater,
            BinOpKind::Le => LessEqual,
            BinOpKind::Ge => GreaterEqual,
            BinOpKind::Eq => Equal,
            BinOpKind::Ne => NotEqual,
            BinOpKind::Mul => Multiply,
            BinOpKind::Div => Divide,
            BinOpKind::Rem => Modulus,
            BinOpKind::Add => Add,
            BinOpKind::Sub => Subtract,
            BinOpKind::Shl => ShiftLeft,
            BinOpKind::Shr => ShiftRight,
            BinOpKind::BitAnd => BitAnd,
            BinOpKind::BitXor => BitXor,
            BinOpKind::BitOr => BitOr,
            BinOpKind::And => LAnd,
            BinOpKind::Or => LOr,
        }
    }

    /// Gets the precedence of this operator
    pub fn precedence(&self) -> usize {
        use AssocOp::*;
        match *self {
            As | Colon => 14,
            Multiply | Divide | Modulus => 13,
            Add | Subtract => 12,
            ShiftLeft | ShiftRight => 11,
            BitAnd => 10,
            BitXor => 9,
            BitOr => 8,
            Less | Greater | LessEqual | GreaterEqual | Equal | NotEqual => 7,
            LAnd => 6,
            LOr => 5,
            DotDot | DotDotEq => 4,
            Assign | AssignOp(_) => 2,
        }
    }

    /// Gets the fixity of this operator
    pub fn fixity(&self) -> Fixity {
        use AssocOp::*;
        // NOTE: it is a bug to have an operators that has same precedence but different fixities!
        match *self {
            Assign | AssignOp(_) => Fixity::Right,
            As | Multiply | Divide | Modulus | Add | Subtract | ShiftLeft | ShiftRight | BitAnd
            | BitXor | BitOr | Less | Greater | LessEqual | GreaterEqual | Equal | NotEqual
            | LAnd | LOr | Colon => Fixity::Left,
            DotDot | DotDotEq => Fixity::None,
        }
    }

    pub fn is_comparison(&self) -> bool {
        use AssocOp::*;
        match *self {
            Less | Greater | LessEqual | GreaterEqual | Equal | NotEqual => true,
            Assign | AssignOp(_) | As | Multiply | Divide | Modulus | Add | Subtract
            | ShiftLeft | ShiftRight | BitAnd | BitXor | BitOr | LAnd | LOr | DotDot | DotDotEq
            | Colon => false,
        }
    }

    pub fn is_assign_like(&self) -> bool {
        use AssocOp::*;
        match *self {
            Assign | AssignOp(_) => true,
            Less | Greater | LessEqual | GreaterEqual | Equal | NotEqual | As | Multiply
            | Divide | Modulus | Add | Subtract | ShiftLeft | ShiftRight | BitAnd | BitXor
            | BitOr | LAnd | LOr | DotDot | DotDotEq | Colon => false,
        }
    }

    pub fn to_ast_binop(&self) -> Option<BinOpKind> {
        use AssocOp::*;
        match *self {
            Less => Some(BinOpKind::Lt),
            Greater => Some(BinOpKind::Gt),
            LessEqual => Some(BinOpKind::Le),
            GreaterEqual => Some(BinOpKind::Ge),
            Equal => Some(BinOpKind::Eq),
            NotEqual => Some(BinOpKind::Ne),
            Multiply => Some(BinOpKind::Mul),
            Divide => Some(BinOpKind::Div),
            Modulus => Some(BinOpKind::Rem),
            Add => Some(BinOpKind::Add),
            Subtract => Some(BinOpKind::Sub),
            ShiftLeft => Some(BinOpKind::Shl),
            ShiftRight => Some(BinOpKind::Shr),
            BitAnd => Some(BinOpKind::BitAnd),
            BitXor => Some(BinOpKind::BitXor),
            BitOr => Some(BinOpKind::BitOr),
            LAnd => Some(BinOpKind::And),
            LOr => Some(BinOpKind::Or),
            Assign | AssignOp(_) | As | DotDot | DotDotEq | Colon => None,
        }
    }

    /// This operator could be used to follow a block unambiguously.
    ///
    /// This is used for error recovery at the moment, providing a suggestion to wrap blocks with
    /// parentheses while having a high degree of confidence on the correctness of the suggestion.
    pub fn can_continue_expr_unambiguously(&self) -> bool {
        use AssocOp::*;
        match self {
            BitXor | // `{ 42 } ^ 3`
            Assign | // `{ 42 } = { 42 }`
            Divide | // `{ 42 } / 42`
            Modulus | // `{ 42 } % 2`
            ShiftRight | // `{ 42 } >> 2`
            LessEqual | // `{ 42 } <= 3`
            Greater | // `{ 42 } > 3`
            GreaterEqual | // `{ 42 } >= 3`
            AssignOp(_) | // `{ 42 } +=`
            As | // `{ 42 } as usize`
            // Equal | // `{ 42 } == { 42 }`    Accepting these here would regress incorrect
            // NotEqual | // `{ 42 } != { 42 }  struct literals parser recovery.
            Colon => true, // `{ 42 }: usize`
            _ => false,
        }
    }
}

pub const PREC_CLOSURE: i8 = -40;
pub const PREC_JUMP: i8 = -30;
pub const PREC_RANGE: i8 = -10;
// The range 2..=14 is reserved for AssocOp binary operator precedences.
pub const PREC_PREFIX: i8 = 50;
pub const PREC_POSTFIX: i8 = 60;
pub const PREC_PAREN: i8 = 99;
pub const PREC_FORCE_PAREN: i8 = 100;

#[derive(Debug, Clone, Copy)]
pub enum ExprPrecedence {
    Closure,
    Break,
    Continue,
    Ret,
    Yield,

    Range,

    Binary(BinOpKind),

    Cast,
    Type,

    Assign,
    AssignOp,

    Box,
    AddrOf,
    Let,
    Unary,

    Call,
    MethodCall,
    Field,
    Index,
    Try,
    InlineAsm,
    Mac,

    Array,
    Repeat,
    Tup,
    Lit,
    Path,
    Paren,
    If,
    While,
    ForLoop,
    Loop,
    Match,
    ConstBlock,
    Block,
    TryBlock,
    Struct,
    Async,
    Await,
    Err,
}

impl ExprPrecedence {
    pub fn order(self) -> i8 {
        match self {
            ExprPrecedence::Closure => PREC_CLOSURE,

            ExprPrecedence::Break |
            ExprPrecedence::Continue |
            ExprPrecedence::Ret |
            ExprPrecedence::Yield => PREC_JUMP,

            // `Range` claims to have higher precedence than `Assign`, but `x .. x = x` fails to
            // parse, instead of parsing as `(x .. x) = x`.  Giving `Range` a lower precedence
            // ensures that `pprust` will add parentheses in the right places to get the desired
            // parse.
            ExprPrecedence::Range => PREC_RANGE,

            // Binop-like expr kinds, handled by `AssocOp`.
            ExprPrecedence::Binary(op) => AssocOp::from_ast_binop(op).precedence() as i8,
            ExprPrecedence::Cast => AssocOp::As.precedence() as i8,
            ExprPrecedence::Type => AssocOp::Colon.precedence() as i8,

            ExprPrecedence::Assign |
            ExprPrecedence::AssignOp => AssocOp::Assign.precedence() as i8,

            // Unary, prefix
            ExprPrecedence::Box |
            ExprPrecedence::AddrOf |
            // Here `let pats = expr` has `let pats =` as a "unary" prefix of `expr`.
            // However, this is not exactly right. When `let _ = a` is the LHS of a binop we
            // need parens sometimes. E.g. we can print `(let _ = a) && b` as `let _ = a && b`
            // but we need to print `(let _ = a) < b` as-is with parens.
            ExprPrecedence::Let |
            ExprPrecedence::Unary => PREC_PREFIX,

            // Unary, postfix
            ExprPrecedence::Await |
            ExprPrecedence::Call |
            ExprPrecedence::MethodCall |
            ExprPrecedence::Field |
            ExprPrecedence::Index |
            ExprPrecedence::Try |
            ExprPrecedence::InlineAsm |
            ExprPrecedence::Mac => PREC_POSTFIX,

            // Never need parens
            ExprPrecedence::Array |
            ExprPrecedence::Repeat |
            ExprPrecedence::Tup |
            ExprPrecedence::Lit |
            ExprPrecedence::Path |
            ExprPrecedence::Paren |
            ExprPrecedence::If |
            ExprPrecedence::While |
            ExprPrecedence::ForLoop |
            ExprPrecedence::Loop |
            ExprPrecedence::Match |
            ExprPrecedence::ConstBlock |
            ExprPrecedence::Block |
            ExprPrecedence::TryBlock |
            ExprPrecedence::Async |
            ExprPrecedence::Struct |
            ExprPrecedence::Err => PREC_PAREN,
        }
    }
}

/// In `let p = e`, operators with precedence `<=` this one requires parenthesis in `e`.
pub fn prec_let_scrutinee_needs_par() -> usize {
    AssocOp::LAnd.precedence()
}

/// Suppose we have `let _ = e` and the `order` of `e`.
/// Is the `order` such that `e` in `let _ = e` needs parenthesis when it is on the RHS?
///
/// Conversely, suppose that we have `(let _ = a) OP b` and `order` is that of `OP`.
/// Can we print this as `let _ = a OP b`?
pub fn needs_par_as_let_scrutinee(order: i8) -> bool {
    order <= prec_let_scrutinee_needs_par() as i8
}

/// Expressions that syntactically contain an "exterior" struct literal i.e., not surrounded by any
/// parens or other delimiters, e.g., `X { y: 1 }`, `X { y: 1 }.method()`, `foo == X { y: 1 }` and
/// `X { y: 1 } == foo` all do, but `(X { y: 1 }) == foo` does not.
pub fn contains_exterior_struct_lit(value: &ast::Expr) -> bool {
    match value.kind {
        ast::ExprKind::Struct(..) => true,

        ast::ExprKind::Assign(ref lhs, ref rhs, _)
        | ast::ExprKind::AssignOp(_, ref lhs, ref rhs)
        | ast::ExprKind::Binary(_, ref lhs, ref rhs) => {
            // X { y: 1 } + X { y: 2 }
            contains_exterior_struct_lit(&lhs) || contains_exterior_struct_lit(&rhs)
        }
        ast::ExprKind::Await(ref x)
        | ast::ExprKind::Unary(_, ref x)
        | ast::ExprKind::Cast(ref x, _)
        | ast::ExprKind::Type(ref x, _)
        | ast::ExprKind::Field(ref x, _)
        | ast::ExprKind::Index(ref x, _) => {
            // &X { y: 1 }, X { y: 1 }.y
            contains_exterior_struct_lit(&x)
        }

        ast::ExprKind::MethodCall(.., ref exprs, _) => {
            // X { y: 1 }.bar(...)
            contains_exterior_struct_lit(&exprs[0])
        }

        _ => false,
    }
}
