# Copyright 2015 Bloomberg Finance L.P.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

r"""

======
Pyplot
======

.. currentmodule:: bqplot.pyplot

.. autosummary::
   :toctree: _generate/

   figure
   show
   axes

   plot
   scatter
   hist
   bar
   ohlc
   geo

   clear
   close
   current_figure

   scales
   xlim
   ylim

   axes
   xlabel
   ylabel

"""
import sys
from collections import OrderedDict
from IPython.display import display
from ipywidgets import VBox
from ipywidgets import Image as ipyImage
from numpy import arange, issubdtype, array, column_stack, shape
from .figure import Figure
from .scales import Scale, LinearScale, Mercator
from .axes import Axis
from .marks import (Lines, Scatter, ScatterGL, Hist, Bars, OHLC, Pie, Map, Image,
                    Label, HeatMap, GridHeatMap, topo_load, Boxplot, Bins)
from .toolbar import Toolbar
from .interacts import (BrushIntervalSelector, FastIntervalSelector,
                        BrushSelector, IndexSelector, MultiSelector,
                        LassoSelector)
from traitlets.utils.sentinel import Sentinel
import functools

SCATTER_SIZE_LIMIT = 10 * 1000  # above this limit, ScatterGL will be used by default

Keep = Sentinel('Keep', 'bqplot.pyplot', '''
        Used in bqplot.pyplot to specify that the same scale should be used for
        a certain dimension.
        ''')
# `_context` object contains the global information for pyplot.
# `figure`: refers to the current figure to which marks will be added.
# `scales`: The current set of scales which will be used for drawing a mark. if
# the scale for an attribute is not present, it is created based on the range
# type.
# `scale_registry`: This is a dictionary where the keys are the context
# names and the values are the set of scales which were used on the last plot
# in that context. This is useful when switching context.
# `last_mark`: refers to the last mark that has been plotted.
# `current_key`: The key for the current context figure. If there is no key,
# then the value is `None`.
_context = {
    'figure': None,
    'figure_registry': {},
    'scales': {},
    'scale_registry': {},
    'last_mark': None,
    'current_key': None
}

LINE_STYLE_CODES = OrderedDict([(':', 'dotted'), ('-.', 'dash_dotted'),
                                ('--', 'dashed'), ('-', 'solid')])

COLOR_CODES = {'b': 'blue', 'g': 'green', 'r': 'red', 'c': 'cyan',
               'm': 'magenta', 'y': 'yellow', 'k': 'black'}

MARKER_CODES = {'o': 'circle', 'v': 'triangle-down', '^': 'triangle-up',
                's': 'square', 'd': 'diamond', '+': 'cross'}


PY2 = sys.version_info[0] == 2

if PY2:
    string_types = basestring,  # noqa
else:
    string_types = str,


def hashable(data, v):
    """Determine whether `v` can be hashed."""
    try:
        data[v]
    except (TypeError, KeyError, IndexError):
        return False
    return True


def show(key=None, display_toolbar=True):
    """Shows the current context figure in the output area.

    Parameters
    ----------

    key : hashable, optional
        Any variable that can be used as a key for a dictionary.
    display_toolbar: bool (default: True)
        If True, a toolbar for different mouse interaction is displayed with
        the figure.

    Raises
    ------

    KeyError
        When no context figure is associated with the provided key.

    Examples
    --------

        >>> import numpy as np
        >>> import pyplot as plt
        >>> n = 100
        >>> x = np.arange(n)
        >>> y = np.cumsum(np.random.randn(n))
        >>> plt.plot(x,y)
        >>> plt.show()

    """
    if key is None:
        figure = current_figure()
    else:
        figure = _context['figure_registry'][key]
    if display_toolbar:
        if not hasattr(figure, 'pyplot'):
            figure.pyplot = Toolbar(figure=figure)
            figure.pyplot_vbox = VBox([figure, figure.pyplot])
        display(figure.pyplot_vbox)
    else:
        display(figure)


def figure(key=None, fig=None, **kwargs):
    """Creates figures and switches between figures.

    If a ``bqplot.Figure`` object is provided via the fig optional argument,
    this figure becomes the current context figure.

    Otherwise:

    - If no key is provided, a new empty context figure is created.
    - If a key is provided for which a context already exists, the
      corresponding context becomes current.
    - If a key is provided and no corresponding context exists, a new context
      is reated for that key and becomes current.

    Besides, optional arguments allow to set or modify Attributes
    of the selected context figure.

    Parameters
    ----------
    key: hashable, optional
        Any variable that can be used as a key for a dictionary
    fig: Figure, optional
        A bqplot Figure

    """
    scales_arg = kwargs.pop('scales', {})
    _context['current_key'] = key
    if fig is not None:                                     # fig provided
        _context['figure'] = fig
        if key is not None:
            _context['figure_registry'][key] = fig
        for arg in kwargs:
            setattr(_context['figure'], arg, kwargs[arg])
    else:                                                   # no fig provided
        if key is None:                                     # no key provided
            _context['figure'] = Figure(**kwargs)
        else:                                               # a key is provided
            if key not in _context['figure_registry']:
                if 'title' not in kwargs:
                    kwargs['title'] = 'Figure' + ' ' + str(key)
                _context['figure_registry'][key] = Figure(**kwargs)
            _context['figure'] = _context['figure_registry'][key]
            for arg in kwargs:
                setattr(_context['figure'], arg, kwargs[arg])
    scales(key, scales=scales_arg)
    # Set the axis reference dictionary. This dictionary contains the mapping
    # from the possible dimensions in the figure to the list of scales with
    # respect to which axes have been drawn for this figure.
    # Used to automatically generate axis.
    if(getattr(_context['figure'], 'axis_registry', None) is None):
        setattr(_context['figure'], 'axis_registry', {})
    return _context['figure']


