from collections import defaultdict
from torch.nn.parallel import DistributedDataParallel
from matplotlib import pyplot as plt
import torch
import requests
from io import BytesIO
from PIL import Image, ImageDraw
from torchvision.transforms import ToPILImage
import torch.nn.functional as F
import numpy as np
import os
import datetime
from tinyllava.data import *
from tinyllava.utils import *
from tinyllava.model import *
import pdb

def load_image(image_file):
    if image_file.startswith("http") or image_file.startswith("https"):
        response = requests.get(image_file)
        image = Image.open(BytesIO(response.content)).convert("RGB")
    else:
        image = Image.open(image_file).convert("RGB")
    return image


def load_images(image_files):
    out = []
    for image_file in image_files:
        image = load_image(image_file)
        out.append(image)
    return out


def extract_max_values_and_indices(tensor, k):
    max_values, max_indices = torch.topk(tensor, k, dim=2)
    max_values_with_indices = torch.stack((max_indices, max_values), dim=3)
    return max_values_with_indices


def visualize_grid_to_grid(i, mask, image, output_dir, grid_size=27, alpha=0.6):
    if not isinstance(grid_size, tuple):
        grid_size = (grid_size, grid_size)
    mask = mask.detach().cpu().numpy()
    mask = Image.fromarray(mask).resize((384, 384))
    fig, ax = plt.subplots(1, 2, figsize=(10, 7))
    fig.tight_layout()

    ax[0].imshow(image)
    ax[0].axis('off')

    ax[1].imshow(image)
    im = ax[1].imshow(mask / np.max(mask), alpha=alpha, cmap='rainbow')
    ax[1].axis('off')
    cbar = fig.colorbar(im, ax=ax[1])
    cbar.set_label('Color Temperature')
    name = os.path.join(output_dir, "hot_image", f"{i}.png")
    plt.savefig(name)
    plt.close(fig)


def generate_square_subsequent_mask(sz):
    mask = (torch.triu(torch.ones(sz, sz)) == 1).transpose(0, 1)
    mask = mask.float().masked_fill(mask == 0, float('-inf')).masked_fill(mask == 1, float(0.0))
    return mask


def generate_word_images(tokenizer, top_words_tensor, num, input_ids, embed_tokens, output_dir):
    num_top_words = top_words_tensor.shape[1]
    for i in range(num_top_words - num, num_top_words):
        fig, ax = plt.subplots()
        word_indices = top_words_tensor[0, i, :, 0].detach().cpu().numpy()
        probabilities = top_words_tensor[0, i, :, 1].detach().cpu().numpy()
        colors = plt.cm.viridis(probabilities)

        for j, (word_index, color, prob) in enumerate(zip(word_indices, colors, probabilities)):
            word = tokenizer.decode([int(word_index)])
            prob_text = f"{word}  P: {prob:.2f}"
            ax.text(0.5, 0.9 - j * 0.1, prob_text, color=color, ha='center', va='center', transform=ax.transAxes)
        ax.axis('off')
        ax.set_title('Top Words for Index {}'.format(i - num_top_words + num + 1))
        plt.savefig(os.path.join(output_dir, 'word', f"word_image_{i - num_top_words + num + 1}.png"))
        plt.close()


def generate_word_images_before(tokenizer, input_ids, tensor, num, top_words_tensor, output_dir):
    num_top_words = tensor.shape[2]
    result = tensor.mean(dim=1)  # [1, len, len]
    input_ids_fir = input_ids[input_ids != -200].unsqueeze(0)
    for i in range(num_top_words - num, num_top_words - 1):
        top1_indices = top_words_tensor[0, i, 0, 0].long()
        fig, ax = plt.subplots()
        result_1 = result[0, i, 0:input_ids.shape[1]]
        result_1 = result_1[input_ids.squeeze() != -200]
        if not i == num_top_words - num:
            result_2 = result[0, i, num_top_words - num + 1:i + 1]
            result_1 = torch.cat((result_1, result_2), dim=0)

        if not i == num_top_words - num:
            output_ids = top_words_tensor[0, num_top_words - num:i, 0, 0].unsqueeze(0).long()
            input_ids_fir = torch.cat((input_ids_fir, output_ids), dim=1)

        tv, ti = torch.topk(result_1.squeeze(), 8)
        tv = tv / torch.max(tv)
        probabilities = tv.detach().cpu().numpy()
        colors = plt.cm.viridis(probabilities)
        for j, (word_index, color, prob) in enumerate(zip(ti, colors, probabilities)):
            word = tokenizer.decode(input_ids_fir[0, word_index.item()])
            prob_text = f"{word}  P: {prob:.2f}"
            ax.text(0.5, 0.9 - j * 0.1, prob_text, color=color, ha='center', va='center', transform=ax.transAxes)
        ax.axis('off')
        ax.set_title(
            'similarities of output word  {}'.format(tokenizer.decode([top1_indices.detach().cpu().numpy()])))
        plt.savefig(os.path.join(output_dir, 'word_before', f"word_image_{i - (num_top_words - num - 1)}.png"))
        plt.close()


