#!/usr/bin/env python
# -*- coding: utf-8 -*-

from __future__ import unicode_literals
import sys
import re
import collections
import copy


__version__ = '1.5.1'


class Pyasciigraph:

    def __init__(self, line_length=79,
                 min_graph_length=50,
                 separator_length=2,
                 force_max_value=None,
                 graphsymbol=None,
                 multivalue=True,
                 human_readable=None,
                 float_format='{0:.0f}',
                 titlebar='#'
                 ):
        """Constructor of Pyasciigraph

        :param line_length: the max number of char on a line
          if any line cannot be shorter,
          it will go over this limit.
          Default: 79
        :type line_length: int
        :param min_graph_length: the min number of char
          used by the graph itself.
          Default: 50
        :type min_graph_length: int
        :param force_max_value: if provided, force a max value in order to graph
          each line with respect to it (only taking the actual max value if
          it is greater).
        :type: force_max_value: int
        :param separator_length: the length of field separator.
          Default: 2
        :type separator_length: int
        :param graphsymbol: the symbol used for the graph bar.
          Default: '█'
        :type graphsymbol: str or unicode (length one)
        :param multivalue: displays all the values if multivalued when True.
          displays only the max value if False
          Default: True
        :type multivalue: boolean
        :param human_readable: trigger human readable display (K, G, etc)
          Default: None (raw value display)

          * 'si' for power of 1000

          * 'cs' for power of 1024

          * any other value for raw value display)

        :type human_readable: string (si, cs, none)
        :param float_format: formatting of the float value
          Default: '{0:.0f}' (convert to integers).
          expample: '{:,.2f}' (2 decimals, '.' to separate decimal and int,
          ',' every three power of tens).
        :param titlebar: sets the character(s) for the horizontal title bar
          Default: '#'
        :type titlebar: string
        """

        self.line_length = line_length
        self.separator_length = separator_length
        self.min_graph_length = min_graph_length
        self.max_value = force_max_value
        self.float_format = float_format
        self.titlebar = titlebar
        if graphsymbol is None:
            self.graphsymbol = self._u('█')
        else:
            self.graphsymbol = graphsymbol
        if self._len_noansi(self.graphsymbol) != 1:
            raise Exception('Bad graphsymbol length, must be 1',
                            self._len_noansi(self.graphsymbol))
        self.multivalue = multivalue
        self.hsymbols = [self._u(''), self._u('K'), self._u('M'),
                         self._u('G'), self._u('T'), self._u('P'),
                         self._u('E'), self._u('Z'), self._u('Y')]

        if human_readable == 'si':
            self.divider = 1000
        elif human_readable == 'cs':
            self.divider = 1024
        else:
            self.divider = None

    @staticmethod
    def _len_noansi(string):
        l = len(re.sub('\x1b[^m]*m', '', string))
        return l

    def _trans_hr(self, value):

        if self.divider is None:
            return self.float_format.format(value)
        vl = value
        for hs in self.hsymbols:
            new_val = vl / self.divider
            if new_val < 1:
                return self.float_format.format(vl) + hs
            else:
                vl = new_val
        return self.float_format.format(vl * self.divider) + hs

    @staticmethod
    def _u(x):
        """Unicode compat helper
        """
        if sys.version < '3':
            return x + ''.decode("utf-8")
        else:
            return x

    @staticmethod
    def _color_string(string, color):
        """append color to a string + reset to white at the end of the string
        """
        if color is None:
            return string
        else:
            return color + string + '\033[0m'

    def _get_thresholds(self, data):
        """get various info (min, max, width... etc)
        from the data to graph.
        """
        all_thre = {}
        all_thre['value_max_length'] = 0
        all_thre['info_max_length'] = 0
        all_thre['max_pos_value'] = 0
        all_thre['min_neg_value'] = 0

        if self.max_value is not None:
            all_thre['max_pos_value'] = self.max_value

        # Iterate on all the items
        for (info, value, color) in data:
            totalvalue_len = 0

            # If we have a list of values for the item
            if isinstance(value, collections.Iterable):
                icount = 0
                maxvalue = 0
                minvalue = 0
                for (ivalue, icolor) in value:
                    if ivalue < minvalue:
                        minvalue = ivalue
                    if ivalue > maxvalue:
                        maxvalue = ivalue
                    # if we are in multivalued mode, the value string is
                    # the concatenation of the values, separeted by a ',',
                    # len() must be computed on it
                    # if we are not in multivalued mode, len() is just the
                    # longer str(value) len ( /!\, value can be negative,
                    # which means that it's not simply len(str(max_value)))
                    if self.multivalue:
                        totalvalue_len += len("," + self._trans_hr(ivalue))
                    else:
                        totalvalue_len = max(totalvalue_len, len(self._trans_hr(ivalue)))

                if self.multivalue:
                    # remove one comma if multivalues
                    totalvalue_len = totalvalue_len - 1

            # If the item only has one value
            else:
                totalvalue_len = len(self._trans_hr(value))
                maxvalue = value
                minvalue = value

            if minvalue < all_thre['min_neg_value']:
                all_thre['min_neg_value'] = minvalue

            if maxvalue > all_thre['max_pos_value']:
                all_thre['max_pos_value'] = maxvalue

            if self._len_noansi(info) > all_thre['info_max_length']:
                all_thre['info_max_length'] = self._len_noansi(info)

            if totalvalue_len > all_thre['value_max_length']:
                all_thre['value_max_length'] = totalvalue_len

        return all_thre

    def _gen_graph_string(
            self, value, max_value, min_neg_value, graph_length, start_value_pos, color):
        """Generate the bar + its paddings (left and right)
        """
        def _gen_graph_string_part(
                value, max_value, min_neg_value, graph_length, color):

            all_width = max_value + abs(min_neg_value)

            if all_width == 0:
                bar_width = 0
            else:
                bar_width = int(abs(float(value)) * float(graph_length) / float(all_width))

            return (Pyasciigraph._color_string(
                    self.graphsymbol * bar_width,
                color),
                bar_width
                )

        all_width = max_value + abs(min_neg_value)

        if all_width == 0:
            bar_width = 0
            neg_width = 0
            pos_width = 0
        else:
            neg_width = int(abs(float(min_neg_value)) * float(graph_length) / float(all_width))
            pos_width = int(abs(max_value) * graph_length / all_width)

        if isinstance(value, collections.Iterable):
            accuvalue = 0
            totalstring = ""
            totalsquares = 0

            sortedvalue = copy.deepcopy(value)
            sortedvalue.sort(reverse=False, key=lambda tup: tup[0])
            pos_value = [x for x in sortedvalue if x[0] >= 0]
            neg_value = [x for x in sortedvalue if x[0] < 0]

            # for the negative values, we build the bar + padding from 0 to the left
            for i in reversed(neg_value):
                ivalue = i[0]
                icolor = i[1]
                scaled_value = ivalue - accuvalue
                (partstr, squares) = _gen_graph_string_part(
                    scaled_value, max_value, min_neg_value, graph_length, icolor)
                totalstring = partstr + totalstring
                totalsquares += squares
                accuvalue += scaled_value

            # left padding
            totalstring = Pyasciigraph._u(' ') * (neg_width - abs(totalsquares)) + totalstring

            # reset some counters
            accuvalue = 0
            totalsquares = 0

            # for the positive values we build the bar from 0 to the right
            for i in pos_value:
                ivalue = i[0]
                icolor = i[1]
                scaled_value = ivalue - accuvalue
                (partstr, squares) = _gen_graph_string_part(
                    scaled_value, max_value, min_neg_value, graph_length, icolor)
                totalstring += partstr
                totalsquares += squares
                accuvalue += scaled_value

            # right padding
            totalstring += Pyasciigraph._u(' ') * (start_value_pos - neg_width - abs(totalsquares))
            return totalstring
        else:
            # handling for single value item
            (partstr, squares) = _gen_graph_string_part(
                value, max_value, min_neg_value, graph_length, color)
            if value >= 0:
                return Pyasciigraph._u(' ') * neg_width + \
                        partstr + \
                        Pyasciigraph._u(' ') * (start_value_pos - (neg_width + squares))
            else:
                return Pyasciigraph._u(' ') * (neg_width - squares) +\
                        partstr +\
                        Pyasciigraph._u(' ') * (start_value_pos - neg_width)


    def _gen_info_string(self, info, start_info_pos, line_length):
        """Generate the info string + padding
        """
        number_of_space = (line_length - start_info_pos - self._len_noansi(info))
        return info + Pyasciigraph._u(' ') * number_of_space

    def _gen_value_string(self, value, min_neg_value, color, start_value_pos, start_info_pos):
        """Generate the value string + padding
        """
        icount = 0
        if isinstance(value, collections.Iterable) and self.multivalue:
            for (ivalue, icolor) in value:
                if icount == 0:
                    # total_len is needed because the color characters count
                    # with the len() function even when they are not printed to
                    # the screen.
                    totalvalue_len = len(self._trans_hr(ivalue))
                    totalvalue = Pyasciigraph._color_string(
                        self._trans_hr(ivalue), icolor)
                else:
                    totalvalue_len += len("," + self._trans_hr(ivalue))
                    totalvalue += "," + \
                        Pyasciigraph._color_string(
                            self._trans_hr(ivalue),
                            icolor)
                icount += 1
        elif isinstance(value, collections.Iterable):
            max_value = min_neg_value
            color = None
            for (ivalue, icolor) in value:
                if ivalue > max_value:
                    max_value = ivalue
                    color = icolor
            totalvalue_len = len(self._trans_hr(max_value))
            totalvalue = Pyasciigraph._color_string(
                self._trans_hr(max_value), color)

        else:
            totalvalue_len = len(self._trans_hr(value))
            totalvalue = Pyasciigraph._color_string(
                self._trans_hr(value), color)

        number_space = start_info_pos -\
            start_value_pos -\
            totalvalue_len -\
            self.separator_length

        # This must not be negitive, this happens when the string length is
        # larger than the separator length
        if number_space < 0:
            number_space = 0

        return  ' ' * number_space + totalvalue +\
                ' ' * \
            ((start_info_pos - start_value_pos - totalvalue_len)
             - number_space)

    def _sanitize_string(self, string):
        """try to convert strings to UTF-8
        """
        # get the type of a unicode string
        unicode_type = type(Pyasciigraph._u('t'))
        input_type = type(string)
        if input_type is str:
            if sys.version < '3':
                info = unicode(string)
            else:
                info = string
        elif input_type is unicode_type:
            info = string
        elif input_type is int or input_type is float:
            if sys.version < '3':
                info = unicode(string)
            else:
                info = str(string)
        else:
            info = str(string)
        return info

    def _sanitize_value(self, value):
        """try to values to UTF-8
        """
        if isinstance(value, collections.Iterable):
            newcollection = []
            for i in value:
                if len(i) == 1:
                    newcollection.append((i[0], None))
                elif len(i) >= 2:
                    newcollection.append((i[0], i[1]))
            return newcollection
        else:
            return value

    def _sanitize_data(self, data):
        ret = []
        for item in data:
            if (len(item) == 2):
                if isinstance(item[1], collections.Iterable):
                    ret.append(
                        (self._sanitize_string(item[0]),
                         self._sanitize_value(item[1]),
                         None))
                else:
                    ret.append(
                        (self._sanitize_string(item[0]),
                         self._sanitize_value(item[1]),
                         None))
            if (len(item) == 3):
                ret.append(
                    (self._sanitize_string(item[0]),
                     self._sanitize_value(item[1]),
                     item[2]))
        return ret

    def graph(self, label=None, data=[]):
        """function generating the graph

        :param string label: the label of the graph
        :param iterable data: the data (list of tuple (info, value))
                info must be "castable" to a unicode string
                value must be an int or a float
        :rtype: a list of strings (each lines of the graph)

        """
        result = []
        san_data = self._sanitize_data(data)
        all_thre = self._get_thresholds(san_data)

        if not label is None:
            san_label = self._sanitize_string(label)
            label_len = self._len_noansi(san_label)
        else:
            label_len = 0

        real_line_length = max(self.line_length, label_len)

        min_line_length = self.min_graph_length +\
            2 * self.separator_length +\
            all_thre['value_max_length'] +\
            all_thre['info_max_length']

        if min_line_length < real_line_length:
            # calcul of where to start info
            start_info_pos = self.line_length -\
                all_thre['info_max_length']
            # calcul of where to start value
            start_value_pos = start_info_pos -\
                self.separator_length -\
                all_thre['value_max_length']
            # calcul of where to end graph
            graph_length = start_value_pos -\
                self.separator_length
        else:
            # calcul of where to start value
            start_value_pos = self.min_graph_length +\
                self.separator_length
            # calcul of where to start info
            start_info_pos = start_value_pos +\
                all_thre['value_max_length'] +\
                self.separator_length
            # calcul of where to end graph
            graph_length = start_value_pos -\
                self.separator_length
            # calcul of the real line length
            real_line_length = min_line_length

        if not label is None:
            result.append(san_label)
            result.append(Pyasciigraph._u(self.titlebar) * real_line_length)

        header_line = (Pyasciigraph._u(" ") * graph_length) + "count"
        header_line += Pyasciigraph._u(" ") * (start_info_pos - len(header_line)) + "bin"

        result.append(header_line)

        for info, value, color in san_data:

            graph_string = self._gen_graph_string(
                value,
                    all_thre['max_pos_value'],
                    all_thre['min_neg_value'],
                    graph_length,
                    start_value_pos,
                    color
            )

            value_string = self._gen_value_string(
                value,
                    all_thre['min_neg_value'],
                    color,
                    start_value_pos,
                    start_info_pos,
            )

            info_string = self._gen_info_string(
                info,
                    start_info_pos,
                    real_line_length
            )
            new_line = graph_string + value_string + info_string
            result.append(new_line)

        return result