def close(key):
    """Closes and unregister the context figure corresponding to the key.

    Parameters
    ----------

    key: hashable
        Any variable that can be used as a key for a dictionary

    """
    figure_registry = _context['figure_registry']
    if key not in figure_registry:
        return
    if _context['figure'] == figure_registry[key]:
        figure()
    fig = figure_registry[key]
    if hasattr(fig, 'pyplot'):
        fig.pyplot.close()
        fig.pyplot_vbox.close()
    fig.close()
    del figure_registry[key]
    del _context['scale_registry'][key]


def _process_data(*kwarg_names):
    """Helper function to handle data keyword argument
    """
    def _data_decorator(func):
        @functools.wraps(func)
        def _mark_with_data(*args, **kwargs):
            data = kwargs.pop('data', None)
            if data is None:
                return func(*args, **kwargs)
            else:
                data_args = [data[i] if hashable(data, i) else i for i in args]
                data_kwargs = {
                    kw: data[kwargs[kw]] if hashable(data, kwargs[kw]) else kwargs[kw] for kw in set(kwarg_names).intersection(list(kwargs.keys()))
                }
                try:
                    # if any of the plots want to use the index_data, they can
                    # use it by referring to this attribute.
                    data_kwargs['index_data'] = data.index
                except AttributeError:
                    pass
                kwargs_update = kwargs.copy()
                kwargs_update.update(data_kwargs)
                return func(*data_args, **kwargs_update)

        return _mark_with_data
    return _data_decorator


def scales(key=None, scales={}):
    """Creates and switches between context scales.

    If no key is provided, a new blank context is created.

    If a key is provided for which a context already exists, the existing
    context is set as the current context.

    If a key is provided and no corresponding context exists, a new context is
    created for that key and set as the current context.

    Parameters
    ----------
    key: hashable, optional
        Any variable that can be used as a key for a dictionary
    scales: dictionary
        Dictionary of scales to be used in the new context

    Example
    -------

        >>> scales(scales={
        >>>    'x': Keep,
        >>>    'color': ColorScale(min=0, max=1)
        >>> })

    This creates a new scales context, where the 'x' scale is kept from the
    previous context, the 'color' scale is an instance of ColorScale
    provided by the user. Other scales, potentially needed such as the 'y'
    scale in the case of a line chart will be created on the fly when
    needed.

    Notes
    -----
    Every call to the function figure triggers a call to scales.

    The `scales` parameter is ignored if the `key` argument is not Keep and
    context scales already exist for that key.
    """
    old_ctxt = _context['scales']
    if key is None:  # No key provided
        _context['scales'] = {_get_attribute_dimension(k): scales[k] if scales[k] is not Keep
                              else old_ctxt[_get_attribute_dimension(k)] for k in scales}
    else:  # A key is provided
        if key not in _context['scale_registry']:
            _context['scale_registry'][key] = {
                _get_attribute_dimension(k): scales[k]
                if scales[k] is not Keep
                else old_ctxt[_get_attribute_dimension(k)]
                for k in scales
            }
        _context['scales'] = _context['scale_registry'][key]


def xlim(min, max):
    """Set the domain bounds of the current 'x' scale.
    """
    return set_lim(min, max, 'x')


def ylim(min, max):
    """Set the domain bounds of the current 'y' scale.
    """
    return set_lim(min, max, 'y')


def set_lim(min, max, name):
    """Set the domain bounds of the scale associated with the provided key.

    Parameters
    ----------
    name: hashable
        Any variable that can be used as a key for a dictionary

    Raises
    ------
    KeyError
        When no context figure is associated with the provided key.

    """
    scale = _context['scales'][_get_attribute_dimension(name)]
    scale.min = min
    scale.max = max
    return scale


def axes(mark=None, options={}, **kwargs):
    """Draws axes corresponding to the scales of a given mark.

    It also returns a dictionary of drawn axes. If the mark is not provided,
    the last drawn mark is used.

    Parameters
    ----------
    mark: Mark or None (default: None)
        The mark to inspect to create axes. If None, the last mark drawn is
        used instead.
    options: dict (default: {})
        Options for the axes to be created. If a scale labeled 'x' is required
        for that mark, options['x'] contains optional keyword arguments for the
        constructor of the corresponding axis type.
    """
    if mark is None:
        mark = _context['last_mark']
    if mark is None:
        return {}
    fig = kwargs.get('figure', current_figure())
    scales = mark.scales
    fig_axes = [axis for axis in fig.axes]
    axes = {}
    for name in scales:
        if name not in mark.class_trait_names(scaled=True):
            # The scale is not needed.
            continue
        scale_metadata = mark.scales_metadata.get(name, {})
        dimension = scale_metadata.get('dimension', scales[name])
        axis_args = dict(scale_metadata,
                         **(options.get(name, {})))

        axis = _fetch_axis(fig, dimension, scales[name])
        if axis is not None:
            # For this figure, an axis exists for the scale in the given
            # dimension. Apply the properties and return back the object.
            _apply_properties(axis, options.get(name, {}))
            axes[name] = axis
            continue

        # An axis must be created. We fetch the type from the registry
        # the key being provided in the scaled attribute decoration
        key = mark.class_traits()[name].get_metadata('atype')
        if(key is not None):
            axis_type = Axis.axis_types[key]
            axis = axis_type(scale=scales[name], **axis_args)
            axes[name] = axis
            fig_axes.append(axis)
            # Update the axis registry of the figure once the axis is added
            _update_fig_axis_registry(fig, dimension, scales[name], axis)
    fig.axes = fig_axes
    return axes


