import inspect
import string

from typing import Callable, List

TEMPLATE_FRAGMENT_KEY_TEMPLATE = "_template_fragment_cache_%s%s"
# Used to remove control characters and whitespace from cache keys.
valid_chars = set(string.ascii_letters + string.digits + "_.")
del_chars = "".join(c for c in map(chr, range(256)) if c not in valid_chars)
null_control = ({k: None for k in del_chars},)


def wants_args(f: Callable) -> bool:
    """Check if the function wants any arguments"""
    arg_spec = inspect.getfullargspec(f)
    return bool(arg_spec.args or arg_spec.varargs or arg_spec.varkw)


def get_function_parameters(f: Callable) -> List:
    """Get function parameters
    :param f
    :return: Parameter list of function
    """
    return list(inspect.signature(f).parameters.values())


def get_arg_names(f: Callable) -> List[str]:
    """Return arguments of function
    :param f:
    :return: String list of arguments
    """
    return [
        parameter.name
        for parameter in get_function_parameters(f)
        if parameter.kind == parameter.POSITIONAL_OR_KEYWORD
    ]


def get_arg_default(f: Callable, position: int):
    arg = get_function_parameters(f)[position]
    arg_def = arg.default
    return arg_def if arg_def != inspect.Parameter.empty else None


def get_id(obj):
    return getattr(obj, "__caching_id__", repr)(obj)


def function_namespace(f, args=None):
    """Attempts to returns unique namespace for function"""
    m_args = get_arg_names(f)

    instance_token = None

    instance_self = getattr(f, "__self__", None)

    if instance_self and not inspect.isclass(instance_self):
        instance_token = get_id(f.__self__)
    elif m_args and m_args[0] == "self" and args:
        instance_token = get_id(args[0])

    module = f.__module__

    if m_args and m_args[0] == "cls" and not inspect.isclass(args[0]):
        raise ValueError(
            "When using `delete_memoized` on a "
            "`@classmethod` you must provide the "
            "class as the first argument"
        )

    if hasattr(f, "__qualname__"):
        name = f.__qualname__
    else:
        klass = getattr(f, "__self__", None)

        if klass and not inspect.isclass(klass):
            klass = klass.__class__

        if not klass:
            klass = getattr(f, "im_class", None)

        if not klass:
            if m_args and args:
                if m_args[0] == "self":
                    klass = args[0].__class__
                elif m_args[0] == "cls":
                    klass = args[0]

        if klass:
            name = klass.__name__ + "." + f.__name__
        else:
            name = f.__name__

    ns = ".".join((module, name)).translate(*null_control)

    ins = ".".join((module, name, instance_token)) \
        .translate(*null_control) if instance_token else None

    return ns, ins


def make_template_fragment_key(
    fragment_name: str,
    vary_on: List[str] = None
) -> str:
    """Make a cache key for a specific fragment name."""
    if vary_on:
        fragment_name = "%s_" % fragment_name
    else:
        vary_on = []
    return TEMPLATE_FRAGMENT_KEY_TEMPLATE % (fragment_name, "_".join(vary_on))
