# (C) Copyright 2004-2022 Enthought, Inc., Austin, TX
# All rights reserved.
#
# This software is provided without warranty under the terms of the BSD
# license included in LICENSE.txt and may be redistributed only under
# the conditions described in the aforementioned license. The license
# is also available online at http://www.enthought.com/licenses/BSD.txt
#
# Thanks for using Enthought open source!

""" Defines the table editor factory for all traits user interface toolkits.
"""

from traits.api import (
    Int,
    Float,
    List,
    Instance,
    Str,
    Any,
    Tuple,
    Dict,
    Enum,
    Bool,
    Callable,
    Range,
    Trait,
    observe,
)

from traitsui.editor_factory import EditorFactory
from traitsui.editors.enum_editor import EnumEditor
from traitsui.handler import Handler
from traitsui.helper import Orientation
from traitsui.item import Item
from traitsui.table_filter import TableFilter
from traitsui.toolkit_traits import Color, Font
from traitsui.ui_traits import AView
from traitsui.view import View

# The filter used to indicate that the user wants to customize the current
# filter
customize_filter = TableFilter(name="Customize...")

# -------------------------------------------------------------------------
#  Trait definitions:
# -------------------------------------------------------------------------

# A trait whose value can be True, False, or a callable function
BoolOrCallable = Trait(False, Bool, Callable)