def _set_label(label, mark, dim, **kwargs):
    """Helper function to set labels for an axis
    """
    if mark is None:
        mark = _context['last_mark']
    if mark is None:
        return {}
    fig = kwargs.get('figure', current_figure())
    scales = mark.scales
    scale_metadata = mark.scales_metadata.get(dim, {})
    scale = scales.get(dim, None)
    if scale is None:
        return
    dimension = scale_metadata.get('dimension', scales[dim])
    axis = _fetch_axis(fig, dimension, scales[dim])

    if axis is not None:
        _apply_properties(axis, {'label': label})


def xlabel(label=None, mark=None, **kwargs):
    """Sets the value of label for an axis whose associated scale has the
    dimension `x`.

    Parameters
    ----------
    label: Unicode or None (default: None)
        The label for x axis
    """
    _set_label(label, mark, 'x', **kwargs)


def ylabel(label=None, mark=None, **kwargs):
    """Sets the value of label for an axis whose associated scale has the
    dimension `y`.

    Parameters
    ----------
    label: Unicode or None (default: None)
        The label for y axis
    """
    _set_label(label, mark, 'y', **kwargs)


def grids(fig=None, value='solid'):
    """Sets the value of the grid_lines for the axis to the passed value.
    The default value is `solid`.

    Parameters
    ----------
    fig: Figure or None(default: None)
        The figure for which the axes should be edited. If the value is None,
        the current figure is used.
    value: {'none', 'solid', 'dashed'}
        The display of the grid_lines
    """

    if fig is None:
        fig = current_figure()
    for a in fig.axes:
        a.grid_lines = value


def title(label, style=None):
    """Sets the title for the current figure.

    Parameters
    ----------
    label : str
        The new title for the current figure.
    style: dict
        The CSS style to be applied to the figure title
    """
    fig = current_figure()
    fig.title = label
    if style is not None:
        fig.title_style = style


def legend():
    """Places legend in the current figure."""
    for m in current_figure().marks:
        m.display_legend = True


def hline(level, **kwargs):
    """Draws a horizontal line at the given level.

    Parameters
    ----------
    level: float
        The level at which to draw the horizontal line.
    preserve_domain: boolean (default: False)
        If true, the line does not affect the domain of the 'y' scale.
    """
    kwargs.setdefault('colors', ['dodgerblue'])
    kwargs.setdefault('stroke_width', 1)
    scales = kwargs.pop('scales', {})
    fig = kwargs.get('figure', current_figure())
    scales['x'] = fig.scale_x

    level = array(level)
    if len(level.shape) == 0:
        x = [0, 1]
        y = [level, level]
    else:
        x = [0, 1]
        y = column_stack([level, level])
    return plot(x, y, scales=scales, preserve_domain={
        'x': True,
        'y': kwargs.get('preserve_domain', False)
    }, axes=False, update_context=False, **kwargs)


def vline(level, **kwargs):
    """Draws a vertical line at the given level.

    Parameters
    ----------
    level: float
        The level at which to draw the vertical line.
    preserve_domain: boolean (default: False)
        If true, the line does not affect the domain of the 'x' scale.
    """
    kwargs.setdefault('colors', ['dodgerblue'])
    kwargs.setdefault('stroke_width', 1)
    scales = kwargs.pop('scales', {})
    fig = kwargs.get('figure', current_figure())
    scales['y'] = fig.scale_y

    level = array(level)
    if len(level.shape) == 0:
        x = [level, level]
        y = [0, 1]
    else:
        x = column_stack([level, level])
        # TODO: repeating [0, 1] should not be required once we allow for
        # 2-D x and 1-D y
        y = [[0, 1]] * len(level)
    return plot(x, y, scales=scales, preserve_domain={
        'x': kwargs.get('preserve_domain', False),
        'y': True
    }, axes=False, update_context=False, **kwargs)


def _process_cmap(cmap):
    '''
    Returns a kwarg dict suitable for a ColorScale
    '''
    option = {}
    if isinstance(cmap, str):
        option['scheme'] = cmap
    elif isinstance(cmap, list):
        option['colors'] = cmap
    else:
        raise ValueError('''`cmap` must be a string (name of a color scheme)
                         or a list of colors, but a value of {} was given
                         '''.format(cmap))
    return option


