""" CollisionModifiers and DataOperators used to specify Chart manipulations.

The general approach for these operations is to use a class for modeling the
operation, which is lazy evaluated, and doesn't require the data on initialization.

An associated, user-facing function is provided for a more friendly interface.
"""

from __future__ import absolute_import

from copy import copy

from bokeh.core.properties import Override, String, List, Instance
from .data_source import DataOperator
from .models import CollisionModifier
from .properties import ColumnLabel
from .stats import Stat, Count, stats


class Stack(CollisionModifier):
    """Cumulates elements in the order of grouped values.

    Useful for area or bar glyphs.
    """
    name = Override(default='stack')
    method_name = Override(default='__stack__')


class Dodge(CollisionModifier):
    """Non-cumulative, moves glyphs around so they don't overlap.

    Useful for bar, box, or dot glyphs.
    """
    name = Override(default='dodge')
    method_name = Override(default='__dodge__')


class Blend(DataOperator):
    """Creates combined variables, increasing the number of distinct items.

    The primary action that blend is taking is combining and collapsing
    multiple columns together, using set algebra. You can think of this
    like taking two columns with similar or different data and stacking
    them on top of each other. The new categories are the union of the two sets. The
    operation is like an OR because a category in either variable is included
    in the blended variable

    .. note::
        The variables not being blended must be duplicated (consider a sample time).
        For example, two variables, 'sensor_a' and 'sensor_b' only contain two values,
        either 'on' or 'off', with one more column of 'datetime'. Blending 'sensor_a'
        and 'sensor_b' results in two columns, 'datetime' and 'sensors_state'.

    Example:

        - cat1 + cat2 = [cat1, cat2]
        - cat1 + num1 = [cat1, cat(num1)]
        - num1 + num2 = [num1, num2]

    Can be used to stack column oriented measures, so they can be colored. In
    this case, a new categorical column will be created that identifies each
    measure by name of the previous column.

    Can be used to combine categorical columns.

    ToDo: address booleans. Consider two columns, 'was_successful', 'was_effective'.
        It seems like blending of booleans would be performing an *or*.

    See Grammar of Graphics pgs. 67, 320
    """

    name = String(default='value', help="""The name of the column to
                          contain the values of the blended columns.""")
    labels_name = String(default='variable', help="""The name of the column
                            to contain the names of the columns that were blended.""")

    def __init__(self, *cols, **properties):
        properties['columns'] = list(cols)
        super(Blend, self).__init__(**properties)

    def apply(self, data):
        data_copy = copy(data)

        data_copy.stack_measures(measures=self.columns, value_name=self.name,
                                 var_name=self.labels_name)
        return data_copy._data


class Aggregate(DataOperator):

    dimensions = List(ColumnLabel)
    stat = Instance(Stat, default=Count())
    agg_column = String()

    def __init__(self, **properties):
        stat = properties.pop('stat')
        if stat is not None and isinstance(stat, str):
            properties['stat'] = stats[stat]()
        col = properties.pop('columns')
        if col is not None:
            properties['columns'] = [col]
        super(Aggregate, self).__init__(**properties)

    def apply(self, data):
        data_copy = copy(data)
        if self.columns is None:
            self.stat = stats['count']()

        stat = self.stat

        agg_name = ''
        if self.columns is not None:
            agg_name += self.columns[0] + '_'
        agg_name += self.stat.__class__.__name__
        self.agg_column = agg_name

        if self.columns is None:
            col = data_copy.columns[0]
        else:
            col = self.columns[0]

        # Add agg value to each row of group
        def stat_func(group):
            stat.set_data(group[col])
            group[agg_name] = stat.value
            return group

        # create groupby
        gb = data_copy.groupby(self.dimensions)

        # apply stat function to groups
        agg = gb.apply(stat_func)

        return agg


def stack(*comp_glyphs, **kwargs):
    """Stacks the :class:`CompositeGlyph`s.

    Stacks the glyphs which results in the glyphs having transformed data,
    which represents the stacked glyphs.

    Args:
        *comp_glyphs (:class:`CompositeGlyph`): a sequence of glyphs to stack
         
    Returns:
        comp_glyphs: a list of composite glyphs

    """

    columns = kwargs.get('columns')

    if comp_glyphs is not None:
        stacker = Stack(comp_glyphs=list(comp_glyphs))
        stacker.apply()
        return comp_glyphs
    elif columns is not None:
        return Stack(columns=columns)
    else:
        raise ValueError('You must stack on renderers or columns')


def blend(*cols, **kwargs):
    """Provides a simple function for specifying a Blend data operation.

    Args:
        cols (str): each column to use for blending by name
        **kwargs: the keyword args supported by :class:`Blend`

            * name (str): name of the column to contain the blended values
            * labels_name (str): name of the column to contain the name of the columns
              used for blending


    See :class:`Blend`

    """

    return Blend(*cols, **kwargs)
