# pylint: skip-file
import functools
import inspect
import json
import os
from contextlib import contextmanager

import gradio as gr
import langcodes
import yaml
from gradio.blocks import Block, BlockContext, Context, LocalContext


# Monkey patch to escape I18nString type being stripped in gradio.Markdown
def escape_caller(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        if args and isinstance(args[0], I18nString):
            add_values = args[0].add_values
            radd_values = args[0].radd_values
            result = I18nString(func(*args, **kwargs))
            result.add_values = add_values
            result.radd_values = radd_values

            return result
        return func(*args, **kwargs)

    return wrapper


inspect.cleandoc = escape_caller(inspect.cleandoc)


class TranslateContext:
    available_languages = ["en"]
    dictionary: dict = {}
    lang_per_session = {}

    def get_available_languages():
        return TranslateContext.available_languages

    def set_available_languages(langs: list):
        if not langs or not isinstance(langs, list):
            raise ValueError("langs must be a list of languages")
        TranslateContext.available_languages = langs

    def get_default_language():
        return TranslateContext.get_available_languages()[0]

    def add_translation(translation: dict):
        for k, v in translation.items():
            if k not in TranslateContext.available_languages:
                continue
            if k not in TranslateContext.dictionary:
                TranslateContext.dictionary[k] = {}
            TranslateContext.dictionary[k].update(v)

    def get_current_language(request: gr.Request):
        return TranslateContext.lang_per_session.get(
            request.session_hash, TranslateContext.get_default_language()
        )

    def set_current_language(request: gr.Request, lang: str):
        TranslateContext.lang_per_session[request.session_hash] = lang

    def get_lang_from_request(request: gr.Request):
        if "Accept-Language" not in request.headers:
            return TranslateContext.get_default_language()

        # Get the first language from the Accept-Language header
        lang = request.headers["Accept-Language"].split(",")[0]
        lang, _ = langcodes.closest_match(
            lang, TranslateContext.get_available_languages()
        )

        if not lang or lang == "und":
            return TranslateContext.get_default_language()
        return lang


class I18nString(str):
    __slots__ = ("_key", "add_values", "radd_values")

    def __new__(cls, value):
        obj = super().__new__(cls, value)
        obj._key = value
        obj.add_values = []
        obj.radd_values = []
        return obj

    def __str__(self):
        try:
            request = LocalContext.request.get()
        except LookupError:
            request = None
        if request is None:
            return self._key

        lang = TranslateContext.get_current_language(request)
        result = TranslateContext.dictionary.get(lang, {}).get(self._key, self._key)

        for v in self.radd_values:
            result = str(v) + result
        for v in self.add_values:
            result = result + str(v)

        while len(result) >= 2 and result.startswith("'") and result.endswith("'"):
            result = result[1:-1]
        return result

    def __add__(self, other):
        self.add_values.append(other)
        return self

    def __radd__(self, other):
        self.radd_values.append(other)
        return self

    def __hash__(self) -> int:
        return super().__hash__()

    def format(self, *args, **kwargs) -> str:
        v = str(self)
        if isinstance(v, I18nString):
            return super().format(*args, **kwargs)
        return v.format(*args, **kwargs)

    def unwrap(self):
        return super().__str__()

    @staticmethod
    def unwrap_strings(obj):
        """Unwrap all keys in I18nStrings in the object"""
        if isinstance(obj, I18nString):
            yield obj.unwrap()
            for v in obj.add_values:
                yield from I18nString.unwrap_strings(v)
            for v in obj.radd_values:
                yield from I18nString.unwrap_strings(v)
            return
        yield obj


def gettext(key: str):
    """Wrapper text string to return I18nString
    :param key: The key of the I18nString
    """
    return I18nString(key)


def iter_i18n_choices(choices):
    """Iterate all I18nStrings in the choice, returns the indices of the I18nStrings"""
    if not isinstance(choices, list) or len(choices) == 0:
        return

    if isinstance(choices[0], tuple):
        for i, (k, v) in enumerate(choices):
            if isinstance(k, I18nString):
                yield i

    else:
        for i, v in enumerate(choices):
            if isinstance(v, I18nString):
                yield i


def iter_i18n_fields(component: gr.components.Component):
    """Iterate all I18nStrings in the component"""
    for name, value in inspect.getmembers(component):
        if name == "value" and hasattr(component, "choices"):
            # for those components with choices, the value will be kept as is
            continue
        if isinstance(value, I18nString):
            yield name
        elif name == "choices" and any(iter_i18n_choices(value)):
            yield name


def iter_i18n_components(block: Block):
    """Iterate all I18nStrings in the block"""
    if isinstance(block, BlockContext):
        for component in block.children:
            for c in iter_i18n_components(component):
                yield c

    if any(iter_i18n_fields(block)):
        yield block


def has_new_i18n_fields(block: Block, existing_translation={}):
    """Check if there are new I18nStrings in the block
    :param block: The block to check
    :param existing_translation: The existing translation dictionary
    :return: True if there are new I18nStrings, False otherwise
    """
    components = list(iter_i18n_components(block))
    for lang in TranslateContext.get_available_languages():
        for component in components:
            for field in iter_i18n_fields(component):
                if field == "choices":
                    for idx in iter_i18n_choices(component.choices):
                        if isinstance(component.choices[idx], tuple):
                            value = component.choices[idx][0]
                        else:
                            value = component.choices[idx]
                        if value not in existing_translation.get(lang, {}):
                            return True
                else:
                    value = getattr(component, field)
                    if value not in existing_translation.get(lang, {}):
                        return True

    return False


def dump_blocks(block: Block, include_translations={}):
    """Dump all I18nStrings in the block to a dictionary
    :param block: The block to dump
    :param include_translations: The existing translation dictionary
    :return: The dumped dictionary
    """
    components = list(iter_i18n_components(block))

    def translate(lang, key):
        return include_translations.get(lang, {}).get(key, key)

    ret = {}

    for lang in TranslateContext.get_available_languages():
        ret[lang] = {}
        for component in components:
            for field in iter_i18n_fields(component):
                if field == "choices":
                    for idx in iter_i18n_choices(component.choices):
                        if isinstance(component.choices[idx], tuple):
                            value = component.choices[idx][0]
                        else:
                            value = component.choices[idx]
                        for key in I18nString.unwrap_strings(value):
                            ret[lang][key] = translate(lang, key)
                else:
                    value = getattr(component, field)
                    for key in I18nString.unwrap_strings(value):
                        ret[lang][key] = translate(lang, key)

    return ret


def translate_blocks(
    block: gr.Blocks = None,
    translation={},
    lang: gr.components.Component = None,
    persistant=False,
):
    """Translate all I18nStrings in the block
    :param block: The block to translate, default is the root block
    :param translation: The translation dictionary
    :param lang: The language component to change the language
    :param persistant: Whether to persist the language
    """
    if block is None:
        block = Context.root_block

    """Translate all I18nStrings in the block"""
    if not isinstance(block, gr.Blocks):
        raise ValueError("block must be an instance of gradio.Blocks")

    components = list(iter_i18n_components(block))
    TranslateContext.add_translation(translation)

    hidden = gr.HTML(
        value="""<style>
                    gradio-app {
                        visibility: hidden;
                    }
                     </style>"""
    )

    if persistant:
        try:
            from gradio import BrowserState
        except ImportError:
            raise ValueError("gradio>=5.6.0 is required for persistant language")

    def on_lang_change(request: gr.Request, lang: str, saved_lang: str):
        if not lang:
            if saved_lang:
                lang = saved_lang
            else:
                lang = TranslateContext.get_lang_from_request(request)

        outputs = [lang, lang, ""]
        TranslateContext.set_current_language(request, lang)

        for component in components:
            fields = list(iter_i18n_fields(component))
            if component == lang and "value" in fields:
                raise ValueError("'lang' component can't has I18nStrings as value")

            modified = {}

            for field in fields:
                if field == "choices":
                    choices = component.choices.copy()
                    for idx in iter_i18n_choices(choices):
                        if isinstance(choices[idx], tuple):
                            k, v = choices[idx]
                            # We don't need to translate the value
                            choices[idx] = (str(k), next(I18nString.unwrap_strings(v)))
                        else:
                            v = choices[idx]
                            choices[idx] = (str(v), next(I18nString.unwrap_strings(v)))
                    modified[field] = choices
                else:
                    modified[field] = str(getattr(component, field))

            new_comp = gr.update(**modified)
            outputs.append(new_comp)

        if len(outputs) == 1:
            return outputs[0]

        return outputs

    if lang is None:
        lang = gr.State()

    if persistant:
        saved_lang = gr.BrowserState(storage_key="lang")
    else:
        saved_lang = gr.State()

    gr.on(
        [block.load, lang.change],
        on_lang_change,
        inputs=[lang, saved_lang],
        outputs=[lang, saved_lang, hidden] + components,
    )


@contextmanager
def Translate(
    translation,
    lang: gr.components.Component = None,
    placeholder_langs=[],
    persistant=False,
):
    """Translate all I18nStrings in the block
    :param translation: The translation dictionary or file path
    :param lang: The language component to change the language
    :param placeholder_langs: The placeholder languages to create a new translation file if translation is a file path
    :param persistant: Whether to persist the language
    :return: The language component
    """
    if lang is None:
        lang = gr.State()
    yield lang

    if isinstance(translation, dict):
        # Static translation
        translation_dict = translation
        pass
    elif isinstance(translation, str):
        if os.path.exists(translation):
            # Regard as a file path
            with open(translation, "r", encoding="utf-8") as f:  # Force utf-8 encoding
                if translation.endswith(".json"):
                    translation_dict = json.load(f)
                elif translation.endswith(".yaml"):
                    translation_dict = yaml.safe_load(f)
                else:
                    raise ValueError("Unsupported file format")
        else:
            translation_dict = {}
    else:
        raise ValueError("Unsupported translation type")

    if placeholder_langs:
        TranslateContext.set_available_languages(placeholder_langs)

    block = Context.block
    translate_blocks(
        block=block, translation=translation_dict, lang=lang, persistant=persistant
    )

    if isinstance(translation, str) and has_new_i18n_fields(
        block, existing_translation=translation_dict
    ):
        merged = dump_blocks(block, include_translations=translation_dict)

        with open(translation, "w") as f:
            if translation.endswith(".json"):
                json.dump(merged, f, indent=2, ensure_ascii=False)
            elif translation.endswith(".yaml"):
                yaml.dump(merged, f, allow_unicode=True, sort_keys=False)
