"""
This module implements bar chart renderers.
"""

from abc import abstractmethod

from asciimatics.constants import DOUBLE_LINE, SINGLE_LINE
from asciimatics.renderers.base import DynamicRenderer
from asciimatics.screen import Screen
from asciimatics.utilities import BoxTool


class _BarChartBase(DynamicRenderer):
    #: Constant to indicate no axes should be rendered.
    NONE = 0
    NO_AXIS = 0

    #: Constant to indicate just the x axis should be rendered.
    X_AXIS = 1

    #: Constant to indicate just the y axis should be rendered.
    Y_AXIS = 2

    #: Constant to indicate both axes should be rendered.
    BOTH = 3
    BOTH_AXES = 3

    def __init__(self, height, width, functions, char="#", colour=Screen.COLOUR_GREEN,
                 bg=Screen.COLOUR_BLACK, gradient=None, scale=None, axes=Y_AXIS, intervals=None,
                 labels=False, border=True, keys=None, gap=None):
        # See children BarChart and VBarChart for argument descriptions and pydocs
        super().__init__(height, width)
        self._functions = functions
        self._char = char
        self._colours = [colour] if isinstance(colour, int) else colour
        self._bgs = [bg] if isinstance(bg, int) else bg
        self._scale = scale
        self._axes = axes
        self._intervals = intervals
        self._labels = labels
        self._border = border
        self._keys = keys
        self._gap = gap

        # Box drawing tool for border, allows user to change the border line style
        self._border_lines = BoxTool(self._canvas.unicode_aware, DOUBLE_LINE) if border else None

        # Box drawing tool for axes
        self._axes_lines = BoxTool(self._canvas.unicode_aware, SINGLE_LINE)

        # Normalize the gradient so that it is 3-tuple wide (bg is optional, if not there, set it)
        self._gradient = None
        if gradient:
            self._gradient = []
            for item in gradient:
                if len(item) == 2:
                    self._gradient.append((item[0], item[1], Screen.COLOUR_BLACK))
                elif len(item) == 3:
                    self._gradient.append(item)
                else:
                    raise ValueError("Gradients must be 2-tuple or 3-tuple in size")

    @abstractmethod
    def _render_now(self):
        pass

    @property
    def border_style(self):
        """
        The current drawing style of the border. Possible values are defined in
        :mod:`~asciimatics.constants`:

        * `ASCII_LINE` -- ASCII safe characters
        * `SINGLE_LINE` -- UNICODE based single line
        * `DOUBLE_LINE` -- UNICODE based double line

        Note that your canvas must support UNICODE style characters to use them
        """
        return self._border_lines.style

    @border_style.setter
    def border_style(self, style):
        if self._border_lines:
            self._border_lines.style = style

    @property
    def axes_style(self):
        """
        The current drawing style of the axes. Possible values are defined in
        :mod:`~asciimatics.constants`:

        * `ASCII_LINE` -- ASCII safe characters
        * `SINGLE_LINE` -- UNICODE based single line

        Note that your canvas must support UNICODE style characters to use them
        """
        return self._axes_lines.style

    @axes_style.setter
    def axes_style(self, style):
        self._axes_lines.style = style

    def _setup_chart(self):
        """
        Draws any borders and returns initial height, width, and starting X and Y.
        """
        # Dimensions for the chart.
        int_h = self._canvas.height
        int_w = self._canvas.width
        start_x = 0
        start_y = 0

        # Create  the box around the chart...
        if self._border:
            draw = self._border_lines.box_top(self._canvas.width)
            self._write(draw, 0, 0)
            for line in range(1, self._canvas.height):
                self._write(self._border_lines.v, 0, line)
                self._write(self._border_lines.v, self._canvas.width - 1, line)
            draw = self._border_lines.box_bottom(self._canvas.width)
            self._write(draw, 0, self._canvas.height - 1)
            int_h -= 4
            int_w -= 6
            start_y += 2
            start_x += 3

        return int_h, int_w, start_x, start_y


