import numpy as np


class Sequential:
    """
    Container for stacking layers sequentially.
    """

    def __init__(self, *layers):
        self.layers = list(layers)
    
    def __call__(self, x):
        return self.forward(x)
    
    def __len__(self):
        return len(self.layers)


    # Forward Pass

    def forward(self, x):

        for layer in self.layers:
            x = layer.forward(x)

        return x


    # Backward Pass

    def backward(self, grad):

        for layer in reversed(self.layers):
            grad = layer.backward(grad)

        return grad

    
    # Collect Parameters

    def parameters(self):

        params = []

        for layer in self.layers:

            for attr in dir(layer):

                if attr.startswith("d"):  
                    param_name = attr[1:]   # remove "d"
                    grad_name = attr

                    if hasattr(layer, param_name):

                        params.append(
                            (layer, param_name, grad_name)
                        )

        return params

    
    # Zero Gradients

    def zero_grad(self):

        for layer, param_name, grad_name in self.parameters():

            grad = getattr(layer, grad_name)

            grad.fill(0)


    # Training Mode

    def train(self):

        for layer in self.layers:
            if hasattr(layer, "training"):
                layer.training = True

    
    # Evaluation Mode

    def eval(self):

        for layer in self.layers:
            if hasattr(layer, "training"):
                layer.training = False


    # Add Layer Dynamically

    def add(self, layer):

        self.layers.append(layer)

    
    # Model Summary

    def summary(self):

        print("Model Summary")
        print("-" * 40)

        for i, layer in enumerate(self.layers):
            print(f"{i+1}. {layer.__class__.__name__}")

        print("-" * 40)