# This file is part of Sympathy for Data.
# Copyright (c) 2013, 2017, Combine Control Systems AB
#
# Sympathy for Data is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, version 3 of the License.
#
# Sympathy for Data is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Sympathy for Data.  If not, see <http://www.gnu.org/licenses/>.
"""
Slice the rows in Tables or elements in lists of Tables or ADAFs.
The slice pattern is expressed with standard Python syntax, [start:stop:step].
See example below to get a clear view how it works for a list.
::
    >>> li = ['elem0', 'elem1', 'elem2', 'elem3', 'elem4']
    >>> li[1:3]
    ['elem1', 'elem2']
    >>> li[1:-1]
    ['elem1', 'elem2', 'elem3']
    >>> li[0:3]
    ['elem0', 'elem1', 'elem2']
    >>> li[:3]
    ['elem0', 'elem1', 'elem2']
    >>> li[3:]
    ['elem3', 'elem4']
    >>> li[:]
    ['elem0', 'elem1', 'elem2', 'elem3', 'elem4']
    >>> li[::2]
    ['elem0', 'elem2', 'elem4']
    >>> li[:4:2]
    ['elem0', 'elem2']
    >>> li[1::2]
    ['elem1', 'elem3']
"""
import numpy
import re
import traceback
import numpy as np
from sympathy.api import qt2 as qt_compat
from sympathy.api import node as synode
from sympathy.api import table
from sympathy.api import ParameterView
from sympathy.api import node_helper
from sympathy.api.nodeconfig import Port, Ports, Tag, Tags
from sympathy.api.exceptions import NoDataError
from sympathy.platform import widget_library as sywidgets
from sympathy.platform.table_viewer import TableModel
QtGui = qt_compat.import_module('QtGui')  # noqa
QtWidgets = qt_compat.import_module('QtWidgets')  # noqa
class SliceError(Exception):
    pass
class GetSlice(object):
    def __getitem__(self, index):
        return index
    @staticmethod
    def from_string(string):
        """
        Construct a slice object from index string.
        >>> GetSlice.from_string('[:]')
        slice(None, None, None)
        >>> GetSlice.from_string('[:1:]')
        slice(None, 1, None)
        >>> GetSlice.from_string('[::1]')
        slice(None, None, 1)
        >>> GetSlice.from_string('[0:-1:-1]')
        slice(0, -1, -1)
        """
        # Compact but insecure method, mitigated by limiting characters.
        if re.match(r'\[[\[\]0-9:, -]*\]', string):
            try:
                return eval('GetSlice(){}'.format(string))
            except SyntaxError:
                pass
    @staticmethod
    def valid_string(string, dims=2, allow_int=True):
        """Validates input string index and returns true if the index was
        valid.
        """
        index = GetSlice.from_string(string)
        if index or index == 0:
            if not allow_int and isinstance(index, int):
                return False
            return len(index) <= dims if isinstance(index, tuple) else True
        else:
            return False
def slice_base_parameters():
    parameters = synode.parameters()
    parameters.set_string(
        'slice', label='Slice', value='[:]',
        description=('Use standard Python syntax to define pattern for slice '
                     'operation, [start:stop:step]'))
    parameters.set_integer(
        'limit', label='Limit preview to', value=100,
        editor=synode.Editors.bounded_spinbox_editor(0, 10000, 1),
        description='Specify the maximum number of rows in the preview table')
    return parameters
[docs]class SliceDataTable(synode.Node):
    """
    Slice rows in Table. The number of columns are conserved during
    the slice operation.
    """
    author = "Erik der Hagopian"
    name = 'Slice data Table'
    nodeid = 'org.sysess.sympathy.slice.slicedatatable'
    version = '1.0'
    icon = 'select_table_rows.svg'
    tags = Tags(Tag.DataProcessing.Select)
    related = ['org.sysess.sympathy.slice.slicedatatables']
    inputs = Ports([Port.Table('Input Table', name='port1')])
    outputs = Ports([Port.Table(
        'Table consisting of the rows that been sliced out from the incoming '
        'Table according to the defined pattern. The number of columns are '
        'conserved during the slice operation', name='port2')])
    parameters = slice_base_parameters()
    def execute(self, node_context):
        itable = node_context.input['port1']
        otable = node_context.output['port2']
        index = node_context.parameters['slice'].value
        slice_index = GetSlice.from_string(index)
        otable.update(itable[slice_index])
        otable.set_name(itable.get_name())
        otable.set_table_attributes(itable.get_table_attributes())
    def verify_parameters(self, node_context):
        return GetSlice.valid_string(node_context.parameters['slice'].value)
    def exec_parameter_view(self, node_context):
        itable = node_context.input['port1']
        if not itable.is_valid():
            itable = None
        return SliceWidget(node_context, itable) 
[docs]@node_helper.list_node_decorator(['port1'], ['port2'])
class SliceDataTables(SliceDataTable):
    name = 'Slice data Tables'
    nodeid = 'org.sysess.sympathy.slice.slicedatatables' 
