import torch
from typing import Callable, Dict, Optional, Union, List
from urllib.parse import urlparse
from transformers import PreTrainedModel, PreTrainedTokenizer, CLIPTokenizer
import copy
import random
from io import BytesIO
from compel.embeddings_provider import BaseTextualInversionManager

class TextualInversionLoaderMixin(BaseTextualInversionManager):
    r"""
    Mixin class for adding textual inversion tokens and embeddings to the tokenizer and text encoder with method:
    - [`~TextualInversionLoaderMixin.load_textual_inversion_embeddings`]
    - [`~TextualInversionLoaderMixin.add_textual_inversion_embedding`]
    """

    def load_textual_inversion_embeddings(
        self,
        embedding_path_dict_or_list: Union[Dict[str, str], List[Dict[str, str]]],
        allow_replacement: bool = False,
    ):
        r"""
        Loads textual inversion embeddings and adds them to the tokenizer's vocabulary and the text encoder's embeddings.
        Arguments:
            embeddings_path_dict_or_list (`Dict[str, str]` or `List[str]`):
                Dictionary of token to embedding path or List of embedding paths to embedding dictionaries.
                The dictionary must have the following keys:
                    - `token`: name of the token to be added to the tokenizers' vocabulary
                    - `embedding`: path to the embedding of the token to be added to the text encoder's embedding matrix
                The list must contain paths to embedding dictionaries where the keys are the tokens and the
                values are the embeddings (same as above dictionary definition).
            allow_replacement (`bool`, *optional*, defaults to `False`):
                Whether to allow replacement of existing tokens in the tokenizer's vocabulary. If `False`
                and a token is already in the vocabulary, an error will be raised.
        Returns:
            None
        """
        # Validate that inheriting class instance contains required attributes
        self._validate_method_call(self.load_textual_inversion_embeddings)

        if isinstance(embedding_path_dict_or_list, dict):
            for token, embedding_path in embedding_path_dict_or_list.items():
                
                embedding_dict = torch.load(embedding_path, map_location=self.text_encoder.device)
                embedding, is_multi_vec_token = self._extract_embedding_from_dict(embedding_dict)

                self._validate_token_update(token, allow_replacement, is_multi_vec_token)
                self.add_textual_inversion_embedding(token, embedding)
        elif isinstance(embedding_path_dict_or_list, list):
            for embedding_path in embedding_path_dict_or_list:
                embedding_dict = torch.load(embedding_path, map_location=self.text_encoder.device)
                token = self._extract_token_from_dict(embedding_dict)
                embedding, is_multi_vec_token = self._extract_embedding_from_dict(embedding_dict)

                self._validate_token_update(token, allow_replacement, is_multi_vec_token)
                self.add_textual_inversion_embedding(token, embedding)
        else:
            raise ValueError(
                f"Type {type(embedding_path_dict_or_list)} is invalid. The value passed to `embedding_path_dict_or_list` "
                "must be a dictionary that maps a token to it's embedding file path "
                "or a list of paths to embedding files containing embedding dictionaries."
            )

    def add_textual_inversion_embedding(self, token: str, embedding: torch.Tensor):
        r"""
        Adds a token to the tokenizer's vocabulary and an embedding to the text encoder's embedding matrix.
        Arguments:
            token (`str`):
                The token to be added to the tokenizers' vocabulary
            embedding (`torch.Tensor`):
                The embedding of the token to be added to the text encoder's embedding matrix
        Returns:
            None
        """
        # NOTE: Not clear to me that we intend for this to be a public/exposed method.
        # Validate that inheriting class instance contains required attributes
        self._validate_method_call(self.load_textual_inversion_embeddings)

        embedding = embedding.to(self.text_encoder.dtype)

        if not isinstance(self.tokenizer, MultiTokenCLIPTokenizer):
            if token in self.tokenizer.get_vocab():
                # If user has allowed replacement and the token exists, we only need to
                # extract the existing id and update the embedding
                token_id = self.tokenizer.convert_tokens_to_ids(token)
                self.text_encoder.get_input_embeddings().weight.data[token_id] = embedding
            else:
                # If the token does not exist, we add it to the tokenizer, then resize and update the
                # text encoder acccordingly
                self.tokenizer.add_tokens([token])

                token_id = self.tokenizer.convert_tokens_to_ids(token)
                self.text_encoder.resize_token_embeddings(len(self.tokenizer))
                self.text_encoder.get_input_embeddings().weight.data[token_id] = embedding
        else:
            if token in self.tokenizer.token_map:
                # If user has allowed replacement and the token exists, we need to
                # remove all existing tokens associated with the old embbedding and
                # upddate with the new ones
                indices_to_remove = []
                for token_to_remove in self.tokenizer.token_map[token]:
                    indices_to_remove.append(self.tokenizer.get_added_vocab()[token_to_remove])

                    # Remove old  tokens from tokenizer
                    self.tokenizer.added_tokens_encoder.pop(token_to_remove)

                # Convert indices to remove to tensor
                indices_to_remove = torch.LongTensor(indices_to_remove)

                # Remove old tokens from text encoder
                token_embeds = self.text_encoder.get_input_embeddings().weight.data
                indices_to_keep = torch.arange(0, token_embeds.shape[0])
                indices_to_keep = indices_to_keep[indices_to_keep != indices_to_remove].squeeze()
                token_embeds = token_embeds[indices_to_keep]

                # Downsize text encoder
                self.text_encoder.resize_token_embeddings(len(self.tokenizer))

                # Remove token from map so MultiTokenCLIPTokenizer doesn't complain
                # on update
                self.tokenizer.token_map.pop(token)

            # Update token with new embedding
            embedding_dims = len(embedding.shape)
            num_vec_per_token = 1 if embedding_dims == 1 else embedding.shape[0]

            self.tokenizer.add_placeholder_tokens(token, num_vec_per_token=num_vec_per_token)
            self.text_encoder.resize_token_embeddings(len(self.tokenizer))
            token_ids = self.tokenizer.encode(token, add_special_tokens=False)

            if embedding_dims > 1:
                for i, token_id in enumerate(token_ids):
                    self.text_encoder.get_input_embeddings().weight.data[token_id] = embedding[i]
            else:
                self.text_encoder.get_input_embeddings().weight.data[token_ids] = embedding

    def _extract_embedding_from_dict(self, embedding_dict: Dict[str, str]) -> torch.Tensor:
        r"""
        Extracts the embedding from the embedding dictionary.
        Arguments:
            embedding_dict (`Dict[str, str]`):
                The embedding dictionary loaded from the embedding path
        Returns:
            embedding (`torch.Tensor`):
                The embedding to be added to the text encoder's embedding matrix
            is_multi_vec_token (`bool`):
                Whether the embedding is a multi-vector token or not
        """
        is_multi_vec_token = False
        # auto1111 embedding case
        if "string_to_param" in embedding_dict:
            embedding_dict = embedding_dict["string_to_param"]
            embedding = embedding_dict["*"]
        else:
            embedding = list(embedding_dict.values())[0]

        if len(embedding.shape) > 1:
            # If the embedding has more than one dimension,
            # We need to ensure the tokenizer is a MultiTokenTokenizer
            # because there is branching logic that depends on that class
            if not isinstance(self.tokenizer, MultiTokenCLIPTokenizer):
                raise ValueError(
                    f"{self.__class__.__name__} requires `self.tokenizer` of type `MultiTokenCLIPTokenizer` for loading embeddings with more than one dimension."
                )
            is_multi_vec_token = True

        return embedding, is_multi_vec_token

    def _extract_token_from_dict(self, embedding_dict: Dict[str, str]) -> str:
        r"""
        Extracts the token from the embedding dictionary.
        Arguments:
            embedding_dict (`Dict[str, str]`):
                The embedding dictionary loaded from the embedding path
        Returns:
            token (`str`):
                The token to be added to the tokenizers' vocabulary
        """
        # auto1111 embedding case
        if "string_to_param" in embedding_dict:
            token = embedding_dict["name"]
            return token

        return list(embedding_dict.keys())[0]

    def _validate_method_call(self, method: Callable):
        r"""
        Validates that the method is being called from a class instance that has the required attributes.
        Arguments:
            method (`function`):
                The class's method being called
        Raises:
            ValueError:
                If the method is being called from a class instance that does not have
                the required attributes, the method will not be callable.
        Returns:
            None
        """
        if not hasattr(self, "tokenizer") or not isinstance(self.tokenizer, PreTrainedTokenizer):
            raise ValueError(
                f"{self.__class__.__name__} requires `self.tokenizer` of type `PreTrainedTokenizer` for calling `{method.__name__}`"
            )

        if not hasattr(self, "text_encoder") or not isinstance(self.text_encoder, PreTrainedModel):
            raise ValueError(
                f"{self.__class__.__name__} requires `self.text_encoder` of type `PreTrainedModel` for calling `{method.__name__}`"
            )

    def _validate_token_update(self, token, allow_replacement=False, is_multi_vec_token=False):
        r"""Validates that the token is not already in the tokenizer's vocabulary.
        Arguments:
            token (`str`):
                The token to be added to the tokenizers' vocabulary
            allow_replacement (`bool`):
                Whether to allow replacement of the token if it already exists in the tokenizer's vocabulary
            is_multi_vec_token (`bool`):
                Whether the embedding is a multi-vector token or not
        Raises:
            ValueError:
                If the token is already in the tokenizer's vocabulary and `allow_replacement` is False.
        Returns:
            None
        """
        if (not is_multi_vec_token and token in self.tokenizer.get_vocab()) or (
            is_multi_vec_token and token in self.tokenizer.token_map
        ):
            if allow_replacement:
                print(
                    f"Token {token} already in tokenizer vocabulary. Overwriting existing token and embedding with the new one."
                )
            else:
                raise ValueError(
                    f"Token {token} already in tokenizer vocabulary. Please choose a different token name."
                )

    
    def expand_textual_inversion_token_ids_if_necessary(self, token_ids: List[int]) -> List[int]:
        pass

