#-----------------------------------------------------------------------------
# Copyright (c) Anaconda, Inc., and Bokeh Contributors.
# All rights reserved.
#
# The full license is in the file LICENSE.txt, distributed with this software.
#-----------------------------------------------------------------------------
''' Helper functions for applying client-side computations such as
transformations to data fields or ``ColumnDataSource`` expressions.

'''

#-----------------------------------------------------------------------------
# Boilerplate
#-----------------------------------------------------------------------------
from __future__ import annotations

import logging # isort:skip
log = logging.getLogger(__name__)

#-----------------------------------------------------------------------------
# Imports
#-----------------------------------------------------------------------------

# Standard library imports
from typing import TYPE_CHECKING, Sequence, Union

# Bokeh imports
from .core.property.vectorization import Expr, Field
from .models.expressions import CumSum, Stack
from .models.mappers import (
    CategoricalColorMapper,
    CategoricalMarkerMapper,
    CategoricalPatternMapper,
    EqHistColorMapper,
    LinearColorMapper,
    LogColorMapper,
)
from .models.transforms import Dodge, Jitter

if TYPE_CHECKING:
    from typing_extensions import TypeAlias

    from .colors import ColorLike
    from .core.enums import JitterRandomDistributionType
    from .models.ranges import Range
    from .models.transforms import Transform

#-----------------------------------------------------------------------------
# Globals and constants
#-----------------------------------------------------------------------------

__all__ = (
    'cumsum',
    'dodge',
    'eqhist_cmap',
    'factor_cmap',
    'factor_hatch',
    'factor_mark',
    'jitter',
    'linear_cmap',
    'log_cmap',
    'stack',
    'transform',
)

#-----------------------------------------------------------------------------
# General API
#-----------------------------------------------------------------------------

if TYPE_CHECKING:
    Factors: TypeAlias = Union[Sequence[str], Sequence[tuple[str, str]], Sequence[tuple[str, str, str]]]

def cumsum(field_name: str, include_zero: bool = False) -> Expr:
    ''' Create a ``DataSpec`` dict to generate a ``CumSum`` expression
    for a ``ColumnDataSource``.

    Args:
        field_name (str) : a field name to configure ``CumSum`` with

        include_zero (bool, optional) : whether to include zero in the sum
            (default: False)

    Examples:

        .. code-block:: python

            p.wedge(start_angle=cumsum('angle', include_zero=True),
                    end_angle=cumsum('angle'),
                    ...)

        will generate a ``CumSum`` expressions that sum the ``"angle"`` column
        of a data source. For the ``start_angle`` value, the cumulative sums
        will start with a zero value. For ``end_angle``, no initial zero will
        be added (i.e. the sums will start with the first angle value, and
        include the last).

    '''
    return Expr(CumSum(field=field_name, include_zero=include_zero))

def dodge(field_name: str, value: float, range: Range | None = None) -> Field:
    ''' Create a ``DataSpec`` dict that applies a client-side ``Dodge``
    transformation to a ``ColumnDataSource`` column.

    Args:
        field_name (str) : a field name to configure ``DataSpec`` with

        value (float) : the fixed offset to add to column data

        range (Range, optional) : a range to use for computing synthetic
            coordinates when necessary, e.g. a ``FactorRange`` when the
            column data is categorical (default: None)

    Returns:
        Field

    '''
    return Field(field_name, Dodge(value=value, range=range))

def eqhist_cmap(
    field_name: str,
    palette: Sequence[ColorLike],
    low: float,
    high: float,
    low_color: ColorLike | None = None,
    high_color: ColorLike | None = None,
    nan_color: ColorLike = "gray",
) -> Field:
    ''' Create a ``DataSpec`` dict that applies a client-side
    ``EqHistColorMapper`` transformation to a ``ColumnDataSource`` column.

    Args:
        field_name (str) : a field name to configure ``DataSpec`` with

        palette (seq[color]) : a list of colors to use for colormapping

        low (float) : a minimum value of the range to map into the palette.
            Values below this are clamped to ``low``.

        high (float) : a maximum value of the range to map into the palette.
            Values above this are clamped to ``high``.

        low_color (color, optional) : color to be used if data is lower than
            ``low`` value. If None, values lower than ``low`` are mapped to the
            first color in the palette. (default: None)

        high_color (color, optional) : color to be used if data is higher than
            ``high`` value. If None, values higher than ``high`` are mapped to
            the last color in the palette. (default: None)

        nan_color (color, optional) : a default color to use when mapping data
            from a column does not succeed (default: "gray")

    '''
    return Field(
        field_name,
        EqHistColorMapper(
            palette=palette,
            low=low,
            high=high,
            nan_color=nan_color,
            low_color=low_color,
            high_color=high_color,
        ),
    )

