'''
Copy of Jonathan Eunice and Georgios Verigakis' ansicolors Python module:
https://pypi.org/project/ansicolors/

Included here to avoid an additional dependency.

Functions here are not intended for direct use by the user, but of course they can be.

New in version 2.0.0.
'''

# Copyright (c) 2012 Giorgos Verigakis <verigak@gmail.com>
#
# Permission to use, copy, modify, and distribute this software for any
# purpose with or without fee is hereby granted, provided that the above
# copyright notice and this permission notice appear in all copies.
#
# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.

import re
from functools import partial

# ANSI color names. There is also a "default"
COLORS = ('black', 'red', 'green', 'yellow', 'blue', 'magenta', 'cyan', 'white')

# ANSI style names
STYLES = ('none', 'bold', 'faint', 'italic', 'underline', 'blink', 'blink2', 'negative', 'concealed', 'crossed')


#%% Functions

def _join(*values):
    """
    Join a series of values with semicolons. The values
    are either integers or strings, so stringify each for
    good measure. Worth breaking out as its own function
    because semicolon-joined lists are core to ANSI coding.
    """
    return ';'.join(str(v) for v in values)


def _color_code(spec, base):
    """
    Workhorse of encoding a color. Give preference to named colors from
    ANSI, then to specific numeric or tuple specs. If those don't work,
    try looking up look CSS color names or parsing CSS hex and rgb color
    specifications.

    "Base" is either 30 or 40, signifying the base value for color encoding
    (foreground and background respectively). Low values are added directly
    to the base. Higher values use ``base + 8`` (i.e. 38 or 48) then extended codes.

    Args:
        spec (str|int|tuple|list): Unparsed color specification
        base (int): see above
    """
    if isinstance(spec, str):
        spec = spec.strip().lower()

    if spec == 'default':
        return _join(base + 9)
    elif spec in COLORS:
        return _join(base + COLORS.index(spec))
    elif isinstance(spec, int) and 0 <= spec <= 255:
        return _join(base + 8, 5, spec)
    elif isinstance(spec, (tuple, list)):
        return _join(base + 8, 2, _join(*spec))
    else:
        rgb = parse_rgb(spec)
        # parse_rgb raises ValueError if cannot parse spec
        return _join(base + 8, 2, _join(*rgb))


def color(s, fg=None, bg=None, style=None):
    """
    Add ANSI colors and styles to a string.

    Args:
        s (str): String to format.
        fg (str|int|tuple): Foreground color specification.
        bg (str|int|tuple( bg): Background color specification.
        style (str): Style names, separated by '+'

    Returns:
        Formatted string.
    """
    codes = []

    if fg:
        codes.append(_color_code(fg, 30))
    if bg:
        codes.append(_color_code(bg, 40))
    if style:
        for style_part in style.split('+'):
            if style_part in STYLES:
                codes.append(STYLES.index(style_part))
            else:
                raise ValueError('Invalid style "%s"' % style_part)

    if codes:
        template = '\x1b[{0}m{1}\x1b[0m'
        return template.format(_join(*codes), s)
    else:
        return s


def strip_color(s):
    """
    Remove ANSI color/style sequences from a string. The set of all
    possibly ANSI sequences is large, so does not try to strip every
    possible one. But does strip some outliers seen not just in text
    generated by this module, but by other ANSI colorizers in the wild.
    Those include `\x1b[K` (aka EL or erase to end of line) and `\x1b[m`
    a terse version of the more common `\x1b[0m`.
    """
    return re.sub('\x1b\\[(K|.*?m)', '', s)


def ansilen(s):
    """
    Given a string with embedded ANSI codes, what would its
    length be without those codes?
    """
    return len(strip_color(s))


# Foreground color shortcuts
black = partial(color, fg='black')
red = partial(color, fg='red')
green = partial(color, fg='green')
yellow = partial(color, fg='yellow')
blue = partial(color, fg='blue')
magenta = partial(color, fg='magenta')
cyan = partial(color, fg='cyan')
white = partial(color, fg='white')

