from typing import List, Union, Tuple

import torch
from torch.nn import Module, CrossEntropyLoss, MSELoss, L1Loss

from pydgn.experiment.util import s2c
from pydgn.static import BATCH_LOSS_EXTRA, ARGS, CLASS_NAME
from pydgn.training.event.handler import EventHandler
from pydgn.training.event.state import State


class Metric(Module, EventHandler):
    r"""
    Metric is the main event handler for all metrics. Other metrics can easily subclass by implementing the :func:`forward`
    method, though sometimes more complex implementations are required.

    Args:
        use_as_loss (bool): whether this metric should act as a loss (i.e., it should act when :func:`on_backward` is called). **Used by PyDGN, no need to care about this.**
        reduction (str): the type of reduction to apply across samples of the mini-batch. Supports ``mean`` and ``sum``. Default is ``mean``.
        accumulate_over_epoch (bool): Whether or not to display the epoch-wise metric rather than an average of per-batch metrics.
                                      If true, it keep a list of predictions and target values across the entire epoch.
                                      Use it especially with batch-sensitive metrics, such as micro AP/F1 scores. Default is ``True``.
        force_cpu (bool): Whether or not to move all predictions to cpu before computing the epoch-wise loss/score. Default is ``True``.
    """
    def __init__(self, use_as_loss: bool=False, reduction: str='mean',
                 accumulate_over_epoch: bool=True, force_cpu: bool=True):
        super().__init__()
        self.batch_metrics = None
        self.use_as_loss = use_as_loss
        self.reduction = reduction
        self.accumulate_over_epoch = accumulate_over_epoch
        self.force_cpu = force_cpu
        self.y_pred, self.y_true = None, None

        # Keeps track of how many times on_compute_metrics has been called in the same batch.
        # Useful for temporal graph learning, for static graph learning it will be always 1
        self.timesteps_in_batch = None


    @property
    def name(self) -> str:
        raise NotImplementedError('You should subclass Metric and implement this method!')

    def get_main_metric_name(self) -> str:
        """
        Return the metric's main name. Useful when a metric is the combination of many.

        Returns:
            the metric's main name
        """
        return self.name

    def get_predictions_and_targets(self,
                                    targets: torch.Tensor,
                                    *outputs: List[torch.Tensor]) -> Tuple[torch.Tensor, torch.Tensor]:
        r"""
        Returns predictions and target tensors to be accumulated for a given metric

        Args:
            targets (:class:`torch.Tensor`): ground truth
            outputs (List[:class:`torch.Tensor`]): outputs of the model

        Returns:
            A tuple of tensors (predicted_values, target_values)
        """
        raise NotImplementedError("You should subclass Metric and implement this method!")

    def compute_metric(self,
                       targets: torch.Tensor,
                       predictions: torch.Tensor) -> torch.tensor:
        r"""
        Computes the metric for a given set of targets and predictions

        Args:
            targets (:class:`torch.Tensor`): tensor of ground truth values
            predictions (:class:`torch.Tensor`): tensor of predictions of the model

        Returns:
            A tensor with the metric value
        """
        raise NotImplementedError("You should subclass Metric and implement this method!")

    def on_training_epoch_start(self, state: State):
        self.batch_metrics = []
        self.y_pred, self.y_true = {self.name: []}, {self.name: []}

    def on_training_batch_start(self, state: State):
        self.timesteps_in_batch = 0.

    def on_training_batch_end(self, state: State):
        if self.use_as_loss:
            batch_metric = state.batch_loss
        else:
            batch_metric = state.batch_score

        if not self.accumulate_over_epoch:
            self.batch_metrics.append(batch_metric[self.name].item() / self.timesteps_in_batch)

        self.timesteps_in_batch = None

    def on_training_epoch_end(self, state: State):
        if not self.accumulate_over_epoch:
            epoch_res = {self.name: torch.tensor(self.batch_metrics).sum() / len(self.batch_metrics)}
        else:
            epoch_res = {self.name: self.compute_metric(torch.cat(self.y_true[self.name], dim=0),
                                                        torch.cat(self.y_pred[self.name], dim=0))}

        if self.use_as_loss:
            state.update(epoch_loss=epoch_res)
        else:
            state.update(epoch_score=epoch_res)

        self.batch_metrics = None
        self.y_pred, self.y_true = None, None

    def on_eval_epoch_start(self, state: State):
        self.batch_metrics = []
        self.y_pred, self.y_true = {self.name: []}, {self.name: []}

    def on_eval_batch_start(self, state: State):
        self.timesteps_in_batch = 0.

    def on_eval_batch_end(self, state: State):
        if self.use_as_loss:
            batch_metric = state.batch_loss
        else:
            batch_metric = state.batch_score

        if not self.accumulate_over_epoch:
            self.batch_metrics.append(batch_metric[self.name].item() / self.timesteps_in_batch)

        self.timesteps_in_batch = None

    def on_eval_epoch_end(self, state: State):
        if not self.accumulate_over_epoch:
            epoch_res = {self.name: torch.tensor(self.batch_metrics).sum() / len(self.batch_metrics)}
        else:
            epoch_res = {self.name: self.compute_metric(torch.cat(self.y_true[self.name], dim=0),
                                                        torch.cat(self.y_pred[self.name], dim=0))}

        if self.use_as_loss:
            state.update(epoch_loss=epoch_res)
        else:
            state.update(epoch_score=epoch_res)

        self.batch_metrics = None
        self.y_pred, self.y_true = None, None

    def accumulate_predictions_and_targets(self,
                                           targets: torch.Tensor,
                                           *outputs: List[torch.Tensor]) -> None:
        """
        Used to specify how to accumulate predictions and targets. This can be customized by subclasses like
        AdditiveLoss and MultiScore to accumulate predictions and targets for different losses/scores.

        Args:
            targets: target tensor
            *outputs: outputs of the model
        """
        y_pred_batch, y_true_batch = self.get_predictions_and_targets(targets, *outputs)
        metric_name = self.name

        self.y_pred[metric_name].append(y_pred_batch.detach().cpu() if self.force_cpu else y_pred_batch.detach())
        self.y_true[metric_name].append(y_true_batch.detach().cpu() if self.force_cpu else y_true_batch.detach())

    def on_compute_metrics(self, state: State):
        self.timesteps_in_batch += 1.

        outputs, targets = state.batch_outputs, state.batch_targets

        # Loss case
        if self.use_as_loss:
            loss = self.forward(targets, *outputs)

            # [temporal graph learning] accumulate, rather than substitute, the losses across multiple snapshots
            # these are reset accordingly by the Training Engine!
            if state.batch_loss is not None:
                old_loss = state.batch_loss
                loss = {k: old_loss[k] + loss[k] for k in loss.keys()}

            # this has to be updated per-batch no matter what
            state.update(batch_loss=loss)

        # Score case
        else:
            if not self.accumulate_over_epoch:
                score = self.forward(targets, *outputs)
                score = {k: score[k].detach().cpu() if self.force_cpu else score[k].detach() for k in score.keys()}

                # [temporal graph learning] accumulate, rather than substitute, the scores across multiple snapshots
                # these are reset accordingly by the Training Engine!
                if state.batch_score is not None:
                    old_score = state.batch_score
                    score = {k: old_score[k] + score[k] for k in score.keys()}

                # mean of batches
                state.update(batch_score=score)

        if self.accumulate_over_epoch:
            self.accumulate_predictions_and_targets(targets, *outputs)

    def on_backward(self, state: State):
        if self.use_as_loss:
            try:
                state.batch_loss[self.name].backward()
            except Exception as e:
                # Here we catch potential multiprocessing related issues
                # see https://github.com/pytorch/pytorch/wiki/Autograd-and-Fork
                raise(e)

    def forward(self,
                targets: torch.Tensor,
                *outputs: List[torch.Tensor]) -> dict:
        r"""
        Computes the metric value. Optionally, and only for scores used as losses, some extra information can be also returned.

        Args:
            targets (:class:`torch.Tensor`): ground truth
            outputs (List[:class:`torch.Tensor`]): outputs of the model
            batch_loss_extra (dict): dictionary of information computed by metrics used as losses

        Returns:
            A dictionary containing associations metric_name - value
        """
        y_pred_batch, y_true_batch = self.get_predictions_and_targets(targets, *outputs)
        return {self.name: self.compute_metric(y_true_batch, y_pred_batch)}


