from typing import List, Optional, Sequence, Union

from draftjs_exporter.constants import BLOCK_TYPES
from draftjs_exporter.dom import DOM
from draftjs_exporter.options import Options, OptionsMap
from draftjs_exporter.types import Block, Element, Props, RenderableType


class Wrapper(object):
    """
    A wrapper is an element that wraps other nodes. It gets created
    when the depth of a block is different than 0, so the DOM elements
    have the appropriate amount of nesting.
    """

    __slots__ = ("depth", "last_child", "type", "props", "elt")

    def __init__(self, depth: int, options: Optional[Options] = None) -> None:
        self.depth = depth
        self.last_child = None

        if options:
            self.type = options.wrapper
            self.props = options.wrapper_props

            wrapper_props = dict(self.props) if self.props else {}
            wrapper_props["block"] = {"type": options.type, "depth": depth}

            self.elt = DOM.create_element(self.type, wrapper_props)
        else:
            self.type = None
            self.props = None
            self.elt = DOM.create_element()

    def is_different(
        self, depth: int, type_: RenderableType, props: Optional[Props]
    ) -> bool:
        return depth > self.depth or type_ != self.type or props != self.props


class WrapperStack(object):
    """
    Stack data structure for element wrappers.
    The bottom of the stack contains the elements closest to the page body.
    The top of the stack contains the most nested nodes.
    """

    __slots__ = "stack"

    def __init__(self) -> None:
        self.stack: List[Wrapper] = []

    def __str__(self) -> str:
        return str(self.stack)

    def length(self) -> int:
        return len(self.stack)

    def append(self, wrapper: Wrapper) -> None:
        return self.stack.append(wrapper)

    def get(self, index: int) -> Wrapper:
        return self.stack[index]

    def slice(self, length: int) -> None:
        self.stack = self.stack[:length]

    def head(self) -> Wrapper:
        if self.length() > 0:
            wrapper = self.stack[-1]
        else:
            wrapper = Wrapper(-1)

        return wrapper

    def tail(self) -> Wrapper:
        return self.stack[0]


class WrapperState(object):
    """
    This class does the initial node building for the tree.
    It sets elements with the right tag, text content, and props.
    It adds a wrapper element around elements, if required.
    """

    __slots__ = ("block_options", "blocks", "stack")

    def __init__(
        self, block_options: OptionsMap, blocks: Sequence[Block]
    ) -> None:
        self.block_options = block_options
        self.blocks = blocks
        self.stack = WrapperStack()

    def __str__(self) -> str:
        return f"<WrapperState: {self.stack}>"

    def element_for(
        self, block: Block, block_content: Union[Element, Sequence[Element]]
    ) -> Element:
        type_ = block["type"] if "type" in block else "unstyled"
        depth = block["depth"] if "depth" in block else 0
        options = Options.get(self.block_options, type_, BLOCK_TYPES.FALLBACK)
        props = dict(options.props)
        props["block"] = block
        props["blocks"] = self.blocks

        # Make an element from the options specified in the block map.
        elt = DOM.create_element(options.element, props, block_content)

        parent = self.parent_for(options, depth, elt)

        return parent

    def parent_for(self, options: Options, depth: int, elt: Element) -> Element:
        if options.wrapper:
            parent = self.get_wrapper_elt(options, depth)
            DOM.append_child(parent, elt)
            self.stack.stack[-1].last_child = elt
        else:
            # Reset the stack if there is no wrapper.
            if self.stack.length() > 0:
                self.stack = WrapperStack()
            parent = elt

        return parent

    def get_wrapper_elt(self, options: Options, depth: int) -> Element:
        head = self.stack.head()
        if head.is_different(depth, options.wrapper, options.wrapper_props):
            self.update_stack(options, depth)

        # If depth is lower than the maximum, we cut the stack.
        if depth < head.depth:
            self.stack.slice(depth + 1)

        return self.stack.get(depth).elt

    def update_stack(self, options: Options, depth: int) -> None:
        if depth >= self.stack.length():
            # If the depth is gte the stack length, we need more wrappers.
            depth_levels = range(self.stack.length(), depth + 1)

            for level in depth_levels:
                new_wrapper = Wrapper(level, options)

                # Determine where to append the new wrapper.
                if self.stack.head().last_child is None:
                    # If there is no content in the current wrapper, we need
                    # to add an intermediary node.
                    props = dict(options.props)
                    props["block"] = {
                        "type": options.type,
                        "depth": depth,
                        "data": {},
                    }
                    props["blocks"] = self.blocks

                    wrapper_parent = DOM.create_element(options.element, props)
                    DOM.append_child(self.stack.head().elt, wrapper_parent)
                else:
                    # Otherwise we can append at the end of the last child.
                    wrapper_parent = self.stack.head().last_child

                DOM.append_child(wrapper_parent, new_wrapper.elt)

                self.stack.append(new_wrapper)
        else:
            # Cut the stack to where it now stops, and add new wrapper.
            self.stack.slice(depth)
            self.stack.append(Wrapper(depth, options))
