import sys, shutil, os, re, math, inspect
from plotext._dict import *

###############################################
#########    Number Manipulation     ##########
###############################################

def round(n, d = 0): # the standard round(0.5) = 0 instead of 1; this version rounds 0.5 to 1
    n *= 10 ** d
    f = math.floor(n)
    r = f if n - f < 0.5 else math.ceil(n)
    return r * 10 ** (-d)

def mean(x, y, p = 1): # mean of x and y with optional power p; if p tends to 0 the minimum is returned; if p tends to infinity the max is returned; p = 1 is the standard mean
    return ((x ** p + y ** p) / 2) ** (1 / p)

def replace(data, data2, element = None): # replace element in data with correspondent in data2 when element is found
    res = []
    for i in range(len(data)):
        el = data[i] if data[i] != element else data2[i]
        res.append(el)
    return res

def try_float(data): # it turn a string into float if it can
    try:
        return float(data)
    except:
        return data

def quantile(data, q): # calculate the quantile of a given array
    data = sorted(data)
    index = q * (len(data) - 1)
    if index.is_integer():
        return data[int(index)]
    else:
        return (data[int(index)] + data[int(index) + 1]) / 2

###############################################
###########    List Creation     ##############
###############################################

def linspace(lower, upper, length = 10): # it returns a lists of numbers from lower to upper with given length
    slope = (upper - lower) / (length - 1) if length > 1 else 0
    return [lower + x * slope for x in range(length)]

def sin(periods = 2, length = 200, amplitude = 1, phase = 0, decay = 0): # sinusoidal data with given parameters
    f = 2 * math.pi * periods / (length - 1)
    phase =  math.pi * phase
    d = decay / length
    return [amplitude * math.sin(f * el + phase) * math.exp(- d * el) for el in range(length)]

def square(periods = 2, length = 200, amplitude = 1):
    T = length / periods
    step = lambda t: amplitude if t % T <= T / 2 else - amplitude
    return [step(i) for i in range(length)]

def to_list(data, length): # eg: to_list(1, 3) = [1, 1 ,1]; to_list([1,2,3], 6) = [1, 2, 3, 1, 2, 3]
    data = data if isinstance(data, list) else [data] * length
    data = data * math.ceil(length / len(data)) if len(data) > 0 else []
    return data[ : length]

def difference(data1, data2) : # elements in data1 not in date2
    return [el for el in data1 if el not in data2]

###############################################
#########    List Transformation     ##########
###############################################

def log(data): # it apply log function to the data
    return [math.log10(el) for el in data] if isinstance(data, list) else math.log10(data)

def power10(data): # it apply log function to the data
    return [10 ** el for el in data]

def floor(data): # it floors a list of data
    return list(map(math.floor, data))

def repeat(data, length): # repeat the same data till length is reached
    l = len(data) if type(data) == list else 1
    data = join([data] * math.ceil(length / l))
    return data[ : length]

###############################################
##########    List Manipulation     ###########
###############################################

def no_duplicates(data): # removes duplicates from a list
    return list(set(list(data)))
    #return list(dict.fromkeys(data)) # it takes double time

def join(data): # flatten lists at first level
    #return [el for row in data for el in row]
    return [el for row in data for el in (join(row) if type (row) == list else [row])]

def cumsum(data): # it returns the cumulative sums of a list; eg: cumsum([0,1,2,3,4]) = [0,1,3,6,10]
    s = [0]
    for i in range(len(data)):
        s.append(s[-1] + data[i])
    return s[1:]

###############################################
#########    Matrix Manipulation     ##########
###############################################

def matrix_size(matrix):  # cols, height
    return [len(matrix[0]), len(matrix)] if matrix != [] else [0, 0]

def transpose(data, length = 1): # it needs no explanation
    return [[]] * length if data == [] else list(map(list, zip(*data)))

def vstack(matrix, extra): # vertical stack of two matrices
    return extra + matrix # + extra

