"""The formatting module includes functions to apply IRC formatting to text.

*Availability: 4.5+*
"""
# Copyright 2014, Elsie Powell, embolalia.com
# Copyright 2019, dgw, technobabbl.es
# Licensed under the Eiffel Forum License 2.
from __future__ import annotations

from enum import Enum
import re
import string


__all__ = [
    # control chars
    'CONTROL_NORMAL',
    'CONTROL_COLOR',
    'CONTROL_HEX_COLOR',
    'CONTROL_BOLD',
    'CONTROL_ITALIC',
    'CONTROL_UNDERLINE',
    'CONTROL_STRIKETHROUGH',
    'CONTROL_MONOSPACE',
    'CONTROL_REVERSE',
    # convenience lists
    'CONTROL_FORMATTING',
    'CONTROL_NON_PRINTING',
    # utility functions
    'color',
    'hex_color',
    'bold',
    'italic',
    'underline',
    'strikethrough',
    'monospace',
    'reverse',
    'plain',
    # utility enum
    'colors',
]

# Color names are as specified at http://www.mirc.com/colors.html

CONTROL_NORMAL = '\x0f'
"""The control code to reset formatting."""
CONTROL_COLOR = '\x03'
"""The control code to start or end color formatting."""
CONTROL_HEX_COLOR = '\x04'
"""The control code to start or end hexadecimal color formatting."""
CONTROL_BOLD = '\x02'
"""The control code to start or end bold formatting."""
CONTROL_ITALIC = '\x1d'
"""The control code to start or end italic formatting."""
CONTROL_UNDERLINE = '\x1f'
"""The control code to start or end underlining."""
CONTROL_STRIKETHROUGH = '\x1e'
"""The control code to start or end strikethrough formatting."""
CONTROL_MONOSPACE = '\x11'
"""The control code to start or end monospace formatting."""
CONTROL_REVERSE = '\x16'
"""The control code to start or end reverse-color formatting."""

CONTROL_FORMATTING = [
    CONTROL_NORMAL,
    CONTROL_COLOR,
    CONTROL_HEX_COLOR,
    CONTROL_BOLD,
    CONTROL_ITALIC,
    CONTROL_UNDERLINE,
    CONTROL_STRIKETHROUGH,
    CONTROL_MONOSPACE,
    CONTROL_REVERSE,
]
"""A list of all control characters expected to appear as formatting."""

CONTROL_NON_PRINTING = [
    '\x00',
    '\x01',
    '\x02',  # CONTROL_BOLD
    '\x03',  # CONTROL_COLOR
    '\x04',  # CONTROL_HEX_COLOR
    '\x05',
    '\x06',
    '\x07',
    '\x08',
    '\x09',
    '\x0a',
    '\x0b',
    '\x0c',
    '\x0d',
    '\x0e',
    '\x0f',  # CONTROL_NORMAL
    '\x10',
    '\x11',  # CONTROL_MONOSPACE
    '\x12',
    '\x13',
    '\x14',
    '\x15',
    '\x16',  # CONTROL_REVERSE
    '\x17',
    '\x18',
    '\x19',
    '\x1a',
    '\x1b',
    '\x1c',
    '\x1d',  # CONTROL_ITALIC
    '\x1e',  # CONTROL_STRIKETHROUGH
    '\x1f',  # CONTROL_UNDERLINE
    '\x7f',
]

# Regex to detect Control Pattern
COLOR_PATTERN = re.escape(CONTROL_COLOR) + r'((\d{1,2},\d{2})|\d{2})?'
HEX_COLOR_PATTERN = '%s(%s)?' % (
    re.escape(CONTROL_HEX_COLOR),
    '|'.join([
        '(' + ','.join([r'[a-fA-F0-9]{6}', r'[a-fA-F0-9]{6}']) + ')',
        r'[a-fA-F0-9]{6}'
    ])
)

PLAIN_PATTERN = '|'.join([
    '(' + COLOR_PATTERN + ')',
    '(' + HEX_COLOR_PATTERN + ')',
])
PLAIN_REGEX = re.compile(PLAIN_PATTERN)


class colors(str, Enum):
    """Mapping of color names to mIRC code values."""
    # Mostly aligned with https://modern.ircdocs.horse/formatting.html#colors
    # which are likely based on mIRC's color names (https://www.mirc.com/colors.html)
    WHITE = '00'
    BLACK = '01'
    BLUE = '02'
    GREEN = '03'
    LIGHT_RED = '04'
    BROWN = '05'
    PURPLE = '06'
    ORANGE = '07'
    YELLOW = '08'
    LIGHT_GREEN = '09'
    TEAL = '10'  # TODO: should be called 'CYAN'
    LIGHT_CYAN = '11'
    LIGHT_BLUE = '12'
    PINK = '13'
    GREY = '14'
    LIGHT_GREY = '15'

    # Create aliases.
    NAVY = BLUE
    RED = LIGHT_RED
    MAROON = BROWN
    OLIVE = ORANGE  # TODO: held over from antiquity; does anyone actually think this?
    LIME = LIGHT_GREEN
    CYAN = LIGHT_CYAN  # TODO: should be what is called 'TEAL' above
    ROYAL = LIGHT_BLUE
    LIGHT_PURPLE = PINK
    FUCHSIA = PINK
    GRAY = GREY
    LIGHT_GRAY = LIGHT_GREY
    SILVER = LIGHT_GREY