def set_cmap(cmap):
    '''
    Set the color map of the current 'color' scale.
    '''
    scale = _context['scales']['color']
    for k, v in _process_cmap(cmap).items():
        setattr(scale, k, v)
    return scale


def _draw_mark(mark_type, options={}, axes_options={}, **kwargs):
    """Draw the mark of specified mark type.

    Parameters
    ----------
    mark_type: type
        The type of mark to be drawn
    options: dict (default: {})
        Options for the scales to be created. If a scale labeled 'x' is
        required for that mark, options['x'] contains optional keyword
        arguments for the constructor of the corresponding scale type.
    axes_options: dict (default: {})
        Options for the axes to be created. If an axis labeled 'x' is required
        for that mark, axes_options['x'] contains optional keyword arguments
        for the constructor of the corresponding axis type.
    figure: Figure or None
        The figure to which the mark is to be added.
        If the value is None, the current figure is used.
    cmap: list or string
        List of css colors, or name of bqplot color scheme
    """
    fig = kwargs.pop('figure', current_figure())
    scales = kwargs.pop('scales', {})
    update_context = kwargs.pop('update_context', True)

    # Set the color map of the color scale
    cmap = kwargs.pop('cmap', None)
    if cmap is not None:
        # Add the colors or scheme to the color scale options
        options['color'] = dict(options.get('color', {}),
                                **_process_cmap(cmap))

    # Going through the list of data attributes
    for name in mark_type.class_trait_names(scaled=True):
        dimension = _get_attribute_dimension(name, mark_type)
        # TODO: the following should also happen if name in kwargs and
        # scales[name] is incompatible.
        if name not in kwargs:
            # The scaled attribute is not being passed to the mark. So no need
            # create a scale for this.
            continue
        elif name in scales:
            if update_context:
                _context['scales'][dimension] = scales[name]
        # Scale has to be fetched from the context or created as it has not
        # been passed.
        elif dimension not in _context['scales']:
            # Creating a scale for the dimension if a matching scale is not
            # present in _context['scales']
            traitlet = mark_type.class_traits()[name]
            rtype = traitlet.get_metadata('rtype')
            dtype = traitlet.validate(None, kwargs[name]).dtype
            # Fetching the first matching scale for the rtype and dtype of the
            # scaled attributes of the mark.
            compat_scale_types = [
                Scale.scale_types[key]
                for key in Scale.scale_types
                if Scale.scale_types[key].rtype == rtype and issubdtype(dtype, Scale.scale_types[key].dtype)
            ]
            sorted_scales = sorted(compat_scale_types,
                                   key=lambda x: x.precedence)
            scales[name] = sorted_scales[-1](**options.get(name, {}))
            # Adding the scale to the context scales
            if update_context:
                _context['scales'][dimension] = scales[name]
        else:
            scales[name] = _context['scales'][dimension]

    mark = mark_type(scales=scales, **kwargs)
    _context['last_mark'] = mark
    fig.marks = [m for m in fig.marks] + [mark]
    if kwargs.get('axes', True):
        axes(mark, options=axes_options)
    return mark


def _infer_x_for_line(y):
    """
    Infers the x for a line if no x is provided.
    """
    array_shape = shape(y)

    if len(array_shape) == 0:
        return []
    if len(array_shape) == 1:
        return arange(array_shape[0])
    if len(array_shape) > 1:
        return arange(array_shape[1])


@_process_data('color')
def plot(*args, **kwargs):
    """Draw lines in the current context figure.

    Signature: `plot(x, y, **kwargs)` or `plot(y, **kwargs)`, depending of the
    length of the list of positional arguments. In the case where the `x` array
    is not provided.

    Parameters
    ----------
    x: numpy.ndarray or list, 1d or 2d (optional)
        The x-coordinates of the plotted line. When not provided, the function
        defaults to `numpy.arange(len(y))`
        x can be 1-dimensional or 2-dimensional.
    y: numpy.ndarray or list, 1d or 2d
        The y-coordinates of the plotted line. If argument `x` is 2-dimensional
        it must also be 2-dimensional.
    marker_str: string
        string representing line_style, marker and color.
        For e.g. 'g--o', 'sr' etc
    options: dict (default: {})
        Options for the scales to be created. If a scale labeled 'x' is
        required for that mark, options['x'] contains optional keyword
        arguments for the constructor of the corresponding scale type.
    axes_options: dict (default: {})
        Options for the axes to be created. If an axis labeled 'x' is required
        for that mark, axes_options['x'] contains optional keyword arguments
        for the constructor of the corresponding axis type.
    figure: Figure or None
        The figure to which the line is to be added.
        If the value is None, the current figure is used.
    """
    marker_str = None
    if len(args) == 1:
        kwargs['y'] = args[0]
        if kwargs.get('index_data', None) is not None:
            kwargs['x'] = kwargs['index_data']
        else:
            kwargs['x'] = _infer_x_for_line(args[0])
    elif len(args) == 2:
        if type(args[1]) == str:
            kwargs['y'] = args[0]
            kwargs['x'] = _infer_x_for_line(args[0])
            marker_str = args[1].strip()
        else:
            kwargs['x'] = args[0]
            kwargs['y'] = args[1]
    elif len(args) == 3:
        kwargs['x'] = args[0]
        kwargs['y'] = args[1]
        if type(args[2]) == str:
            marker_str = args[2].strip()

    if marker_str:
        line_style, color, marker = _get_line_styles(marker_str)

        # only marker specified => draw scatter
        if marker and not line_style:
            kwargs['marker'] = marker
            if color:
                kwargs['colors'] = [color]
            return _draw_mark(Scatter, **kwargs)
        else:  # draw lines in all other cases
            kwargs['line_style'] = line_style or 'solid'

            if marker:
                kwargs['marker'] = marker
            if color:
                kwargs['colors'] = [color]
            return _draw_mark(Lines, **kwargs)
    else:
        return _draw_mark(Lines, **kwargs)


