# CSci 556, Multiparadigm Programming, Fall 2018
# Expression Tree Calculator; Function Module Version
# H. Conrad Cunningham

# 345678901234567890123456789012345678901234567890123456789012345678901234567890

# 2018-09-14: Develop from ExprOO.py code

# Note: This program uses None in place of invalid Tree instances in
# many casses.


def is_valid_name(name):  # any nonempty string for now
    return isinstance(name, str) and len(name) > 0
    # None must not be valid name!


def is_valid_value(value):  # any number for now
    return isinstance(value, (int, float, complex))
    # None must not be valid value!


class Tree:
    pass


class Sum(Tree):
    def __init__(self, l, r):
        self.left = l if isinstance(l, Tree) else None
        self.right = r if isinstance(r, Tree) else None

    def __repr__(self):
        return f"Sum({repr(self.left)},{repr(self.right)})"


class Var(Tree):
    def __init__(self, n):
        self.name = n if is_valid_name(n) else None

    def __repr__(self):
        return f"Var({self.name})"


class Const(Tree):
    def __init__(self, v):
        self.value = v if is_valid_value(v) else None

    def __repr__(self):
        return f"Const({self.value})"


def eval(tree, env={}):
    if isinstance(tree, Sum):
        lv = eval(tree.left, env) if tree.left else None
        rv = eval(tree.right, env) if tree.right else None
        return lv + rv if lv is not None and rv is not None else None
    elif isinstance(tree, Var):
        return env.get(tree.name)
    elif isinstance(tree, Const):
        return tree.value
    else:
        return None


def derive(tree, n):
    if isinstance(tree, Sum):
        dl = derive(tree.left, n) if tree.left else None
        dr = derive(tree.right, n) if tree.right else None
        return Sum(dl, dr) if dl and dr else None
    elif isinstance(tree, Var):
        if is_valid_name(n) and n == tree.name:
            return Const(1)
        elif tree.name is not None:
            return Const(0)
        else:
            return None
    elif isinstance(tree, Const):
        return Const(0) if tree.value is not None else None
    else:
        return None


def simplify(tree):
    if isinstance(tree, Sum):
        sl = simplify(tree.left) if tree.left else None
        sr = simplify(tree.right) if tree.right else None
        lc, rc = isinstance(sl, Const), isinstance(sr, Const)
        if lc and rc:
            return Const(sl.value + sr.value)
        elif lc and sl.value == 0:  # Additive identity
            return sr
        elif rc and sr.value == 0:  # Additive identity
            return sl
        return Sum(sl, sr) if sl and sr else None
    elif isinstance(tree, Var):
        return tree if tree.name is not None else None
    elif isinstance(tree, Const):
        return tree if tree.value is not None else None
    else:
        return None


