use std::borrow::Cow;
use swc_common::{
    pass::{CompilerPass, Repeated},
    util::take::Take,
};
use swc_ecma_ast::*;
use swc_ecma_transforms_base::{pass::RepeatedJsPass, scope::IdentType};
use swc_ecma_utils::{contains_this_expr, find_pat_ids, undefined};
use swc_ecma_visit::{
    as_folder, noop_visit_mut_type, noop_visit_type, visit_obj_and_computed, Visit, VisitMut,
    VisitMutWith, VisitWith,
};
use tracing::{span, Level};
use self::scope::{Scope, ScopeKind, VarType};
mod scope;
#[derive(Debug, Default)]
pub struct Config {}
pub fn inlining(_: Config) -> impl 'static + RepeatedJsPass + VisitMut {
    as_folder(Inlining {
        phase: Phase::Analysis,
        is_first_run: true,
        changed: false,
        scope: Default::default(),
        var_decl_kind: VarDeclKind::Var,
        ident_type: IdentType::Ref,
        in_test: false,
        pat_mode: PatFoldingMode::VarDecl,
        pass: Default::default(),
    })
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum Phase {
    Analysis,
    Inlining,
}
impl CompilerPass for Inlining<'_> {
    fn name() -> Cow<'static, str> {
        Cow::Borrowed("inlining")
    }
}
impl Repeated for Inlining<'_> {
    fn changed(&self) -> bool {
        self.changed
    }
    fn reset(&mut self) {
        self.changed = false;
        self.is_first_run = false;
        self.pass += 1;
    }
}
struct Inlining<'a> {
    phase: Phase,
    is_first_run: bool,
    changed: bool,
    scope: Scope<'a>,
    var_decl_kind: VarDeclKind,
    ident_type: IdentType,
    in_test: bool,
    pat_mode: PatFoldingMode,
    pass: usize,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum PatFoldingMode {
    Assign,
    Param,
    CatchParam,
    VarDecl,
}
impl Inlining<'_> {
    fn visit_with_child<T>(&mut self, kind: ScopeKind, node: &mut T)
    where
        T: 'static + for<'any> VisitMutWith<Inlining<'any>>,
    {
        self.with_child(kind, |child| {
            node.visit_mut_children_with(child);
        });
    }
}
impl VisitMut for Inlining<'_> {
    noop_visit_mut_type!();
    fn visit_mut_arrow_expr(&mut self, node: &mut ArrowExpr) {
        self.visit_with_child(ScopeKind::Fn { named: false }, node)
    }
    fn visit_mut_assign_expr(&mut self, e: &mut AssignExpr) {
        tracing::trace!("{:?}; Fold<AssignExpr>", self.phase);
        self.pat_mode = PatFoldingMode::Assign;
        e.left.map_with_mut(|n| n.normalize_expr());
        match e.op {
            op!("=") => {
                let mut v = WriteVisitor {
                    scope: &mut self.scope,
                };
                e.left.visit_with(&mut v);
                e.right.visit_with(&mut v);
                match &mut e.left {
                    PatOrExpr::Expr(left) => {
                        if let Expr::Member(ref left) = &**left {
                            tracing::trace!("Assign to member expression!");
                            let mut v = IdentListVisitor {
                                scope: &mut self.scope,
                            };
                            left.visit_with(&mut v);
                            e.right.visit_with(&mut v);
                        }
                    }
                    PatOrExpr::Pat(p) => {
                        p.visit_mut_with(self);
                    }
                }
            }
            _ => {
                let mut v = IdentListVisitor {
                    scope: &mut self.scope,
                };
                e.left.visit_with(&mut v);
                e.right.visit_with(&mut v)
            }
        }
        e.right.visit_mut_with(self);
        if self.scope.is_inline_prevented(&e.right) {
            let ids: Vec<Id> = find_pat_ids(&e.left);
            for id in ids {
                self.scope.prevent_inline(&id);
            }
            return;
        }
        if let Some(i) = e.left.as_ident() {
            let id = i.to_id();
            self.scope.add_write(&id, false);
            if let Some(var) = self.scope.find_binding(&id) {
                if !var.is_inline_prevented() {
                    match *e.right {
                        Expr::Lit(..) | Expr::Ident(..) => {
                            *var.value.borrow_mut() = Some(*e.right.clone());
                        }
                        _ => {
                            *var.value.borrow_mut() = None;
                        }
                    }
                }
            }
        }
    }
    fn visit_mut_block_stmt(&mut self, node: &mut BlockStmt) {
        self.visit_with_child(ScopeKind::Block, node)
    }
    fn visit_mut_call_expr(&mut self, node: &mut CallExpr) {
        node.callee.visit_mut_with(self);
        if self.phase == Phase::Analysis {
            if let Callee::Expr(ref callee) = node.callee {
                self.scope.mark_this_sensitive(callee);
            }
        }
        node.args.visit_children_with(&mut WriteVisitor {
            scope: &mut self.scope,
        });
        node.args.visit_mut_with(self);
        self.scope.store_inline_barrier(self.phase);
    }
    fn visit_mut_catch_clause(&mut self, node: &mut CatchClause) {
        self.with_child(ScopeKind::Block, move |child| {
            child.pat_mode = PatFoldingMode::CatchParam;
            node.param.visit_mut_with(child);
            match child.phase {
                Phase::Analysis => {
                    let ids: Vec<Id> = find_pat_ids(&node.param);
                    for id in ids {
                        child.scope.prevent_inline(&id);
                    }
                }
                Phase::Inlining => {}
            }
            node.body.visit_mut_with(child);
        })
    }
    fn visit_mut_do_while_stmt(&mut self, node: &mut DoWhileStmt) {
        {
            node.test.visit_with(&mut IdentListVisitor {
                scope: &mut self.scope,
            });
        }
        node.test.visit_mut_with(self);
        self.visit_with_child(ScopeKind::Loop, &mut node.body);
    }
    fn visit_mut_expr(&mut self, node: &mut Expr) {
        node.visit_mut_children_with(self);
        if self.phase == Phase::Inlining {
            if let Expr::Assign(e @ AssignExpr { op: op!("="), .. }) = node {
                if let Some(i) = e.left.as_ident() {
                    if let Some(var) = self.scope.find_binding_from_current(&i.to_id()) {
                        if var.is_undefined.get()
                            && !var.is_inline_prevented()
                            && !self.scope.is_inline_prevented(&e.right)
                        {
                            *var.value.borrow_mut() = Some(*e.right.clone());
                            var.is_undefined.set(false);
                            *node = *e.right.take();
                            return;
                        }
                    }
                }
                return;
            }
        }
        if let Expr::Ident(ref i) = node {
            let id = i.to_id();
            if self.is_first_run {
                if let Some(expr) = self.scope.find_constant(&id) {
                    self.changed = true;
                    let mut expr = expr.clone();
                    expr.visit_mut_with(self);
                    *node = expr;
                    return;
                }
            }
            match self.phase {
                Phase::Analysis => {
                    if self.in_test {
                        if let Some(var) = self.scope.find_binding(&id) {
                            match &*var.value.borrow() {
                                Some(Expr::Ident(..)) | Some(Expr::Lit(..)) => {}
                                _ => {
                                    self.scope.prevent_inline(&id);
                                }
                            }
                        }
                    }
                    self.scope.add_read(&id);
                }
                Phase::Inlining => {
                    tracing::trace!("Trying to inline: {:?}", id);
                    let expr = if let Some(var) = self.scope.find_binding(&id) {
                        tracing::trace!("VarInfo: {:?}", var);
                        if !var.is_inline_prevented() {
                            let expr = var.value.borrow();
                            if let Some(expr) = &*expr {
                                tracing::debug!("Inlining: {:?}", id);
                                if *node != *expr {
                                    self.changed = true;
                                }
                                Some(expr.clone())
                            } else {
                                tracing::debug!("Inlining: {:?} as undefined", id);
                                if var.is_undefined.get() {
                                    *node = *undefined(i.span);
                                    return;
                                } else {
                                    tracing::trace!("Not a cheap expression");
                                    None
                                }
                            }
                        } else {
                            tracing::trace!("Inlining is prevented");
                            None
                        }
                    } else {
                        None
                    };
                    if let Some(expr) = expr {
                        *node = expr;
                    }
                }
            }
        }
        if let Expr::Bin(b) = node {
            match b.op {
                BinaryOp::LogicalAnd | BinaryOp::LogicalOr => {
                    self.visit_with_child(ScopeKind::Cond, &mut b.right);
                }
                _ => {}
            }
        }
    }
    fn visit_mut_fn_decl(&mut self, node: &mut FnDecl) {
        if self.phase == Phase::Analysis {
            self.declare(
                node.ident.to_id(),
                None,
                true,
                VarType::Var(VarDeclKind::Var),
            );
        }
        self.with_child(ScopeKind::Fn { named: true }, |child| {
            child.pat_mode = PatFoldingMode::Param;
            node.function.params.visit_mut_with(child);
            match &mut node.function.body {
                None => {}
                Some(v) => {
                    v.visit_mut_children_with(child);
                }
            };
        });
    }
    fn visit_mut_fn_expr(&mut self, node: &mut FnExpr) {
        if let Some(ref ident) = node.ident {
            self.scope.add_write(&ident.to_id(), true);
        }
        node.function.visit_mut_with(self)
    }
    fn visit_mut_for_in_stmt(&mut self, node: &mut ForInStmt) {
        self.pat_mode = PatFoldingMode::Param;
        node.left.visit_mut_with(self);
        {
            node.left.visit_with(&mut IdentListVisitor {
                scope: &mut self.scope,
            });
        }
        {
            node.right.visit_with(&mut IdentListVisitor {
                scope: &mut self.scope,
            });
        }
        node.right.visit_mut_with(self);
        self.visit_with_child(ScopeKind::Loop, &mut node.body);
    }
    fn visit_mut_for_of_stmt(&mut self, node: &mut ForOfStmt) {
        self.pat_mode = PatFoldingMode::Param;
        node.left.visit_mut_with(self);
        {
            node.left.visit_with(&mut IdentListVisitor {
                scope: &mut self.scope,
            });
        }
        {
            node.right.visit_with(&mut IdentListVisitor {
                scope: &mut self.scope,
            });
        }
        node.right.visit_mut_with(self);
        self.visit_with_child(ScopeKind::Loop, &mut node.body);
    }
    fn visit_mut_for_stmt(&mut self, node: &mut ForStmt) {
        {
            node.init.visit_with(&mut IdentListVisitor {
                scope: &mut self.scope,
            });
        }
        {
            node.test.visit_with(&mut IdentListVisitor {
                scope: &mut self.scope,
            });
        }
        {
            node.update.visit_with(&mut IdentListVisitor {
                scope: &mut self.scope,
            });
        }
        node.init.visit_mut_with(self);
        node.test.visit_mut_with(self);
        node.update.visit_mut_with(self);
        self.visit_with_child(ScopeKind::Loop, &mut node.body);
        if node.init.is_none() && node.test.is_none() && node.update.is_none() {
            self.scope.store_inline_barrier(self.phase);
        }
    }
    fn visit_mut_function(&mut self, node: &mut Function) {
        self.with_child(ScopeKind::Fn { named: false }, move |child| {
            child.pat_mode = PatFoldingMode::Param;
            node.params.visit_mut_with(child);
            match &mut node.body {
                None => None,
                Some(v) => {
                    v.visit_mut_children_with(child);
                    Some(())
                }
            };
        })
    }
    fn visit_mut_if_stmt(&mut self, stmt: &mut IfStmt) {
        let old_in_test = self.in_test;
        self.in_test = true;
        stmt.test.visit_mut_with(self);
        self.in_test = old_in_test;
        self.visit_with_child(ScopeKind::Cond, &mut stmt.cons);
        self.visit_with_child(ScopeKind::Cond, &mut stmt.alt);
    }
    fn visit_mut_module_items(&mut self, items: &mut Vec<ModuleItem>) {
        let _tracing = span!(Level::ERROR, "inlining", pass = self.pass).entered();
        let old_phase = self.phase;
        self.phase = Phase::Analysis;
        items.visit_mut_children_with(self);
        tracing::trace!("Switching to Inlining phase");
        self.phase = Phase::Inlining;
        items.visit_mut_children_with(self);
        self.phase = old_phase;
    }
    fn visit_mut_new_expr(&mut self, node: &mut NewExpr) {
        node.callee.visit_mut_with(self);
        if self.phase == Phase::Analysis {
            self.scope.mark_this_sensitive(&node.callee);
        }
        node.args.visit_mut_with(self);
        self.scope.store_inline_barrier(self.phase);
    }
    fn visit_mut_pat(&mut self, node: &mut Pat) {
        node.visit_mut_children_with(self);
        if let Pat::Ident(ref i) = node {
            match self.pat_mode {
                PatFoldingMode::Param => {
                    self.declare(
                        i.to_id(),
                        Some(Cow::Owned(Expr::Ident(i.id.clone()))),
                        false,
                        VarType::Param,
                    );
                }
                PatFoldingMode::CatchParam => {
                    self.declare(
                        i.to_id(),
                        Some(Cow::Owned(Expr::Ident(i.id.clone()))),
                        false,
                        VarType::Var(VarDeclKind::Var),
                    );
                }
                PatFoldingMode::VarDecl => {}
                PatFoldingMode::Assign => {
                    if let Some(..) = self.scope.find_binding_from_current(&i.to_id()) {
                    } else {
                        self.scope.add_write(&i.to_id(), false);
                    }
                }
            }
        }
    }
    fn visit_mut_stmts(&mut self, items: &mut Vec<Stmt>) {
        let old_phase = self.phase;
        match old_phase {
            Phase::Analysis => {
                items.visit_mut_children_with(self);
            }
            Phase::Inlining => {
                self.phase = Phase::Analysis;
                items.visit_mut_children_with(self);
                self.phase = Phase::Inlining;
                items.visit_mut_children_with(self);
                self.phase = old_phase
            }
        }
    }
    fn visit_mut_switch_case(&mut self, node: &mut SwitchCase) {
        self.visit_with_child(ScopeKind::Block, node)
    }
    fn visit_mut_try_stmt(&mut self, node: &mut TryStmt) {
        node.block.visit_with(&mut IdentListVisitor {
            scope: &mut self.scope,
        });
        node.handler.visit_mut_with(self)
    }
    fn visit_mut_unary_expr(&mut self, node: &mut UnaryExpr) {
        if let op!("delete") = node.op {
            let mut v = IdentListVisitor {
                scope: &mut self.scope,
            };
            node.arg.visit_with(&mut v);
            return;
        }
        node.visit_mut_children_with(self)
    }
    fn visit_mut_update_expr(&mut self, node: &mut UpdateExpr) {
        let mut v = IdentListVisitor {
            scope: &mut self.scope,
        };
        node.arg.visit_with(&mut v);
    }
    fn visit_mut_var_decl(&mut self, decl: &mut VarDecl) {
        self.var_decl_kind = decl.kind;
        decl.visit_mut_children_with(self)
    }
    fn visit_mut_var_declarator(&mut self, node: &mut VarDeclarator) {
        let kind = VarType::Var(self.var_decl_kind);
        node.init.visit_mut_with(self);
        self.pat_mode = PatFoldingMode::VarDecl;
        match self.phase {
            Phase::Analysis => {
                if let Pat::Ident(ref name) = node.name {
                    match &node.init {
                        None => {
                            if self.var_decl_kind != VarDeclKind::Const {
                                self.declare(name.to_id(), None, true, kind);
                            }
                        }
                        Some(e)
                            if (e.is_lit() || e.is_ident())
                                && self.var_decl_kind == VarDeclKind::Const =>
                        {
                            if self.is_first_run {
                                self.scope
                                    .constants
                                    .insert(name.to_id(), Some((**e).clone()));
                            }
                        }
                        Some(..) if self.var_decl_kind == VarDeclKind::Const => {
                            if self.is_first_run {
                                self.scope.constants.insert(name.to_id(), None);
                            }
                        }
                        Some(e) | Some(e) if e.is_lit() || e.is_ident() => {
                            self.declare(name.to_id(), Some(Cow::Borrowed(e)), false, kind);
                            if self.scope.is_inline_prevented(e) {
                                self.scope.prevent_inline(&name.to_id());
                            }
                        }
                        Some(ref e) => {
                            if self.var_decl_kind != VarDeclKind::Const {
                                self.declare(name.to_id(), Some(Cow::Borrowed(e)), false, kind);
                                if contains_this_expr(&node.init) {
                                    self.scope.prevent_inline(&name.to_id());
                                    return;
                                }
                            }
                        }
                    }
                }
            }
            Phase::Inlining => {
                if let Pat::Ident(ref name) = node.name {
                    if self.var_decl_kind != VarDeclKind::Const {
                        let id = name.to_id();
                        tracing::trace!("Trying to optimize variable declaration: {:?}", id);
                        if self
                            .scope
                            .is_inline_prevented(&Expr::Ident(name.id.clone()))
                            || !self.scope.has_same_this(&id, node.init.as_deref())
                        {
                            tracing::trace!("Inline is prevented for {:?}", id);
                            return;
                        }
                        let mut init = node.init.take();
                        init.visit_mut_with(self);
                        tracing::trace!("\tInit: {:?}", init);
                        if let Some(init) = &init {
                            if let Expr::Ident(ri) = &**init {
                                self.declare(
                                    name.to_id(),
                                    Some(Cow::Owned(Expr::Ident(ri.clone()))),
                                    false,
                                    kind,
                                );
                            }
                        }
                        if let Some(ref e) = init {
                            if self.scope.is_inline_prevented(e) {
                                tracing::trace!(
                                    "Inlining is not possible as inline of the initialization was \
                                     prevented"
                                );
                                node.init = init;
                                self.scope.prevent_inline(&name.to_id());
                                return;
                            }
                        }
                        let e = match init {
                            None => None,
                            Some(e) if e.is_lit() || e.is_ident() => Some(e),
                            Some(e) => {
                                let e = *e;
                                if self
                                    .scope
                                    .is_inline_prevented(&Expr::Ident(name.id.clone()))
                                {
                                    node.init = Some(Box::new(e));
                                    return;
                                }
                                if let Some(cnt) = self.scope.read_cnt(&name.to_id()) {
                                    if cnt == 1 {
                                        Some(Box::new(e))
                                    } else {
                                        node.init = Some(Box::new(e));
                                        return;
                                    }
                                } else {
                                    node.init = Some(Box::new(e));
                                    return;
                                }
                            }
                        };
                        self.declare(name.to_id(), e.map(|e| Cow::Owned(*e)), false, kind);
                        return;
                    }
                }
            }
        }
        node.name.visit_mut_with(self);
    }
    fn visit_mut_while_stmt(&mut self, node: &mut WhileStmt) {
        {
            node.test.visit_with(&mut IdentListVisitor {
                scope: &mut self.scope,
            });
        }
        node.test.visit_mut_with(self);
        self.visit_with_child(ScopeKind::Loop, &mut node.body);
    }
}
#[derive(Debug)]
struct IdentListVisitor<'a, 'b> {
    scope: &'a mut Scope<'b>,
}
impl Visit for IdentListVisitor<'_, '_> {
    noop_visit_type!();
    visit_obj_and_computed!();
    fn visit_ident(&mut self, node: &Ident) {
        self.scope.add_write(&node.to_id(), true);
    }
}
struct WriteVisitor<'a, 'b> {
    scope: &'a mut Scope<'b>,
}
impl Visit for WriteVisitor<'_, '_> {
    noop_visit_type!();
    visit_obj_and_computed!();
    fn visit_ident(&mut self, node: &Ident) {
        self.scope.add_write(&node.to_id(), false);
    }
}