def imshow(image, format, **kwargs):
    """Draw an image in the current context figure.
    Parameters
    ----------
    image: image data
        Image data, depending on the passed format, can be one of:
            - an instance of an ipywidgets Image
            - a file name
            - a raw byte string
    format: {'widget', 'filename', ...}
        Type of the input argument.
        If not 'widget' or 'filename', must be a format supported by
        the ipywidgets Image.
    options: dict (default: {})
        Options for the scales to be created. If a scale labeled 'x' is
        required for that mark, options['x'] contains optional keyword
        arguments for the constructor of the corresponding scale type.
    axes_options: dict (default: {})
        Options for the axes to be created. If an axis labeled 'x' is required
        for that mark, axes_options['x'] contains optional keyword arguments
        for the constructor of the corresponding axis type.
    """
    if format == 'widget':
        ipyimage = image
    elif format == 'filename':
        with open(image, 'rb') as f:
            data = f.read()
            ipyimage = ipyImage(value=data)
    else:
        ipyimage = ipyImage(value=image, format=format)
    kwargs['image'] = ipyimage

    kwargs.setdefault('x', [0., 1.])
    kwargs.setdefault('y', [0., 1.])

    return _draw_mark(Image, **kwargs)


def ohlc(*args, **kwargs):
    """Draw OHLC bars or candle bars in the current context figure.

    Signature: `ohlc(x, y, **kwargs)` or `ohlc(y, **kwargs)`, depending of the
    length of the list of positional arguments. In the case where the `x` array
    is not provided

    Parameters
    ----------
    x: numpy.ndarray or list, 1d (optional)
        The x-coordinates of the plotted line. When not provided, the function
        defaults to `numpy.arange(len(y))`.
    y: numpy.ndarray or list, 2d
        The ohlc (open/high/low/close) information. A two dimensional array. y
        must have the shape (n, 4).
    options: dict (default: {})
        Options for the scales to be created. If a scale labeled 'x' is
        required for that mark, options['x'] contains optional keyword
        arguments for the constructor of the corresponding scale type.
    axes_options: dict (default: {})
        Options for the axes to be created. If an axis labeled 'x' is required
        for that mark, axes_options['x'] contains optional keyword arguments
        for the constructor of the corresponding axis type.
    """
    if len(args) == 2:
        kwargs['x'] = args[0]
        kwargs['y'] = args[1]
    elif len(args) == 1:
        kwargs['y'] = args[0]
        length = len(args[0])
        kwargs['x'] = arange(length)
    return _draw_mark(OHLC, **kwargs)


@_process_data('color', 'opacity', 'size', 'skew', 'rotation')
def scatter(x, y, use_gl=None, **kwargs):
    """Draw a scatter in the current context figure.

    Parameters
    ----------

    x: numpy.ndarray, 1d
        The x-coordinates of the data points.
    y: numpy.ndarray, 1d
        The y-coordinates of the data points.
    use_gl: If true, will use the ScatterGL mark (pixelized but faster), if false a normal
        Scatter mark is used. If None, a choised is made automatically depending on the length
        of x.
    options: dict (default: {})
        Options for the scales to be created. If a scale labeled 'x' is
        required for that mark, options['x'] contains optional keyword
        arguments for the constructor of the corresponding scale type.
    axes_options: dict (default: {})
        Options for the axes to be created. If an axis labeled 'x' is required
        for that mark, axes_options['x'] contains optional keyword arguments
        for the constructor of the corresponding axis type.
    """
    kwargs['x'] = x
    kwargs['y'] = y
    if use_gl is None:
        mark_class = ScatterGL if len(x) >= SCATTER_SIZE_LIMIT else Scatter
    else:
        mark_class = ScatterGL if use_gl else Scatter
    return _draw_mark(mark_class, **kwargs)


@_process_data()
def hist(sample, options={}, **kwargs):
    """Draw a histogram in the current context figure.

    Parameters
    ----------
    sample: numpy.ndarray, 1d
        The sample for which the histogram must be generated.
    options: dict (default: {})
        Options for the scales to be created. If a scale labeled 'counts'
        is required for that mark, options['counts'] contains optional keyword
        arguments for the constructor of the corresponding scale type.
    axes_options: dict (default: {})
        Options for the axes to be created. If an axis labeled 'counts' is
        required for that mark, axes_options['counts'] contains optional
        keyword arguments for the constructor of the corresponding axis type.
    """
    kwargs['sample'] = sample
    scales = kwargs.pop('scales', {})
    if 'count' not in scales:
        dimension = _get_attribute_dimension('count', Hist)
        if dimension in _context['scales']:
            scales['count'] = _context['scales'][dimension]
        else:
            scales['count'] = LinearScale(**options.get('count', {}))
            _context['scales'][dimension] = scales['count']
    kwargs['scales'] = scales
    return _draw_mark(Hist, options=options, **kwargs)


