// Small, safe expression evaluator — a Go port of web/src/lib/expr.ts.
//
// Supports numbers, booleans (true/false → 1/0), arithmetic (+ - * / %),
// comparison (< <= > >= == !=), boolean (&& || !), ternary (a ? b : c),
// parentheses, and a handful of math functions. Two kinds of variable
// reference are resolved live at evaluation time:
//
//	{ds:name}   a data-source signal value (the brace content is split on the
//	            FIRST ':' so EPICS PV names like "MY:PV:NAME" work).
//	bareIdent   a graph-local state variable (data source "local").
//
// Booleans are represented as numbers: comparisons / logical ops yield 1 or 0,
// and any nonzero value is truthy. The evaluator never uses reflection or eval;
// it walks a parsed AST against a caller-supplied Resolver.
package controllogic

import (
	"fmt"
	"math"
	"strconv"
	"strings"
	"sync"
)

// Resolver returns the current numeric value of a signal/local reference.
type Resolver func(ds, name string) float64

// RefLite identifies one signal/local reference read by an expression.
type RefLite struct {
	DS   string
	Name string
}

// ── AST ──────────────────────────────────────────────────────────────────────

type exprNode interface{ eval(R Resolver) float64 }

type numNode struct{ v float64 }
type sigNode struct{ ds, name string }
type varNode struct{ name string }
type unNode struct {
	op string
	a  exprNode
}
type binNode struct {
	op   string
	a, b exprNode
}
type ternNode struct{ c, a, b exprNode }
type callNode struct {
	fn   string
	args []exprNode
}

func (n numNode) eval(R Resolver) float64 { return n.v }
func (n sigNode) eval(R Resolver) float64 { return R(n.ds, n.name) }
func (n varNode) eval(R Resolver) float64 { return R("local", n.name) }
func (n unNode) eval(R Resolver) float64 {
	if n.op == "-" {
		return -n.a.eval(R)
	}
	if n.a.eval(R) == 0 {
		return 1
	}
	return 0
}
func (n ternNode) eval(R Resolver) float64 {
	if n.c.eval(R) != 0 {
		return n.a.eval(R)
	}
	return n.b.eval(R)
}
func (n callNode) eval(R Resolver) float64 {
	args := make([]float64, len(n.args))
	for i, a := range n.args {
		args[i] = a.eval(R)
	}
	return funcs[n.fn](args)
}
func (n binNode) eval(R Resolver) float64 {
	a, b := n.a.eval(R), n.b.eval(R)
	switch n.op {
	case "+":
		return a + b
	case "-":
		return a - b
	case "*":
		return a * b
	case "/":
		return a / b
	case "%":
		return math.Mod(a, b)
	case "<":
		return boolf(a < b)
	case "<=":
		return boolf(a <= b)
	case ">":
		return boolf(a > b)
	case ">=":
		return boolf(a >= b)
	case "==":
		return boolf(a == b)
	case "!=":
		return boolf(a != b)
	case "&&":
		return boolf(a != 0 && b != 0)
	case "||":
		return boolf(a != 0 || b != 0)
	}
	return math.NaN()
}

func boolf(b bool) float64 {
	if b {
		return 1
	}
	return 0
}

var funcs = map[string]func([]float64) float64{
	"abs":   func(a []float64) float64 { return math.Abs(a[0]) },
	"min":   func(a []float64) float64 { return minSlice(a) },
	"max":   func(a []float64) float64 { return maxSlice(a) },
	"sqrt":  func(a []float64) float64 { return math.Sqrt(a[0]) },
	"floor": func(a []float64) float64 { return math.Floor(a[0]) },
	"ceil":  func(a []float64) float64 { return math.Ceil(a[0]) },
	"round": func(a []float64) float64 { return math.Round(a[0]) },
	"sign":  func(a []float64) float64 { return float64(sign(a[0])) },
	"pow":   func(a []float64) float64 { return math.Pow(a[0], a[1]) },
	"log":   func(a []float64) float64 { return math.Log(a[0]) },
	"exp":   func(a []float64) float64 { return math.Exp(a[0]) },
	"sin":   func(a []float64) float64 { return math.Sin(a[0]) },
	"cos":   func(a []float64) float64 { return math.Cos(a[0]) },
}

func minSlice(a []float64) float64 {
	if len(a) == 0 {
		return math.Inf(1)
	}
	m := a[0]
	for _, x := range a[1:] {
		m = math.Min(m, x)
	}
	return m
}
func maxSlice(a []float64) float64 {
	if len(a) == 0 {
		return math.Inf(-1)
	}
	m := a[0]
	for _, x := range a[1:] {
		m = math.Max(m, x)
	}
	return m
}
func sign(x float64) int {
	switch {
	case x > 0:
		return 1
	case x < 0:
		return -1
	default:
		return 0
	}
}