def hstack(matrix, extra): # horizontal stack of two matrices
    lm, le = len(matrix), len(extra)
    l = max(lm, le)
    return [matrix[i] + extra[i] for i in range(l)]

def turn_gray(matrix): # it takes a standard matrix and turns it into an grayscale one
    M, m = max(join(matrix), default = 0), min(join(matrix), default = 0)
    to_gray = lambda el: tuple([int(255 * (el - m) / (M - m))] * 3) if M != m else (127, 127, 127)
    return [[to_gray(el) for el in l] for l in matrix]

def brush(*lists): # remove duplicates from lists x, y, z ...
    l = min(map(len, lists))
    lists = [el[:l] for el in lists]
    z = list(zip(*lists))
    z = no_duplicates(z)
    #z = sorted(z)#, key = lambda x: x[0])
    lists = transpose(z, len(lists))
    return lists

###############################################
#########   String Manipulation     ###########
###############################################

nl = "\n"

def only_spaces(string): # it returns True if string is made of only empty spaces or is None or ''
    return (type(string) == str) and (string == len(string) * space) #and len(string) != 0

def format_time(time): # it properly formats the computational time
    t = time if time is not None else 0
    unit = 's' if t >= 1 else 'ms' if t >= 10 ** -3 else 'µs'
    p = 0 if unit == 's' else 3 if unit == 'ms' else 6
    t = round(10 ** p * t, 1)
    l = len(str(int(t)))
    t = str(t)
    #t = ' ' * (3 - l) + t
    return t[ : l + 2] + ' ' + unit

positive_color = 'green+'
negative_color = 'red'
title_color = 'cyan+'

def format_strings(string1, string2, color = positive_color): # returns string1 in bold and with color + string2 with a pre-formatted style
    return colorize(string1, color, "bold") + " " + colorize(string2, style = info_style)

def correct_coord(string, label, coord): # In the attempt to insert a label in string at given coordinate, the coordinate is adjusted so not to hit the borders of the string
    l = len(label)
    b, e = max(coord - l + 1, 0), min(coord + l, len(string) - 1)
    data = [i for i in range(b, e) if string[i] is space]
    b, e = min(data, default = coord - l + 1), max(data, default = coord + l)
    b, e = e - l + 1, b + l
    return (b + e - l) // 2

def no_char_duplicates(string, char): # it remove char duplicates from string
    pattern = char + '{2,}'
    string = re.sub(pattern, char, string)
    return string

def read_lines(text, delimiter = None, columns = None): # from a long text to well formatted data
    delimiter = " " if delimiter is None else delimiter
    data = []
    columns = len(no_char_duplicates(text[0], delimiter).split(delimiter)) if columns is None else columns
    for i in range(len(text)):
        row = text[i]
        row = no_char_duplicates(row, delimiter)
        row = row.split(delimiter)
        row = [el.replace('\n', '') for el in row]
        cols = len(row)
        row = [row[col].replace('\n', '') if col in range(cols) else '' for col in range(columns)]
        row = [try_float(el) for el in row]
        data.append(row)
    return data

def pad_string(num, length): # pad a number with spaces before to reach length
    num = str(num)
    l = len(num)
    return num + ' ' * (length - l)

def max_length(strings):
    strings = map(str, strings)
    return max(map(len, strings), default = 0)

###############################################
##########   File Manipulation     ############
###############################################

def correct_path(path):
    folder, base = os.path.dirname(path), os.path.basename(path)
    folder = os.path.expanduser("~") if folder in ['', '~'] else folder
    path = os.path.join(folder, base)
    return path

def is_file(path, log = True): # returns True if path exists
    res = os.path.isfile(path)
    print(format_strings("not a file:", path, negative_color)) if not res and log else None
    return res

def script_folder(): # the folder of the script executed
    return parent_folder(inspect.getfile(sys._getframe(1)))