@_process_data()
def bin(sample, options={}, **kwargs):
    """Draw a histogram in the current context figure.
    Parameters
    ----------
    sample: numpy.ndarray, 1d
        The sample for which the histogram must be generated.
    options: dict (default: {})
        Options for the scales to be created. If a scale labeled 'x'
        is required for that mark, options['x'] contains optional keyword
        arguments for the constructor of the corresponding scale type.
    axes_options: dict (default: {})
        Options for the axes to be created. If an axis labeled 'x' is
        required for that mark, axes_options['x'] contains optional
        keyword arguments for the constructor of the corresponding axis type.
    """
    kwargs['sample'] = sample
    scales = kwargs.pop('scales', {})
    for xy in ['x', 'y']:
        if xy not in scales:
            dimension = _get_attribute_dimension(xy, Bars)
            if dimension in _context['scales']:
                scales[xy] = _context['scales'][dimension]
            else:
                scales[xy] = LinearScale(**options.get(xy, {}))
                _context['scales'][dimension] = scales[xy]
    kwargs['scales'] = scales
    return _draw_mark(Bins, options=options, **kwargs)


@_process_data('color')
def bar(x, y, **kwargs):
    """Draws a bar chart in the current context figure.

    Parameters
    ----------

    x: numpy.ndarray, 1d
        The x-coordinates of the data points.
    y: numpy.ndarray, 1d
        The y-coordinates of the data pints.
    options: dict (default: {})
        Options for the scales to be created. If a scale labeled 'x' is
        required for that mark, options['x'] contains optional keyword
        arguments for the constructor of the corresponding scale type.
    axes_options: dict (default: {})
        Options for the axes to be created. If an axis labeled 'x' is required
        for that mark, axes_options['x'] contains optional keyword arguments
        for the constructor of the corresponding axis type.
    """
    kwargs['x'] = x
    kwargs['y'] = y
    return _draw_mark(Bars, **kwargs)


@_process_data()
def boxplot(x, y, **kwargs):
    """Draws a boxplot in the current context figure.

    Parameters
    ----------

    x: numpy.ndarray, 1d
        The x-coordinates of the data points.
    y: numpy.ndarray, 2d
        The data from which the boxes are to be created. Each row of the data
        corresponds to one box drawn in the plot.
    options: dict (default: {})
        Options for the scales to be created. If a scale labeled 'x' is
        required for that mark, options['x'] contains optional keyword
        arguments for the constructor of the corresponding scale type.
    axes_options: dict (default: {})
        Options for the axes to be created. If an axis labeled 'x' is required
        for that mark, axes_options['x'] contains optional keyword arguments
        for the constructor of the corresponding axis type.
    """
    kwargs['x'] = x
    kwargs['y'] = y
    return _draw_mark(Boxplot, **kwargs)


@_process_data('color')
def barh(*args, **kwargs):
    """Draws a horizontal bar chart in the current context figure.

    Parameters
    ----------

    x: numpy.ndarray, 1d
        The domain of the data points.
    y: numpy.ndarray, 1d
        The range of the data pints.
    options: dict (default: {})
        Options for the scales to be created. If a scale labeled 'x' is
        required for that mark, options['x'] contains optional keyword
        arguments for the constructor of the corresponding scale type.
    axes_options: dict (default: {})
        Options for the axes to be created. If an axis labeled 'x' is required
        for that mark, axes_options['x'] contains optional keyword arguments
        for the constructor of the corresponding axis type.
    """
    kwargs['orientation'] = "horizontal"
    return bar(*args, **kwargs)


@_process_data('color')
def pie(sizes, **kwargs):
    """Draws a Pie in the current context figure.

    Parameters
    ----------
    sizes: numpy.ndarray, 1d
        The proportions to be represented by the Pie.
    options: dict (default: {})
        Options for the scales to be created. If a scale labeled 'x' is
        required for that mark, options['x'] contains optional keyword
        arguments for the constructor of the corresponding scale type.
    axes_options: dict (default: {})
        Options for the axes to be created. If an axis labeled 'x' is required
        for that mark, axes_options['x'] contains optional keyword arguments
        for the constructor of the corresponding axis type.
    """
    kwargs['sizes'] = sizes
    return _draw_mark(Pie, **kwargs)


def label(text, **kwargs):
    """Draws a Label in the current context figure.

    Parameters
    ----------

    text: string
        The label to be displayed.
    options: dict (default: {})
        Options for the scales to be created. If a scale labeled 'x' is
        required for that mark, options['x'] contains optional keyword
        arguments for the constructor of the corresponding scale type.
    axes_options: dict (default: {})
        Options for the axes to be created. If an axis labeled 'x' is required
        for that mark, axes_options['x'] contains optional keyword arguments
        for the constructor of the corresponding axis type.
    """
    kwargs['text'] = text
    return _draw_mark(Label, **kwargs)