class TableEditor(EditorFactory):
    """Editor factory for table editors."""

    # -------------------------------------------------------------------------
    #  Trait definitions:
    # -------------------------------------------------------------------------

    #: List of initial table column descriptors
    columns = List(Instance("traitsui.table_column.TableColumn"))

    #: List of other table column descriptors (not initially displayed)
    other_columns = List(Instance("traitsui.table_column.TableColumn"))

    #: The object trait containing the list of column descriptors
    columns_name = Str()

    #: The desired number of visible rows in the table
    rows = Int()

    #: The optional extended name of the trait used to specify an external
    #: filter for the table data. The value of the trait must either be an
    #: instance of TableEditor, a callable that accepts one argument
    #: (a table row) and returns True or False to indicate whether the
    #: specified object passes the filter or not, or **None** to indicate that
    #: no filter is to be applied:
    filter_name = Str()

    #: Initial filter that should be applied to the table
    filter = Instance("traitsui.table_filter.TableFilter")

    #: List of available filters that can be applied to the table
    filters = List(Instance("traitsui.table_filter.TableFilter"))

    #: The optional extended trait name of the trait used to notify that the
    #: filter has changed and the displayed objects should be updated.
    #: It should be an Event.
    update_filter_name = Str()

    #: Filter object used to allow a user to search the table.
    #: NOTE: If left as None, the table will not be searchable.
    search = Instance("traitsui.table_filter.TableFilter")

    #: Default context menu to display when any cell is right-clicked
    menu = Instance("traitsui.menu.Menu")

    #: Default trait name containg menu
    menu_name = Str()

    #: Are objects deletable from the table?
    deletable = BoolOrCallable(False)

    #: Is the table editable?
    editable = Bool(True)

    #: Should the editor become active after the first click
    edit_on_first_click = Bool(True)

    #: Can the user reorder the items in the table?
    reorderable = Bool(False)

    #: Can the user configure the table columns?
    configurable = Bool(True)

    #: Should the cells of the table automatically size to the optimal size?
    auto_size = Bool(True)

    #: Mirrors the Qt QSizePolicy.Policy attribute, for horizontal and vertical
    #: dimensions.  For these to be useful, set auto_size to False.  If these
    #: are None, then the table size policy will not be set in that dimension
    #: (for backwards compatibility).
    h_size_policy = Enum(
        None,
        "preferred",
        "fixed",
        "minimum",
        "maximum",
        "expanding",
        "minimum_expanding",
        "ignored",
    )
    v_size_policy = Enum(
        None,
        "preferred",
        "fixed",
        "minimum",
        "maximum",
        "expanding",
        "minimum_expanding",
        "ignored",
    )

    #: Should a new row automatically be added to the end of the table to allow
    #: the user to create new entries? If True, **row_factory** must be set.
    auto_add = Bool(False)

    #: Should the table items be presented in reverse order?
    reverse = Bool(False)

    #: The DockWindow graphical theme:
    dock_theme = Any()

    #: View to use when editing table items.
    #: NOTE: If not specified, the table items are not editable in a separate
    #: pane of the editor.
    edit_view = AView(" ")

    #: The handler to apply to **edit_view**
    edit_view_handler = Instance(Handler)

    #: Width to use for the edit view
    edit_view_width = Float(-1.0)

    #: Height to use for the edit view
    edit_view_height = Float(-1.0)

    #: Layout orientation of the table and its associated editor pane. This
    #: attribute applies only if **edit_view** is not ' '.
    orientation = Orientation

    #: Is the table sortable by clicking on the column headers?
    sortable = Bool(True)

    #: Does sorting affect the model (vs. just the view)?
    sort_model = Bool(False)

    #: Should grid lines be shown on the table?
    show_lines = Bool(True)

    #: Should the toolbar be displayed? (Note that False will override settings
    #: such as 'configurable', etc., and is a quick way to prevent the toolbar
    #: from being displayed; but True will not cause a toolbar to appear if one
    #: would not otherwise have been displayed)
    show_toolbar = Bool(False)

    #: The vertical scroll increment for the table:
    scroll_dy = Range(1, 32)

    #: Grid line color.  Does not work on Qt.
    line_color = Color(0xC4C0A9)

    #: Show column labels?
    show_column_labels = Bool(True)

    #: Show row labels?
    show_row_labels = Bool(False)

    #: Font to use for text in cells
    cell_font = Font

    #: Color to use for text in cells
    cell_color = Color(default=None, allow_none=True)

    #: Color to use for cell backgrounds
    cell_bg_color = Color(default=None, allow_none=True)

    #: Color to use for read-only cell backgrounds
    cell_read_only_bg_color = Color(default=None, allow_none=True)

    #: Whether to even-odd alternate the background color. Qt only.
    alternate_bg_color = Bool(False)

    #: Font to use for text in labels
    label_font = Font

    #: Color to use for text in labels
    label_color = Color(default=None, allow_none=True)

    #: Color to use for label backgrounds. Some Qt styles (eg. MacOS) ignore
    #: this.
    label_bg_color = Color(default=None, allow_none=True)

    #: Background color of selected item.
    selection_bg_color = Color(default=None, allow_none=True)

    #: Color of selected text.
    selection_color = Color(default=None, allow_none=True)

    #: Height (in pixels) of column labels
    column_label_height = Int(25)

    #: Width (in pixels) of row labels
    row_label_width = Int(82)

    #: The initial height of each row (<= 0 means use default value):
    row_height = Int(0)

    #: The optional extended name of the trait that the indices of the items
    #: currently passing the table filter are synced with:
    filtered_indices = Str()

    #: The selection mode of the table. The meaning of the various values are
    #: as follows:
    #:
    #: row
    #:   Entire rows are selected. At most one row can be selected at once.
    #:   This is the default.
    #: rows
    #:   Entire rows are selected. More than one row can be selected at once.
    #: column
    #:   Entire columns are selected. At most one column can be selected at
    #:   once.
    #: columns
    #:   Entire columns are selected. More than one column can be selected at
    #:   once.
    #: cell
    #:   Single cells are selected. Only one cell can be selected at once.
    #: cells
    #:   Single cells are selected. More than one cell can be selected at once.
    selection_mode = Enum("row", "rows", "column", "columns", "cell", "cells")

    #: The optional extended name of the trait that the current selection is
    #: synced with:
    selected = Str()

    #: The optional extended trait name of the trait that the indices of the
    #: current selection are synced with:
    selected_indices = Str()

    #: The optional extended trait name of the trait that should be assigned
    #: an ( object, column ) tuple when a table cell is clicked on (Note: If
    #: you want to receive repeated clicks on the same cell, make sure the
    #: trait is defined as an Event):
    click = Str()

    #: The optional extended trait name of the trait that should be assigned
    #: an ( object, column ) tuple when a table cell is double-clicked on
    #: (Note: if you want to receive repeated double-clicks on the same cell,
    #: make sure the trait is defined as an Event):
    dclick = Str()

    #: Called when a table item is selected
    on_select = Any()

    #: Called when a table item is double clicked
    on_dclick = Any()

    #: A factory to generate new rows.
    #: NOTE: If None, then the user will not be able to add new rows to the
    #: table. If not None, then it must be a callable that accepts
    #: **row_factory_args** and **row_factory_kw** and returns a new object
    #: that can be added to the table.
    row_factory = Any()

    #: Arguments to pass to the **row_factory** callable when a new row is
    #: created
    row_factory_args = Tuple()

    #: Keyword arguments to pass to the **row_factory** callable when a new row
    #: is created
    row_factory_kw = Dict()

    #: Hooks for replacing parts of the implementation.
    table_view_factory = Callable()
    source_model_factory = Callable()
    model_factory = Callable()

    # -------------------------------------------------------------------------
    #  Traits view definitions:
    # -------------------------------------------------------------------------

    traits_view = View(
        [
            "{Initial columns}@",
            Item("columns", resizable=True),
            "{Other columns}@",
            Item("other_columns", resizable=True),
            "|{Columns}<>",
        ],
        [
            [
                "deletable{Are items deletable?}",
                "9",
                "editable{Are items editable?}",
                "9",
                "-[Item Options]>",
            ],
            [
                "show_column_labels{Show column labels?}",
                "9",
                "configurable{Are columns user configurable?}",
                "9",
                "auto_size{Should columns auto size?}",
                "-[Column Options]>",
            ],
            [
                "sortable{Are columns sortable?}",
                Item(
                    "sort_model{Does sorting affect the model?}",
                    enabled_when="sortable",
                ),
                "-[Sorting Options]>",
            ],
            [
                ["show_lines{Show grid lines?}", "|>"],
                ["_", "line_color{Grid line color}@", "|<>"],
                "|[Grid Line Options]",
            ],
            "|{Options}",
        ],
        [
            [
                "cell_color{Text color}@",
                "cell_bg_color{Background color}@",
                "cell_read_only_bg_color{Read only color}@",
                "|[Cell Colors]",
            ],
            ["cell_font", "|[Cell Font]<>"],
            "|{Cell}",
        ],
        [
            [
                "label_color{Text color}@",
                "label_bg_color{Background color}@",
                "|[Label Colors]",
            ],
            ["label_font@", "|[Label Font]<>"],
            "|{Label}",
        ],
        [
            [
                "selection_color{Text color}@",
                "selection_bg_color{Background color}@",
                "|[Selection Colors]",
            ],
            "|{Selection}",
        ],
        height=0.5,
    )

    # -------------------------------------------------------------------------
    #  'Editor' factory methods:
    # -------------------------------------------------------------------------

    def readonly_editor(self, ui, object, name, description, parent):
        """Generates an "editor" that is read-only.
        Overridden to set the value of the editable trait to False before
        generating the editor.

        """
        self.editable = False
        return super().readonly_editor(ui, object, name, description, parent)

    # -------------------------------------------------------------------------
    #  Event handlers:
    # -------------------------------------------------------------------------

    @observe("filters.items")
    def _update_filter_editor(self, event):
        """Handles the set of filters associated with the editor's factory
        being changed.
        """
        values = {None: "000:No filter"}
        i = 0
        for filter in self.filters:
            if not filter.template:
                i += 1
                values[filter] = "%03d:%s" % (i, filter.name)
        values[customize_filter] = "%03d:%s" % ((i + 1), customize_filter.name)
        if self._filter_editor is None:
            self._filter_editor = EnumEditor(values=values)
        else:
            self._filter_editor.values = values


