"""Dashing GUI module.

Based on https://github.com/FedericoCeratto/dashing
"""
from datetime import datetime
from collections import deque, namedtuple
from buzio import formatStr
try:
    unichr
except NameError:
    unichr = chr

# "graphic" elements

border_bl = u'└'
border_br = u'┘'
border_tl = u'┌'
border_tr = u'┐'
border_h = u'─'
border_v = u'│'
hbar_elements = (u"▏", u"▎", u"▍", u"▌", u"▋", u"▊", u"▉")
vbar_elements = (u"▁", u"▂", u"▃", u"▄", u"▅", u"▆", u"▇", u"█")
braille_left = (0x01, 0x02, 0x04, 0x40, 0)
braille_right = (0x08, 0x10, 0x20, 0x80, 0)
braille_r_left = (0x04, 0x02, 0x01)
braille_r_right = (0x20, 0x10, 0x08)

TBox = namedtuple('TBox', 't x y w h')


class Tile(object):
    """Tile Object."""

    def __init__(
        self,
        title=None,
        border_color=None,
        color=0,
        terminal=None,
        main=False
    ):
        """Init class."""
        self.title = title
        self.color = color
        self.border_color = border_color
        self._terminal = terminal
        self._main = main

    def _display(self, tbox, parent):
        raise NotImplementedError

    def _draw_borders(self, tbox):
        # top border
        print(tbox.t.color(self.border_color) + tbox.t.move(tbox.x, tbox.y) +
              border_tl + border_h * (tbox.w - 2) + border_tr)
        # left and right
        for dx in range(1, tbox.h - 1):
            print(tbox.t.move(tbox.x + dx, tbox.y) + border_v)
            print(tbox.t.move(tbox.x + dx, tbox.y + tbox.w - 1) + border_v)
        # bottom
        print(tbox.t.move(tbox.x + tbox.h - 1, tbox.y) + border_bl +
              border_h * (tbox.w - 2) + border_br)

    def _draw_borders_and_title(self, tbox):
        if self.border_color is not None:
            self._draw_borders(tbox)
        if self.title:
            fill_all_width = (self.border_color is None)
            self._draw_title(tbox, fill_all_width)

        if self.border_color is not None:
            return TBox(tbox.t, tbox.x + 1, tbox.y + 1, tbox.w - 2, tbox.h - 2)

        elif self.title is not None:
            return TBox(tbox.t, tbox.x + 1, tbox.y, tbox.w - 1, tbox.h - 1)

        return TBox(tbox.t, tbox.x, tbox.y, tbox.w, tbox.h)

    def _fill_area(self, tbox, char=""):
        usable_height = tbox.h - 1
        usable_width = tbox.w - 1
        line = "{:{c}^{num}}".format('', c=char, num=usable_width)
        for count in range(1, usable_height):
            print(tbox.t.move(tbox.x + count, tbox.y + 1) + line)

    def display(self):
        """Render current tile and its items.

        Recurse into nested splits if any.
        """
        t = self._terminal
        tbox = TBox(t, 0, 0, t.width, t.height - 1)
        self._fill_area(tbox)

        if self._main:
            self._terminal.clear()
        tbox = TBox(t, 0, 0, t.width, t.height - 1)
        self._display(tbox, None)

    def _draw_title(self, tbox, fill_all_width):
        if not self.title:
            return
        margin = int((tbox.w - len(self.title)) / 20)
        col = '' if self.border_color is None else \
            tbox.t.color(self.border_color)
        if fill_all_width:
            title = ' ' * margin + self.title + \
                ' ' * (tbox.w - margin - len(self.title))
            if ":" in title:
                print(tbox.t.move(tbox.x, tbox.y) + col + "".join(title.split(":")[0]))
                print(tbox.t.move(tbox.x + tbox.h - 1, tbox.y + 1) + col + title.split(":")[1])
            else:
                print(tbox.t.move(tbox.x, tbox.y) + col + title)
        else:
            title = ' ' * margin + self.title + ' ' * margin
            if ":" in title:
                print(tbox.t.move(tbox.x, tbox.y + margin) + col + "".join(title.split(":")[0]))
                print(tbox.t.move(tbox.x + tbox.h - 1, tbox.y + 1) + col + title.split(":")[1])
            else:
                print(tbox.t.move(tbox.x, tbox.y + margin) + col + title)