def factor_cmap(
    field_name: str,
    palette: Sequence[ColorLike],
    factors: Factors,
    start: float = 0,
    end: float | None = None,
    nan_color: ColorLike = "gray",
) -> Field:
    ''' Create a ``DataSpec`` dict that applies a client-side
    ``CategoricalColorMapper`` transformation to a ``ColumnDataSource``
    column.

    Args:
        field_name (str) : a field name to configure ``DataSpec`` with

        palette (seq[color]) : a list of colors to use for colormapping

        factors (seq) : a sequence of categorical factors corresponding to
            the palette

        start (int, optional) : a start slice index to apply when the column
            data has factors with multiple levels. (default: 0)

        end (int, optional) : an end slice index to apply when the column
            data has factors with multiple levels. (default: None)

        nan_color (color, optional) : a default color to use when mapping data
            from a column does not succeed (default: "gray")

    Returns:
        Field

    '''
    return Field(
        field_name,
        CategoricalColorMapper(
            palette=palette,
            factors=factors,
            start=start,
            end=end,
            nan_color=nan_color,
        ),
    )

def factor_hatch(
    field_name: str,
    patterns: Sequence[str],
    factors: Factors,
    start: float = 0,
    end: float | None = None,
) -> Field:
    ''' Create a ``DataSpec`` dict that applies a client-side
    ``CategoricalPatternMapper`` transformation to a ``ColumnDataSource``
    column.

    Args:
        field_name (str) : a field name to configure ``DataSpec`` with

        patterns (seq[string]) : a list of hatch patterns to use to map to

        factors (seq) : a sequences of categorical factors corresponding to
            the palette

        start (int, optional) : a start slice index to apply when the column
            data has factors with multiple levels. (default: 0)

        end (int, optional) : an end slice index to apply when the column
            data has factors with multiple levels. (default: None)

    Returns:
        Field

    Added in version 1.1.1

    '''
    return Field(
        field_name,
        CategoricalPatternMapper(
            patterns=patterns,
            factors=factors,
            start=start,
            end=end,
        ),
    )

def factor_mark(
    field_name: str,
    markers: Sequence[str],
    factors: Factors,
    start: float = 0,
    end: float | None = None,
) -> Field:
    ''' Create a ``DataSpec`` dict that applies a client-side
    ``CategoricalMarkerMapper`` transformation to a ``ColumnDataSource``
    column.

    .. note::
        This transform is primarily only useful with ``scatter``, which
        can be parameterized by glyph type.

    Args:
        field_name (str) : a field name to configure ``DataSpec`` with

        markers (seq[string]) : a list of markers to use to map to

        factors (seq) : a sequences of categorical factors corresponding to
            the palette

        start (int, optional) : a start slice index to apply when the column
            data has factors with multiple levels. (default: 0)

        end (int, optional) : an end slice index to apply when the column
            data has factors with multiple levels. (default: None)

    Returns:
        Field

    '''
    return Field(
        field_name,
        CategoricalMarkerMapper(
            markers=markers,
            factors=factors,
            start=start,
            end=end,
        ),
    )

def jitter(
    field_name: str,
    width: float,
    mean: float = 0,
    distribution: JitterRandomDistributionType = "uniform",
    range: Range | None = None,
) -> Field:
    ''' Create a ``DataSpec`` dict that applies a client-side ``Jitter``
    transformation to a ``ColumnDataSource`` column.

    Args:
        field_name (str) : a field name to configure ``DataSpec`` with

        width (float) : the width of the random distribution to apply

        mean (float, optional) : an offset to apply (default: 0)

        distribution (str, optional) : ``"uniform"`` or ``"normal"``
            (default: ``"uniform"``)

        range (Range, optional) : a range to use for computing synthetic
            coordinates when necessary, e.g. a ``FactorRange`` when the
            column data is categorical (default: None)

    Returns:
        Field

    '''
    return Field(
        field_name,
        Jitter(
            mean=mean,
            width=width,
            distribution=distribution,
            range=range,
        ),
    )