# Style shortcuts
bold = partial(color, style='bold')
none = partial(color, style='none')
faint = partial(color, style='faint')
italic = partial(color, style='italic')
underline = partial(color, style='underline')
blink = partial(color, style='blink')
blink2 = partial(color, style='blink2')
negative = partial(color, style='negative')
concealed = partial(color, style='concealed')
crossed = partial(color, style='crossed')


#%% Map of CSS color names to RGB integer values.

css_colors = {
    'aliceblue':      (240, 248, 255),
    'antiquewhite':   (250, 235, 215),
    'aqua':           (0, 255, 255),
    'aquamarine':     (127, 255, 212),
    'azure':          (240, 255, 255),
    'beige':          (245, 245, 220),
    'bisque':         (255, 228, 196),
    'black':          (0, 0, 0),
    'blanchedalmond': (255, 235, 205),
    'blue':           (0, 0, 255),
    'blueviolet':     (138, 43, 226),
    'brown':          (165, 42, 42),
    'burlywood':      (222, 184, 135),
    'cadetblue':      (95, 158, 160),
    'chartreuse':     (127, 255, 0),
    'chocolate':      (210, 105, 30),
    'coral':          (255, 127, 80),
    'cornflowerblue': (100, 149, 237),
    'cornsilk':       (255, 248, 220),
    'crimson':        (220, 20, 60),
    'cyan':           (0, 255, 255),
    'darkblue':       (0, 0, 139),
    'darkcyan':       (0, 139, 139),
    'darkgoldenrod':  (184, 134, 11),
    'darkgray':       (169, 169, 169),
    'darkgreen':      (0, 100, 0),
    'darkgrey':       (169, 169, 169),
    'darkkhaki':      (189, 183, 107),
    'darkmagenta':    (139, 0, 139),
    'darkolivegreen': (85, 107, 47),
    'darkorange':     (255, 140, 0),
    'darkorchid':     (153, 50, 204),
    'darkred':        (139, 0, 0),
    'darksalmon':     (233, 150, 122),
    'darkseagreen':   (143, 188, 143),
    'darkslateblue':  (72, 61, 139),
    'darkslategray':  (47, 79, 79),
    'darkslategrey':  (47, 79, 79),
    'darkturquoise':  (0, 206, 209),
    'darkviolet':     (148, 0, 211),
    'deeppink':       (255, 20, 147),
    'deepskyblue':    (0, 191, 255),
    'dimgray':        (105, 105, 105),
    'dimgrey':        (105, 105, 105),
    'dodgerblue':     (30, 144, 255),
    'firebrick':      (178, 34, 34),
    'floralwhite':    (255, 250, 240),
    'forestgreen':    (34, 139, 34),
    'fuchsia':        (255, 0, 255),
    'gainsboro':      (220, 220, 220),
    'ghostwhite':     (248, 248, 255),
    'gold':           (255, 215, 0),
    'goldenrod':      (218, 165, 32),
    'gray':           (128, 128, 128),
    'green':          (0, 128, 0),
    'greenyellow':    (173, 255, 47),
    'grey':           (128, 128, 128),
    'honeydew':       (240, 255, 240),
    'hotpink':        (255, 105, 180),
    'indianred':      (205, 92, 92),
    'indigo':         (75, 0, 130),
    'ivory':          (255, 255, 240),
    'khaki':          (240, 230, 140),
    'lavender':       (230, 230, 250),
    'lavenderblush':  (255, 240, 245),
    'lawngreen':      (124, 252, 0),
    'lemonchiffon':   (255, 250, 205),
    'lightblue':      (173, 216, 230),
    'lightcoral':     (240, 128, 128),
    'lightcyan':      (224, 255, 255),
    'lightgoldenrodyellow': (250, 250, 210),
    'lightgray':      (211, 211, 211),
    'lightgreen':     (144, 238, 144),
    'lightgrey':      (211, 211, 211),
    'lightpink':      (255, 182, 193),
    'lightsalmon':    (255, 160, 122),
    'lightseagreen':  (32, 178, 170),
    'lightskyblue':   (135, 206, 250),
    'lightslategray': (119, 136, 153),
    'lightslategrey': (119, 136, 153),
    'lightsteelblue': (176, 196, 222),
    'lightyellow':    (255, 255, 224),
    'lime':           (0, 255, 0),
    'limegreen':      (50, 205, 50),
    'linen':          (250, 240, 230),
    'magenta':        (255, 0, 255),
    'maroon':         (128, 0, 0),
    'mediumaquamarine': (102, 205, 170),
    'mediumblue':     (0, 0, 205),
    'mediumorchid':   (186, 85, 211),
    'mediumpurple':   (147, 112, 219),
    'mediumseagreen': (60, 179, 113),
    'mediumslateblue': (123, 104, 238),
    'mediumspringgreen': (0, 250, 154),
    'mediumturquoise': (72, 209, 204),
    'mediumvioletred': (199, 21, 133),
    'midnightblue':   (25, 25, 112),
    'mintcream':      (245, 255, 250),
    'mistyrose':      (255, 228, 225),
    'moccasin':       (255, 228, 181),
    'navajowhite':    (255, 222, 173),
    'navy':           (0, 0, 128),
    'oldlace':        (253, 245, 230),
    'olive':          (128, 128, 0),
    'olivedrab':      (107, 142, 35),
    'orange':         (255, 165, 0),
    'orangered':      (255, 69, 0),
    'orchid':         (218, 112, 214),
    'palegoldenrod':  (238, 232, 170),
    'palegreen':      (152, 251, 152),
    'paleturquoise':  (175, 238, 238),
    'palevioletred':  (219, 112, 147),
    'papayawhip':     (255, 239, 213),
    'peachpuff':      (255, 218, 185),
    'peru':           (205, 133, 63),
    'pink':           (255, 192, 203),
    'plum':           (221, 160, 221),
    'powderblue':     (176, 224, 230),
    'purple':         (128, 0, 128),
    'rebeccapurple':  (102, 51, 153),
    'red':            (255, 0, 0),
    'rosybrown':      (188, 143, 143),
    'royalblue':      (65, 105, 225),
    'saddlebrown':    (139, 69, 19),
    'salmon':         (250, 128, 114),
    'sandybrown':     (244, 164, 96),
    'seagreen':       (46, 139, 87),
    'seashell':       (255, 245, 238),
    'sienna':         (160, 82, 45),
    'silver':         (192, 192, 192),
    'skyblue':        (135, 206, 235),
    'slateblue':      (106, 90, 205),
    'slategray':      (112, 128, 144),
    'slategrey':      (112, 128, 144),
    'snow':           (255, 250, 250),
    'springgreen':    (0, 255, 127),
    'steelblue':      (70, 130, 180),
    'tan':            (210, 180, 140),
    'teal':           (0, 128, 128),
    'thistle':        (216, 191, 216),
    'tomato':         (255, 99, 71),
    'turquoise':      (64, 224, 208),
    'violet':         (238, 130, 238),
    'wheat':          (245, 222, 179),
    'white':          (255, 255, 255),
    'whitesmoke':     (245, 245, 245),
    'yellow':         (255, 255, 0),
    'yellowgreen':    (154, 205, 50)
}


def parse_rgb(s):
    ''' Convert string to an RGB color '''
    if not isinstance(s, str):
        raise ValueError(f"Could not parse color '{s}'")
    s = s.strip().replace(' ', '').lower()
    # simple lookup
    rgb = css_colors.get(s)
    if rgb is not None:
        return rgb

    # 6-digit hex
    match = re.match('#([a-f0-9]{6})$', s)
    if match:
        core = match.group(1)
        return tuple(int(core[i:i+2], 16) for i in range(0, 6, 2))

    # 3-digit hex
    match = re.match('#([a-f0-9]{3})$', s)
    if match:
        return tuple(int(c*2, 16) for c in match.group(1))

    # rgb(x,y,z)
    match = re.match(r'rgb\((\d+,\d+,\d+)\)', s)
    if match:
        return tuple(int(v) for v in match.group(1).split(','))

    raise ValueError(f"Could not parse color '{s}'")
