from typing import ClassVar

import traitlets as t

from lonboard._base import BaseExtension
from lonboard.traits import (
    DashArrayAccessor,
    FilterValueAccessor,
    FloatAccessor,
    PointAccessor,
    VariableLengthTuple,
)


class BrushingExtension(BaseExtension):
    """Adds GPU-based data brushing functionalities to layers.

    It allows the layer to show/hide objects based on the current pointer position.

    # Example

    An example is in the [County-to-County Migration
    notebook](https://developmentseed.org/lonboard/latest/examples/migration/).

    # Layer Properties

    This extension dynamically enables the following properties onto the layer(s) where
    it is included:

    ## `brushing_enabled`

    Enable/disable brushing. If brushing is disabled, all objects are rendered.

    - Type: `bool`, optional
    - Default: `True`

    ## `brushing_target`

    The position used to filter each object by. One of the following:

    - `"source"`: Use the primary position for each object. This can mean different
      things depending on the layer. It usually refers to the coordinates returned by
      `getPosition` or `getSourcePosition` accessors.
    - `"target"`: Use the secondary position for each object. This may not be available
      in some layers. It usually refers to the coordinates returned by
      `getTargetPosition` accessor.
    - `"source_target"`: Use both the primary position and secondary position for each
      object. Show object if either is in brushing range.
    - `"custom"`: Some layers may not describe their data objects with one or two
      coordinates, for example `PathLayer` and `PolygonLayer`. Use this option with the
      `get_brushing_target` prop to provide a custom position that each object should be
      filtered by.

    - Type: `str`, optional

        One of: "source" | "target" | "source_target" | "custom"

    - Default: `"source"`

    ## `brushing_radius`

    The brushing radius centered at the pointer, in meters. If a data object is within
    this circle, it is rendered; otherwise it is hidden.

    - Type: `float`, optional
    - Default: `10000`

    ## `get_brushing_target`

    An arbitrary position for each object that it will be filtered by.

    Only effective if `brushing_target` is set to `"custom"`.

    - Type: [PointAccessor][lonboard.traits.PointAccessor], optional
        - If a point is provided, it is used as the target for all rows.
        - If an array of points is provided, each value in the array will be used as the
          target for the row at the same row index.
    - Default: `None`.
    """

    _extension_type = t.Unicode("brushing").tag(sync=True)

    _layer_traits: ClassVar = {
        "brushing_enabled": t.Bool(default_value=True).tag(sync=True),
        "brushing_target": t.Unicode(None, allow_none=True).tag(sync=True),
        "brushing_radius": t.Float(None, allow_none=True, min=0).tag(sync=True),
        "get_brushing_target": PointAccessor(None, allow_none=True),
    }


class CollisionFilterExtension(BaseExtension):
    """Allows layers to hide overlapping objects.

    # Layer Properties

    This extension dynamically enables the following properties onto the layer(s) where
    it is included:

    ## `collision_enabled`

    Enable/disable collisions. If collisions are disabled, all objects are rendered.

    - Type: `bool`, optional
    - Default: `True`

    ## `collision_group`

    Collision group this layer belongs to. If it is not set, the 'default' collision
    group is used

    - Type: `str`, optional
    - Default: `None`

    ## `get_collision_priority`

    Accessor for collision priority. Must return a number in the range -1000 -> 1000.
    Features with higher values are shown preferentially.

    - Type: [FloatAccessor][lonboard.traits.FloatAccessor], optional
        - If a number is provided, it is used as the priority for all objects.
        - If an array is provided, each value in the array will be used as the priority
          for the object at the same row index.
    - Default: `0`.

    """

    _extension_type = t.Unicode("collision-filter").tag(sync=True)

    _layer_traits: ClassVar = {
        "collision_enabled": t.Bool(default_value=True).tag(sync=True),
        "collision_group": t.Unicode(None, allow_none=True).tag(sync=True),
        "get_collision_priority": FloatAccessor(None, allow_none=True),
    }


