'''
Reverse mode automatic differentiation algorithm.

Representation - GRAPH
Build the computation graph, with variables as nodes, corresponding to graph VERTICES.
And vertices are connected by EDGES, a.k.a operators in computation context.

And the gradient are also represented as a node, thus the computation graph
only propagates FORWARD, no backward propagation, as indicated by "reverse".

In the representation, the OBJECTIVE FUNCTION can be viewed as a MULTIVARIATE FUNCTION
of nodes depending on.
And to compute the gradients is to take the full derivatives.
Full derivatives is computed by summing up all PARTIAL DERIVATIVES.

Reference
---------
https://github.com/dlsys-course/
'''
import numpy as np


class Node(object):
    """Node in a computation graph."""
    def __init__(self):
        """Constructor, new node is indirectly created by Op object __call__ method.

            Instance variables
            ------------------
            self.inputs: the list of input nodes.
            self.op: the associated op object,
                e.g. add_op object if this node is created by adding two other nodes.
            self.const_attr: the add or multiply constant,
                e.g. self.const_attr=5 if this node is created by x+5.
            self.name: node name for debugging purposes.
        """
        self.inputs = []
        self.op = None
        self.const_attr = None
        self.name = ""
        self.op_params = None # parameters passed to operator `op`

    def __add__(self, other):
        """Adding two nodes return a new node."""
        if isinstance(other, Node):
            new_node = add_op(self, other)
        else:
            # Add by a constant stores the constant in the new node's const_attr field.
            # 'other' argument is a constant
            new_node = add_byconst_op(self, other)
        return new_node

    def __mul__(self, other):
        """Multiplying two nodes return a new node"""
        # TODO: Your code here
        if isinstance(other, Node):
            new_node = mul_op(self, other)
        else:
            new_node = mul_byconst_op(self, other)

        return new_node

    def __truediv__(self, other):
        new_node = mul_op(self, other ** -1)

        return new_node

    def __pow__(self, order):
        return exp_op(order * log_op(self))

    # Allow left-hand-side add and multiply.
    __radd__ = __add__
    __rmul__ = __mul__
    __rtruediv__ = lambda self, x: mul_op(x, self ** -1)

    def __str__(self):
        """Allow print to display node name."""
        return self.name

    __repr__ = __str__

def Variable(name):
    """User defined variables in an expression.
        e.g. x = Variable(name = "x")
    """
    placeholder_node = placeholder_op()
    placeholder_node.name = name
    return placeholder_node