class MultiScore(Metric):
    r"""
    This class is used to keep track of multiple additional metrics used as scores, rather than losses.

    Args:
        use_as_loss (bool): whether this metric should act as a loss (i.e., it should act when :func:`on_backward` is called). **Used by PyDGN, no need to care about this.**
        reduction (str): the type of reduction to apply across samples of the mini-batch. Supports ``mean`` and ``sum``. Default is ``mean``.
        accumulate_over_epoch (bool): Whether or not to display the epoch-wise metric rather than an average of per-batch metrics.
                                      If true, it keep a list of predictions and target values across the entire epoch.
                                      Use it especially with batch-sensitive metrics, such as micro AP/F1 scores. Default is ``True``.
        force_cpu (bool): Whether or not to move all predictions to cpu before computing the epoch-wise loss/score. Default is ``True``.

    """
    def __init__(self, use_as_loss, reduction='mean',
                 accumulate_over_epoch: bool=True, force_cpu: bool=True,
                 main_scorer=None, **extra_scorers):

        assert not use_as_loss, "MultiScore cannot be used as loss"
        assert not main_scorer is None, "You have to provide a main scorer"
        super().__init__(use_as_loss, reduction, accumulate_over_epoch, force_cpu)
        self.scores = [self._istantiate_scorer(main_scorer)] + [self._istantiate_scorer(score) for score in
                                                                extra_scorers.values()]

    @property
    def name(self) -> str:
        return "Multi Score"

    def _istantiate_scorer(self, scorer):
        if isinstance(scorer, dict):
            args = scorer[ARGS]
            return s2c(scorer[CLASS_NAME])(use_as_loss=False, **args)
        else:
            return s2c(scorer)(use_as_loss=False)

    def get_main_metric_name(self):
        return self.scores[0].get_main_metric_name()

    def on_training_epoch_start(self, state: State):
        self.batch_metrics = {s.name: [] for s in self.scores}
        self.y_pred = {s.name: [] for s in self.scores}
        self.y_true = {s.name: [] for s in self.scores}

    def on_training_batch_end(self, state: State):
        if not self.accumulate_over_epoch:
            for k, v in state.batch_score.items():
                self.batch_metrics[k].append(v.item() / self.timesteps_in_batch)

        self.timesteps_in_batch = None

    def on_training_epoch_end(self, state: State):
        if not self.accumulate_over_epoch:
            epoch_res = {s.name: torch.tensor(self.batch_metrics[s.name]).sum() / len(self.batch_metrics[s.name])
                         for s in self.scores}
        else:
            epoch_res = {s.name: s.compute_metric(torch.cat(self.y_true[s.name], dim=0),
                                                  torch.cat(self.y_pred[s.name], dim=0))
                         for s in self.scores}

        state.update(epoch_score=epoch_res)

        self.batch_metrics = None
        self.y_pred, self.y_true = None, None

    def on_eval_epoch_start(self, state: State):
        self.batch_metrics = {s.name: [] for s in self.scores}
        self.y_pred = {s.name: [] for s in self.scores}
        self.y_true = {s.name: [] for s in self.scores}

    def on_eval_batch_end(self, state: State):
        if not self.accumulate_over_epoch:
            for k, v in state.batch_score.items():
                self.batch_metrics[k].append(v.item() / self.timesteps_in_batch)

        self.timesteps_in_batch = None

    def on_eval_epoch_end(self, state: State):
        if not self.accumulate_over_epoch:
            epoch_res = {s.name: torch.tensor(self.batch_metrics[s.name]).sum() / len(self.batch_metrics[s.name])
                         for s in self.scores}
        else:
            epoch_res = {s.name: s.compute_metric(torch.cat(self.y_true[s.name], dim=0),
                                                  torch.cat(self.y_pred[s.name], dim=0))
                         for s in self.scores}

        state.update(epoch_score=epoch_res)

        self.batch_metrics = None
        self.y_pred, self.y_true = None, None

    def accumulate_predictions_and_targets(self,
                                           targets: torch.Tensor,
                                           *outputs: List[torch.Tensor]) -> None:
        for s in self.scores:
            y_pred_batch, y_true_batch = s.get_predictions_and_targets(targets, *outputs)
            metric_name = s.name

            self.y_pred[metric_name].append(y_pred_batch.detach().cpu() if self.force_cpu else y_pred_batch.detach())
            self.y_true[metric_name].append(y_true_batch.detach().cpu() if self.force_cpu else y_true_batch.detach())

    def forward(self,
                targets: torch.Tensor,
                *outputs: List[torch.Tensor]) -> Union[dict, float]:
        res = {}
        for s in self.scores:
            y_pred_batch, y_true_batch = s.get_predictions_and_targets(targets, *outputs)
            res.update({s.name: s.compute_metric(y_true_batch, y_pred_batch)})
        return res