def parent_folder(path, level = 1): # it return the parent folder of the path or file given; if level is higher then 1 the process is iterated
    if level <= 0:
        return path
    elif level == 1:
        return os.path.abspath(os.path.join(path, os.pardir))
    else:
        return parent_folder(parent_folder(path, level - 1))

def join_paths(*args): # it join a list of string in a proper file path; if the first argument is ~ it is turnded into the used home folder path
    args = list(args)
    args[0] = _correct_path(args[0]) if args[0] == "~" else args[0]
    return os.path.abspath(os.path.join(*args))

def delete_file(path, log = True): # remove the file if it exists
    path = correct_path(path)
    if is_file(path):
        os.remove(path)
        print(format_strings("file removed:", path, negative_color)) if log else None

def read_data(path, delimiter = None, columns = None, first_row = None, log = True): # it turns a text file into data lists
    path = correct_path(path)
    first_row = 0 if first_row is None else int(first_row)
    file = open(path, "r")
    text = file.readlines()[first_row:]
    file.close()
    print(format_strings("data read from", path)) if log else None
    return read_lines(text, delimiter, columns)

def write_data(data, path, delimiter = None, columns = None, log = True): # it turns a matrix into a text file
    delimiter = " " if delimiter is None else delimiter
    cols = len(data[0])
    cols = range(1, cols + 1) if columns is None else columns
    text = ""
    for row in data:
        row = [row[i - 1] for i in cols]
        row = list(map(str, row))
        text += delimiter.join(row) + '\n'
    save_text(text, path, log = log)

def save_text(text, path, append = False, log = True): # it saves some text to the path selected
    path = correct_path(path)
    mode = "a" if append else "w+"
    with open(path , mode, encoding='utf-8') as file:
        file.write(text)
    print(format_strings("text saved in", path)) if log else None

def download(url, path, log = True): # it download the url (image, video, gif etc) to path
    from urllib.request import urlretrieve
    path = correct_path(path)
    urlretrieve(url, path)
    print(format_strings('url saved in', path)) if log else None

###############################################
#########    Platform Utilities    ############
###############################################

def is_ipython(): # true if running in ipython shenn
    try:
        __IPYTHON__
        return True
    except NameError:
        return False

def platform(): # the platform (unix or windows) you are using plotext in
   platform = sys.platform
   if platform in {'win32', 'cygwin'}:
       return 'windows'
   else:
       return 'unix'

platform = platform()

# to enable ascii escape color sequences
if platform == "windows":
    import subprocess
    subprocess.call('', shell = True)

def terminal_size(): # it returns the terminal size as [width, height]
    try:
        size = shutil.get_terminal_size()
        return list(size)
    except OSError:
        return [None, None]

terminal_width = lambda: terminal_size()[0]
tw = terminal_width

terminal_height = lambda: terminal_size()[1]
th = terminal_height

def clear_terminal(lines = None): # it cleat the entire terminal, or the specified number of lines
    if lines is None:
        write('\033c')
    else:
        for r in range(lines):
            write("\033[A") # moves the curson up
            write("\033[2K") # clear the entire line

def write(string): # the print function used by plotext
    sys.stdout.write(string)

class memorize: # it memorise the arguments of a function, when used as its decorator, to reduce computational time
    def __init__(self, f):
        self.f = f
        self.memo = {}
    def __call__(self, *args):
        if not args in self.memo:
            self.memo[args] = self.f(*args)
        return self.memo[args]

##############################################
#########    Marker Utilities      ###########
##############################################

space = ' ' # the default null character that appears as background to all plots
plot_marker = "hd" if platform == 'unix' else 'dot'

hd_markers = {hd_codes[el] : el for el in hd_codes}
fhd_markers = {fhd_codes[el] : el for el in fhd_codes}
braille_markers = {braille_codes[el] : el for el in braille_codes}
simple_bar_marker = '▇'

