"""
An automatic differentiation engine.

Adapted from an implementation by NAME_1.
"""
import numpy as np
import collections.abc as collections


class Value:
    """
    Represents a single scalar value and its gradient.
    """
    def __init__(self, data, _children=()):
        self.data = data
        self.grad = 0
        
        # internal variables used for autograd graph construction
        self._backward = lambda: None
        self._prev = set(_children) if isinstance(_children, collections.Iterable) else {_children}

    
    def __add__(self, other):
        """ self + other """
        #more here
        other = other if isinstance(other, Value) else Value(other)
        out = Value(self.data + other.data, (self, other))

        def _backward():
            self.grad += 1.0 * out.grad
            other.grad += 1.0 * out.grad
        out._backward = _backward

        return out
    
    ######### SECTION OF CODE YOU NEED TO IMPLEMENT #########
    
    def __mul__(self, other):
        other = other if isinstance(other, Value) else Value(other)
        out = Value(self.data * other.data, (self, other))

        def _backward():
            self.grad *= other.data
            other.grad *= self.data
        out._backward = _backward
            
        return out

    def __pow__(self, other):
        """ self ** other """
        other = other if isinstance(other, Value) else Value(other)
        out = Value(self.data ** other.data, (self, other))

        def _backward():
            self.grad *= other.data * (self.data ** (other.data - 1))
            other.grad *= self.data ** other.data * np.log(self.data)
        out._backward = _backward
        return out
    
    def exp(self):
        """ e^(self) """
        # other = other if isinstance(other, Value) else Value(other)
        out = Value(np.exp(self.data), (self))

        def _backward():
            self.grad *= self.data
            # other.grad *= np.exp(other.data)
        out._backward = _backward
        return out
    
    def log(self):
        """ log(self) (natural logarithm) """
        out = Value(np.log(self.data), (self))

        def _backward():
            self.grad *= 1.0 / self.data
        out._backward = _backward
        return out
    
    ################## END SECTION ##################

    def backward(self):
        """ Runs backpropagation to compute gradients of all nodes. """
        # topological order all of the children in the graph
        topo = []
        visited = set()
        def build_to