from __future__ import annotations

import copy
import inspect
import string
import typing

from .lazy import Lazy, resolve_lazy

EXCEPTION_HANDLER_NAME = "_htmlgenerator_exception_handler"
"Must be a function without arguments, will be called when an exception happens during rendering an element"

# for integration with the django safe string objects, optional
try:
    from django.utils.html import conditional_escape  # type: ignore
except ImportError:
    from .safestring import conditional_escape


class BaseElement(list):
    """The base render element
    All nodes used in a render tree should have this class as a base. Leaves in the tree may be strings or Lazy objects.
    """

    def __init__(self, *children):
        """Uses the given arguments to initialize the list which represents the child objects"""
        super().__init__(children)
        from . import DEBUG

        if DEBUG:
            # This will add the source location of where this element has been instantiated as a data attributte
            # and a python attribute _src_location with (filename, linenumber, functionname) to this object
            for frame in inspect.stack():
                if frame.function != "__init__":
                    break
            if hasattr(self, "attributes"):
                self.attributes[
                    "data_source_location"
                ] = f"{frame.filename}:{frame.lineno} in {frame.function}"
            self._src_location = (frame.filename, frame.lineno, frame.function)

    def _try_render(
        self, element: typing.Any, context: dict, stringify: bool
    ) -> typing.Generator[str, None, None]:
        """Renders an element as a generator which yields strings
        The stringify parameter should normally be true in order return
        escaped strings. In some circumstances if is however desirable to
        get the actual value and not a string returned. For such cases
        stringify can be set to ``False``. An example are HTML attribute values
        which us a ``hg.If`` element and return ``True`` or ``False`` to dynamically
        control the appearance of the attribute.
        The render_children method will use a default of ``True`` for strinfigy.
        That behaviour should only be overriden by elements which consciously want
        to be able to return non-string objects during rendering.
        """
        while isinstance(element, Lazy):
            element = element.resolve(context)
        if isinstance(element, BaseElement):
            yield from element.render(context)
        elif element is not None:
            yield conditional_escape(element) if stringify else element

    def render_children(
        self, context: dict, stringify: bool = True
    ) -> typing.Generator[str, None, None]:
        """Renders all elements inside the list. Can be used by subclassing elements if they need to controll where child elements are rendered."""
        for element in self:
            yield from self._try_render(element, context, stringify)

    def render(
        self, context: dict, stringify: bool = True
    ) -> typing.Generator[str, None, None]:
        """Renders this element and its children. Can be overwritten by subclassing elements."""
        try:
            yield from self.render_children(context, stringify)
        except (Exception, RuntimeError) as e:
            import sys
            import traceback

            def default_handler(context, message):
                traceback.print_exc()
                print(message, file=sys.stderr)

            last_obj = None
            indent = 0
            message = []
            for i in traceback.StackSummary.extract(
                traceback.walk_tb(sys.exc_info()[2]), capture_locals=True
            ):
                if (
                    i.locals is not None
                    and "self" in i.locals
                    and i.locals["self"] != last_obj
                ):
                    message.append(" " * indent + str(i.locals["self"]))
                    last_obj = i.locals["self"]
                    indent += 2
            message.append(" " * indent + str(e))
            message = "\n".join(message)

            context.get(EXCEPTION_HANDLER_NAME, default_handler)(context, message)

            yield (
                ""
                + '<pre style="border: solid 1px red; color: red; padding: 1rem; background-color: #ffdddd">'
                + f"    <code>~~~ Exception: {conditional_escape(e)} ~~~</code>"
                + "</pre>"
                + f'<script>alert("Error: {conditional_escape(e)}")</script>'
            )

    """
    Tree functions
    Tree functions can be used to modify or gathering information from the sub-tree of a BaseElement.
    Tree functions walk the tree with the calling BaseElement as root. The root is not walked itself.
    Tree functions always take an argument filter_func which has the signature (element, ancestors) => bool where ancestors is a tuple of all elements
    filter_func will determine which Child elements inside the sub-tree should be considered.
    Tree functions:
    - filter
    - wrap
    - delete
    """

    def filter(
        self,
        filter_func: typing.Callable[
            [BaseElement, typing.Tuple[BaseElement, ...]], bool
        ],
    ) -> typing.Generator[BaseElement, None, None]:
        """Walks through the tree (including self) and yields each element for which a call to filter_func evaluates to True.
        filter_func expects an element and a tuple of all ancestors as arguments.
        returns: A generater which yields the matching elements
        """

        return treewalk(BaseElement(self), (), filter_func=filter_func)

    def wrap(
        self,
        filter_func: typing.Callable[
            [BaseElement, typing.Tuple[BaseElement, ...]], bool
        ],
        wrapperelement: BaseElement,
    ) -> list[BaseElement]:
        """Walks through the tree (including self) and wraps each element for which a call to filter_func evaluates to True.
        filter_func expects an element and a tuple of all ancestors as arguments.
        wrapper: the element which will wrap the
        """

        def wrappingfunc(container, i, e):
            wrapper = wrapperelement.copy()
            wrapper.append(container[i])
            container[i] = wrapper

        return list(
            treewalk(BaseElement(self), (), filter_func=filter_func, apply=wrappingfunc)
        )

    def delete(
        self,
        filter_func: typing.Callable[
            [BaseElement, typing.Tuple[BaseElement, ...]], bool
        ],
    ) -> list[BaseElement]:
        """Walks through the tree (including self) and removes each element for which a call to filter_func evaluates to True.
        filter_func expects an element and a tuple of all ancestors as arguments.
        """

        def delfunc(container, i, e):
            container.remove(e)

        return list(
            treewalk(BaseElement(self), (), filter_func=filter_func, apply=delfunc)
        )

    def replace(self, *args, **kwargs):
        return self._replace(*args, **kwargs)

    # untested code
    def _replace(
        self, select_func: typing.Callable, replacement: BaseElement, all: bool = False
    ):
        """Replaces an element which matches a certain condition with another element"""
        from .htmltags import HTMLElement

        class ReachFirstException(Exception):
            pass

        def walk(element: typing.List, ancestors: typing.Tuple[BaseElement, ...]):
            replacment_indices = []
            for i, e in enumerate(element):
                if isinstance(e, BaseElement):
                    if select_func(e, ancestors):
                        replacment_indices.append(i)
                    if isinstance(e, HTMLElement):
                        walk(list(e.attributes.values()), ancestors=ancestors + (e,))
                    walk(e, ancestors=ancestors + (e,))
            for i in replacment_indices:
                element.pop(i)
                if replacement is not None:
                    element.insert(i, replacement)
                if not all:
                    raise ReachFirstException()

        try:
            walk(self, (self,))
        except ReachFirstException:
            pass

    def copy(self) -> BaseElement:
        return copy.deepcopy(self)