class AdditiveLoss(Metric):
    r"""
    AdditiveLoss sums an arbitrary number of losses together.

    Args:
        use_as_loss (bool): whether this metric should act as a loss (i.e., it should act when :func:`on_backward` is called). **Used by PyDGN, no need to care about this.**
        reduction (str): the type of reduction to apply across samples of the mini-batch. Supports ``mean`` and ``sum``. Default is ``mean``.
        accumulate_over_epoch (bool): Whether or not to display the epoch-wise metric rather than an average of per-batch metrics.
                                      If true, it keep a list of predictions and target values across the entire epoch.
                                      Use it especially with batch-sensitive metrics, such as micro AP/F1 scores. Default is ``True``.
        force_cpu (bool): Whether or not to move all predictions to cpu before computing the epoch-wise loss/score. Default is ``True``.
        losses (dict): dictionary of metrics to add together
    """
    def __init__(self, use_as_loss, reduction='mean',
                 accumulate_over_epoch: bool=True, force_cpu: bool=True, **losses: dict):
        assert use_as_loss, "Additive loss can only be used as a loss"
        super().__init__(use_as_loss, reduction, accumulate_over_epoch, force_cpu)
        self.losses = [self._instantiate_loss(loss) for loss in losses.values()]

    @property
    def name(self) -> str:
        return "Additive Loss"

    def _instantiate_loss(self, loss):
        if isinstance(loss, dict):
            args = loss[ARGS]
            return s2c(loss[CLASS_NAME])(use_as_loss=True, **args)
        else:
            return s2c(loss)(use_as_loss=True)

    def on_training_epoch_start(self, state: State):
        self.batch_metrics = {l.name: [] for l in [self] + self.losses}
        self.y_pred = {l.name: [] for l in [self] + self.losses}
        self.y_true = {l.name: [] for l in [self] + self.losses}

    def on_training_batch_end(self, state: State):
        if not self.accumulate_over_epoch:
            for k, v in state.batch_loss.items():
                self.batch_metrics[k].append(v.item() / self.timesteps_in_batch)

        self.timesteps_in_batch = None

    def on_training_epoch_end(self, state: State):
        if not self.accumulate_over_epoch:
            epoch_res = {l.name: torch.tensor(self.batch_metrics[l.name]).sum() / len(self.batch_metrics[l.name])
                                 for l in [self] + self.losses}
        else:
            epoch_res = {l.name: l.compute_metric(torch.cat(self.y_true[l.name], dim=0),
                                                  torch.cat(self.y_pred[l.name], dim=0))
                         for l in self.losses}
            additive_epoch_loss = 0.
            for l in self.losses:
                additive_epoch_loss += epoch_res[l.name]
            epoch_res[self.name] = additive_epoch_loss

        state.update(epoch_loss=epoch_res)

        self.batch_metrics = None
        self.y_pred, self.y_true = None, None

    def on_eval_epoch_start(self, state: State):
        self.batch_metrics = {l.name: [] for l in [self] + self.losses}
        self.y_pred = {l.name: [] for l in [self] + self.losses}
        self.y_true = {l.name: [] for l in [self] + self.losses}

    def on_eval_epoch_end(self, state: State):
        if not self.accumulate_over_epoch:
            epoch_res = {l.name: torch.tensor(self.batch_metrics[l.name]).sum() / len(self.batch_metrics[l.name])
                                 for l in [self] + self.losses}
        else:
            epoch_res = {l.name: l.compute_metric(torch.cat(self.y_true[l.name], dim=0),
                                                  torch.cat(self.y_pred[l.name], dim=0))
                         for l in self.losses}
            additive_epoch_loss = 0.
            for l in self.losses:
                additive_epoch_loss += epoch_res[l.name]
            epoch_res[self.name] = additive_epoch_loss

        state.update(epoch_loss=epoch_res)

        self.batch_metrics = None
        self.y_pred, self.y_true = None, None

    def on_eval_batch_end(self, state: State):
        if not self.accumulate_over_epoch:
            for k, v in state.batch_loss.items():
                self.batch_metrics[k].append(v.item() / self.timesteps_in_batch)

        self.timesteps_in_batch = None


    def compute_metric(self,
                       targets: torch.Tensor,
                       predictions: torch.Tensor) -> torch.tensor:
        loss_sum = 0.
        for l in self.losses:
            single_loss = l.compute_metric(targets, predictions)
            loss_sum += single_loss
        return loss_sum

    def accumulate_predictions_and_targets(self,
                                           targets: torch.Tensor,
                                           *outputs: List[torch.Tensor]) -> None:
        for l in self.losses:
            y_pred_batch, y_true_batch = l.get_predictions_and_targets(targets, *outputs)
            metric_name = l.name

            self.y_pred[metric_name].append(y_pred_batch.detach().cpu() if self.force_cpu else y_pred_batch.detach())
            self.y_true[metric_name].append(y_true_batch.detach().cpu() if self.force_cpu else y_true_batch.detach())

    def forward(self,
                targets: torch.Tensor,
                *outputs: List[torch.Tensor]) -> dict:
        res = {}
        loss_sum = 0.

        for l in self.losses:
            y_pred_batch, y_true_batch = l.get_predictions_and_targets(targets, *outputs)
            single_loss = l.compute_metric(y_true_batch, y_pred_batch)

            res[l.name] = single_loss
            loss_sum += single_loss

        res[self.name] = loss_sum
        return res


