package analytics

// formulaDivisionAnalysis records whether each AST subtree contains a
// division. It is populated bottom-up once per distinct node, making the
// nested-DECIMAL-fence decision O(1) while the compiler walks the tree.
type formulaDivisionAnalysis struct {
	contains map[Node]bool
}

// formulaCompileWork is an opt-in package-private test probe for guarding the
// single-pass AST analysis/emission invariant without exposing compiler controls.
type formulaCompileWork struct {
	analyzedNodes int
	compiledNodes int
}

func newFormulaDivisionAnalysis() *formulaDivisionAnalysis {
	return &formulaDivisionAnalysis{contains: make(map[Node]bool)}
}

func (a *formulaDivisionAnalysis) ensure(node Node, work *formulaCompileWork) {
	if a == nil || node == nil {
		return
	}
	if _, ok := a.contains[node]; ok {
		return
	}
	a.analyze(node, work)
}

func (a *formulaDivisionAnalysis) containsDivision(nodes ...Node) bool {
	for _, node := range nodes {
		if node != nil && a.contains[node] {
			return true
		}
	}
	return false
}

func (a *formulaDivisionAnalysis) analyze(node Node, work *formulaCompileWork) bool {
	if node == nil {
		return false
	}
	if contains, ok := a.contains[node]; ok {
		return contains
	}
	if work != nil {
		work.analyzedNodes++
	}

	contains := false
	merge := func(child Node) {
		contains = a.analyze(child, work) || contains
	}

	switch n := node.(type) {
	case *BinaryOp:
		merge(n.Left)
		merge(n.Right)
		contains = n.Op == "/" || contains
	case *UnaryOp:
		merge(n.Operand)
	case *LogicalOp:
		for _, operand := range n.Operands {
			merge(operand)
		}
	case *FuncCall:
		for _, arg := range n.Args {
			merge(arg)
		}
	case *IfExpr:
		merge(n.Condition)
		merge(n.Then)
		merge(n.Else)
	case *CaseExpr:
		merge(n.Expr)
		for _, branch := range n.Branches {
			merge(branch.Value)
			merge(branch.Result)
		}
		merge(n.Default)
	}

	a.contains[node] = contains
	return contains
}
