"""
Level generator utilities.
"""

import weakref
from math import floor
from random import randrange, choice

from .blt.nice_terminal import terminal
from .geom import Point, Rect, Size
from .draw import draw_rect


class BSPNode:
    """
    Node in a binary space partitioning tree

    .. py:attribute:: rect

      :py:class:`Rect` represented by this node

    .. py:attribute:: is_horz

      ``True`` iff this node is divided down its Y axis; ``False`` otherwise

    .. py:attribute:: value

      Int representing split point between the two children of this node. So
      if this is a horizontal node and the width is 10, the value could be 6,
      with the left node taking up 6 cells and the right taking up 4.

    .. py:attribute:: child_a

      :py:class:`BSPNode` either on the left (horizontal) or on top (vertical).

    .. py:attribute:: child_b

      :py:class:`BSPNode` either on the right (horizontal) or on bottom (vertical).

    .. py:attribute:: level

      How many levels of parents does this node have?

    .. py:attribute:: data

      Dict of arbitrary data for your game's use.
    """

    def __init__(self, rect, is_horz=True, value=None, level=0):
        """
        :param Rect rect:
        :param bool is_horz:
        :param int|None value:
        :param int level:
        """
        self.parent_weakref = lambda: None
        self.rect = rect
        self.level = level
        self.is_horz = is_horz
        self.value = value
        self.child_a = None
        self.child_b = None
        self.data = {}  # put whatever you want in here

    @property
    def max_value(self):
        """Max value of :py:attr:`BSPNode.value`"""
        if self.is_horz:
            return self.rect.size.width - 1
        else:
            return self.rect.size.height - 1

    def _get_next_rect(self, is_a):
        if self.is_horz:
            if is_a:
                return Rect(self.rect.origin, Size(self.value, self.rect.height))
            else:
                return Rect(
                    self.rect.origin + Point(self.value + 1, 0),
                    Size(self.rect.width - self.value - 1, self.rect.height))
        else:
            if is_a:
                return Rect(self.rect.origin, Size(self.rect.width, self.value))
            else:
                return Rect(
                    self.rect.origin + Point(0, self.value + 1),
                    Size(self.rect.width, self.rect.height - self.value - 1))

    @property
    def rect_a(self):
        """Assuming :py:attr:`BSPNode.value` has already been set, return the
        :py:class:`Rect` of child A"""
        return self._get_next_rect(True)

    @property
    def rect_b(self):
        """Assuming :py:attr:`BSPNode.value` has already been set, return the
        :py:class:`Rect` of child B"""
        return self._get_next_rect(False)

    def get_node_at_path(self, spec=''):
        """
        Given a string containing only the characters ``'a'`` and ``'b'``, return
        the node matching the given branches. For example, in a tree with 4
        leaves, ``root.get_node_at_path('aa')`` would return the left/top-most
        leaf.
        """
        if spec:
            if spec[0] == 'a':
                return self.child_a.get_node_at_path(spec[1:])
            elif spec[0] == 'b':
                return self.child_b.get_node_at_path(spec[1:])
            else:
                raise ValueError("Invalid character: {}".format(spec[0]))
        else:
            return self

    def __repr__(self):
        tag = 'horz' if self.is_horz else 'vert'
        return 'BSPNode({}, {})'.format(tag, self.value)

    @property
    def leaves(self):
        """Iterator of all leaves, left/top-to-right/bottom"""
        if self.child_a and self.child_b:
            yield from self.child_a.leaves
            yield from self.child_b.leaves
        else:
            yield self

    @property
    def sibling_pairs(self):
        """Iterator of all pairs of siblings"""
        if not self.child_a or not self.child_b:
            return
        yield from self.child_a.sibling_pairs
        yield from self.child_b.sibling_pairs
        yield (self.child_a, self.child_b)

    @property
    def leftmost_leaf(self):
        """The left/top-most leaf in the tree"""
        if self.child_a:
            return self.child_a.leftmost_leaf
        else:
            return self

    @property
    def rightmost_leaf(self):
        """The right/bottom-most leaf in the tree"""
        if self.child_b:
            return self.child_b.leftmost_leaf
        else:
            return self

    def random_leaf(self):
        """Returns a random leaf"""
        if self.child_a or self.child_b:
            return choice((self.child_a, self.child_b)).random_leaf
        else:
            return self

    @property
    def ancestors(self):
        """Iterator of ``self`` and all parents, starting with first parent"""
        yield self
        parent = self.parent_weakref()
        if parent:
            yield from parent.ancestors


def DEFAULT_RANDRANGE_FUNC(_, a, b): return randrange(a, b)


class RandomBSPTree:
    """
    A randomly generated BSP tree. Pass a dungeon size and minimum leaf size.
    After initialization, the root's leaves represent non-overlapping rectangles
    that completely fill the space.

    .. py:attribute:: root

      :py:class:`BSPNode` root of all children
    """

    def __init__(self, size, min_leaf_size, randrange_func=DEFAULT_RANDRANGE_FUNC):
        """
        :param Size size:
        :param int min_leaf_size: Minimum size of leaf nodes on both axes
        :param function randrange_func: A function ``fn(level, min_size, max_size)``
                                        that returns the ``value`` (see
                                        :py:class:`BSPnode`) of the node at the
                                        given level of recursion. Defaults to
                                        ``randrange()``, but you can use this to
                                        de-randomize specific splits.
        """
        self.randrange_func = randrange_func
        self.min_leaf_size = min_leaf_size
        self.root = BSPNode(Rect(Point(0, 0), size))
        self.subdivide(self.root)

    def subdivide(self, node, iterations_left=8):
        if iterations_left < 1:
            return
        if self.add_children(node):
            self.subdivide(node.child_a, iterations_left=iterations_left - 1)
            self.subdivide(node.child_b, iterations_left=iterations_left - 1)

    def add_children(self, node):
        a = self.min_leaf_size
        b = node.max_value - self.min_leaf_size * 2
        if b - a < 1:
            return False
        node.value = self.randrange_func(node.level, a, b)
        node.child_a = BSPNode(
            node.rect_a, not node.is_horz, level=node.level + 1)
        node.child_a.parent_weakref = weakref.ref(node)
        node.child_b = BSPNode(
            node.rect_b, not node.is_horz, level=node.level + 1)
        node.child_b.parent_weakref = weakref.ref(node)
        return True

    def draw(self, n=None, color_so_far='#f'):
        if n is None:
            n = self.root
        if n.child_a or n.child_b:
            if n.child_a:
                self.draw(n.child_a, color_so_far + 'f')
            if n.child_b:
                self.draw(n.child_b, color_so_far + '0')
            return
        color = color_so_far + '0' * (7 - len(color_so_far))
        terminal.color(color)
        draw_rect(n.rect)
        terminal.print(n.rect.origin, color)
        terminal.bkcolor('#000000')