class Classification(Metric):
    r"""
    Generic metric for classification tasks. Used to maximize code reuse for classical metrics.
    """
    def __init__(self, use_as_loss=False, reduction='mean',
                 accumulate_over_epoch: bool=True, force_cpu: bool=True):
        super().__init__(use_as_loss=use_as_loss, reduction=reduction,
                         accumulate_over_epoch=accumulate_over_epoch, force_cpu=force_cpu)
        self.metric = None

    @property
    def name(self) -> str:
        return 'Classification Metric'


    def get_predictions_and_targets(self,
                                    targets: torch.Tensor,
                                    *outputs: List[torch.Tensor]) -> Tuple[torch.Tensor, torch.Tensor]:
        outputs = outputs[0].squeeze(dim=1)

        if len(targets.shape) == 2:
            targets = targets.squeeze(dim=1)

        return outputs, targets

    def compute_metric(self,
                       targets: torch.Tensor,
                       predictions: torch.Tensor) -> torch.tensor:
        metric = self.metric(predictions, targets)
        return metric


class Regression(Metric):
    r"""
    Generic metric for regression tasks. Used to maximize code reuse for classical metrics.
    """
    def __init__(self, use_as_loss=False, reduction='mean',
                 accumulate_over_epoch: bool=True, force_cpu: bool=True):
        super().__init__(use_as_loss=use_as_loss, reduction=reduction,
                         accumulate_over_epoch=accumulate_over_epoch, force_cpu=force_cpu)
        self.metric = None

    @property
    def name(self) -> str:
        return 'Regression Metric'

    def get_predictions_and_targets(self,
                                    targets: torch.Tensor,
                                    *outputs: List[torch.Tensor]) -> Tuple[torch.Tensor, torch.Tensor]:
        outputs = outputs[0].squeeze(dim=1)

        if len(targets.shape) == 2:
            targets = targets.squeeze(dim=1)

        return outputs, targets

    def compute_metric(self,
                       targets: torch.Tensor,
                       predictions: torch.Tensor) -> torch.tensor:
        metric = self.metric(predictions, targets)
        return metric


