import numpy as np


class Loss:
    """Base class for all loss functions."""

    def forward(self, y_pred, y_true):
        raise NotImplementedError

    def backward(self):
        raise NotImplementedError



# 1. Mean Squared Error

class MeanSquaredError(Loss):

    def forward(self, y_pred, y_true):
        self.y_pred = y_pred
        self.y_true = y_true
        return np.mean((y_pred - y_true) ** 2)

    def backward(self):
        n = self.y_true.size
        return 2 * (self.y_pred - self.y_true) / n



# 2. Mean Absolute Error

class MeanAbsoluteError(Loss):

    def forward(self, y_pred, y_true):
        self.y_pred = y_pred
        self.y_true = y_true
        return np.mean(np.abs(y_pred - y_true))

    def backward(self):
        n = self.y_true.size
        return np.sign(self.y_pred - self.y_true) / n



# 3. Log Loss (Binary Cross Entropy)

class LogLoss(Loss):

    def forward(self, y_pred, y_true):

        eps = 1e-15
        y_pred = np.clip(y_pred, eps, 1 - eps)

        self.y_pred = y_pred
        self.y_true = y_true

        loss = -np.mean(
            y_true * np.log(y_pred) +
            (1 - y_true) * np.log(1 - y_pred)
        )

        return loss

    def backward(self):

        n = self.y_true.shape[0]

        grad = (
            (self.y_pred - self.y_true) /
            (self.y_pred * (1 - self.y_pred))
        ) / n

        return grad



# 4. Cross Entropy Loss (Multi-class)

class CrossEntropyLoss(Loss):

    def forward(self, y_pred, y_true):

        eps = 1e-15
        y_pred = np.clip(y_pred, eps, 1 - eps)

        self.y_pred = y_pred
        self.y_true = y_true

        n = y_true.shape[0]

        loss = -np.sum(y_true * np.log(y_pred)) / n

        return loss

    def backward(self):

        n = self.y_true.shape[0]

        grad = - (self.y_true / self.y_pred) / n

        return grad



# 5. Huber Loss

class HuberLoss(Loss):

    def __init__(self, delta=1.0):
        self.delta = delta

    def forward(self, y_pred, y_true):

        self.y_pred = y_pred
        self.y_true = y_true

        error = y_pred - y_true
        abs_error = np.abs(error)

        quadratic = np.minimum(abs_error, self.delta)
        linear = abs_error - quadratic

        loss = np.mean(
            0.5 * quadratic ** 2 +
            self.delta * linear
        )

        return loss

    def backward(self):

        error = self.y_pred - self.y_true

        grad = np.where(
            np.abs(error) <= self.delta,
            error,
            self.delta * np.sign(error)
        )

        return grad / self.y_true.size



# 6. Hinge Loss (SVM)

class HingeLoss(Loss):

    def forward(self, y_pred, y_true):

        # y_true must be -1 or +1

        self.y_pred = y_pred
        self.y_true = y_true

        loss = np.mean(
            np.maximum(0, 1 - y_true * y_pred)
        )

        return loss

    def backward(self):

        grad = np.zeros_like(self.y_pred)

        mask = (1 - self.y_true * self.y_pred) > 0

        grad[mask] = -self.y_true[mask]

        return grad / self.y_true.shape[0]