# This alias is deprecated and will be removed in TraitsUI 8.
ToolkitEditorFactory = TableEditor

# -------------------------------------------------------------------------
#  Base class for toolkit-specific editors
# -------------------------------------------------------------------------


class BaseTableEditor(object):
    """Base class for toolkit-specific editors."""

    # -------------------------------------------------------------------------
    #  Interface for toolkit-specific editors:
    # -------------------------------------------------------------------------

    def set_menu_context(self, selection, object, column):
        """Call before creating a context menu for a cell, then set self as the
        controller for the menu.
        """
        self._menu_context = {
            "selection": selection,
            "object": object,
            "column": column,
            "editor": self,
            "info": self.ui.info,
            "handler": self.ui.handler,
        }

    # -------------------------------------------------------------------------
    #  pyface.action 'controller' interface implementation:
    # -------------------------------------------------------------------------

    def add_to_menu(self, menu_item):
        """Adds a menu item to the menu bar being constructed."""
        action = menu_item.item.action
        self.eval_when(action.enabled_when, menu_item, "enabled")
        self.eval_when(action.checked_when, menu_item, "checked")

    def add_to_toolbar(self, toolbar_item):
        """Adds a toolbar item to the toolbar being constructed."""
        self.add_to_menu(toolbar_item)

    def can_add_to_menu(self, action):
        """Returns whether the action should be defined in the user interface."""
        if action.defined_when != "":
            if not eval(action.defined_when, globals(), self._menu_context):
                return False

        if action.visible_when != "":
            if not eval(action.visible_when, globals(), self._menu_context):
                return False

        return True

    def can_add_to_toolbar(self, action):
        """Returns whether the toolbar action should be defined in the user
        interface.
        """
        return self.can_add_to_menu(action)

    def perform(self, action, action_event=None):
        """Performs the action described by a specified Action object."""
        self.ui.do_undoable(self._perform, action)

    def _perform(self, action):
        method_name = action.action
        info = self.ui.info
        handler = self.ui.handler
        context = self._menu_context
        self._menu_context = None
        selection = context["selection"]

        if method_name.find(".") >= 0:
            if method_name.find("(") < 0:
                method_name += "()"
            try:
                eval(method_name, globals(), context)
            except:
                # fixme: Should the exception be logged somewhere?
                pass
            return

        method = getattr(handler, method_name, None)
        if method is not None:
            method(info, selection)
            return

        if action.on_perform is not None:
            action.on_perform(selection)
            return

        action.perform(selection)

    # -------------------------------------------------------------------------
    #  Menu support methods:
    # -------------------------------------------------------------------------

    def eval_when(self, condition, object, trait):
        """Evaluates a condition within a defined context and sets a specified
        object trait based on the result, which is assumed to be a Boolean.
        """
        if condition != "":
            value = bool(eval(condition, globals(), self._menu_context))
            setattr(object, trait, value)


# -------------------------------------------------------------------------
#  Helper class for toolkit-specific editors to implement 'reversed' option:
# -------------------------------------------------------------------------


class ReversedList(object):
    """A list whose order is the reverse of its input."""

    def __init__(self, list):
        self.list = list

    def insert(self, index, value):
        """Inserts a value at a specified index in the list."""
        return self.list.insert(self._index(index - 1), value)

    def index(self, value):
        """Returns the index of the first occurrence of the specified value in
        the list.
        """
        list = self.list[:]
        list.reverse()

        return list.index(value)

    def __len__(self):
        """Returns the length of the list."""
        return len(self.list)

    def __getitem__(self, index):
        """Returns the value at a specified index in the list."""
        return self.list[self._index(index)]

    def __setslice__(self, i, j, values):
        """Sets a slice of a list to the contents of a specified sequence."""
        return self.list.__setslice__(self._index(i), self._index(j), values)

    def __delitem__(self, index):
        """Deletes the item at a specified index."""
        return self.list.__delitem__(self._index(index))

    def _index(self, index):
        """Returns the "reversed" value for a specified index."""
        if index < 0:
            return -1 - index

        result = len(self.list) - index - 1
        if result >= 0:
            return result

        return index