@memorize
def get_hd_marker(code):
   return hd_codes[code] if len(code) == 4 else fhd_codes[code] if len(code) == 6 else braille_codes[code]

def marker_factor(marker, hd, fhd, braille): # useful to improve the resolution of the canvas for higher resolution markers
   return hd if marker == 'hd' else fhd if marker == 'fhd' else braille if marker == 'braille' else 1

##############################################
###########    Color Utilities    ############
##############################################

# A user could specify three types of colors
  # an integer for 256 color codes
  # a tuple    for RGB color codes
  # a string   for 16 color codes or styles

# Along side the user needs to specify whatever it is for background / fullground / style
# which plotext calls 'character' = 0 / 1 / 2


#colors_no_plus = [el for el in colors if '+' not in el and el + '+' not in colors and el is not no_color] # basically just [black, white]

def get_color_code(color): # the color number code from color string
    color = color.strip()
    return color_codes[color]

def get_color_name(code): # the color string from color number code
    codes = list(color_codes.values())
    return colors[codes.index(code)] if code in codes else no_color

def is_string_color(color):
    return isinstance(color, str) and color.strip() in colors

def is_integer_color(color):
    return isinstance(color, int) and 0 <= color <= 255

def is_rgb_color(color):
    is_rgb = isinstance(color, list) or isinstance(color, tuple)
    is_rgb = is_rgb and len(color) == 3
    is_rgb = is_rgb and all([is_integer_color(el) for el in color])
    return is_rgb

def is_color(color):
    return is_string_color(color) or is_integer_color(color) or is_rgb_color(color)

def colorize(string, color = None, style = None, background = None, show = False): # it paints a text with given fullground and background color
    string = apply_ansi(string, background, 0)
    string = apply_ansi(string, color, 1)
    string = apply_ansi(string, style, 2)
    if show:
        print(string)
    return string

def uncolorize(string): # remove color codes from colored string
    colored = lambda: ansi_begin in string
    while colored():
        b = string.index(ansi_begin)
        e = string[b : ].index('m') + b + 1
        string = string.replace(string[b : e], '')
    return string

def apply_ansi(string, color, character):
    begin, end = ansi(color, character)
    return begin + string + end

#ansi_begin = '\033['
ansi_begin = '\x1b['
ansi_end = ansi_begin + '0m'

@memorize
def colors_to_ansi(fullground, style, background):
    color = [background, fullground, style]
    return ''.join([ansi(color[i], i)[0] for i in range(3)])

@memorize
def ansi(color, character):
    if color == no_color:
        return ['', '']
    col, fg, tp = '', '', ''
    if character == 2 and is_style(color):
        col = get_style_codes(color)
        col = ';'.join([str(el) for el in col])
    elif character != 2:
        fg = '38;' if character == 1 else '48;'
        tp = '5;'
        if is_string_color(color):
            col = str(get_color_code(color))
        elif is_integer_color(color):
            col = str(color)
        elif is_rgb_color(color):
            col = ';'.join([str(el) for el in color])
            tp = '2;'
    is_color = col != ''
    begin = ansi_begin + fg + tp + col + 'm' if is_color else ''
    end = ansi_end if is_color else ''
    return [begin, end]

## This section is useful to produce html colored version of the plot and to translate all color types (types 0 and 1) in rgb (type 2 in plotext) and avoid confusion. the match is almost exact and it depends on the terminal i suppose

def to_rgb(color):
    if is_string_color(color): # from 0 to 1
        color = get_color_code(color)
        #color = type0_to_type1_codes[code]
    if is_integer_color(color): # from 0 or 1 to 2
        return type1_to_type2_codes[color]
    return color

##############################################
############     Style Codes    ##############
##############################################

no_style = 'default'

styles = list(style_codes.keys()) + [no_style]

info_style = 'dim'

def get_style_code(style): # from single style to style number code
    style = style.strip()
    return style_codes[style]

