#  ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
#  ┃ ██████ ██████ ██████       █      █      █      █      █ █▄  ▀███ █       ┃
#  ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█  ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄  ▀█ █ ▀▀▀▀▀ ┃
#  ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄   █ ▄▄▄▄▄ ┃
#  ┃ █      ██████ █  ▀█▄       █ ██████      █      ███▌▐███ ███████▄ █       ┃
#  ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
#  ┃ Copyright (c) 2017, the Perspective Authors.                              ┃
#  ┃ ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ ┃
#  ┃ This file is part of the Perspective library, distributed under the terms ┃
#  ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃
#  ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛

from random import random
from .viewer_traitlets import PerspectiveTraitlets
import perspective

# from .. import Server

# ─  │  ┌ ┬ ┐
# ┄  ┆  ├ ┼ ┤ ╲ ╱
# ┈  ┊  └ ┴ ┘
# ━  ┃  ┏ ┳ ┓ ┏ ┯ ┓ ┏ ┳ ┓ ┏ ┯ ┓
# ┅  ┇  ┣ ╋ ┫ ┣ ┿ ┫ ┠ ╂ ┨ ┠ ┼ ┨
# ┉  ┋  ┗ ┻ ┛ ┗ ┷ ┛ ┗ ┻ ┛ ┗ ┷ ┛

# global_server = PySyncServer()