class Split(Tile):
    """Split Base Class."""

    def __init__(self, *items, **kw):
        """Init class."""
        super(Split, self).__init__(**kw)
        self.items = items

    def _display(self, tbox, parent):
        tbox = self._draw_borders_and_title(tbox)

        if not self.items:
            # empty split
            self._fill_area(tbox, ' ')
            return

        if isinstance(self, VSplit):
            item_height = tbox.h // len(self.items)
            item_width = tbox.w
        else:
            item_height = tbox.h
            item_width = tbox.w // len(self.items)

        x = tbox.x
        y = tbox.y
        for i in self.items:
            i._display(TBox(tbox.t, x, y, item_width, item_height),
                       self)
            if isinstance(self, VSplit):
                x += item_height
            else:
                y += item_width

        # Fill leftover area
        if isinstance(self, VSplit):
            leftover_x = tbox.h - x + 1
            if leftover_x > 0:
                self._fill_area(TBox(tbox.t, x, y, tbox.w, leftover_x), ' ')
        else:
            leftover_y = tbox.w - y + 1
            if leftover_y > 0:
                self._fill_area(TBox(tbox.t, x, y, leftover_y, tbox.h), ' ')


class VSplit(Split):
    """Split Subclass for verical split."""

    pass


class HSplit(Split):
    """Split Subclass for horizontal split."""

    pass


class Text(Tile):
    """Text UI widget."""

    def __init__(self, text, color=0, *args, **kw):
        """Init Class."""
        super(Text, self).__init__(**kw)
        self.text = text
        self.color = color

    def _display(self, tbox, parent):
        tbox = self._draw_borders_and_title(tbox)
        for dx, line in enumerate(self.text.splitlines()):
            print(tbox.t.color(self.color) + tbox.t.move(tbox.x + dx, tbox.y) +
                  line + ' ' * (tbox.w - len(line)))
        dx += 1
        while dx < tbox.h:
            print(tbox.t.move(tbox.x + dx, tbox.y) + ' ' * tbox.w)
            dx += 1


class Log(Tile):
    """Logger-like UI widget."""

    def __init__(self, date_format, *args, **kw):
        """Init class."""
        self.date_format = date_format
        self.logs = deque(maxlen=50)
        super(Log, self).__init__(**kw)

    def _display(self, tbox, parent):
        tbox = self._draw_borders_and_title(tbox)
        n_logs = len(self.logs)
        log_range = min(n_logs, tbox.h)
        start = n_logs - log_range
        print(tbox.t.color(self.color))
        for i in range(0, log_range):
            line = self.logs[start + i]
            print(tbox.t.move(tbox.x + i, tbox.y) + line +
                  ' ' * (tbox.w - len(line)))

        if i < tbox.h:
            for i2 in range(i + 1, tbox.h):
                print(tbox.t.move(tbox.x + i2, tbox.y) + ' ' * tbox.w)

    def append(self, msg):
        """Append new messages to Log."""
        self.logs.append(msg)

    def _get_header(self):
        now = datetime.now()
        date_string = now.strftime(self.date_format)
        return date_string

    def info(self, msg):
        """Append info messages to log."""
        self.logs.append(
            "[{}]".format(self._get_header())
        )
        msg = "[INFO]: {}".format(msg)
        self.logs.append(msg)

    def warn(self, msg):
        """Append warning messages to log."""
        self.logs.append(
            "[{}]".format(self._get_header())
        )
        msg = formatStr.warning(
            "[WARNING]: {}".format(msg),
            use_prefix=False
        )
        self.logs.append(msg)