// ── Tokenizer ────────────────────────────────────────────────────────────────

type tok struct {
	k string
	v string
}

func tokenize(src string) ([]tok, error) {
	var toks []tok
	two := map[string]bool{"<=": true, ">=": true, "==": true, "!=": true, "&&": true, "||": true}
	r := []rune(src)
	i := 0
	for i < len(r) {
		c := r[i]
		switch {
		case c == ' ' || c == '\t' || c == '\n' || c == '\r':
			i++
			continue
		case c == '{':
			end := -1
			for j := i + 1; j < len(r); j++ {
				if r[j] == '}' {
					end = j
					break
				}
			}
			if end < 0 {
				return nil, fmt.Errorf("unterminated { in expression")
			}
			toks = append(toks, tok{k: "sig", v: string(r[i+1 : end])})
			i = end + 1
			continue
		}
		if isDigit(c) || (c == '.' && i+1 < len(r) && isDigit(r[i+1])) {
			j := i + 1
			for j < len(r) && (isDigit(r[j]) || r[j] == '.') {
				j++
			}
			toks = append(toks, tok{k: "num", v: string(r[i:j])})
			i = j
			continue
		}
		if isIdentStart(c) {
			j := i + 1
			for j < len(r) && isIdentPart(r[j]) {
				j++
			}
			toks = append(toks, tok{k: "ident", v: string(r[i:j])})
			i = j
			continue
		}
		if i+1 < len(r) {
			pair := string(r[i : i+2])
			if two[pair] {
				toks = append(toks, tok{k: pair})
				i += 2
				continue
			}
		}
		if strings.ContainsRune("+-*/%<>!()?:,", c) {
			toks = append(toks, tok{k: string(c)})
			i++
			continue
		}
		return nil, fmt.Errorf("unexpected character %q in expression", string(c))
	}
	return toks, nil
}