class PerspectiveViewer(PerspectiveTraitlets, object):
    """PerspectiveViewer wraps the `perspective.Table` API and exposes an API
    around creating views, loading data, and updating data.
    """

    # Viewer attributes that should be saved in `save()` and restored using
    # `restore()`. Symmetric to `PERSISTENT_ATTRIBUTES` in `perspective-viewer`.
    PERSISTENT_ATTRIBUTES = (
        "group_by",
        "split_by",
        "filter",
        "sort",
        "aggregates",
        "columns",
        "expressions",
        "plugin",
        "plugin_config",
        "theme",
        "settings",
        "title",
        "version",
    )

    def __init__(
        self,
        plugin="Datagrid",
        columns=None,
        group_by=None,
        split_by=None,
        aggregates=None,
        sort=None,
        filter=None,
        expressions=None,
        plugin_config=None,
        settings=True,
        theme=None,
        title=None,
        # ignored, here for restore compatibility
        version=None,
    ):
        """Initialize an instance of `PerspectiveViewer` with the given viewer
        configuration.  Do not pass a `Table` or data into the constructor -
        use the :func:`load()` method to provide the viewer with data.

        Keyword Arguments:
            columns (:obj:`list` of :obj:`str`): A list of column names to be
                visible to the user.
            group_by (:obj:`list` of :obj:`str`): A list of column names to
                use as group by.
            split_by (:obj:`list` of :obj:`str`): A list of column names
                to use as split by.
            aggregates (:obj:`dict` of :obj:`str` to :obj:`str`):  A dictionary
                of column names to aggregate types, which specify aggregates
                for individual columns.
            sort (:obj:`list` of :obj:`list` of :obj:`str`): A list of lists,
                each list containing a column name and a sort direction
                (``asc``, ``desc``, ``asc abs``, ``desc abs``, ``col asc``,
                ``col desc``, ``col asc abs``, ``col desc abs``).
            filter (:obj:`list` of :obj:`list` of :obj:`str`): A list of lists,
                each list containing a column name, a filter comparator, and a
                value to filter by.
            expressions (:obj:`list` of :obj:`str`): A list of string
                expressions which are applied to the view.
            plugin (:obj:`str`/:obj:`perspective.Plugin`): Which plugin to
                select by default.
            plugin_config (:obj:`dict`): A configuration for the plugin, i.e.
                the datagrid plugin or a chart plugin.
            settings(:obj:`bool`): Whether the perspective query settings
                panel should be open.
            theme (:obj:`str`): The color theme to use.
            version (:obj:`str`): The version this configuration is restored from.
                This should only be used when restoring a configuration,
                and should not be set manually.

        Examples:
            >>> viewer = PerspectiveViewer(
            ...     aggregates={"a": "avg"},
            ...     group_by=["a"],
            ...     sort=[["b", "desc"]],
            ...     filter=[["a", ">", 1]],
            ...     expressions=["\"a\" + 100"]
            ... )
        """

        # The Table under management by this viewer and its
        # attached PerspectiveManager
        self._table = None
        self._client = None

        # Viewer configuration
        self.plugin = plugin  # validate_plugin(plugin)
        self.columns = columns or []  # validate_columns(columns) or []
        self.group_by = group_by or []  # validate_group_by(group_by) or []
        self.split_by = split_by or []  # validate_split_by(split_by) or []
        self.aggregates = aggregates or {}  # validate_aggregates(aggregates) or {}
        self.sort = sort or []  # validate_sort(sort) or []
        self.filter = filter or []  # validate_filter(filter) or []
        self.expressions = expressions or {}  # validate_expressions(expressions) or {}
        self.plugin_config = (
            plugin_config or {}
        )  # validate_plugin_config(plugin_config) or {}
        self.settings = settings
        self.theme = theme
        self.title = title

    def new_proxy_session(self, cb):
        return perspective.ProxySession(self._client, cb)

    @property
    def client(self):
        """Returns the ``perspective.Client`` or ``perspective.AsyncClient`` under management by the viewer."""
        return self._client

    @property
    def table(self):
        """Returns the ``perspective.Table`` or ``perspective.AsyncTable`` under management by the viewer."""
        return self._table

    def is_async(self):
        """Returns whether this widget has an async interface or synchronous"""
        return isinstance(self._table, perspective.perspective.AsyncTable)

    def load(self, data, **options):
        """Given a ``perspective.Table``, a ``perspective.AsyncTable``,
        or data that can be handled by ``perspective.Table``, pass it to the
        viewer.  Like `__init__`, load accepts a `perspective.Table`, a dataset,
        or a schema.

        ``load()`` resets the state of the viewer: if a ``perspective.Table``
        has already been  loaded, ``**options`` is ignored as the options
        already set on the ``Table`` take precedence.

        If data is passed in, a ``perspective.Table`` is automatically created
        by this method, and the options passed to ``**config`` are extended to
        the new Table.  If the widget already has a dataset, and the new data
        has different columns to the old one, then the widget state (pivots,
        sort, etc.) is cleared to prevent applying settings on columns that
        don't exist.

        When a ``perspective.AsyncTable`` is loaded, the widget's interface
        becomes async. Methods which operate on the underlying Perspective
        view, inclusive of the ``load()`` call itself, return coroutine values
        which must be awaited.

        Loading a ``perspective.Table`` or plain data will make the interface
        synchronous again.

        Args:
            data (:obj:`Table`|:obj:`AsyncTable`|:obj:`dict`|:obj:`list`|:obj:`pandas.DataFrame`|:obj:`bytes`|:obj:`str`): a
                `perspective.Table` instance, a `perspective.AsyncTable`
                instance, or a dataset to be loaded in the viewer.

        Keyword Arguments:
            name (:obj:`str`): An optional name to reference the table by so it can
                be accessed from the front-end. If not provided, a name will
                be generated.
            index (:obj:`str`): A column name to be used as the primary key.
                Ignored if a ``Table`` or ``AsyncTable`` is supplied.
            limit (:obj:`int`): A upper limit on the number of rows in the Table.
                Cannot be set at the same time as `index`. Ignored if a
                ``Table`` or ``AsyncTable`` is supplied.

        Returns:
            coro (:obj:`coroutine`): when `AsyncTable` is passed, the `load()` call must be awaited
        """
        name = options.pop("name", str(random()))

        # Reset the viewer when `load()` is called multiple times.
        if self.table is not None:
            self.reset()

        if isinstance(data, perspective.perspective.AsyncTable):
            self._table = data
            async def load_table():
                self._client = await self.table.get_client()
                self.table_name = self.table.get_name()
                # If the user does not set columns to show, synchronize viewer state
                # with dataset.
                if len(self.columns) == 0:
                    self.columns = await self.table.columns()

            return load_table()
        elif isinstance(data, perspective.perspective.Table):
            self._table = data
            self._client = data.get_client()
        elif isinstance(data, perspective.perspective.View) or isinstance(data, perspective.perspective.AsyncView):
            raise TypeError(
                "Views cannot be loaded directly, load a table or raw data instead"
            )
        else:
            self._table = perspective.table(data, name=name, **options)
            self._client = perspective.GLOBAL_CLIENT

        # If the user does not set columns to show, synchronize viewer state
        # with dataset.
        if len(self.columns) == 0:
            self.columns = self.table.columns()

        self.table_name = self.table.get_name()

    def update(self, data):
        """Update the table under management by the viewer with new data.
        This function follows the semantics of `Table.update()`, and will be
        affected by whether an index is set on the underlying table.

        When this widget has loaded an ``AsyncTable``, returns a coroutine
        which must be awaited.

        Args:
            data (:obj:`dict`|:obj:`list`|:obj:`pandas.DataFrame`): the
            update data for the table.

        Returns:
            coro (:obj:`coroutine`): when async, must be awaited
        """
        return self.table.update(data)

    def clear(self):
        """Clears the rows of this viewer's ``Table``."""
        if self.table is not None:
            return self.table.clear()

    def replace(self, data):
        """Replaces the rows of this viewer's `Table` with new data.

        Args:
            data (:obj:`dict`|:obj:`list`|:obj:`pandas.DataFrame`): new data
            to set into the table - must conform to the table's schema.

        Returns:
            coro (:obj:`coroutine`): when async, must be awaited
        """
        if self.table is not None:
            return self.table.replace(data)

    def save(self):
        """Get the viewer's attributes as a dictionary, symmetric with `restore`
        so that a viewer's configuration can be reproduced."""
        return {
            attr: getattr(self, attr)
            for attr in PerspectiveViewer.PERSISTENT_ATTRIBUTES
        }

    def restore(self, **kwargs):
        """Restore a given set of attributes, passed as kwargs
        (e.g. dictionary). Symmetric with `save` so that a given viewer's
        configuration can be reproduced."""
        for k, v in kwargs.items():
            if k in PerspectiveViewer.PERSISTENT_ATTRIBUTES:
                setattr(self, k, v)

    def to_kwargs(self):
        """Get the viewer's attributes as a list of kwargs, which can be passed to
        the viewer constructor"""
        attrs = self.save()
        defaults = {
            "columns": [],
            "group_by": [],
            "split_by": [],
            "aggregates": {},
            "sort": [],
            "filter": [],
            "expressions": {},
            "plugin_config": {},
            "version": "2.10.0",
        }
        kwargs = {}
        for key, default in defaults.items():
            if attrs.get(key) != default:
                kwargs[key] = attrs[key]
        return ", ".join(
            ["{}={}".format(attr, repr(val)) for attr, val in kwargs.items()]
        )

    def reset(self):
        """Resets the viewer's attributes and state, but does not delete or
        modify the underlying `Table`.

        Example:
            widget = PerspectiveWidget(data, group_by=["date"], plugin=Plugin.XBAR)
            widget.reset()
            widget.plugin  #
        """
        self.group_by = []
        self.split_by = []
        self.filter = []
        self.sort = []
        self.expressions = {}
        self.aggregates = {}
        self.columns = []
        self.plugin = "Datagrid"
        self.plugin_config = {}

    def delete(self, delete_table=True):
        """Delete the Viewer's data and clears its internal state. If
        ``delete_table`` is True, the underlying `perspective.Table` and the
        internal `View` object will be deleted.

        Args:
            delete_table (:obj:`bool`) : whether the underlying `Table` will be
                deleted. Defaults to True.

        Returns:
            coro (:obj:`coroutine`): when async and `delete_table` is `True`,
                must be awaited
        """
        ret = None
        if delete_table:
            # Delete table
            ret = self.table.delete()
            self.table_name = None
            self._table = None

        self.reset()
        return ret