class If(BaseElement):
    def __init__(
        self,
        condition: typing.Union[bool, Lazy],
        true_child: BaseElement,
        false_child: typing.Optional[BaseElement] = None,
    ):
        """condition: Value which determines which child to render (true_child or false_child. Can also be ContextValue or ContextFunction"""
        super().__init__(true_child, false_child)
        self.condition = condition

    def render(self, context: dict, stringify=True):
        """The stringy argument can be set to False in order to get a python object
        instead of a rendered string returned. This is usefull when evaluating"""
        if resolve_lazy(self.condition, context):
            yield from self._try_render(self[0], context, stringify)
        elif len(self) > 1:
            yield from self._try_render(self[1], context, stringify)


class Iterator(BaseElement):
    def __init__(
        self,
        iterator: typing.Union[typing.Iterable, Lazy],
        loopvariable: str,
        content: BaseElement,
    ):
        self.iterator = iterator
        self.loopvariable = loopvariable
        super().__init__(content)

    def render(self, context: dict, stringify: bool = True):
        context = dict(context)
        for i, value in enumerate(resolve_lazy(self.iterator, context)):
            context[self.loopvariable] = value
            context[self.loopvariable + "_index"] = i
            yield from self.render_children(context, stringify)


class WithContext(BaseElement):
    """
    Pass additional names into the context.
    The additional context names are namespaced to the current element and its child elements.
    It can be helpfull for shadowing or aliasing names in the context.
    This element is required because context is otherwise only set by the render function and the loop-variable of Iterator which can be limiting.
    """

    additional_context: dict = {}

    def __init__(self, *children, **kwargs):
        self.additional_context = kwargs
        super().__init__(*children)

    def render(self, context):
        return super().render({**context, **self.additional_context})