def get_style_codes(style): # from many styles (separated by space) to as many number codes
    style = style.strip().split()
    codes = [get_style_code(el) for el in style if el in styles]
    codes = no_duplicates(codes)
    return codes

def get_style_name(code): # from style number code to style name
    codes = list(style_codes.values())
    return styles[codes.index(code)] if code in codes else no_style

def clean_styles(style): # it returns a well written sequence of styles (separated by spaces) from a possible confused one
    codes = get_style_codes(style)
    return ' '.join([get_style_name(el) for el in codes])

def is_style(style):
    style = style.strip().split() if isinstance(style, str) else ['']
    return any([el in styles for el in style])

##############################################
###########     Plot Utilities    ############
##############################################

def set_data(x = None, y = None): # it return properly formatted x and y data lists
   if x is None and y is None :
       x, y = [], []
   elif x is not None and y is None:
       y = x
       x = list(range(1, len(y) + 1))
   lx, ly = len(x), len(y)
   if lx != ly:
       l = min(lx, ly)
       x = x[ : l]
       y = y[ : l]
   return [list(x), list(y)]

##############################################
#######     Figure Class Utilities    ########
##############################################

def set_sizes(sizes, size_max): # given certain widths (or heights) - some of them are None -  it sets them so to respect max value
    bins = len(sizes)
    for s in range(bins):
        size_set = sum([el for el in sizes[0 : s] + sizes[s + 1 : ] if el is not None])
        available = max(size_max - size_set, 0)
        to_set = len([el for el in sizes[s : ] if el is None])
        sizes[s] = available // to_set if sizes[s] is None else sizes[s]
    return sizes

def fit_sizes(sizes, size_max): # honestly forgot the point of this function: yeeeeei :-) but it is useful - probably assumes all sizes not None (due to set_sizes) and reduces those that exceed size_max from last one to first
    bins = len(sizes)
    s = bins - 1
    #while (sum(sizes) != size_max if not_less else sum(sizes) > size_max) and s >= 0:
    while sum(sizes) > size_max and s >= 0:
        other_sizes = sum([sizes[i] for i in range(bins) if i != s])
        sizes[s] = max(size_max - other_sizes, 0)
        s -= 1
    return sizes

##############################################
#######     Build Class Utilities    #########
##############################################

def get_first(data, test = True): # if test take the first element, otherwise the second
    return data[0] if test else data[1]

def apply_scale(data, test = False): # apply log scale if test
    return log(data) if test else data

def reverse_scale(data, test = False): # apply log scale if test
    return power10(data) if test else data

def replace_none(data, num_data): # replace None elements in data with correspondent in num_data
    return [data[i] if data[i] is not None else num_data[i] for i in range(len(data))]

numerical = lambda el:  not (el is None or math.isnan(el)) or isinstance(el, str) # in the case of string datetimes
all_numerical = lambda data: all([numerical(el) for el in data])

def get_lim(data): # it returns the data minimum and maximum limits
    data = [el for el in data if numerical(el)]
    m = min(data, default = 0)
    M = max(data, default = 0)
    m, M = (m, M) if m != M else (0.5 * m, 1.5 * m) if m == M != 0 else (-1, 1)
    return [m, M]

def get_matrix_data(data, lim, bins): # from data to relative canvas coordinates
    change = lambda el: 0.5 + (bins - 1) * (el - lim[0]) / (lim[1] - lim[0])
    # round is so that for example 9.9999 = 10, otherwise the floor function will give different results
    return [math.floor(round(change(el), 8)) if numerical(el) else el for el in data]