def geo(map_data, **kwargs):
    """Draw a map in the current context figure.

    Parameters
    ----------
    map_data: string or bqplot.map (default: WorldMap)
        Name of the map or json file required for the map data.
    options: dict (default: {})
        Options for the scales to be created. If a scale labeled 'x' is
        required for that mark, options['x'] contains optional keyword
        arguments for the constructor of the corresponding scale type.
    axes_options: dict (default: {})
        Options for the axes to be created. If an axis labeled 'x' is required
        for that mark, axes_options['x'] contains optional keyword arguments
        for the constructor of the corresponding axis type.
    """
    scales = kwargs.pop('scales', _context['scales'])
    options = kwargs.get('options', {})
    if 'projection' not in scales:
        scales['projection'] = Mercator(**options.get('projection', {}))
    kwargs['scales'] = scales
    if isinstance(map_data, string_types):
        kwargs['map_data'] = topo_load('map_data/' + map_data + '.json')
    else:
        kwargs['map_data'] = map_data
    return _draw_mark(Map, **kwargs)


def heatmap(color, **kwargs):
    """Draw a heatmap in the current context figure.

    Parameters
    ----------
    color: numpy.ndarray, 2d
        Matrix of color of the data points
    options: dict (default: {})
        Options for the scales to be created. If a scale labeled 'x' is
        required for that mark, options['x'] contains optional keyword
        arguments for the constructor of the corresponding scale type.
    axes_options: dict (default: {})
        Options for the axes to be created. If an axis labeled 'x' is required
        for that mark, axes_options['x'] contains optional keyword arguments
        for the constructor of the corresponding axis type.
    """
    kwargs['color'] = color
    return _draw_mark(HeatMap, **kwargs)


def gridheatmap(color, **kwargs):
    """Draw a GridHeatMap in the current context figure.

    Parameters
    ----------
    color: numpy.ndarray, 2d
        Matrix of color of the data points
    options: dict (default: {})
        Options for the scales to be created. If a scale labeled 'x' is
        required for that mark, options['x'] contains optional keyword
        arguments for the constructor of the corresponding scale type.
    axes_options: dict (default: {})
        Options for the axes to be created. If an axis labeled 'x' is required
        for that mark, axes_options['x'] contains optional keyword arguments
        for the constructor of the corresponding axis type.
    """
    kwargs['color'] = color
    return _draw_mark(GridHeatMap, **kwargs)


def _add_interaction(int_type, **kwargs):
    """Add the interaction for the specified type.

    If a figure is passed using the key-word argument `figure` it is used. Else
    the context figure is used.
    If a list of marks are passed using the key-word argument `marks` it
    is used. Else the latest mark that is passed is used as the only mark
    associated with the selector.

    Parameters
    ----------
    int_type: type
        The type of interaction to be added.
    """

    fig = kwargs.pop('figure', current_figure())
    marks = kwargs.pop('marks', [_context['last_mark']])

    for name, traitlet in int_type.class_traits().items():
        dimension = traitlet.get_metadata('dimension')
        if dimension is not None:
            # only scales have this attribute in interactions
            kwargs[name] = _get_context_scale(dimension)
    kwargs['marks'] = marks
    interaction = int_type(**kwargs)
    if fig.interaction is not None:
        fig.interaction.close()
    fig.interaction = interaction
    return interaction


def _get_context_scale(dimension):
    """Return the scale instance in the current context for a given dimension.

    Parameters
    ----------

    dimension: string
        The dimension along which the current context scale is to be fetched.
    """
    return _context['scales'][dimension]


def _create_selector(int_type, func, trait, **kwargs):
    """Create a selector of the specified type.

    Also attaches the function `func` as an `on_trait_change` listener
    for the trait `trait` of the selector.

    This is an internal function which should not be called by the user.

    Parameters
    ----------

    int_type: type
        The type of selector to be added.
    func: function
        The call back function. It should take at least two arguments. The name
        of the trait and the value of the trait are passed as arguments.
    trait: string
        The name of the Selector trait whose change triggers the
        call back function `func`.
    """
    interaction = _add_interaction(int_type, **kwargs)
    if func is not None:
        interaction.on_trait_change(func, trait)
    return interaction


def brush_int_selector(func=None, trait='selected', **kwargs):
    """Create a `BrushIntervalSelector` interaction for the `figure`.

    Also attaches the function `func` as an event listener for the
    specified trait.

    Parameters
    ----------

    func: function
        The call back function. It should take at least two arguments. The name
        of the trait and the value of the trait are passed as arguments.
    trait: string
        The name of the BrushIntervalSelector trait whose change triggers the
        call back function `func`.
    """
    return _create_selector(BrushIntervalSelector, func, trait, **kwargs)


def int_selector(func=None, trait='selected', **kwargs):
    """Creates a `FastIntervalSelector` interaction for the `figure`.

    Also attaches the function `func` as an event listener for the
    trait `trait`.

    Parameters
    ----------

    func: function
        The call back function. It should take at least two arguments. The name
        of the trait and the value of the trait are passed as arguments.
    trait: string
        The name of the IntervalSelector trait whose change triggers the
        call back function `func`.
    """
    return _create_selector(FastIntervalSelector, func, trait, **kwargs)