class DataFilterExtension(BaseExtension):
    """Adds GPU-based data filtering functionalities to layers.

    It allows the layer to show/hide objects based on user-defined properties.

    # Example

    ```py
    from lonboard import Map, ScatterplotLayer
    from lonboard.colormap import apply_continuous_cmap
    from lonboard.layer_extension import DataFilterExtension

    gdf = gpd.GeoDataFrame(...)
    extension = DataFilterExtension()
    layer = ScatterplotLayer.from_geopandas(
        gdf,
        extensions=[extension],
        get_filter_value=gdf["filter_value"], # replace with desired column
        filter_range=[0, 5] # replace with desired filter range
    )
    ```

    The `DataFilterExtension` allows filtering on 1 to 4 attributes at the same time.
    So if you have four numeric columns of interest, you can filter on the
    intersection of all of them.

    For easy visualization, we suggest connecting the `DataFilterExtension` to an
    interactive slider from `ipywidgets`.

    ```py
    from ipywidgets import FloatRangeSlider

    slider = FloatRangeSlider(
        value=(2, 5),
        min=0,
        max=10,
        step=0.1,
        description="Slider: "
    )
    slider

    jsdlink(
        (slider, "value"),
        (layer, "filter_range")
    )
    ```

    If you have 2 to 4 columns, use a
    [`MultiRangeSlider`][lonboard.controls.MultiRangeSlider], which combines multiple
    `FloatRangeSlider` objects in a form that the `DataFilterExtension` expects.

    ```py
    from ipywidgets import FloatRangeSlider, jsdlink

    slider1 = FloatRangeSlider(
        value=(2, 5),
        min=0,
        max=10,
        step=0.1,
        description="First slider: "
    )
    slider2 = FloatRangeSlider(
        value=(30, 40),
        min=0,
        max=50,
        step=1,
        description="Second slider: "
    )
    multi_slider = MultiRangeSlider([slider1, slider2])
    multi_slider

    jsdlink(
        (multi_slider, "value"),
        (layer, "filter_range")
    )
    ```

    # Important notes

    - The DataFilterExtension only supports float32 data, so integer data will be casted
      to float32.
    - The DataFilterExtension copies all data referenced by `get_filter_value` to the
      GPU, so it will increase memory pressure on the GPU.

    # Layer Properties

    This extension dynamically enables the following properties onto the layer(s) where
    it is included:

    ## `filter_categories`

    The list of categories that should be rendered. If an object's filtered category is
    in the list, the object will be rendered; otherwise it will be hidden. This prop can
    be updated on user input or animation with very little cost.

    Format:

    - If category_size is 1: ['category1', 'category2']
    - If category_size is 2 to 4:
        [['category1', 'category2', ...], ['category3', ...], ...] for each filtered
        property, respectively.

    The maximum number of supported is determined by the category_size:

    - If category_size is 1: 128 categories
    - If category_size is 2: 64 categories per dimension
    - If category_size is 3 or 4: 32 categories per dimension.

    If this value is exceeded any categories beyond the limit will be ignored.

    Default: `[0]`

    ## `filter_enabled`

    Enable/disable the data filter. If the data filter is disabled, all objects are
    rendered.

    - Type: `bool`, optional
    - Default: `True`

    ## `filter_range`

    The bounds which defines whether an object should be rendered. If an object's
    filtered value is within the bounds, the object will be rendered; otherwise it will
    be hidden. This prop can be updated on user input or animation with very little
    cost.

    Format:

    If `filter_size` is 1, provide a single tuple of `(min, max)`.

    If `filter_size` is 2 to 4, provide a list of tuples: `[(min0, max0), (min1,
    max1), ...]` for each filtered property, respectively.

    - Type: either Tuple[float, float] or List[Tuple[float, float]], optional
    - Default: `(-1, 1)`

    ## `filter_soft_range`

    If specified, objects will be faded in/out instead of abruptly shown/hidden.

    When the filtered value is outside of the bounds defined by `filter_soft_range` but
    still within the bounds defined by `filter_range`, the object will be rendered as
    "faded".

    - Type: Tuple[float, float], optional
    - Default: `None`

    ## `filter_transform_size`

    When an object is "faded", manipulate its size so that it appears smaller or
    thinner. Only works if `filter_soft_range` is specified.

    - Type: `bool`, optional
    - Default: `True`

    ## `filter_transform_color`

    When an object is "faded", manipulate its opacity so that it appears more
    translucent. Only works if `filter_soft_range` is specified.

    - Type: `bool`, optional
    - Default: `True`

    ## `get_filter_value`

    Accessor to retrieve the value for each object that it will be filtered by.

    - Type: [FilterValueAccessor][lonboard.traits.FilterValueAccessor]
        - If a scalar value is provided, it is used as the value for all objects.
        - If an array is provided, each value in the array will be used as the value for
          the object at the same row index.

    ## `get_filter_category`

    Accessor to retrieve the category for each object that it will be filtered by.

    - Type: [FilterValueAccessor][lonboard.traits.FilterValueAccessor]
        - If a scalar value is provided, it is used as the value for all objects.
        - If an array is provided, each value in the array will be used as the value for
          the object at the same row index.
    """

    _extension_type = t.Unicode("data-filter").tag(sync=True)

    _layer_traits: ClassVar = {
        "filter_categories": t.Union(
            [
                VariableLengthTuple(t.Any()),
                VariableLengthTuple(
                    VariableLengthTuple(t.Any()),
                    minlen=2,
                    maxlen=4,
                ),
            ],
            default_value=None,
            allow_none=True,
        ).tag(sync=True),
        "filter_enabled": t.Bool(default_value=True).tag(sync=True),
        "filter_range": t.Union(
            [
                VariableLengthTuple(t.Float(), minlen=2, maxlen=2),
                VariableLengthTuple(
                    VariableLengthTuple(t.Float(), minlen=2, maxlen=2),
                    minlen=2,
                    maxlen=4,
                ),
            ],
            default_value=None,
            allow_none=True,
        ).tag(sync=True),
        "filter_soft_range": t.Tuple(
            t.Float(),
            t.Float(),
            default_value=None,
            allow_none=True,
        ).tag(sync=True),
        "filter_transform_size": t.Bool(default_value=True).tag(sync=True),
        "filter_transform_color": t.Bool(default_value=True).tag(sync=True),
        "get_filter_value": FilterValueAccessor(default_value=None, allow_none=True),
        "get_filter_category": FilterValueAccessor(default_value=None, allow_none=True),
    }

    filter_size = t.Int(None, min=1, max=4, allow_none=True).tag(sync=True)
    """The size of the filter (number of columns to filter by).

    The data filter can show/hide data based on 1-4 numeric properties of each object.

    - Type: `int`. This is required if using range-based filtering.
    - Default 1.
    """

    category_size = t.Int(None, min=1, max=4, allow_none=True).tag(sync=True)
    """The size of the category filter (number of columns to filter by).

    The category filter can show/hide data based on 1-4 properties of each object.

    - Type: `int`. This is required if using category-based filtering.
    - Default 0.
    """