class MulticlassClassification(Classification):
    r"""
    Wrapper around :class:`torch.nn.CrossEntropyLoss`
    """
    def __init__(self, use_as_loss=False, reduction='mean',
                 accumulate_over_epoch: bool=True, force_cpu: bool=True):
        super().__init__(use_as_loss=use_as_loss, reduction=reduction,
                         accumulate_over_epoch=accumulate_over_epoch, force_cpu=force_cpu)
        self.metric = CrossEntropyLoss(reduction=reduction)

    @property
    def name(self) -> str:
        return 'Multiclass Classification'


class MeanSquareError(Regression):
    r"""
    Wrapper around :class:`torch.nn.MSELoss`
    """
    def __init__(self, use_as_loss=False, reduction='mean',
                 accumulate_over_epoch: bool=True, force_cpu: bool=True):
        super().__init__(use_as_loss=use_as_loss, reduction=reduction,
                         accumulate_over_epoch=accumulate_over_epoch, force_cpu=force_cpu)
        self.metric = MSELoss(reduction=reduction)

    @property
    def name(self) -> str:
        return 'Mean Square Error'


class MeanAverageError(Regression):
    r"""
    Wrapper around :class:`torch.nn.MSELoss`
    """
    def __init__(self, use_as_loss=False, reduction='mean',
                 accumulate_over_epoch: bool=True, force_cpu: bool=True):
        super().__init__(use_as_loss=use_as_loss, reduction=reduction,
                         accumulate_over_epoch=accumulate_over_epoch, force_cpu=force_cpu)
        self.metric = L1Loss(reduction=reduction)

    @property
    def name(self) -> str:
        return 'Mean Average Error'