class MultiTokenCLIPTokenizer(CLIPTokenizer):
    """Tokenizer for CLIP models that have multi-vector tokens."""

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.token_map = {}

    def add_placeholder_tokens(self, placeholder_token, *args, num_vec_per_token=1, **kwargs):
        r"""Adds placeholder tokens to the tokenizer's vocabulary.
        Arguments:
            placeholder_token (`str`):
                The placeholder token to be added to the tokenizers' vocabulary and token map.
            num_vec_per_token (`int`):
                The number of vectors per token. Defaults to 1.
            *args:
                The arguments to be passed to the tokenizer's `add_tokens` method.
            **kwargs:
                The keyword arguments to be passed to the tokenizer's `add_tokens` method.
        Returns:
            None
        """
        output = []
        if num_vec_per_token == 1:
            self.add_tokens(placeholder_token, *args, **kwargs)
            output.append(placeholder_token)
        else:
            output = []
            for i in range(num_vec_per_token):
                ith_token = placeholder_token + f"_{i}"
                self.add_tokens(ith_token, *args, **kwargs)
                output.append(ith_token)
        # handle cases where there is a new placeholder token that contains the current placeholder token but is larger
        for token in self.token_map:
            if token in placeholder_token:
                raise ValueError(
                    f"The tokenizer already has placeholder token {token} that can get confused with"
                    f" {placeholder_token}keep placeholder tokens independent"
                )
        self.token_map[placeholder_token] = output

    def replace_placeholder_tokens_in_text(self, text, vector_shuffle=False, prop_tokens_to_load=1.0):
        r"""Replaces placeholder tokens in text with the tokens in the token map.
        Opttionally, implements:
            a) vector shuffling (https://github.com/rinongal/textual_inversion/pull/119)where
            shuffling tokens were found to force the model to learn the concepts more descriptively.
            b) proportional token loading so that not every token in the token map is loaded on each call;
            used as part of progressive token loading during training which can improve generalization
            during inference.
        Arguments:
            text (`str`):
                The text to be processed.
            vector_shuffle (`bool`):
                Whether to shuffle the vectors in the token map. Defaults to False.
            prop_tokens_to_load (`float`):
                The proportion of tokens to load from the token map. Defaults to 1.0.
        Returns:
            `str`: The processed text.
        """
        if isinstance(text, list):
            output = []
            for i in range(len(text)):
                output.append(self.replace_placeholder_tokens_in_text(text[i], vector_shuffle=vector_shuffle))
            return output
        for placeholder_token in self.token_map:
            if placeholder_token in text:
                tokens = self.token_map[placeholder_token]
                tokens = tokens[: 1 + int(len(tokens) * prop_tokens_to_load)]
                if vector_shuffle:
                    tokens = copy.copy(tokens)
                    random.shuffle(tokens)
                text = text.replace(placeholder_token, " ".join(tokens))
        return text

    def __call__(self, text, *args, vector_shuffle=False, prop_tokens_to_load=1.0, **kwargs):
        """Wrapper around [`~transformers.tokenization_utils.PreTrainedTokenizerBase.__call__`] method
        but first replace placeholder tokens in text with the tokens in the token map.
        Returns:
            [`~transformers.tokenization_utils_base.BatchEncoding`]
        """
        return super().__call__(
            self.replace_placeholder_tokens_in_text(
                text,
                vector_shuffle=vector_shuffle,
                prop_tokens_to_load=prop_tokens_to_load,
            ),
            *args,
            **kwargs,
        )

    def encode(self, text, *args, vector_shuffle=False, prop_tokens_to_load=1.0, **kwargs):
        """Wrapper around the tokenizer's [`transformers.tokenization_utils.PreTrainedTokenizerBase.encode`] method
        but first replaces placeholder tokens in text with the tokens in the token map.
        Arguments:
            text (`str`):
                The text to be encoded.
            *args:
                The arguments to be passed to the tokenizer's `encode` method.
            vector_shuffle (`bool`):
                Whether to shuffle the vectors in the token map. Defaults to False.
            prop_tokens_to_load (`float`):
                The proportion of tokens to load from the token map. Defaults to 1.0.
            **kwargs:
                The keyword arguments to be passed to the tokenizer's `encode` method.
        Returns:
            List[`int`]: sequence of ids (integer)
        """
        return super().encode(
            self.replace_placeholder_tokens_in_text(
                text,
                vector_shuffle=vector_shuffle,
                prop_tokens_to_load=prop_tokens_to_load,
            ),
            *args,
            **kwargs,
        )