def _get_color(color):
    if color is None:
        return None

    # You can pass an int or string of the code
    try:
        color = int(color)
    except ValueError:
        pass
    if isinstance(color, int):
        if color > 99:
            raise ValueError('Can not specify a color above 99.')
        return str(color).rjust(2, '0')

    # You can also pass the name of the color
    color_name = color.upper()
    color_dict = colors.__dict__
    try:
        return color_dict[color_name]
    except KeyError:
        raise ValueError('Unknown color name {}'.format(color))


def color(text, fg=None, bg=None):
    """Return the text, with the given colors applied in IRC formatting.

    :param str text: the text to format
    :param mixed fg: the foreground color
    :param mixed bg: the background color
    :raises TypeError: if ``text`` is not a string
    :raises ValueError: if ``fg`` or ``bg`` is an unrecognized color value
    :rtype: str

    The color can be a string of the color name, or an integer in the range
    0-99. The known color names can be found in the :class:`colors` class of
    this module.
    """
    if fg is None and bg is None:
        return text

    fg = _get_color(fg)
    bg = _get_color(bg)

    if not bg:
        text = ''.join([CONTROL_COLOR, fg, text, CONTROL_COLOR])
    else:
        text = ''.join([CONTROL_COLOR, fg, ',', bg, text, CONTROL_COLOR])
    return text


def _get_hex_color(color):
    if color is None:
        return None

    try:
        color = color.upper()
        if not all(c in string.hexdigits for c in color):
            raise AttributeError
    except AttributeError:
        raise ValueError('Hexadecimal color value must be passed as string.')

    if len(color) == 3:
        return ''.join([c * 2 for c in color])
    elif len(color) == 6:
        return color
    else:  # invalid length
        raise ValueError('Hexadecimal color value must have either 3 or 6 digits.')


def hex_color(text, fg=None, bg=None):
    """Return the text, with the given colors applied in IRC formatting.

    :param str text: the text to format
    :param str fg: the foreground color
    :param str bg: the background color
    :raises TypeError: if ``text`` is not a string
    :raises ValueError: if ``fg`` or ``bg`` is an unrecognized color value
    :rtype: str

    The color can be provided with a string of either 3 or 6 hexadecimal digits.
    As in CSS, 3-digit colors will be interpreted as if they were 6-digit colors
    with each digit repeated (e.g. color ``c90`` is identical to ``cc9900``). Do
    not include the leading ``#`` symbol.

    .. note::
        This is a relatively new IRC formatting convention. Use it only when you
        can afford to have its meaning lost, as not many clients support it yet.
    """
    if not fg and not bg:
        return text

    fg = _get_hex_color(fg)
    bg = _get_hex_color(bg)

    if not bg:
        text = ''.join([CONTROL_HEX_COLOR, fg, text, CONTROL_HEX_COLOR])
    else:
        text = ''.join([CONTROL_HEX_COLOR, fg, ',', bg, text, CONTROL_HEX_COLOR])
    return text


def bold(text):
    """Return the text, with bold IRC formatting.

    :param str text: the text to format
    :raises TypeError: if ``text`` is not a string
    :rtype: str
    """
    return ''.join([CONTROL_BOLD, text, CONTROL_BOLD])


def italic(text):
    """Return the text, with italic IRC formatting.

    :param str text: the text to format
    :raises TypeError: if ``text`` is not a string
    :rtype: str
    """
    return ''.join([CONTROL_ITALIC, text, CONTROL_ITALIC])


def underline(text):
    """Return the text, with underline IRC formatting.

    :param str text: the text to format
    :raises TypeError: if ``text`` is not a string
    :rtype: str
    """
    return ''.join([CONTROL_UNDERLINE, text, CONTROL_UNDERLINE])


def strikethrough(text):
    """Return the text, with strikethrough IRC formatting.

    :param str text: the text to format
    :raises TypeError: if ``text`` is not a string
    :rtype: str

    .. note::
        This is a relatively new IRC formatting convention. Use it only when you
        can afford to have its meaning lost, as not many clients support it yet.
    """
    return ''.join([CONTROL_STRIKETHROUGH, text, CONTROL_STRIKETHROUGH])


def monospace(text):
    """Return the text, with monospace IRC formatting.

    :param str text: the text to format
    :raises TypeError: if ``text`` is not a string
    :rtype: str

    .. note::
        This is a relatively new IRC formatting convention. Use it only when you
        can afford to have its meaning lost, as not many clients support it yet.
    """
    return ''.join([CONTROL_MONOSPACE, text, CONTROL_MONOSPACE])


def reverse(text):
    """Return the text, with reverse-color IRC formatting.

    :param str text: the text to format
    :raises TypeError: if ``text`` is not a string
    :rtype: str

    .. note::
        This code isn't super well supported, and its behavior even in clients
        that understand it (e.g. mIRC) can be unpredictable. Use it carefully.
    """
    return ''.join([CONTROL_REVERSE, text, CONTROL_REVERSE])


def plain(text):
    """Return the text without any IRC formatting.

    :param str text: text with potential IRC formatting control code(s)
    :raises TypeError: if ``text`` is not a string
    :rtype: str
    """
    if '\x03' in text or '\x04' in text:
        text = PLAIN_REGEX.sub('', text)
    return ''.join(c for c in text if ord(c) >= 0x20 and c != '\x7F')
