use oxc_allocator::TakeIn;
use oxc_ast::ast::*;
use oxc_ecmascript::constant_evaluation::{ConstantEvaluation, IsInt32OrUint32};
use oxc_span::GetSpan;

use crate::TraverseCtx;

use super::PeepholeOptimizations;

impl<'a> PeepholeOptimizations {
    /// Simplify syntax when we know it's used inside a boolean context, e.g. `if (boolean_context) {}`.
    ///
    /// `SimplifyBooleanExpr`: <https://github.com/evanw/esbuild/blob/v0.24.2/internal/js_ast/js_ast_helpers.go#L2059>
    pub fn minimize_expression_in_boolean_context(
        expr: &mut Expression<'a>,
        ctx: &mut TraverseCtx<'a>,
    ) {
        match expr {
            // "!!a" => "a"
            Expression::UnaryExpression(u1) if u1.operator.is_not() => {
                if let Expression::UnaryExpression(u2) = &mut u1.argument
                    && u2.operator.is_not()
                {
                    let mut e = u2.argument.take_in(ctx);
                    Self::minimize_expression_in_boolean_context(&mut e, ctx);
                    ctx.replace_expression(expr, e);
                }
            }
            Expression::BinaryExpression(e)
                if e.operator.is_equality()
                    && e.right.is_number_0()
                    && e.left.is_int32_or_uint32(ctx) =>
            {
                let argument = e.left.take_in(ctx);
                let new_expr = if matches!(
                    e.operator,
                    BinaryOperator::StrictInequality | BinaryOperator::Inequality
                ) {
                    // `if ((a | b) !== 0)` -> `if (a | b);`
                    argument
                } else {
                    // `if ((a | b) === 0);", "if (!(a | b));")`
                    Expression::new_unary_expression(
                        e.span,
                        UnaryOperator::LogicalNot,
                        argument,
                        ctx,
                    )
                };
                ctx.replace_expression(expr, new_expr);
            }
            // "if (!!a && !!b)" => "if (a && b)"
            Expression::LogicalExpression(e) if e.operator.is_and() => {
                Self::minimize_expression_in_boolean_context(&mut e.left, ctx);
                Self::minimize_expression_in_boolean_context(&mut e.right, ctx);
                // "if (anything && truthyNoSideEffects)" => "if (anything)"
                if e.right.get_side_free_boolean_value(ctx) == Some(true) {
                    let new_expr = e.left.take_in(ctx);
                    ctx.replace_expression(expr, new_expr);
                }
            }
            // "if (!!a ||!!b)" => "if (a || b)"
            Expression::LogicalExpression(e) if e.operator == LogicalOperator::Or => {
                Self::minimize_expression_in_boolean_context(&mut e.left, ctx);
                Self::minimize_expression_in_boolean_context(&mut e.right, ctx);
                // "if (anything || falsyNoSideEffects)" => "if (anything)"
                if e.right.get_side_free_boolean_value(ctx) == Some(false) {
                    let new_expr = e.left.take_in(ctx);
                    ctx.replace_expression(expr, new_expr);
                }
            }
            Expression::ConditionalExpression(e) => {
                // "if (a ? !!b : !!c)" => "if (a ? b : c)"
                Self::minimize_expression_in_boolean_context(&mut e.consequent, ctx);
                Self::minimize_expression_in_boolean_context(&mut e.alternate, ctx);
                if let Some(boolean) = e.consequent.get_side_free_boolean_value(ctx) {
                    let right = e.alternate.take_in(ctx);
                    let left = e.test.take_in(ctx);
                    let span = e.span;
                    let (op, left) = if boolean {
                        // "if (anything1 ? truthyNoSideEffects : anything2)" => "if (anything1 || anything2)"
                        (LogicalOperator::Or, left)
                    } else {
                        // "if (anything1 ? falsyNoSideEffects : anything2)" => "if (!anything1 && anything2)"
                        (LogicalOperator::And, Self::minimize_not(left.span(), left, ctx))
                    };
                    let new_expr = Self::join_with_left_associative_op(span, op, left, right, ctx);
                    ctx.replace_expression(expr, new_expr);
                    return;
                }
                if let Some(boolean) = e.alternate.get_side_free_boolean_value(ctx) {
                    let left = e.test.take_in(ctx);
                    let right = e.consequent.take_in(ctx);
                    let span = e.span;
                    let (op, left) = if boolean {
                        // "if (anything1 ? anything2 : truthyNoSideEffects)" => "if (!anything1 || anything2)"
                        (LogicalOperator::Or, Self::minimize_not(left.span(), left, ctx))
                    } else {
                        // "if (anything1 ? anything2 : falsyNoSideEffects)" => "if (anything1 && anything2)"
                        (LogicalOperator::And, left)
                    };
                    let new_expr = Self::join_with_left_associative_op(span, op, left, right, ctx);
                    ctx.replace_expression(expr, new_expr);
                }
            }
            Expression::SequenceExpression(seq_expr) => {
                if let Some(last) = seq_expr.expressions.last_mut() {
                    Self::minimize_expression_in_boolean_context(last, ctx);
                }
            }
            // A binding that is provably falsy in boolean context (a write-once
            // falsy `var` whose constant was withheld from value-context folding,
            // e.g. a bundled `var hydrating = false` flag) folds to `false` here,
            // where `undefined`-before-init is indistinguishable from the falsy
            // init. The existing `if (false)` dead-code pass removes the branch.
            Expression::Identifier(ident) => {
                let reference_id = ident.reference_id();
                let span = ident.span;
                if let Some(symbol_id) = ctx.scoping().get_reference(reference_id).symbol_id()
                    && ctx
                        .state
                        .symbol_values
                        .get_symbol_value(symbol_id)
                        .is_some_and(|sv| sv.boolean_falsy)
                {
                    let new_expr = Expression::new_boolean_literal(span, false, ctx);
                    ctx.replace_expression(expr, new_expr);
                }
            }
            _ => {}
        }
    }
}