class DotProductLink(Metric):
    """
    Implements a dot product link prediction metric, as defined in https://arxiv.org/abs/1611.07308.
    """
    @property
    def name(self) -> str:
        return 'Dot Product Link Prediction'

    def get_predictions_and_targets(self,
                                    targets: torch.Tensor,
                                    *outputs: List[torch.Tensor]) -> Tuple[torch.Tensor, torch.Tensor]:
        node_embs = outputs[1]
        _, pos_edges, neg_edges = targets

        loss_edge_index = torch.cat((pos_edges, neg_edges), dim=1)
        loss_target = torch.cat((torch.ones(pos_edges.shape[1]),
                                 torch.zeros(neg_edges.shape[1])))

        # Taken from https://github.com/rusty1s/pytorch_geometric/blob/master/examples/link_pred.py
        x_j = torch.index_select(node_embs, 0, loss_edge_index[0])
        x_i = torch.index_select(node_embs, 0, loss_edge_index[1])
        link_logits = torch.einsum("ef,ef->e", x_i, x_j)

        return link_logits, loss_target.to(link_logits.device)

    def compute_metric(self,
                       targets: torch.Tensor,
                       predictions: torch.Tensor) -> torch.tensor:
        metric = torch.nn.functional.binary_cross_entropy_with_logits(predictions, targets)
        return metric


class MulticlassAccuracy(Metric):
    """
    Implements multiclass classification accuracy.
    """

    @property
    def name(self) -> str:
        return 'Multiclass Accuracy'

    @staticmethod
    def _get_correct(output):
        return torch.argmax(output, dim=1)

    def get_predictions_and_targets(self,
                                    targets: torch.Tensor,
                                    *outputs: List[torch.Tensor]) -> Tuple[torch.Tensor, torch.Tensor]:
        pred = outputs[0]
        correct = self._get_correct(pred)

        if len(targets.shape) == 2:
            targets = targets.squeeze(dim=1)

        return correct, targets

    def compute_metric(self,
                       targets: torch.Tensor,
                       predictions: torch.Tensor) -> torch.tensor:
        metric = 100. * (predictions == targets).sum().float() / targets.size(0)
        return metric


class ToyMetric(Metric):
    r"""
    Implements a toy metric.
    """
    @property
    def name(self) -> str:
        return 'Toy Metric'

    @staticmethod
    def _get_correct(output):
        return torch.argmax(output, dim=1)

    def get_predictions_and_targets(self,
                                    targets: torch.Tensor,
                                    *outputs: List[torch.Tensor]) -> Tuple[torch.Tensor, torch.Tensor]:
        return outputs[0], targets

    def compute_metric(self,
                       targets: torch.Tensor,
                       predictions: torch.Tensor) -> torch.tensor:
        metric = 100. * torch.ones(1)
        return metric


class ToyUnsupervisedMetric(Metric):

    @property
    def name(self) -> str:
        return 'Toy Unsupervised Metric'

    def get_predictions_and_targets(self,
                                    targets: torch.Tensor,
                                    *outputs: List[torch.Tensor]) -> Tuple[torch.Tensor, torch.Tensor]:
        return outputs[0], targets

    def compute_metric(self,
                       targets: torch.Tensor,
                       predictions: torch.Tensor) -> torch.tensor:
        metric = (predictions*0.).mean()
        return metric