def get_lines(x, y, *other): # it returns the lines between all couples of data points like x[i], y[i] to x[i + 1], y[i + 1]; other are the lisXt of markers and colors that needs to be elongated
    # if len(x) * len(y) == 0:
    #     return [], [], *[[]] * len(other)
    o = transpose(other, len(other))
    xl, yl, ol = [[] for i in range(3)]
    for n in range(len(x) - 1):
        xn, yn = x[n : n + 2], y[n : n + 2]
        xn, yn = get_line(xn, yn)
        xl += xn[:-1]
        yl += yn[:-1]
        ol += [o[n]] * len(xn[:-1])
    xl = xl + [x[-1]] if x != [] else xl
    yl = yl + [y[-1]] if x != [] else yl
    ol = ol + [o[-1]] if x != [] else ol
    return [xl, yl] + transpose(ol, len(other))

def get_line(x, y): # it returns a line of points from x[0],y[0] to x[1],y[1] distanced between each other in x and y by at least 1.
    if not all_numerical(join([x, y])):
        return x, y
    x0, x1 = x
    y0, y1 = y
    dx, dy = int(x1) - int(x0), int(y1) - int(y0)
    ax, ay = abs(dx), abs(dy)
    a = int(max(ax, ay) + 1)
    x = [int(el) for el in linspace(x0, x1, a)]
    y = [int(el) for el in linspace(y0, y1, a)]
    return [x, y]

def get_fill_level(fill, lim, bins):
    if fill is False:
        return False
    elif isinstance(fill, str):
        return fill
    else:
        fill = min(max(fill, lim[0]), lim[1])
        fill = get_matrix_data([fill], lim, bins)[0]
        return fill

def find_filling_values(x, y, y0):
    xn, yn, yf = [[]] * 3
    l = len(x);
    while len(x) > 0:
        i = len(xn)
        xn.append(x[i])
        yn.append(y[i])
        J = [j for j in range(l) if x[j] == x[i]]
        if J != []:
            Y = [y[j] for j in J]
            j = Y.index(min(Y))
            J.pop(j)
            [x.pop(j) for j in J]
            [y.pop(j) for j in J]
            yf.append(y[j])
    return xn, yn, yf

def get_fill_boundaries(x, y):
    xm = []
    l = len(x)
    for i in range(l):
        xi, yi = x[i], y[i]
        I = [j for j in range(l) if x[j] == xi and y[j] < yi]
        Y = [y[j] for j in I]
        m = min(Y, default = yi)
        xm.append([x[i], m])
    x, m = transpose(xm)
    return m

def fill_data(x, y, y0, *other): # it fills x, y with y data points reaching y0; and c are the list of markers and colors that needs to be elongated
    #y0 = get_fill_boundaries(x, y)
    y0 = get_fill_boundaries(x, y) if isinstance(y0, str) else [y0] * len(x)
    o = transpose(other, len(other))
    xf, yf, of = [[] for i in range(3)]
    xy = []
    for i in range(len(x)):
        xi, yi, y0i = x[i], y[i], y0[i]
        if [xi, yi] not in xy:
            xy.append([xi, yi])
            yn = range(y0i, yi + 1) if y0i < yi else range(yi, y0i) if y0i > yi else [y0i]
            yn = list(yn)
            xn = [xi] * len(yn)
            xf += xn
            yf += yn
            of += [o[i]] * len(xn)
    return [xf, yf] + transpose(of, len(other))

def remove_outsiders(x, y, width, height, *other):
    I = [i for i in range(len(x)) if x[i] in range(width) and y[i] in range(height)]
    o = transpose(other, len(other))
    return transpose([(x[i], y[i], *o[i]) for i in I], 2 + len(other))

def get_labels(ticks): # it returns the approximated string version of the data ticks
    d = distinguishing_digit(ticks)
    formatting_string = "{:." + str(d + 1) + "f}"
    labels = [formatting_string.format(el) for el in ticks]
    pos = [el.index('.') + d + 2 for el in labels]
    labels = [labels[i][: pos[i]] for i in range(len(labels))]
    all_integers = all(map(lambda el: el == int(el), ticks))
    labels = [add_extra_zeros(el, d) if len(labels) > 1 else el for el in labels] if not all_integers else [str(int(el)) for el in ticks]
    #sign = any([el < 0 for el in ticks])
    #labels = ['+' + labels[i] if ticks[i] > 0 and sign else labels[i] for i in range(len(labels))]
    return labels