class Monitor:
    def __init__(self, args, model, llm_layers_index):
        self.model = model
        self.args = args
        self.input_ids = None
        self.image = None
        self.params = list(model.parameters())
        self.output = defaultdict(dict)
        self.attentions = []
        self.hidden = []
        self.logit = []
        self.image_token = []
        self.llm_layers_index = llm_layers_index
        self._register(llm_layers_index)

    def _register(self, llm_layers_index):
        def attention_hook(module, input, output):
            self.hidden.append(input[0])

        def output_hook(module, input, output):
            self.logit.append(output)

        def image_hook(module, input, output):
            self.image_token.append(output)

        mod = self.model
        mod.language_model.model.layers[llm_layers_index].register_forward_hook(attention_hook)
        mod.language_model.lm_head.register_forward_hook(output_hook)
        mod.connector.register_forward_hook(image_hook)

    def prepare_input(self):
        #  获得input_ids
        qs = self.args.query
        qs = DEFAULT_IMAGE_TOKEN + "\n" + qs
        text_processor = TextPreprocess(self.model.tokenizer, self.args.conv_mode)
        msg = Message()
        msg.add_message(qs)
        result = text_processor(msg.messages, mode='eval')
        self.input_ids = result['input_ids'].unsqueeze(0).cuda()
        #  获得图片tensor
        data_args = self.model.config
        image_processor = self.model.vision_tower._image_processor
        image_processor = ImagePreprocess(image_processor, data_args)
        image_files = self.args.image_file.split(self.args.sep)
        images = load_images(image_files)[0]
        images_tensor = image_processor(images)
        image_tensor = 255 * (images_tensor - images_tensor.min()) / (images_tensor.max() - images_tensor.min())
        image_tensor = image_tensor.clamp(0, 255)
        image_tensor = image_tensor.byte()
        to_pil = ToPILImage()
        self.image = to_pil(image_tensor).convert('RGB')
        self.model.cuda()
        self.logit = F.softmax(torch.cat(self.logit, dim=1), dim=2)
        hidden_tensor = torch.cat(self.hidden, dim=1)
        length = hidden_tensor.shape[1]
        attention_mask = torch.unsqueeze(
            torch.unsqueeze(generate_square_subsequent_mask(length).clone().detach(), dim=0),
            dim=0).cuda()

        self.hidden = self.model.language_model.model.layers[self.llm_layers_index](hidden_tensor,
                                                                                    output_attentions=True,
                                                                                    attention_mask=attention_mask)
        self.image_token = self.image_token[0].squeeze()
        self.image_token = torch.cat((torch.zeros(1, 2560).cuda(), self.image_token), dim=0)

    def get_output(self, output_dir='results/'):
        print("Starting visualization...")
        self.prepare_input()

        timestamp = datetime.datetime.now().strftime("%Y%m%d_%H%M%S")
        output_dir = os.path.join(output_dir, f"run_{timestamp}")
        os.makedirs(output_dir, exist_ok=True)

        os.makedirs(os.path.join(output_dir, 'word'), exist_ok=True)
        os.makedirs(os.path.join(output_dir, 'word_before'), exist_ok=True)
        os.makedirs(os.path.join(output_dir, 'hot_image'), exist_ok=True)

        num = self.logit.shape[1] - 726 - len(self.input_ids[0])
        result = extract_max_values_and_indices(self.logit, 8)
        generate_word_images(self.model.tokenizer, result, num, self.input_ids,
                             self.model.language_model.model.embed_tokens.weight, output_dir)

        generate_word_images_before(self.model.tokenizer, self.input_ids, self.hidden[1], num, result, output_dir)

        result_top1 = result[0, :, 0, 0].squeeze()
        for i in range(len(result_top1) - num, len(result_top1)):
            word_id = result_top1[i]
            word_id_tensor = torch.tensor([word_id]).long().cuda()
            word_vector = self.model.language_model.model.embed_tokens(word_id_tensor).squeeze().detach()
            vector_expanded = word_vector.unsqueeze(0).expand_as(self.image_token)
            vector_norm = F.normalize(vector_expanded, p=2, dim=1)
            matrix_norm = F.normalize(self.image_token, p=2, dim=1)
            cosine_similarities = torch.sum(vector_norm * matrix_norm, dim=1)
            normalized_similarities = F.softmax(cosine_similarities, dim=0)
            visualize_grid_to_grid('hot_image_' + str(i - (len(result_top1) - num) + 1),
                                   normalized_similarities.view(27, 27),
                                   self.image, output_dir)
        print("Completed visualization.")