def linear_cmap(
    field_name: str,
    palette: Sequence[ColorLike],
    low: float,
    high: float,
    low_color: ColorLike | None = None,
    high_color: ColorLike | None = None,
    nan_color: ColorLike = "gray",
) -> Field:
    ''' Create a ``DataSpec`` dict that applies a client-side
    ``LinearColorMapper`` transformation to a ``ColumnDataSource`` column.

    Args:
        field_name (str) : a field name to configure ``DataSpec`` with

        palette (seq[color]) : a list of colors to use for colormapping

        low (float) : a minimum value of the range to map into the palette.
            Values below this are clamped to ``low``.

        high (float) : a maximum value of the range to map into the palette.
            Values above this are clamped to ``high``.

        low_color (color, optional) : color to be used if data is lower than
            ``low`` value. If None, values lower than ``low`` are mapped to the
            first color in the palette. (default: None)

        high_color (color, optional) : color to be used if data is higher than
            ``high`` value. If None, values higher than ``high`` are mapped to
            the last color in the palette. (default: None)

        nan_color (color, optional) : a default color to use when mapping data
            from a column does not succeed (default: "gray")

    '''
    return Field(
        field_name,
        LinearColorMapper(
            palette=palette,
            low=low,
            high=high,
            nan_color=nan_color,
            low_color=low_color,
            high_color=high_color,
        ),
    )

def log_cmap(
    field_name: str,
    palette: Sequence[ColorLike],
    low: float,
    high: float,
    low_color: ColorLike | None = None,
    high_color: ColorLike | None = None,
    nan_color: ColorLike = "gray",
) -> Field:
    ''' Create a ``DataSpec`` dict that applies a client-side ``LogColorMapper``
    transformation to a ``ColumnDataSource`` column.

    Args:
        field_name (str) : a field name to configure ``DataSpec`` with

        palette (seq[color]) : a list of colors to use for colormapping

        low (float) : a minimum value of the range to map into the palette.
            Values below this are clamped to ``low``.

        high (float) : a maximum value of the range to map into the palette.
            Values above this are clamped to ``high``.

        low_color (color, optional) : color to be used if data is lower than
            ``low`` value. If None, values lower than ``low`` are mapped to the
            first color in the palette. (default: None)

        high_color (color, optional) : color to be used if data is higher than
            ``high`` value. If None, values higher than ``high`` are mapped to
            the last color in the palette. (default: None)

        nan_color (color, optional) : a default color to use when mapping data
            from a column does not succeed (default: "gray")

    '''
    return Field(
        field_name,
        LogColorMapper(
            palette=palette,
            low=low,
            high=high,
            nan_color=nan_color,
            low_color=low_color,
            high_color=high_color,
        ),
    )

def stack(*fields: str) -> Expr:
    ''' Create a Create a ``DataSpec`` dict to generate a ``Stack`` expression
    for a ``ColumnDataSource``.

    Examples:

        .. code-block:: python

            p.vbar(bottom=stack("sales", "marketing"), ...

        will generate a ``Stack`` that sums the ``"sales"`` and ``"marketing"``
        columns of a data source, and use those values as the ``top``
        coordinate for a ``VBar``.

    '''

    return Expr(Stack(fields=fields))

def transform(field_name: str, transform: Transform) -> Field:
    ''' Create a ``DataSpec`` dict that applies an arbitrary client-side
    ``Transform`` to a ``ColumnDataSource`` column.

    Args:
        field_name (str) : A field name to configure ``DataSpec`` with

        transform (Transform) : A transforms to apply to that field

    Returns:
        Field

    '''
    return Field(field_name, transform)

#-----------------------------------------------------------------------------
# Dev API
#-----------------------------------------------------------------------------

#-----------------------------------------------------------------------------
# Private API
#-----------------------------------------------------------------------------

#-----------------------------------------------------------------------------
# Code
#-----------------------------------------------------------------------------
