
# import numpy as np
# import Linear,Dropout,Tanh ,Sigmoid , Relu
# class NeuralNetwork:
    

#     def __init__(self, input_size, hidden_layers, output_size, activation="ReLU", dropout_rate=0.0):
#         self.input_size = input_size
#         self.hidden_layers = hidden_layers  # قائمة بأحجام الطبقات المخفية
#         self.output_size = output_size
#         self.activation = activation
#         self.dropout_rate = dropout_rate

#         self.layers = []  # سيتم ملؤها تلقائيًا

#         # بناء الشبكة تلقائيًا
#         prev_size = input_size
#         for i, size in enumerate(hidden_layers):
#             # طبقة خطية
#             self.layers.append(Linear(prev_size, size))
#             # دالة تنشيط
#             if activation == "ReLU":
#                 self.layers.append(Relu())
#             elif activation == "Sigmoid":
#                 self.layers.append(Sigmoid())
#             elif activation == "Tanh":
#                 self.layers.append(Tanh())
#             # Dropout (اختياري)
#             if dropout_rate > 0:
#                 self.layers.append(Dropout(dropout_rate))
#             prev_size = size

#         # الطبقة النهائية
#         self.layers.append(Linear(prev_size, output_size))

#         # تهيئة الأوزان
#         self.init_weight()

#     def init_weight(self):
#         """تهيئة أوزان جميع الطبقات"""
#         for layer in self.layers:
#             if hasattr(layer, 'init_weights'):
#                 layer.init_weights()

#     def predict(self, x):
#         """التنبؤ: المرور الأمامي عبر جميع الطبقات"""
#         for layer in self.layers:
#             x = layer.forward(x)
#         return x

#     def calculate_loss(self, y_pred, y_true):
#         """حساب الخسارة — يجب أن يكون loss_fn محددًا في compile"""
#         if not hasattr(self, 'loss_fn') or self.loss_fn is None:
#             raise ValueError("Loss function not set. Call compile() first.")
#         return self.loss_fn.forward(y_pred, y_true)

#     def calculate_accuracy(self, y_pred, y_true):
#         """حساب الدقة — فقط للتصنيف"""
#         if y_pred.ndim == 2 and y_pred.shape[1] > 1:
#             y_pred_labels = np.argmax(y_pred, axis=1)
#         else:
#             y_pred_labels = (y_pred > 0.5).astype(int).flatten()
#         return np.mean(y_pred_labels == y_true)

#     def calculate_gradient(self, y_pred, y_true):
#         """حساب التدرجات — الـ backward"""
#         grad = self.loss_fn.backward(y_pred, y_true)
#         for layer in reversed(self.layers):
#             if hasattr(layer, 'backward'):
#                 grad = layer.backward(grad)

#     @property
#     def parameters(self):
#         """إرجاع جميع الأوزان والعوامل القابلة للتدريب"""
#         params = []
#         for layer in self.layers:
#             if hasattr(layer, 'W'):
#                 params.append(layer.W)
#             if hasattr(layer, 'b'):
#                 params.append(layer.b)
#         return params

#     def compile(self, loss_fn, optimizer):
#         self.loss_fn = loss_fn
#         self.optimizer = optimizer
        
#     # def __init__(self):
#     #     self.layers = []      # List of layers
#     #     self.loss_fn = None   # Loss function object
#     #     self.optimizer = None # Optimizer object

#     # def add_layer(self, layer):
#     #     self.layers.append(layer)

#     # def init_weight(self):
#     #     for layer in self.layers:
#     #         if hasattr(layer, 'init_weights'):
#     #             layer.init_weights()

#     # def predict(self, x):
#     #     for layer in self.layers:
#     #         x = layer.forward(x)
#     #     return x

#     # def loss(self, y_pred, y_true):
#     #     if self.loss_fn is None:
#     #         raise ValueError("Loss function not set. Call compile() first.")
#     #     return self.loss_fn.forward(y_pred, y_true)

#     # def accuracy(self, y_pred, y_true):
#     #     if y_pred.ndim == 2 and y_pred.shape[1] > 1:
#     #         y_pred_labels = np.argmax(y_pred, axis=1)
#     #     else:
#     #         y_pred_labels = (y_pred > 0.5).astype(int).flatten()
#     #     return np.mean(y_pred_labels == y_true)

#     # def gradient(self, y_pred, y_true):
#     #     grad = self.loss_fn.backward(y_pred, y_true)
#     #     for layer in reversed(self.layers):
#     #         if hasattr(layer, 'backward'):
#     #             grad = layer.backward(grad)

#     # @property
#     # def parameters(self):
#     #     params = []
#     #     for layer in self.layers:
#     #         if hasattr(layer, 'W'):
#     #             params.append(layer.W)
#     #         if hasattr(layer, 'b'):
#     #             params.append(layer.b)
#     #    return params


# neural_network.py
import numpy as np

class NeuralNetwork:
    """
    شبكة عصبية مرنة — تأخذ قائمة طبقات مخصصة عند الإنشاء.
    مثال:
        layers = [
            Linear(2, 4),
            ReLU(),
            Linear(4, 4),
            ReLU(),
            Linear(4, 1),
            Sigmoid()
        ]
        model = NeuralNetwork(layers)
    """

    def __init__(self, layers):
        self.layers = layers  # القائمة الكاملة للطبقات

    def predict(self, x):
        """المرور الأمامي عبر كل طبقة"""
        for layer in self.layers:
            x = layer.forward(x)
        return x

    def compile(self, loss_fn, optimizer):
        """ربط دالة الخسارة وخوارزمية التحسين"""
        self.loss_fn = loss_fn
        self.optimizer = optimizer

    def calculate_loss(self, y_pred, y_true):
        """حساب الخسارة"""
        return self.loss_fn.forward(y_pred, y_true)

    def calculate_accuracy(self, y_pred, y_true):
        """حساب الدقة — يدعم تصنيف ثنائي ومتعدد الفئات"""
        if y_pred.ndim > 1 and y_pred.shape[1] > 1:
            # تصنيف متعدد الفئات
            preds = np.argmax(y_pred, axis=1)
            labels = y_true if y_true.ndim == 1 else np.argmax(y_true, axis=1)
        else:
            # تصنيف ثنائي
            preds = (y_pred > 0.5).flatten()
            labels = y_true.flatten()
        return np.mean(preds == labels)

    @property
    def parameters(self):
        """جمع جميع الأوزان والعوامل القابلة للتدريب"""
        params = []
        for layer in self.layers:
            if hasattr(layer, 'W'):
                params.append(layer.W)
            if hasattr(layer, 'b'):
                params.append(layer.b)
        return params

    def update_parameters(self, lr):
        """تحديث أوزان الطبقات القابلة للتدريب"""
        for layer in self.layers:
            if hasattr(layer, 'update_parameters'):
                layer.update_parameters(lr)