if __name__ == "__main__":
    print("Begin smoke testing expression tree program")
    env = {"x": 5, "y": 7}

    x = Var("x")
    y = Var("y")
    z = Var("z")  # no value in env

    c0 = Const(0.0)
    c1 = Const(1.0)
    c3 = Const(3.0)
    c6 = Const(6.0)
    c7 = Const(7.0)
    cm3 = Const(-3.0)

    print(f"Expression: {c0}")
    print(f"Evaluation with x=5, y=7:\n  {eval(c0,env)}")
    print(f"Derivative relative to x:\n  {derive(c0,'x')}")
    print(f"Derivative relative to y:\n  {derive(c0,'y')}")
    print(f"Derivative relative to None:\n  {derive(c0,None)}")
    print(f"Simplification:\n  {simplify(c0)}")
    print("")

    print(f"Expression:\n  {cm3}")
    print(f"Evaluation with x=5, y=7:\n  {eval(cm3,env)}")
    print(f"Derivative relative to x:\n  {derive(cm3,f'x')}")
    print(f"Derivative relative to y:\n  {derive(cm3,f'y')}")
    print(f"Derivative relative to None:\n  {derive(cm3,None)}")
    print(f"Simplification:\n  {simplify(cm3)}")
    print("")

    x = Var("x")
    y = Var("y")
    z = Var("z")  # no value in env

    print(f"Expression:\n  {x}")
    print(f"Evaluation with x=5, y=7:\n  {eval(x,env)}")
    print(f"Derivative relative to x:\n  {derive(x,'x')}")
    print(f"Derivative relative to y:\n  {derive(x,'y')}")
    print(f"Derivative relative to None:\n  {derive(x,None)}")
    print(f"Simplification:\n  {simplify(x)}")
    print("")

    print(f"Expression:\n  {y}")
    print(f"Evaluation with x=5, y=7:\n  {eval(y,env)}")
    print(f"Derivative relative to x:\n  {derive(y,'x')}")
    print(f"Derivative relative to y:\n  {derive(y,'y')}")
    print(f"Derivative relative to None:\n  {derive(y,None)}")
    print(f"Simplification:\n  {simplify(y)}")
    print("")

    print(f"Expression:\n  {z}")
    #   Variable not in environment
    print(f"Evaluation with x=5, y=7:\n  {eval(z,env)}")
    print(f"Derivative relative to x:\n  {derive(z,'x')}")
    print(f"Derivative relative to y:\n  {derive(z,'y')}")
    print(f"Derivative relative to None:\n  {derive(x,None)}")
    print(f"Simplification:\n  {simplify(z)}")
    print("")

    s0L = Sum(c0, c3)
    s0R = Sum(c3, c0)
    s1 = Sum(c7, cm3)
    s2 = Sum(c1, y)
    s3 = Sum(x, c3)
    s4 = Sum(x, y)
    s5 = Sum(s1, s0L)
    s6 = Sum(Sum(s1, s2), Sum(s1, s4))

    print(f"Expression:\n  {s0L}")
    print(f"Evaluation with x=5, y=7:\n  {eval(s0L,env)}")
    print(f"Derivative relative to x:\n  {derive(s0L,'x')}")
    print(f"Derivative relative to y:\n  {derive(s0L,'y')}")
    print(f"Derivative relative to None:\n  {derive(s0L,None)}")
    print(f"Simplification:\n  {simplify(s0L)}")
    print("")

    print(f"Expression:\n  {s0R}")
    print(f"Evaluation with x=5, y=7:\n  {eval(s0R,env)}")
    print(f"Derivative relative to x:\n  {derive(s0R,'x')}")
    print(f"Derivative relative to y:\n  {derive(s0R,'y')}")
    print(f"Derivative relative to None:\n  {derive(s0R,None)}")
    print(f"Simplification:\n  {simplify(s0R)}")
    print(f" ")

    print(f"Expression:\n  {s1}")
    print(f"Evaluation with x=5, y=7:\n  {eval(s1,env)}")
    print(f"Derivative relative to x:\n  {derive(s1,'x')}")
    print(f"Derivative relative to y:\n  {derive(s1,'y')}")
    print(f"Derivative relative to None:\n  {derive(s1,None)}")
    print(f"Simplification:\n  {simplify(s1)}")
    print("")

    print(f"Expression:\n  {s2}")
    print(f"Evaluation with x=5, y=7:\n  {eval(s2,env)}")
    print(f"Derivative relative to x:\n  {derive(s2,'x')}")
    print(f"Derivative relative to y:\n  {derive(s2,'y')}")
    print(f"Derivative relative to None:\n  {derive(s2,None)}")
    print(f"Simplification:\n  {simplify(s2)}")
    print("")

    print(f"Expression:\n  {s3}")
    print(f"Evaluation with x=5, y=7:\n  {eval(s3,env)}")
    print(f"Derivative relative to x:\n  {derive(s3,'x')}")
    print(f"Derivative relative to y:\n  {derive(s3,'y')}")
    print(f"Derivative relative to None:\n  {derive(s3,None)}")
    print(f"Simplification:\n  {simplify(s3)}")
    print("")

    print(f"Expression:\n  {s4}")
    print(f"Evaluation with x=5, y=7:\n  {eval(s4,env)}")
    print(f"Derivative relative to x:\n  {derive(s4,'x')}")
    print(f"Derivative relative to y:\n  {derive(s4,'y')}")
    print(f"Derivative relative to None:\n  {derive(s4,None)}")
    print(f"Simplification:\n  {simplify(s4)}")
    print("")

    print(f"Expression:\n  {s5}")
    print(f"Evaluation with x=5, y=7:\n  {eval(s5,env)}")
    print(f"Derivative relative to x:\n  {derive(s5,'x')}")
    print(f"Derivative relative to y:\n  {derive(s5,'y')}")
    print(f"Derivative relative to None:\n  {derive(s5,None)}")
    print(f"Simplification:\n  {simplify(s5)}")
    print("")

    print(f"Expression:\n  {s6}")
    print(f"Evaluation with x=5, y=7:\n  {eval(s6,env)}")
    print(f"Derivative relative to x:\n  {derive(s6,'x')}")
    print(f"Derivative relative to y:\n  {derive(s6,'y')}")
    print(f"Derivative relative to None:\n  {derive(s6,None)}")
    print(f"Simplification:\n  {simplify(s6)}")
    print("")

    exp = Sum(Sum(x, x), Sum(c7, y))

    print(f"Expression:\n  {exp}")
    print(f"Evaluation with x=5, y=7:\n  {eval(exp,env)}")
    print(f"Derivative relative to x:\n  {derive(exp,'x')}")
    print(f"Derivative relative to y:\n  {derive(exp,'y')}")
    print(f"Derivative relative to None:\n  {derive(exp,None)}")
    print(f"Simplification:\n  {simplify(exp)}")
    print("")

    exp2 = Sum(Sum(Const(0), Const(0)), Sum(Const(0), Const(1)))

    print(f"Expression:\n  {exp2}")
    print(f"Evaluation with x=5, y=7:\n  {eval(exp2,env)}")
    print(f"Derivative relative to x:\n  {derive(exp2,'x')}")
    print(f"Derivative relative to y:\n  {derive(exp2,'y')}")
    print(f"Derivative relative to None:\n  {derive(exp2,None)}")
    print(f"Simplification:\n  {simplify(exp2)}")
    print("")

    n1 = Const(None)
    n2 = Var(None)
    n3 = Sum(None, None)

    print(f"Expression:\n  {n1}")
    print(f"Evaluation with x=5, y=7:\n  {eval(n1,env)}")
    print(f"Derivative relative to x:\n  {derive(n1,'x')}")
    print(f"Derivative relative to y:\n  {derive(n1,'y')}")
    print(f"Derivative relative to None:\n  {derive(n1,None)}")
    print(f"Simplification:\n  {simplify(n1)}")
    print("")

    print(f"Expression:\n  {n2}")
    print(f"Evaluation with x=5, y=7:\n  {eval(n2,env)}")
    print(f"Derivative relative to x:\n  {derive(n2,'x')}")
    print(f"Derivative relative to y:\n  {derive(n2,'y')}")
    print(f"Derivative relative to None:\n  {derive(n2,None)}")
    print(f"Simplification:\n  {simplify(n2)}")
    print("")

    print(f"Expression:\n  {n3}")
    print(f"Evaluation with x=5, y=7:\n  {eval(n3,env)}")
    print(f"Derivative relative to x:\n  {derive(n3,'x')}")
    print(f"Derivative relative to y:\n  {derive(n3,'y')}")
    print(f"Derivative relative to None:\n  {derive(n3,None)}")
    print(f"Simplification:\n  {simplify(n3)}")
    print("")