func isDigit(c rune) bool      { return c >= '0' && c <= '9' }
func isIdentStart(c rune) bool { return c == '_' || (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') }
func isIdentPart(c rune) bool  { return isIdentStart(c) || isDigit(c) }

// ── Parser (recursive descent) ────────────────────────────────────────────────

type parser struct {
	toks []tok
	p    int
}

func (ps *parser) peek() (tok, bool) {
	if ps.p < len(ps.toks) {
		return ps.toks[ps.p], true
	}
	return tok{}, false
}

func (ps *parser) eat(k string) (tok, error) {
	if ps.p >= len(ps.toks) {
		return tok{}, fmt.Errorf("unexpected end of expression")
	}
	t := ps.toks[ps.p]
	if k != "" && t.k != k {
		return tok{}, fmt.Errorf("expected %q in expression", k)
	}
	ps.p++
	return t, nil
}

func parse(src string) (exprNode, error) {
	toks, err := tokenize(src)
	if err != nil {
		return nil, err
	}
	ps := &parser{toks: toks}
	root, err := ps.ternary()
	if err != nil {
		return nil, err
	}
	if ps.p < len(ps.toks) {
		return nil, fmt.Errorf("trailing tokens in expression")
	}
	return root, nil
}

func (ps *parser) primary() (exprNode, error) {
	t, ok := ps.peek()
	if !ok {
		return nil, fmt.Errorf("unexpected end of expression")
	}
	switch t.k {
	case "num":
		ps.eat("")
		v, err := strconv.ParseFloat(t.v, 64)
		if err != nil {
			return nil, fmt.Errorf("bad number %q", t.v)
		}
		return numNode{v: v}, nil
	case "sig":
		ps.eat("")
		idx := strings.IndexByte(t.v, ':')
		if idx < 0 {
			return sigNode{ds: t.v, name: ""}, nil
		}
		return sigNode{ds: t.v[:idx], name: t.v[idx+1:]}, nil
	case "ident":
		ps.eat("")
		id := t.v
		if id == "true" {
			return numNode{v: 1}, nil
		}
		if id == "false" {
			return numNode{v: 0}, nil
		}
		if nx, ok := ps.peek(); ok && nx.k == "(" {
			ps.eat("(")
			var args []exprNode
			if nx2, ok := ps.peek(); ok && nx2.k != ")" {
				a, err := ps.ternary()
				if err != nil {
					return nil, err
				}
				args = append(args, a)
				for {
					nx3, ok := ps.peek()
					if !ok || nx3.k != "," {
						break
					}
					ps.eat(",")
					a, err := ps.ternary()
					if err != nil {
						return nil, err
					}
					args = append(args, a)
				}
			}
			if _, err := ps.eat(")"); err != nil {
				return nil, err
			}
			if _, ok := funcs[id]; !ok {
				return nil, fmt.Errorf("unknown function %q", id)
			}
			return callNode{fn: id, args: args}, nil
		}
		return varNode{name: id}, nil
	case "(":
		ps.eat("(")
		e, err := ps.ternary()
		if err != nil {
			return nil, err
		}
		if _, err := ps.eat(")"); err != nil {
			return nil, err
		}
		return e, nil
	}
	return nil, fmt.Errorf("unexpected token %q in expression", t.k)
}

func (ps *parser) unary() (exprNode, error) {
	if t, ok := ps.peek(); ok && (t.k == "-" || t.k == "!") {
		ps.eat("")
		a, err := ps.unary()
		if err != nil {
			return nil, err
		}
		return unNode{op: t.k, a: a}, nil
	}
	return ps.primary()
}

func (ps *parser) binLevel(next func() (exprNode, error), ops ...string) (exprNode, error) {
	a, err := next()
	if err != nil {
		return nil, err
	}
	for {
		t, ok := ps.peek()
		if !ok || !contains(ops, t.k) {
			return a, nil
		}
		op, _ := ps.eat("")
		b, err := next()
		if err != nil {
			return nil, err
		}
		a = binNode{op: op.k, a: a, b: b}
	}
}

func (ps *parser) mul() (exprNode, error) { return ps.binLevel(ps.unary, "*", "/", "%") }
func (ps *parser) add() (exprNode, error) { return ps.binLevel(ps.mul, "+", "-") }
func (ps *parser) cmp() (exprNode, error) { return ps.binLevel(ps.add, "<", "<=", ">", ">=") }
func (ps *parser) eq() (exprNode, error)  { return ps.binLevel(ps.cmp, "==", "!=") }
func (ps *parser) and() (exprNode, error) { return ps.binLevel(ps.eq, "&&") }
func (ps *parser) or() (exprNode, error)  { return ps.binLevel(ps.and, "||") }

func (ps *parser) ternary() (exprNode, error) {
	c, err := ps.or()
	if err != nil {
		return nil, err
	}
	if t, ok := ps.peek(); ok && t.k == "?" {
		ps.eat("?")
		a, err := ps.ternary()
		if err != nil {
			return nil, err
		}
		if _, err := ps.eat(":"); err != nil {
			return nil, err
		}
		b, err := ps.ternary()
		if err != nil {
			return nil, err
		}
		return ternNode{c: c, a: a, b: b}, nil
	}
	return c, nil
}

func contains(s []string, v string) bool {
	for _, x := range s {
		if x == v {
			return true
		}
	}
	return false
}

// ── Cache + public API ─────────────────────────────────────────────────────────

type cacheEntry struct {
	node exprNode
	err  error
}

var (
	cacheMu sync.Mutex
	cache   = map[string]cacheEntry{}
)

func parseCached(src string) (exprNode, error) {
	cacheMu.Lock()
	e, ok := cache[src]
	cacheMu.Unlock()
	if ok {
		return e.node, e.err
	}
	n, err := parse(src)
	cacheMu.Lock()
	cache[src] = cacheEntry{node: n, err: err}
	cacheMu.Unlock()
	return n, err
}

// EvalExpr evaluates an expression string, returning NaN on parse/eval failure.
func EvalExpr(src string, resolve Resolver) float64 {
	n, err := parseCached(src)
	if err != nil {
		return math.NaN()
	}
	return safeEval(n, resolve)
}

func safeEval(n exprNode, resolve Resolver) (out float64) {
	defer func() {
		if recover() != nil {
			out = math.NaN()
		}
	}()
	return n.eval(resolve)
}

// EvalBool reports whether the expression evaluates to a nonzero, non-NaN value.
func EvalBool(src string, resolve Resolver) bool {
	v := EvalExpr(src, resolve)
	return !math.IsNaN(v) && v != 0
}

// CollectRefs returns every signal/local reference an expression reads, for
// subscription. Returns nil for an unparseable expression.
func CollectRefs(src string) []RefLite {
	root, err := parseCached(src)
	if err != nil {
		return nil
	}
	var out []RefLite
	seen := map[string]bool{}
	add := func(ds, name string) {
		k := ds + "\x00" + name
		if !seen[k] {
			seen[k] = true
			out = append(out, RefLite{DS: ds, Name: name})
		}
	}
	var walk func(n exprNode)
	walk = func(n exprNode) {
		switch t := n.(type) {
		case sigNode:
			add(t.ds, t.name)
		case varNode:
			add("local", t.name)
		case unNode:
			walk(t.a)
		case binNode:
			walk(t.a)
			walk(t.b)
		case ternNode:
			walk(t.c)
			walk(t.a)
			walk(t.b)
		case callNode:
			for _, a := range t.args {
				walk(a)
			}
		}
	}
	walk(root)
	return out
}

// CheckExpr validates an expression; returns an error message or "" if it parses.
func CheckExpr(src string) string {
	if strings.TrimSpace(src) == "" {
		return ""
	}
	if _, err := parse(src); err != nil {
		return err.Error()
	}
	return ""
}