class BarChart(_BarChartBase):
    """
    Renderer to create a horizontal bar chart using the specified functions as inputs for each
    entry.  Can be used to chart distributions or for more graphical effect - e.g. to imitate a
    sound equalizer or a progress indicator.
    """

    def __init__(self, height, width, functions, char="#", colour=Screen.COLOUR_GREEN,
                 bg=Screen.COLOUR_BLACK, gradient=None, scale=None, axes=_BarChartBase.Y_AXIS,
                 intervals=None, labels=False, border=True, keys=None, gap=None):
        """
        :param height: The max height of the rendered image.
        :param width: The max width of the rendered image.
        :param functions: List of functions to chart.
        :param char: Character to use for the bar. Defaults to '#'
        :param colour: Colour(s) to use for the bars.  This can be a single value or list of
            values (to cycle around for each bar). Defaults to green.
        :param bg: Background colour to use for the bars.  This can be a single value or list of
            values (to cycle around for each bar). Defaults to black.
        :param gradient: Colour gradient to use for the bars.  This is a list of tuple pairs
            specifying a threshold and a colour, or triplets to include a background colour too.
            Defaults to no gradients.
        :param scale: Maximum value for the bars.  This is used to scale the function values to
            the maximum space available.  Any value over this will be truncated when drawn.
            Defaults to the number of available characters in the chart.
        :param axes: Which axes to draw.
        :param intervals: Units for interval markers on the main axis. Defaults to none.
        :param labels: Whether to draw size indication labels on the x-axis.
        :param border: Whether to draw a border around the chart.
        :param keys: Optional keys to name each bar on the y-axis.
        :param gap: distance between bars. A value of None will auto-calculate (default).

        If the scale parameter is not specified, the maximum length of the bar is based on the
        available space. A chart with no borders, no axes, no keys or labels will have a bar
        length based solely on the width of the graph.

            * Borders use 4 characters height and 6 characters width
            * Keys use the width of the widest key plus 1
            * Labels use a height of 1
            * An X_AXIS uses a height of 1
            * A Y_AXIS uses a width of 1
        """
        # Have to have a call to super as the defaults for the class are different than the parent
        super().__init__(
            height, width, functions, char, colour, bg, gradient, scale, axes, intervals, labels, border,
            keys, gap)

    def _render_now(self):
        int_h, int_w, start_x, start_y = self._setup_chart()

        # Make room for the keys if supplied.
        if self._keys:
            max_key = max(len(x) for x in self._keys)
            key_x = start_x
            int_w -= max_key + 1
            start_x += max_key + 1

        # Now add the axes - resizing chart space as required...
        if (self._axes & BarChart.X_AXIS) > 0:
            int_h -= 1

        if (self._axes & BarChart.Y_AXIS) > 0:
            int_w -= 1
            start_x += 1

        if self._labels:
            int_h -= 1

        # Use given scale or whatever space is left in the grid
        scale = int_w if self._scale is None else self._scale

        if self._axes & BarChart.X_AXIS:
            self._write(self._axes_lines.h * int_w, start_x, start_y + int_h)
        if self._axes & BarChart.Y_AXIS:
            for line in range(int_h):
                self._write(self._axes_lines.v, start_x - 1, start_y + line)
        if self._axes & BarChart.BOTH == BarChart.BOTH:
            self._write(self._axes_lines.up_right, start_x - 1, start_y + int_h)

        if self._labels:
            pos = start_y + int_h
            if self._axes & BarChart.X_AXIS:
                pos += 1

            self._write("0", start_x, pos)
            text = str(scale)
            self._write(text, start_x + int_w - len(text), pos)

        # Now add any interval markers if required...
        if self._intervals is not None:
            i = self._intervals
            while i < scale:
                x = start_x + int(i * int_w / scale) - 1
                for line in range(int_h):
                    self._write(self._axes_lines.v_inside, x, start_y + line)
                self._write(self._axes_lines.h_up, x, start_y + int_h)
                if self._labels:
                    val = str(i)
                    self._write(val, x - (len(val) // 2), start_y + int_h + 1)
                i += self._intervals

        # Allow double-width bars if there's space.
        bar_size = 2 if int_h >= (3 * len(self._functions)) - 1 else 1

        gap = self._gap
        if self._gap is None:
            gap = 0 if len(self._functions) <= 1 else (int_h - (bar_size * len(
                self._functions))) / (len(self._functions) - 1)

        # Now add the bars...
        for i, fn in enumerate(self._functions):
            bar_len = int(fn() * int_w / scale)
            y = start_y + (i * bar_size) + int(i * gap)

            # First draw the key if supplied
            if self._keys:
                key = self._keys[i]
                pos = max_key - len(key)
                self._write(key, key_x + pos, y)

            # Now draw the bar
            colour = self._colours[i % len(self._colours)]
            bg = self._bgs[i % len(self._bgs)]
            if self._gradient:
                # Colour gradient required - break down into chunks for each colour.
                last = 0
                size = 0
                for threshold, colour, bg in self._gradient:
                    value = int(threshold * int_w / scale)
                    if value - last > 0:
                        # Size to fit the available space
                        size = value if bar_len >= value else bar_len
                        size = min(size, int_w)
                        for line in range(bar_size):
                            self._write(
                                self._char * (size - last), start_x + last, y + line, colour, bg=bg)

                    # Stop if we reached the end of the line or the chart
                    if bar_len < value or size >= int_w:
                        break
                    last = value
            else:
                # Solid colour - just write the whole block out.
                for line in range(bar_size):
                    self._write(self._char * bar_len, start_x, y + line, colour, bg=bg)

        return self._plain_image, self._colour_map


class VBarChart(_BarChartBase):
    """
    Renderer to create a vertical bar chart using the specified functions as inputs for each
    entry.  Can be used to chart distributions or for more graphical effect - e.g. to imitate a
    sound equalizer or a progress indicator.
    """

    def __init__(self, height, width, functions, char="#", colour=Screen.COLOUR_GREEN,
                 bg=Screen.COLOUR_BLACK, gradient=None, scale=None, axes=_BarChartBase.X_AXIS,
                 intervals=None, labels=False, border=True, keys=None, gap=None):
        """
        :param height: The max height of the rendered image.
        :param width: The max width of the rendered image.
        :param functions: List of functions to chart.
        :param char: Character to use for the bar. Defaults to '#'
        :param colour: Colour(s) to use for the bars.  This can be a single value or list of
            values (to cycle around for each bar). Defaults to green.
        :param bg: Background colour to use for the bars.  This can be a single value or list of
            values (to cycle around for each bar). Defaults to black.
        :param gradient: Colour gradient to use for the bars.  This is a list of tuple pairs
            specifying a threshold and a colour, or triplets to include a background colour too.
            Defaults to no gradients.
        :param scale: Maximum value for the bars.  This is used to scale the function values to
            the maximum space available.  Any value over this will be truncated when drawn.
            Defaults to the number of available characters in the chart.
        :param axes: Which axes to draw.
        :param intervals: Units for interval markers on the main axis. Defaults to none.
        :param labels: Whether to draw size indication labels on the y-axis.
        :param border: Whether to draw a border around the chart.
        :param keys: Optional keys to name each bar on the x-axis.
        :param gap: distance between bars. A value of None will auto-calculate (default). Minimum
            value when auto-calculated is 1, for no gaps specify 0.

        If the scale parameter is not specified, the maximum length of the bar is based on the
        available space. A chart with no borders, no axes, no keys or labels will have a bar
        height based solely on the width of the graph.

            * Borders use 4 characters height and 6 characters width
            * Keys use a height of 1
            * Labels vertical bar chart use the width of the widest label plus 1 (label values
              depend on the scale of the chart)
            * An X_AXIS uses a height of 1
            * A Y_AXIS uses a width of 1
        """
        super().__init__(
            height, width, functions, char, colour, bg, gradient, scale, axes, intervals, labels, border,
            keys, gap)

    def _render_now(self):
        int_h, int_w, start_x, start_y = self._setup_chart()

        # Make room for the keys if supplied.
        if self._keys:
            int_h -= 1

        # Now add the axes - resizing chart space as required...
        if self._axes & VBarChart.X_AXIS:
            int_h -= 1

        if self._axes & VBarChart.Y_AXIS:
            int_w -= 1
            start_x += 1

        # Use given scale or whatever space is left in the grid
        scale = int_h if self._scale is None else self._scale

        # Calculate labels and intervals, adjust width based on widest label
        if self._labels:
            labels = [('', False) for x in range(int_h)]
            labels[0] = ('0', False)
            labels[-1] = (str(scale), False)
            if self._intervals:
                next_interval = self._intervals
                # pylint: disable-next=consider-using-enumerate
                for i in range(0, len(labels)):
                    value = (i + 1) * scale / int_h
                    if value >= next_interval:
                        labels[i] = (str(next_interval), True)
                        next_interval += self._intervals

            # Change size based on
            widest_label = max(len(x[0]) for x in labels)
            int_w -= widest_label + 1
            start_x += widest_label + 1

        if self._axes & VBarChart.X_AXIS:
            self._write(self._axes_lines.h * int_w, start_x, start_y + int_h)
        if self._axes & VBarChart.Y_AXIS:
            for line in range(int_h):
                self._write(self._axes_lines.v, start_x - 1, start_y + line)
        if self._axes & VBarChart.BOTH == BarChart.BOTH:
            self._write(self._axes_lines.up_right, start_x - 1, start_y + int_h)

        # Draw labels and intervals
        if self._labels:
            y = start_y + int_h - 1
            for label, interval in labels:
                x = start_x - len(label) - 1
                if label != '':
                    self._write(label, x, y)
                    if interval:
                        self._write(self._axes_lines.v_right, start_x - 1, y)
                        self._write(self._axes_lines.h_inside * int_w, start_x, y)
                y -= 1

        # Size bars based on available space
        bar_width = int_w
        gap = 0
        if len(self._functions) > 1:
            if self._gap is None:
                # Evenly size bars and gaps
                bars_and_gaps = 2 * len(self._functions) - 1
                bar_width = int_w // bars_and_gaps
                gap = bar_width
                total_gap_space = gap * (len(self._functions) - 1)
            else:
                # Use given gap size, calculate bar width
                gap = self._gap
                total_gap_space = gap * (len(self._functions) - 1)
                total_bar_space = int_w - total_gap_space
                bar_width = total_bar_space // len(self._functions)

        if bar_width <= 0:
            raise ValueError(
                f"Not enough space. {len(self._functions)} bars + {total_gap_space} space for gaps "
                f"is > your graph width of {int_w}")

        # Write keys
        if self._keys:
            y = start_y + int_h
            if self._axes & VBarChart.X_AXIS:
                y += 1

            x = start_x
            for key in self._keys:
                self._write(key, x, y)
                x += bar_width + gap

        # Write bars
        values = [fn() for fn in self._functions]
        scale_factor = scale / int_h

        for pos in range(1, int_h + 1):
            x = start_x
            threshold = pos * scale_factor - (scale_factor / 2)

            for index, value in enumerate(values):
                colour = self._colours[index % len(self._colours)]
                bg = self._bgs[index % len(self._bgs)]
                if value >= threshold:
                    if self._gradient:
                        # First gradient is the base colour
                        draw_colour = self._gradient[0][1]
                        draw_bg = self._gradient[0][2]

                        # Loop through gradients to see if the colour should
                        # be incremented to next value
                        pos_value = scale * (pos / int_h)
                        for gradient in self._gradient[1:]:
                            if pos_value >= gradient[0]:
                                draw_colour = gradient[1]
                                draw_bg = gradient[2]
                            else:
                                break
                    else:
                        draw_colour = colour
                        draw_bg = bg

                    self._write(self._char * bar_width, x, start_y + int_h - pos, draw_colour, bg=draw_bg)

                x += bar_width + gap

        return self._plain_image, self._colour_map