class HGauge(Tile):
    """Horizontal Gauge UI widget."""

    def __init__(self, label=None, val=100, color=2, **kw):
        """Init class."""
        kw['color'] = color
        super(HGauge, self).__init__(**kw)
        self.value = val
        self.label = label

    def _display(self, tbox, parent):
        tbox = self._draw_borders_and_title(tbox)
        if self.label:
            wi = (tbox.w - len(self.label) - 3) * self.value / 100
            v_center = int((tbox.h) * 0.5)
        else:
            wi = tbox.w * self.value / 100.0
        index = int((wi - int(wi)) * 7)
        bar = hbar_elements[-1] * int(wi) + hbar_elements[index]
        print(tbox.t.color(self.color) + tbox.t.move(tbox.x, tbox.y + 1))
        if self.label:
            pad = tbox.w - 1 - len(self.label) - len(bar)
        else:
            pad = tbox.w - len(bar)
        bar += hbar_elements[0] * pad
        # draw bar
        for dx in range(0, tbox.h):
            m = tbox.t.move(tbox.x + dx, tbox.y)
            if self.label:
                if dx == v_center:
                    # draw label
                    print(m + self.label + ' ' + bar)
                else:
                    print(m + ' ' * len(self.label) + ' ' + bar)
            else:
                print(m + bar)


class VGauge(Tile):
    """Vertical Gauge UI widget."""

    def __init__(self, val=100, color=2, **kw):
        """Init class."""
        kw['color'] = color
        super(VGauge, self).__init__(**kw)
        self.value = val

    def _display(self, tbox, parent):
        tbox = self._draw_borders_and_title(tbox)
        nh = tbox.h * (self.value / 100.5)
        print(tbox.t.move(tbox.x, tbox.y) + tbox.t.color(self.color))
        for dx in range(tbox.h):
            m = tbox.t.move(tbox.x + tbox.h - dx - 1, tbox.y)
            if dx < int(nh):
                bar = vbar_elements[-1] * tbox.w
            elif dx == int(nh):
                index = int((nh - int(nh)) * 8)
                bar = vbar_elements[index] * tbox.w
            else:
                bar = ' ' * tbox.w

            print(m + bar)


class ColorRangeVGauge(Tile):
    """Vertical gauge with color map.

    E.g.: green gauge for values below 50, red otherwise:
    colormap=((50, 2), (100, 1))
    """

    def __init__(self, val=100, colormap=(), **kw):
        """Init class."""
        self.colormap = colormap
        super(ColorRangeVGauge, self).__init__(**kw)
        self.value = val

    def _display(self, tbox, parent):
        tbox = self._draw_borders_and_title(tbox)
        nh = tbox.h * (self.value / 100.5)
        filled_element = vbar_elements[-1]
        for thresh, col in self.colormap:
            if thresh > self.value:
                break
        print(tbox.t.move(tbox.x, tbox.y) + tbox.t.color(col))
        for dx in range(tbox.h):
            m = tbox.t.move(tbox.x + tbox.h - dx - 1, tbox.y)
            if dx < int(nh):
                bar = filled_element * tbox.w
            elif dx == int(nh):
                index = int((nh - int(nh)) * 8)
                bar = vbar_elements[index] * tbox.w
            else:
                bar = ' ' * tbox.w

            print(m + bar)


class VChart(Tile):
    """Vertical chart.

    Values must be between 0 and 100 and can be float.
    """

    def __init__(self, val=100, *args, **kw):
        """Init class."""
        super(VChart, self).__init__(**kw)
        self.value = val
        self.datapoints = deque(maxlen=50)

    def append(self, dp):
        """Append points to chart."""
        self.datapoints.append(dp)

    def _display(self, tbox, parent):
        tbox = self._draw_borders_and_title(tbox)
        filled_element = hbar_elements[-1]
        scale = tbox.w / 100.0
        print(tbox.t.color(self.color))
        for dx in range(tbox.h):
            index = 50 - (tbox.h) + dx
            try:
                dp = self.datapoints[index] * scale
                index = int((dp - int(dp)) * 8)
                bar = filled_element * int(dp) + hbar_elements[index]
                assert len(bar) <= tbox.w, dp
                bar += ' ' * (tbox.w - len(bar))
            except IndexError:
                bar = ' ' * tbox.w
            print(tbox.t.move(tbox.x + dx, tbox.y) + bar)


