# Copyright (c) ModelScope Contributors. All rights reserved.
import numpy as np
import os
import torch
from transformers import EvalPrediction
from transformers.utils import strtobool
from typing import Dict

from swift.loss.embedding import _parse_multi_negative_sentences, _parse_pair_sentence
from .base import EvalMetrics
from .utils import Metric


class EmbedddingMetricMixin(Metric):

    def __init__(self):
        super().__init__()
        self.add_state('last_hidden_state', default_factory=list)
        self.add_state('labels', default_factory=list)

    def update(self, last_hidden_state, labels):
        self.last_hidden_state.append(last_hidden_state.cpu().numpy())
        self.labels.append(labels.cpu().numpy())

    def compute(self):
        predictions = np.concatenate(self.last_hidden_state)
        labels = np.concatenate(self.labels)
        return self._calculate_metrics(predictions, labels)


class PairedMetrics(EvalMetrics, EmbedddingMetricMixin):

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        EmbedddingMetricMixin.__init__(self)

    def compute_metrics(self, eval_prediction: EvalPrediction) -> Dict[str, float]:
        predictions = eval_prediction.predictions
        labels = eval_prediction.label_ids
        return self._calculate_metrics(predictions, labels)

    def _calculate_metrics(self, predictions, labels):
        from scipy.stats import pearsonr, spearmanr
        from sklearn.metrics.pairwise import (paired_cosine_distances, paired_euclidean_distances,
                                              paired_manhattan_distances)
        embeddings1, embeddings2 = _parse_pair_sentence(predictions)
        cosine_scores = 1 - (paired_cosine_distances(embeddings1, embeddings2))
        manhattan_distances = -paired_manhattan_distances(embeddings1, embeddings2)
        euclidean_distances = -paired_euclidean_distances(embeddings1, embeddings2)
        dot_products = [np.dot(emb1, emb2) for emb1, emb2 in zip(embeddings1, embeddings2)]

        eval_pearson_cosine, _ = pearsonr(labels, cosine_scores)
        eval_spearman_cosine, _ = spearmanr(labels, cosine_scores)

        eval_pearson_manhattan, _ = pearsonr(labels, manhattan_distances)
        eval_spearman_manhattan, _ = spearmanr(labels, manhattan_distances)

        eval_pearson_euclidean, _ = pearsonr(labels, euclidean_distances)
        eval_spearman_euclidean, _ = spearmanr(labels, euclidean_distances)

        eval_pearson_dot, _ = pearsonr(labels, dot_products)
        eval_spearman_dot, _ = spearmanr(labels, dot_products)

        return {
            'pearson_cosine': eval_pearson_cosine,
            'pearson_euclidean': eval_pearson_euclidean,
            'pearson_manhattan': eval_pearson_manhattan,
            'pearson_dot_product': eval_pearson_dot,
            'spearman_cosine': eval_spearman_cosine,
            'spearman_euclidean': eval_spearman_euclidean,
            'spearman_manhattan': eval_spearman_manhattan,
            'spearman_dot_product': eval_spearman_dot,
        }


class InfonceMetrics(EvalMetrics, EmbedddingMetricMixin):

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        EmbedddingMetricMixin.__init__(self)

    def compute_metrics(self, eval_prediction: EvalPrediction) -> Dict[str, float]:
        predictions = eval_prediction.predictions
        labels = eval_prediction.label_ids
        return self._calculate_metrics(predictions, labels)

    def _calculate_metrics(self, predictions, labels):
        hard_negatives = os.environ.get('INFONCE_HARD_NEGATIVES', None)
        use_batch = strtobool(os.environ.get('INFONCE_USE_BATCH', 'True'))
        if hard_negatives is not None:
            hard_negatives = int(hard_negatives)
        split_tensors = _parse_multi_negative_sentences(torch.tensor(predictions), torch.tensor(labels), hard_negatives)
        split_tensors = [t.numpy() for t in split_tensors]
        can_batched = hard_negatives is not None
        if hard_negatives is None and len(set([s.shape[0] for s in split_tensors])) == 1:
            can_batched = True
        all_similarity_matrix = []
        all_labels = []
        pos_neg_margins = []
        if not use_batch:
            if can_batched:
                sentences = np.stack(split_tensors, axis=0)
                similarity_matrix = np.matmul(sentences[:, 0:1], sentences[:, 1:].transpose((0, 2, 1))).squeeze(1)
                all_similarity_matrix.append(similarity_matrix)
                labels = np.zeros_like(similarity_matrix)
                labels[:, 0] = 1
                all_labels.append(labels)
            else:
                for tensor in split_tensors:
                    similarity_matrix = np.matmul(tensor[0], tensor[1:].T)
                    all_similarity_matrix.append(similarity_matrix)
                    labels = np.zeros_like(similarity_matrix)
                    labels[0] = 1
                    all_labels.append(labels)
                    max_neg_scores = np.max(similarity_matrix[labels == 0], axis=-1)
                    pos_neg_margins.append(np.mean(similarity_matrix[labels == 1] - max_neg_scores).item())
        else:
            if can_batched:
                sentences = np.stack(split_tensors, axis=0)
                similarity_matrix = np.matmul(sentences[:, 0], sentences[:, 1:].reshape(-1, sentences.shape[2]).T)
                all_similarity_matrix.append(similarity_matrix)
                labels = np.zeros_like(similarity_matrix)
                for row, col in enumerate(
                        range(0, sentences.shape[0] * (sentences.shape[1] - 1), sentences.shape[1] - 1)):
                    labels[row, col] = 1
                all_labels.append(labels)
            else:
                all_tensors = []
                for tensor in split_tensors:
                    all_tensors.append(tensor[1:])
                sentences = np.concatenate(all_tensors, axis=0)
                length = 0
                for idx, tensor in enumerate(split_tensors):
                    similarity_matrix = np.matmul(tensor[0], sentences.T)
                    all_similarity_matrix.append(similarity_matrix)
                    labels = np.zeros_like(similarity_matrix)
                    labels[length] = 1
                    all_labels.append(labels)
                    length += tensor.shape[0] - 1
                    max_neg_scores = np.max(similarity_matrix[labels == 0], axis=-1)
                    pos_neg_margins.append(np.mean(similarity_matrix[labels == 1] - max_neg_scores).item())

        similarity_matrix = np.concatenate(all_similarity_matrix, axis=0)
        labels = np.concatenate(all_labels, axis=0)
        if can_batched:
            pos_scores = similarity_matrix[labels == 1].reshape(similarity_matrix.shape[0], -1)
            neg_scores = similarity_matrix[labels == 0].reshape(similarity_matrix.shape[0], -1)
            max_neg_scores = np.max(neg_scores, axis=-1)
            pos_neg_margin = np.mean(pos_scores - max_neg_scores).item()
        else:
            pos_scores = similarity_matrix[labels == 1]
            neg_scores = similarity_matrix[labels == 0]
            pos_neg_margin = np.mean(pos_neg_margins)

        mean_neg = np.mean(neg_scores)
        mean_pos = np.mean(pos_scores)
        return {'margin': pos_neg_margin, 'mean_neg': mean_neg, 'mean_pos': mean_pos}