class PathStyleExtension(BaseExtension):
    """Adds selected features to the PathLayer and composite layers that render
    the [PathLayer][lonboard.PathLayer], e.g. [PolygonLayer][lonboard.PolygonLayer].

    # Example

    Deck.gl example https://deck.gl/docs/api-reference/extensions/path-style-extension

    # Layer Properties

    This extension dynamically enables the following properties onto the layer(s) where
    it is included:

    ## `get_dash_array`

    Must be specified if the dash option is enabled. The dash array to draw each path
    with `[dash_size, gap_size]` relative to the width of the path.

    - Type: [DashArrayAccessor][lonboard.traits.DashArrayAccessor], optional
        - If a scalar value is provided, it is used as the value for all objects. Pass
          `[0, 0]` to draw the path in solid line.
        - If an array is provided, each value in the array will be used as the value
          for the object at the same row index.
    - Default: `[0, 0]`.

    ## `dash_justified`

    Only effective if `get_dash_array` is specified. If `True`, adjust gaps for the
    dashes to align at both ends. Overrides the effect of `high_precision_dash`.

    - Type: `boolean`, optional
    - Default `False`.

    ## `get_offset`

    Must be specified if the `offset` option is enabled.

    The offset to draw each path with, relative to the width of the path. Negative
    offset is to the left hand side, and positive offset is to the right hand side. `0`
    extrudes the path so that it is centered at the specified coordinates.

    - Type: [FloatAccessor][lonboard.traits.FloatAccessor], optional
        - If a number is provided, it is used as the offset for all objects.
        - If an array is provided, each value in the array will be used as the offset
          for the object at the same row index.
    - Default: `None`.

    ## `dash_gap_pickable`

    Only effective if `get_dash_array` is specified. If `True`, gaps between solid
    strokes are pickable. If `False`, only the solid strokes are pickable.

    # Remarks

    ## Limitations

    WebGL2 has guaranteed support for up to 16 attributes per shader. The current
    implementation of `PathLayer` uses 13 attributes. Each one of the options of this
    extension adds one more attribute. In other words, if all options are enabled, the
    layer will not be able to use other extensions.

    ## Tips on Rendering Dash Lines

    There are three modes to render dash lines with this extension:

    1. Default: dash starts from the beginning of each line segment
    2. Justified: dash is stretched to center on each line segment
    3. High precision: dash is evaluated continuously from the beginning of a path

    ![Comparison between dash modes](https://user-images.githubusercontent.com/2059298/93418881-33555280-f860-11ea-82cc-b57ecf2e48ce.png)

    The above table illustrates the visual behavior of the three modes.

    The default mode works best if the data consists of long, disjoint paths. It renders
    dashes at exactly the defined lengths.

    The justified mode is guaranteed to render sharp, well-defined corners. This is
    great for rendering polyline shapes. However, the gap size may look inconsistent
    across line segments due to stretching.

    The high precision mode pre-calculates path length on the CPU, so it may be slower
    and use more resources for large datasets. When a path contains a lot of short
    segments, this mode yields the best result.
    """  # noqa: D205

    _extension_type = t.Unicode("path-style").tag(sync=True)

    _layer_traits: ClassVar = {
        "get_dash_array": DashArrayAccessor(None, allow_none=True),
        "dash_justified": t.Bool(None, allow_none=True).tag(sync=True),
        "get_offset": FloatAccessor(None, allow_none=True),
        "dash_gap_pickable": t.Bool(None, allow_none=True).tag(sync=True),
    }

    dash = t.Bool(None, allow_none=True).tag(sync=True)
    """Add capability to render dashed lines.

    - Type: `boolean`, optional
    - Default False.
    """

    high_precision_dash = t.Bool(None, allow_none=True).tag(sync=True)
    """Improve dash rendering quality in certain circumstances.

    Note that this option introduces additional performance overhead. See "Remarks".

    - Type: `boolean`, optional
    - Default `False`.
    """

    offset = t.Bool(None, allow_none=True).tag(sync=True)
    """Add capability to offset lines.

    - Type: `boolean`, optional
    - Default `False`.
    """