class SliceWidget(ParameterView):
    def __init__(self, node_context, itable, dims=2, allow_int=True,
                 parent=None):
        super(SliceWidget, self).__init__(parent=parent)
        self._node_context = node_context
        self._itable = itable
        self._parameters = node_context.parameters
        self._dims = dims
        self._allow_int = allow_int
        self._init_gui()
        self._connect_gui()
    def _init_gui(self):
        vlayout = QtWidgets.QVBoxLayout()
        limit_hlayout = QtWidgets.QHBoxLayout()
        self.slice_label = QtWidgets.QLabel('Slice <I>[start:stop:step]</I>')
        self.slice_lineedit = sywidgets.ValidatedTextLineEdit()
        self.limit_label = QtWidgets.QLabel('Limit preview to:')
        self._limit_gui = self._parameters['limit'].gui()
        self.limit_spinbox = self._limit_gui.editor()
        self.preview_button = QtWidgets.QPushButton('Preview')
        self._preview_table = sywidgets.BasePreviewTable()
        self._preview_table_model = TableModel()
        self._preview_table.setModel(self._preview_table_model)
        limit_hlayout.addWidget(self.limit_label)
        limit_hlayout.addWidget(self.limit_spinbox)
        vlayout.addWidget(self.slice_label)
        vlayout.addWidget(self.slice_lineedit)
        vlayout.addLayout(limit_hlayout)
        vlayout.addWidget(self.preview_button)
        vlayout.addWidget(self._preview_table)
        self.slice_lineedit.clear()
        self._init_line_validator()
        self.slice_lineedit.setText(self._parameters['slice'].value)
        self._preview_table.setEditTriggers(
            QtWidgets.QAbstractItemView.NoEditTriggers)
        self.preview()
        self.setLayout(vlayout)
    def _connect_gui(self):
        self.slice_lineedit.valueChanged[str].connect(self.slice)
        self.preview_button.clicked.connect(self.preview)
    @property
    def valid(self):
        return GetSlice.valid_string(self._parameters['slice'].value,
                                     self._dims, self._allow_int)
    def _init_line_validator(self):
        def slice_validator(value):
            try:
                valid = GetSlice.valid_string(value,
                                              self._dims, self._allow_int)
                if not valid:
                    raise sywidgets.ValidationError(
                        '"{}" is not a valid slice value.'.format(value))
            except Exception as e:
                raise sywidgets.ValidationError(str(e))
            return value
        self.slice_lineedit.setBuilder(slice_validator)
    def slice(self, text):
        self._parameters['slice'].value = text
        self.status_changed.emit()
    def limit(self, value):
        self._parameters['limit'].value = int(value)
    def clear_preview(self, err_msg=''):
        empty_table = table.File()
        if err_msg:
            empty_table['Error'] = np.array([err_msg])
        self._preview_table_model.set_table(empty_table)
    def preview(self):
        # Fail immediately if there is no input data
        if self._itable is None:
            self.clear_preview('No input data')
            return
        try:
            index = self._parameters['slice'].value
            limit = self._parameters['limit'].value
            slice_index = GetSlice.from_string(index)
            slice_data = self._itable[slice_index]
            slice_data = slice_data[:limit]
            self._preview_table_model.set_table(slice_data)
            if not GetSlice.valid_string(index, self._dims):
                raise SliceError
        except SliceError:
            self.clear_preview('Invalid slice')
        except:
            traceback.print_exc()
            self.clear_preview('Failed to create preview')
class SlicesWidget(ParameterView):
    def __init__(self, node_context, itables, parent=None):
        super(SlicesWidget, self).__init__(parent=parent)
        self._node_context = node_context
        self._parameters = node_context.parameters
        self._single = None
        self._itables = itables
        self._init_gui()
        self._connect_gui()
    @property
    def valid(self):
        return self._single.valid
    def _init_gui(self):
        self.vlayout = QtWidgets.QVBoxLayout()
        group_hlayout = QtWidgets.QHBoxLayout()
        self.group_label = QtWidgets.QLabel('Preview group nr:')
        self.group_spinbox = sywidgets.ValidatedIntSpinBox()
        group_hlayout.addWidget(self.group_label)
        group_hlayout.addWidget(self.group_spinbox)
        self.vlayout.addLayout(group_hlayout)
        self.group_spinbox.setMinimum(0)
        if self._itables.is_valid():
            self.group_spinbox.setMaximum(max(0, len(self._itables) - 1))
        else:
            self.group_spinbox.setMaximum(0)
        self.group_spinbox.setValue(self._parameters['group_index'].value)
        self.group(self._parameters['group_index'].value)
        self.setLayout(self.vlayout)
    def _connect_gui(self):
        self.group_spinbox.valueChanged[int].connect(self.group)
        self._single.status_changed.connect(self.status_changed)
    def group(self, value):
        try:
            self._single.hide()
        except:
            pass
        self._parameters['group_index'].value = int(value)
        if self._itables.is_valid() and len(self._itables):
            itable = self._itables[int(value)]
        else:
            itable = None
        self._single = SliceWidget(self._node_context, itable)
        self.vlayout.addWidget(self._single)
[docs]class SliceList(synode.Node):
    """Slice elements in a list."""
    author = "Erik der Hagopian"
    name = 'Slice List'
    icon = 'slice_list.svg'
    nodeid = 'org.sysess.sympathy.slice.slicelist'
    version = '1.0'
    tags = Tags(Tag.Generic.List, Tag.DataProcessing.List)
    inputs = Ports([
        Port.Custom('[<a>]', 'Input List', name='list')])
    outputs = Ports([
        Port.Custom('[<a>]', 'Sliced output List', name='list')])
    parameters = slice_base_parameters()
    def execute(self, node_context):
        slice_index = GetSlice.from_string(
            node_context.parameters['slice'].value)
        itables = node_context.input['list']
        otables = node_context.output['list']
        for itable in list(itables)[slice_index]:
            otables.append(itable)
    def verify_parameters(self, node_context):
        return GetSlice.valid_string(
            node_context.parameters['slice'].value, 1, allow_int=False)
    def exec_parameter_view(self, node_context):
        try:
            itable = table.File()
            itable.set_column_from_array(
                '0',
                numpy.arange(len(node_context.input['list'])))
        except NoDataError:
            itable = None
        return SliceWidget(node_context, itable, 1, allow_int=False)