def distinguishing_digit(data): # it return the minimum amount of decimal digits necessary to distinguish all elements of a list
    #data = [el for el in data if 'e' not in str(el)]
    d = [_distinguishing_digit(data[i], data[i + 1]) for i in range(len(data) - 1)]
    return max(d, default = 1)

def _distinguishing_digit(a, b): # it return the minimum amount of decimal digits necessary to distinguish a from b (when both are rounded to those digits).
    d = abs(a - b)
    d = 0 if d == 0 else - math.log10(2 * d)
    #d = round(d, 10)
    d = 0 if d < 0 else math.ceil(d)
    d = d + 1 if round(a, d) == round(b, d) else d
    return d

def add_extra_zeros(label, d): # it adds 0s at the end of a label if necessary
    zeros = len(label) - 1 - label.index('.' if 'e' not in label else 'e')
    if zeros < d:
        label += '0' * (d - zeros)
    return label

def add_extra_spaces(labels, side): # it adds empty spaces before or after the labels if necessary
    length = 0 if labels == [] else max_length(labels)
    if side == "left":
        labels = [space * (length - len(el)) + el for el in labels]
    if side == "right":
        labels = [el + space * (length - len(el)) for el in labels]
    return labels

def hd_group(x, y, xf, yf): # it returns the real coordinates of the HD markers and the matrix that defines the marker
    l, xfm, yfm = len(x), max(xf), max(yf)
    xm = [el // xfm if numerical(el) else el for el in x]
    ym = [el // yfm if numerical(el) else el for el in y]
    m = {}
    for i in range(l):
        xyi = xm[i], ym[i]
        xfi, yfi = xf[i], yf[i]
        mi = [[0 for x in range(xfi)] for y in range(yfi)]
        m[xyi] = mi
    for i in range(l):
        xyi = xm[i], ym[i]
        if all_numerical(xyi):
            xk, yk = x[i] % xfi, y[i] % yfi
            xk, yk = math.floor(xk), math.floor(yk)
            m[xyi][yk][xk] = 1
    x, y = transpose(m.keys(), 2)
    m = [tuple(join(el[::-1])) for el in m.values()]
    return x, y, m

###############################################
#############   Bar Functions    ##############
###############################################

def bars(x, y, width, minimum): # given the bars center coordinates and height, it returns the full bar coordinates
   # if x == []:
   #     return [], []
   bins = len(x)
   #bin_size_half = (max(x) - min(x)) / (bins - 1) * width / 2
   bin_size_half = width / 2
   # adjust the bar width according to the number of bins
   if bins > 1:
       bin_size_half *= (max(x) - min(x)) / (bins - 1)
   xbar, ybar = [], []
   for i in range(bins):
       xbar.append([x[i] - bin_size_half, x[i] + bin_size_half])
       ybar.append([minimum, y[i]])
   return xbar, ybar

def set_multiple_bar_data(*args):
    l = len(args)
    Y = [] if l == 0 else args[0] if l == 1 else args[1]
    Y = [Y] if not isinstance(Y, list) or len(Y) == 0 else Y
    m = len(Y[0])
    x = [] if l == 0 else list(range(1, m + 1)) if l == 1 else args[0]
    return x, Y

def hist_data(data, bins = 10, norm = False): # it returns data in histogram form if norm is False. Otherwise, it returns data in density form where all bins sum to 1.
    #data = [round(el, 15) for el in data]
    # if data == []:
    #     return [], []
    bins = 0 if len(data) == 0 else bins
    m, M = min(data, default = 0), max(data, default = 0)
    data = [(el - m) / (M - m) * bins if el != M else bins - 1 for el in data]
    data = [int(el) for el in data]
    histx = linspace(m, M, bins)
    histy = [0] * bins
    for el in data:
        histy[el] += 1
    if norm:
        histy = [el / len(data) for el in histy]
    return histx, histy

def single_bar(x, y, ylabel, marker, colors):
    l = len(y)
    lc = len(colors)
    xs = colorize(str(x), 'gray+', 'bold')
    bar = [marker * el for el in y]
    bar = [apply_ansi(bar[i], colors[i % lc], 1) for i in range(l)]
    ylabel = colorize(f'{int(round(ylabel))}', 'gray+', 'bold')
    bar = xs + space + ''.join(bar) + space + ylabel
    return bar

def bar_data(*args, width = None, mode = 'stacked'):
    x, Y = set_multiple_bar_data(*args)
    x = list(map(str, x))
    x = add_extra_spaces(x, 'right')
    lx = len(x[0])
    y = [sum(el) for el in transpose(Y)] if mode == 'stacked' else Y
    ly = max_length([round(el) for el in join(y)])

    width_term = terminal_width()
    width = width_term if width is None else min(width, width_term)
    width = max(width, lx + ly + 2 + 1)

    my = max(join(y))
    my = 1 if my == 0 else my
    dx = my / (width - lx - ly - 2)
    Yi = [[round(el / dx, 0) for el in y] for y in Y]
    Yi = transpose(Yi)

    return x, y, Yi, width

def correct_marker(marker = None):
    return simple_bar_marker if marker is None else marker[0]

def get_title(title, width):
    out = ''
    if title is not None:
        l = len(uncolorize(title))
        w1 = (width - 2 - l) // 2; w2 = width - l - 2 - w1
        l1 = '─' * w1 + space
        l2 = space + '─' * w2
        out = colorize(l1 + title + l2, 'gray+', 'bold') + '\n'
    return out

def get_simple_labels(marker, labels, colors, width):
    out = '\n'
    if labels != None:
        l = len(labels)
        lc = len(colors)
        out =  space.join([colorize(marker * 3, colors[i % lc]) + space + colorize(labels[i], 'gray+', 'bold') for i in range(l)])
        out = '\n' + get_title(out, width)
    return out

###############################################
#############   Box Functions    ##############
###############################################

def box(x, y, width, minimum): # given the bars center coordinates and height, it returns the full bar coordinates
    # if x == []:
    #     return [], []
    bins = len(x)
    #bin_size_half = (max(x) - min(x)) / (bins - 1) * width / 2
    bin_size_half = width / 2
    # adjust the bar width according to the number of bins
    if bins > 1:
        bin_size_half *= (max(x) - min(x)) / (bins - 1)
    c, q1, q2, q3, h, l = [], [], [], [], [], []
    xbar, ybar, mybar = [], [], []

    for i in range(bins):
        c.append(x[i])
        xbar.append([x[i] - bin_size_half, x[i] + bin_size_half])
        q1.append(quantile(y[i], 0.25))
        q2.append(quantile(y[i], 0.50))
        q3.append(quantile(y[i], 0.75))
        h.append(max(y[i]))
        l.append(min(y[i]))

    return q1, q2, q3, h, l, c, xbar

##############################################
##########    Image Utilities    #############
##############################################

def update_size(size_old, size_new): # it resize an image to the desired size, maintaining or not its size ratio and adding or not a pixel averaging factor with resample = True
    size_old = [size_old[0], size_old[1] / 2]
    ratio_old = size_old[1] / size_old[0]
    size_new = replace(size_new, size_old)
    ratio_new = size_new[1] / size_new[0]
    #ratio_new = size_new[1] / size_new[0]
    size_new = [1 if el == 0 else el for el in size_new]
    return [int(size_new[0]), int(size_new[1])]

def image_to_matrix(image): # from image to a matrix of pixels
    pixels = list(image.getdata())
    width, height = image.size
    return [pixels[i * width:(i + 1) * width] for i in range(height)]
