import math
import numpy as np

"""
Value class adapted from micrograd by Andrej Karpathy (MIT License).
Original source: https://github.com/karpathy/micrograd
"""

class Value:
    def __init__(self , data , _childeren = () , _op = '' , label = ''):
        self._prev = set(_childeren)
        self.grad = 0.0
        self._op = _op
        self.label = label
        self._backward = lambda :None
        self.data = data

    def __repr__(self):
        return f"Value(data = {self.data} , grad = {self.grad})"
    
    def __add__(self , other):
        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
    
    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*out.grad
            other.grad += self.data*out.grad

        out._backward = _backward
        return out
    
    def __rmul__(self , other):
        return self * other
    
    def __neg__(self):
        return self * -1
    
    def __sub__(self , other):
        return self +(-other)
    
    def __truediv__(self , other):
        return self * other **-1
    
    def __pow__(self , other):
        assert isinstance(other , (int , float)) , "only supporting int and float powers for now"
        out = Value(self.data**other , (self , ) , f"**{other}")

        def _backward():
            self.grad += other *self.data**(other -1) * out.grad
        out._backward = _backward
        return out
    
    def tanh(self):
        x = self.data
        t = (math.exp(2*x) - 1) / (math.exp(2*x) + 1)
        out = Value(t , (self , ) , 'tanh')

        def _backward():
            self.grad += (1 - t*t) * out.grad

        out._backward = _backward
        return out
    
    def exp(self):
        x = self.data
        out = Value(math.exp(x) , (self , ) , "exp")
        def _backward():
            self.grad += out.data * out.grad

        out._backward = _backward
        return out
    
    def backward(self):
        topo = []
        visited = set()

        def build_topo(v):
            if v not in visited:
                visited.add(v)
                for child in v._prev:
                    build_topo(child)
                topo.append(v)

        build_topo(self)
        # print("visited  : -  " , visited , "  ,  topo   : - " , topo)
        self.grad = 1.0

        for node in reversed(topo):
            node._backward()
                
    