def treewalk(
    element: typing.List,
    ancestors: typing.Tuple[BaseElement, ...],
    filter_func: typing.Optional[
        typing.Callable[[BaseElement, typing.Tuple[BaseElement, ...]], bool]
    ],
    apply: typing.Optional[
        typing.Callable[[BaseElement, int, BaseElement], None]
    ] = None,
) -> typing.Generator[BaseElement, None, None]:
    from .htmltags import HTMLElement

    matchelements = []

    for i, e in enumerate(list(element)):
        if isinstance(e, BaseElement):
            if filter_func is None or filter_func(e, ancestors):
                yield e
                matchelements.append((i, e))
            if isinstance(e, HTMLElement):
                yield from treewalk(
                    list(e.attributes.values()),
                    ancestors + (e,),
                    filter_func=filter_func,
                    apply=apply,
                )
            yield from treewalk(
                e, ancestors + (e,), filter_func=filter_func, apply=apply
            )

    if apply:
        for i, e in matchelements:
            apply(element, i, e)


def render(root: BaseElement, basecontext: dict) -> str:
    """Shortcut to serialize an object tree into a string"""
    return "".join(root.render(basecontext))


def print_logical_tree(root: BaseElement) -> None:
    from .htmltags import HTMLElement

    def print_node(node: BaseElement, level: int) -> None:
        attrlist = [
            f"{attr}: {getattr(node, attr)}"
            for attr in dir(node)
            if not attr.startswith("_")
            and not callable(getattr(node, attr))
            and attr not in ("attributes", "tag")
        ]
        attrs = ", ".join(attrlist)
        name = type(node).__name__
        if name == "__proxy__":
            name = "str"
        neednewlevel = False
        if isinstance(node, str):
            print(level * "    " + f'"{node}"')
            neednewlevel = True
        elif HTMLElement not in type(node).__bases__:
            print(level * "    " + f"{name}({attrs})")
            neednewlevel = True
        if isinstance(node, BaseElement):
            for child in node:
                if child is not None:
                    print_node(child, level + (1 if neednewlevel else 0))

    print_node(root, level=0)


html_id_cache = set()


def html_id(object: typing.Any, prefix: str = "id") -> str:
    """Generate a unique HTML id from an object"""
    # Explanation of the chained call:
    # 1. id: We want a guaranteed unique ID. Since the lifespan of an HTML-response is
    #       shorted than that of the working process, this should be fine. Python
    #       might re-use objects and their according memory, but it seems unlikely
    #       that this will be an issue.
    # 2. str: passing an int (from the id function) into the hash-function will result
    #        in the int beeing passed back. We need to convert the id to a string in
    #        order have the hash function doing some actual hashing.
    # 3. hash: Prevent the leaking of any memory layout information. The python hash
    #         function is hard to reverse (https://en.wikipedia.org/wiki/SipHash)
    # 4. str: Because html-ids need to be strings we convert again to string
    # 5. [1:]: in order to prevent negative numbers we remove the first character which might be a "-"
    _id = prefix + "-" + str(hash(str(id(object))))[1:]
    n = 0
    nid = _id
    while nid in html_id_cache:
        nid = f"{_id}-{n}"
        n += 1
    html_id_cache.add(nid)
    return nid


class ContextFormatter(string.Formatter):
    context: dict

    def __init__(self, context: dict):
        super().__init__()
        self.context = context

    def get_value(self, key, args, kwds):
        return render(BaseElement(super().get_value(key, args, kwds)), self.context)


class FormatString(BaseElement):
    def __init__(self, *args, **kwargs):
        super().__init__()
        self.args = args
        self.kwargs = kwargs

    def render(self, context):
        return ContextFormatter(context).format(*self.args, **self.kwargs)


def format(*args, **kwargs):
    return FormatString(*args, **kwargs)