class HChart(Tile):
    """Horizontal chart, filled."""

    def __init__(self, val=100, *args, **kw):
        """Init class."""
        super(HChart, self).__init__(**kw)
        self.value = val
        self.datapoints = deque(maxlen=500)

    def append(self, dp):
        """Append points to chart."""
        self.datapoints.append(dp)

    def _display(self, tbox, parent):
        tbox = self._draw_borders_and_title(tbox)
        print(tbox.t.color(self.color))
        for dx in range(tbox.h):
            bar = ''
            for dy in range(tbox.w):
                dp_index = - tbox.w + dy
                try:
                    dp = self.datapoints[dp_index]
                    q = (1 - dp / 100) * tbox.h
                    if dx == int(q):
                        index = int((int(q) - q) * 8 - 1)
                        bar += vbar_elements[index]
                    elif dx < int(q):
                        bar += ' '
                    else:
                        bar += vbar_elements[-1]

                except IndexError:
                    bar += ' '

            # assert len(bar) == tbox.w
            print(tbox.t.move(tbox.x + dx, tbox.y) + bar)


class HBrailleChart(Tile):
    """Horizontal Braille Chart UI widget."""

    def __init__(self, val=100, *args, **kw):
        """Init class."""
        super(HBrailleChart, self).__init__(**kw)
        self.value = val
        self.datapoints = deque(maxlen=500)

    def append(self, dp):
        """Append points to chart."""
        self.datapoints.append(dp)

    def _generate_braille(self, l, r):
        v = 0x28 * 256 + (braille_left[l] + braille_right[r])
        return unichr(v)

    def _display(self, tbox, parent):
        tbox = self._draw_borders_and_title(tbox)
        print(tbox.t.color(self.color))
        for dx in range(tbox.h):
            bar = ''
            for dy in range(tbox.w):
                dp_index = (dy - tbox.w) * 2
                try:
                    dp1 = self.datapoints[dp_index]
                    dp2 = self.datapoints[dp_index + 1]
                except IndexError:
                    # no data (yet)
                    bar += ' '
                    continue

                q1 = (1 - dp1 / 100) * tbox.h
                q2 = (1 - dp2 / 100) * tbox.h
                if dx == int(q1):
                    index1 = int((q1 - int(q1)) * 4)
                    if dx == int(q2):  # both datapoints in the same rune
                        index2 = int((q2 - int(q2)) * 4)
                    else:
                        index2 = -1  # no dot
                    bar += self._generate_braille(index1, index2)
                elif dx == int(q2):
                    # the right dot only is in the current rune
                    index2 = int((q2 - int(q2)) * 4)
                    bar += self._generate_braille(-1, index2)
                else:
                    bar += ' '

            print(tbox.t.move(tbox.x + dx, tbox.y) + bar)


class HBrailleFilledChart(Tile):
    """Horizontal Filled Braille Chart UI Widget."""

    def __init__(self, val=100, *args, **kw):
        """Init class."""
        super(HBrailleFilledChart, self).__init__(**kw)
        self.value = val
        self.datapoints = deque(maxlen=500)

    def append(self, dp):
        """Append points to chart."""
        self.datapoints.append(dp)

    def _generate_braille(self, lmax, rmax):
        v = 0x28 * 256
        for l in range(lmax):
            v += braille_r_left[l]
        for r in range(rmax):
            v += braille_r_right[r]
        return unichr(v)

    def _display(self, tbox, parent):
        tbox = self._draw_borders_and_title(tbox)
        print(tbox.t.color(self.color))
        for dx in range(tbox.h):
            bar = ''
            for dy in range(tbox.w):
                dp_index = (dy - tbox.w) * 2
                try:
                    dp1 = self.datapoints[dp_index]
                    dp2 = self.datapoints[dp_index + 1]
                except IndexError:
                    # no data (yet)
                    bar += ' '
                    continue

                q1 = (1 - dp1 / 100.0) * tbox.h
                q2 = (1 - dp2 / 100.0) * tbox.h
                if dx == int(q1):
                    index1 = 3 - int((q1 - int(q1)) * 4)
                elif dx > q1:
                    index1 = 3
                else:
                    index1 = 0
                if dx == int(q2):
                    index2 = 3 - int((q2 - int(q2)) * 4)
                elif dx > q2:
                    index2 = 3
                else:
                    index2 = 0
                bar += self._generate_braille(index1, index2)

            print(tbox.t.move(tbox.x + dx, tbox.y) + bar)
