#!/usr/bin/env python3
"""
expr_tree.py — Expression Tree Data Structure for SilverSight Symbolic Regression

Expression trees are the core data structure for representing mathematical
expressions. Each node is either:
  - A leaf: variable 'x' or a constant float
  - An internal node: unary operator (sin, cos, log, exp, sqrt, neg)
                   or binary operator (+, -, *, /, pow)

Operations:
  - evaluate(xs): compute expression value for input array
  - to_string(): human-readable expression
  - tree_size(): node count
  - tree_depth(): max depth
  - copy(): deep copy
"""

from __future__ import annotations

import math
import numpy as np
from dataclasses import dataclass, field
from typing import Optional, List


# ── Operator definitions ────────────────────────────────────────────────

UNARY_OPS = {
    "sin":  (np.sin,  "sin"),
    "cos":  (np.cos,  "cos"),
    "log":  (np.log,  "log"),    # safe log: |x|
    "exp":  (np.exp,  "exp"),
    "sqrt": (np.sqrt, "sqrt"),   # safe sqrt: |x|
    "neg":  (np.negative, "neg"),
    "abs":  (np.abs,  "abs"),
}

BINARY_OPS = {
    "+":   (np.add,      "+"),
    "-":   (np.subtract, "-"),
    "*":   (np.multiply, "*"),
    "/":   (np.divide,   "/"),    # safe div: x/y with floor
    "pow": (np.power,    "pow"),  # safe pow: |x|^y
}

# Cost weights for BIC complexity
OP_COST = {
    "x": 1, "const": 3,
    "sin": 2, "cos": 2, "log": 2, "exp": 2, "sqrt": 2, "neg": 1, "abs": 1,
    "+": 1, "-": 1, "*": 1, "/": 2, "pow": 3,
}


# ── Expression Node ─────────────────────────────────────────────────────

@dataclass
class ExprNode:
    """A node in an expression tree."""
    op: str                                   # operator name
    left: Optional['ExprNode'] = None         # left child (or unary child)
    right: Optional['ExprNode'] = None        # right child (None for unary/leaf)
    value: Optional[float] = None             # constant value (for const leaves)
    depth: int = 0                            # max depth from this node
    size: int = 0                             # total node count from this node

    def evaluate(self, xs: np.ndarray) -> np.ndarray:
        """Evaluate expression on input array."""
        if self.op == "x":
            return xs.copy()
        elif self.op == "const":
            return np.full_like(xs, self.value, dtype=np.float64)
        elif self.op in UNARY_OPS:
            child_val = self.left.evaluate(xs)
            if self.op == "log":
                return np.log(np.abs(child_val) + 1e-15)
            elif self.op == "sqrt":
                return np.sqrt(np.abs(child_val))
            elif self.op == "neg":
                return -child_val
            elif self.op == "abs":
                return np.abs(child_val)
            else:
                return UNARY_OPS[self.op][0](child_val)
        elif self.op in BINARY_OPS:
            left_val = self.left.evaluate(xs)
            right_val = self.right.evaluate(xs)
            if self.op == "/":
                return left_val / np.where(np.abs(right_val) < 1e-15, 1.0, right_val)
            elif self.op == "pow":
                return np.power(np.abs(left_val), np.clip(right_val, -10, 10))
            else:
                return BINARY_OPS[self.op][0](left_val, right_val)
        else:
            raise ValueError(f"Unknown op: {self.op}")

    def to_string(self) -> str:
        """Human-readable expression string."""
        if self.op == "x":
            return "x"
        elif self.op == "const":
            if self.value == int(self.value) and abs(self.value) < 100:
                return str(int(self.value))
            return f"{self.value:.4g}"
        elif self.op in UNARY_OPS:
            child_str = self.left.to_string()
            if self.op == "neg":
                return f"(-{child_str})"
            return f"{self.op}({child_str})"
        elif self.op in BINARY_OPS:
            left_str = self.left.to_string()
            right_str = self.right.to_string()
            return f"({left_str} {self.op} {right_str})"
        return f"?{self.op}?"

    def update_stats(self) -> None:
        """Recompute depth and size from this node down."""
        if self.left is None and self.right is None:
            self.depth = 0
            self.size = 1
        elif self.right is None:
            self.left.update_stats()
            self.depth = self.left.depth + 1
            self.size = self.left.size + 1
        else:
            self.left.update_stats()
            self.right.update_stats()
            self.depth = max(self.left.depth, self.right.depth) + 1
            self.size = self.left.size + self.right.size + 1

    def copy(self) -> ExprNode:
        """Deep copy of this subtree."""
        node = ExprNode(op=self.op, value=self.value)
        if self.left is not None:
            node.left = self.left.copy()
        if self.right is not None:
            node.right = self.right.copy()
        node.depth = self.depth
        node.size = self.size
        return node

    def complexity(self) -> int:
        """Weighted complexity for BIC (variables=1, constants=3, ops=cost)."""
        cost = OP_COST.get(self.op, 1)
        if self.left is not None:
            cost += self.left.complexity()
        if self.right is not None:
            cost += self.right.complexity()
        return cost


# ── Factory functions ────────────────────────────────────────────────────

def var_x() -> ExprNode:
    """Variable leaf: x"""
    return ExprNode(op="x", depth=0, size=1)

def const(v: float) -> ExprNode:
    """Constant leaf."""
    return ExprNode(op="const", value=v, depth=0, size=1)

def unary(op: str, child: ExprNode) -> ExprNode:
    """Unary operator node."""
    node = ExprNode(op=op, left=child)
    node.update_stats()
    return node

def binary(op: str, left: ExprNode, right: ExprNode) -> ExprNode:
    """Binary operator node."""
    node = ExprNode(op=op, left=left, right=right)
    node.update_stats()
    return node


# ── Common expressions ──────────────────────────────────────────────────

def x_squared() -> ExprNode:
    """x²"""
    return binary("*", var_x(), var_x())

def x_cubed() -> ExprNode:
    """x³"""
    return binary("*", x_squared(), var_x())

def sqrt_x() -> ExprNode:
    """√x"""
    return unary("sqrt", var_x())

def x_sqrt_x() -> ExprNode:
    """x·√x = x^1.5"""
    return binary("*", var_x(), sqrt_x())

def sin_x() -> ExprNode:
    """sin(x)"""
    return unary("sin", var_x())

def cos_x() -> ExprNode:
    """cos(x)"""
    return unary("cos", var_x())

def exp_neg_x2() -> ExprNode:
    """exp(-x²)"""
    return unary("exp", binary("*", const(-1.0), x_squared()))