def index_selector(func=None, trait='selected', **kwargs):
    """Creates an `IndexSelector` interaction for the `figure`.

    Also attaches the function `func` as an event listener for the
    trait `trait`.

    Parameters
    ----------

    func: function
        The call back function. It should take at least two arguments. The name
        of the trait and the value of the trait are passed as arguments.
    trait: string
        The name of the IndexSelector trait whose change triggers the
        call back function `func`.
    """
    return _create_selector(IndexSelector, func, trait, **kwargs)


def brush_selector(func=None, trait='selected', **kwargs):
    """Creates a `BrushSelector` interaction for the `figure`.

    Also attaches the function `func` as an event listener for the
    trait `trait`.

    Parameters
    ----------

    func: function
        The call back function. It should take at least two arguments. The name
        of the trait and the value of the trait are passed as arguments.
    trait: string
        The name of the BrushSelector trait whose change triggers the
        call back function `func`.
    """
    return _create_selector(BrushSelector, func, trait, **kwargs)


def multi_selector(func=None, trait='selected', **kwargs):
    """Creates a `MultiSelector` interaction for the `figure`.

    Also attaches the function `func` as an event listener for the
    trait `trait`.

    Parameters
    ----------

    func: function
        The call back function. It should take at least two arguments. The name
        of the trait and the value of the trait are passed as arguments.
    trait: string
        The name of the MultiSelector trait whose change triggers the
        call back function `func`.
    """
    return _create_selector(MultiSelector, func, trait, **kwargs)


def lasso_selector(func=None, trait='selected', **kwargs):
    """Creates a `LassoSelector` interaction for the `figure`.

    Also attaches the function `func` as an event listener for the
    specified trait.

    Parameters
    ----------

    func: function
        The call back function. It should take at least two arguments. The name
        of the trait and the value of the trait are passed as arguments.
    trait: string
        The name of the LassoSelector trait whose change triggers the
        call back function `func`.
    """
    return _create_selector(LassoSelector, func, trait, **kwargs)


def clear():
    """Clears the current context figure of all marks axes and grid lines."""
    fig = _context['figure']
    if fig is not None:
        fig.marks = []
        fig.axes = []
        setattr(fig, 'axis_registry', {})
        _context['scales'] = {}
        key = _context['current_key']
        if key is not None:
            _context['scale_registry'][key] = {}


def current_figure():
    """Returns the current context figure."""
    if _context['figure'] is None:
        figure()
    return _context['figure']

# FOR DEBUG ONLY


def get_context():
    """Used for debug only. Return a copy of the current global
    context dictionary."""
    return {k: v for k, v in _context.items()}


def set_context(context):
    """Sets the current global context dictionary. All the attributes to be set
    should be set. Otherwise, it will result in unpredictable behavior."""
    global _context
    _context = {k: v for k, v in context.items()}


def _fetch_axis(fig, dimension, scale):
    # Internal utility function.
    # Given a figure instance `fig`, the dimension of the scaled attribute and
    # the instance of a scale, returns the axis if an axis is present for that
    # combination. Else returns `None`
    axis_registry = getattr(fig, 'axis_registry', {})
    dimension_data = axis_registry.get(dimension, [])
    dimension_scales = [dim['scale'] for dim in dimension_data]
    dimension_axes = [dim['axis'] for dim in dimension_data]
    try:
        return dimension_axes[dimension_scales.index(scale)]
    except (ValueError, IndexError):
        return None


def _update_fig_axis_registry(fig, dimension, scale, axis):
    axis_registry = fig.axis_registry
    dimension_scales = axis_registry.get(dimension, [])
    dimension_scales.append({'scale': scale, 'axis': axis})
    axis_registry[dimension] = dimension_scales
    setattr(fig, 'axis_registry', axis_registry)


def _get_attribute_dimension(trait_name, mark_type=None):
    """Returns the dimension for the name of the trait for the specified mark.

    If `mark_type` is `None`, then the `trait_name` is returned
    as is.
    Returns `None` if the `trait_name` is not valid for `mark_type`.
    """
    if(mark_type is None):
        return trait_name
    scale_metadata = mark_type.class_traits()['scales_metadata']\
        .default_args[0]
    return scale_metadata.get(trait_name, {}).get('dimension', None)


def _apply_properties(widget, properties={}):
    """Applies the specified properties to the widget.

    `properties` is a dictionary with key value pairs corresponding
    to the properties to be applied to the widget.
    """
    with widget.hold_sync():
        for key, value in properties.items():
            setattr(widget, key, value)


def _get_line_styles(marker_str):
    """Return line style, color and marker type from specified marker string.

    For example, if ``marker_str`` is 'g-o' then the method returns
    ``('solid', 'green', 'circle')``.
    """
    def _extract_marker_value(marker_str, code_dict):
        """Extracts the marker value from a given marker string.

        Looks up the `code_dict` and returns the corresponding marker for a
        specific code.

        For example if `marker_str` is 'g-o' then the method extracts
        - 'green' if the code_dict is color_codes,
        - 'circle' if the code_dict is marker_codes etc.
        """
        val = None
        for code in code_dict:
            if code in marker_str:
                val = code_dict[code]
                break
        return val

    return [_extract_marker_value(marker_str, code_dict) for
            code_dict in [LINE_STYLE_CODES, COLOR_CODES, MARKER_CODES]]
