import string
import re
from manifest_edit.context import Context
import inspect
import tokenize
from io import BytesIO
from .iso639 import ISO639


class UtilitiesCallableFromYAML:
    """
    We want to be able to provide users with utility functions that can be
    invoked from YAML.

    The usage would be for example to apply transformations such as
    translating a language code to its name based on the ISO639 standard.

    We'd like to enable a syntax such as label : 'ISO639Name({lang})'

    which would involve reading the @lang attribute, and tranforming it to
    its name, then use it for the Label field.

    I don't want people to urn whatever they want but only methods of this
    class. I'll run a sanity check on the string to see if
    it calls an allowed function. Otherwise, nothing will
    be eval-ed. Hopefully this is enough in terms of security.

    Notice that this are supposed to be string to string methods, should never
    throw and all errors should be intercepted.
    """

    @staticmethod
    def ISO639Name(language_code: str) -> str:
        if language_code:
            try:
                language = ISO639.get_by_code(language_code)
                if language:
                    return language.name
                else:
                    Context.log_warning(
                        f"ISO639Name Error: Language Code {language_code} not found"
                    )
                    return f"Language Code {language_code} not found"
            except Exception as e:
                Context.log_warning(
                    f"Error while executing function ISO639Name invoked from YAML:\n"
                    f"{e}\n"
                    f"The corresponding manifest field may be empty"
                )
        return ""

    @staticmethod
    def ISO639Alpha2(language_name: str) -> str:
        if language_name:
            try:
                language = ISO639.get_by_any(language_name)
                if language:
                    try:
                        return language.alpha_2
                    except AttributeError:
                        # Some languages do not have an alpha2 representations
                        return f"No alpha-2 representations exists for {language_name}"
                else:
                    Context.log_warning(
                        f"ISO639Alpha2 Error: Language Name {language_name} not found"
                    )
                    return f"Language Name {language_name} not found"
            except Exception as e:
                Context.log_warning(
                    f"Error while executing function ISO639Alpha2 invoked from YAML:\n"
                    f"{e}\n"
                    f"The corresponding manifest field may be empty"
                )
        return ""

    @staticmethod
    def ISO639Alpha3(language_name: str) -> str:
        if language_name:
            try:
                language = ISO639.get_by_any(language_name)
                if language:
                    return language.alpha_3
                else:
                    Context.log_warning(
                        f"ISO639Alpha3 Error: Language Name {language_name} not found"
                    )
                    return f"Language Name {language_name} not found"
            except Exception as e:
                Context.log_warning(
                    f"Error while executing function ISO639Alpha3 invoked from YAML:\n"
                    f"{e}\n"
                    f"The corresponding manifest field may be empty"
                )
        return ""


def _tokenize(field: str):
    tokens = [tk for tk in tokenize.tokenize(BytesIO(field.encode("utf-8")).readline)]
    # First token must be an allowed function name
    if tokens[1].type == tokenize.NAME:
        allowed_functions = [
            name
            for name, function in inspect.getmembers(
                UtilitiesCallableFromYAML, inspect.isfunction
            )
        ]
        if tokens[1].string in allowed_functions:
            # Then a parenthesys
            if tokens[2].type == tokenize.OP and tokens[2].string == "(":
                # Then the argument to the function
                if tokens[3].type == tokenize.NAME:
                    return tokens[1].string, tokens[3].string

    return None, None


def renderFormatString(element, user_string):
    """
    NOTICE: this is practical but a bit dangerous.

    user_string can be a format string as in 'language={lang}'
    This will be treated as a python format string, where the
    variable "lang" will be resolved as getattr(element, "lang").

    i.e. if a label is being added to a representation, the user
    string can include any attribute of that representation:

    data: {width}x{height}
    """
    # Get the list of field names
    fields = [
        tup[1] for tup in string.Formatter().parse(user_string) if tup[1] is not None
    ]

    rendered_string = user_string
    for field in fields:
        # Try and see if the field includes one of the admitted functions
        (function, argument) = _tokenize(field)
        # replace each argument with the getattr
        if function and argument:
            pattern = f"{{{function}\\({argument}\\)}}"
            repl = f'{{UtilitiesCallableFromYAML.{function}(getattr(element,"{argument}",None))}}'
        else:
            pattern = f"{{{field}}}"
            repl = f'{{getattr(element,"{field}",None)}}'

        rendered_string = re.sub(pattern, repl, rendered_string)

    return eval(f"f'{rendered_string}'")
