"""
Defines valid style options, validation and utilities
"""

import numpy as np
from bokeh.core.properties import (
    Angle,
    Color,
    DashPattern,
    FontSize,
    MarkerType,
    Percent,
    Size,
)

try:
    from matplotlib import cm, colors
except ImportError:
    cm, colors = None, None

from ...core.options import abbreviated_exception
from ...core.util import arraylike_types
from ...util.transform import dim
from ..util import COLOR_ALIASES, RGB_HEX_REGEX, rgb2hex

# Define shared style properties for bokeh plots

property_prefixes = ['selection', 'nonselection', 'muted', 'hover']

base_properties = ['visible', 'muted']

line_properties = ['line_color', 'line_alpha', 'color', 'alpha', 'line_width',
                   'line_join', 'line_cap', 'line_dash']
line_properties += [f'{prefix}_{prop}' for prop in line_properties
                    for prefix in property_prefixes]

fill_properties = ['fill_color', 'fill_alpha']
fill_properties += [f'{prefix}_{prop}' for prop in fill_properties
                    for prefix in property_prefixes]

text_properties = ['text_font', 'text_font_size', 'text_font_style', 'text_color',
                   'text_alpha', 'text_align', 'text_baseline']

legend_dimensions = ['label_standoff', 'label_width', 'label_height', 'glyph_width',
                     'glyph_height', 'legend_padding', 'legend_spacing', 'click_policy']

# Conversion between matplotlib and bokeh markers

markers = {
    '+': {'marker': 'cross'},
    'x': {'marker': 'cross', 'angle': np.pi/4},
    's': {'marker': 'square'},
    'd': {'marker': 'diamond'},
    '^': {'marker': 'triangle', 'angle': 0},
    '>': {'marker': 'triangle', 'angle': -np.pi/2},
    'v': {'marker': 'triangle', 'angle': np.pi},
    '<': {'marker': 'triangle', 'angle': np.pi/2},
    '1': {'marker': 'triangle', 'angle': 0},
    '2': {'marker': 'triangle', 'angle': -np.pi/2},
    '3': {'marker': 'triangle', 'angle': np.pi},
    '4': {'marker': 'triangle', 'angle': np.pi/2},
    'o': {'marker': 'circle'},
    '*': {'marker': 'asterisk'},
}


def mpl_to_bokeh(properties):
    """
    Utility to process style properties converting any
    matplotlib specific options to their nearest bokeh
    equivalent.
    """
    new_properties = {}
    for k, v in properties.items():
        if isinstance(v, dict):
            new_properties[k] = v
        elif k == 's':
            new_properties['size'] = v
        elif k == 'marker':
            new_properties.update(markers.get(v, {'marker': v}))
        elif (k == 'color' or k.endswith('_color')) and not isinstance(v, (dict, dim)):
            with abbreviated_exception():
                v = COLOR_ALIASES.get(v, v)
            if isinstance(v, tuple):
                with abbreviated_exception():
                    v = rgb2hex(v)
            new_properties[k] = v
        else:
            new_properties[k] = v
    new_properties.pop('cmap', None)
    return new_properties

# Validation

alpha     = Percent()
angle     = Angle()
color     = Color()
dash_pattern = DashPattern()
font_size = FontSize()
marker    = MarkerType()
size      = Size()

validators = {
    'angle'     : angle.is_valid,
    'alpha'     : alpha.is_valid,
    'color'     : lambda x: (
        color.is_valid(x) or (isinstance(x, str) and RGB_HEX_REGEX.match(x))
    ),
    'font_size' : font_size.is_valid,
    'line_dash' : dash_pattern.is_valid,
    'marker'    : lambda x: marker.is_valid(x) or x in markers,
    'size'      : size.is_valid,
}

def get_validator(style):
    for k, v in validators.items():
        if style.endswith(k):
            return v


def validate(style, value, scalar=False):
    """
    Validates a style and associated value.

    Arguments
    ---------
    style: str
       The style to validate (e.g. 'color', 'size' or 'marker')
    value:
       The style value to validate
    scalar: bool

    Returns
    -------
    valid: boolean or None
       If validation is supported returns boolean, otherwise None
    """
    validator = get_validator(style)
    if validator is None:
        return None
    if isinstance(value, arraylike_types+(list,)):
        if scalar:
            return False
        return all(validator(v) for v in value)
    return validator(value)

# Utilities

def rgba_tuple(rgba):
    """
    Ensures RGB(A) tuples in the range 0-1 are scaled to 0-255.
    """
    if isinstance(rgba, tuple):
        return tuple(int(c*255) if i<3 else c for i, c in enumerate(rgba))
    else:
        return COLOR_ALIASES.get(rgba, rgba)


def expand_batched_style(style, opts, mapping, nvals):
    """
    Computes styles applied to a batched plot by iterating over the
    supplied list of style options and expanding any options found in
    the supplied style dictionary returning a data and mapping defining
    the data that should be added to the ColumnDataSource.
    """
    opts = sorted(opts, key=lambda x: x in ['color', 'alpha'])
    applied_styles = set(mapping)
    style_data, style_mapping = {}, {}
    for opt in opts:
        if 'color' in opt:
            alias = 'color'
        elif 'alpha' in opt:
            alias = 'alpha'
        else:
            alias = None
        if opt not in style or opt in mapping:
            continue
        elif opt == alias:
            if alias in applied_styles:
                continue
            elif 'line_'+alias in applied_styles:
                if 'fill_'+alias not in opts:
                    continue
                opt = 'fill_'+alias
                val = style[alias]
            elif 'fill_'+alias in applied_styles:
                opt = 'line_'+alias
                val = style[alias]
            else:
                val = style[alias]
        else:
            val = style[opt]
        style_mapping[opt] = {'field': opt}
        applied_styles.add(opt)
        if 'color' in opt and isinstance(val, tuple):
            val = rgb2hex(val)
        style_data[opt] = [val]*nvals
    return style_data, style_mapping
