# --- imports
# pygame imports
import pygame
# local imports
from .text_widget import TextWidget
from ..designs import getDefaultDesign, getFallbackDesign
from ..util import inherit_docstrings_from_superclass
# constants
START = 0
"""A index-constant for the first index of a widget's content."""
END = 'e'
"""A index-constant for the last index of a widget's content."""
CURSOR = 'c'
"""A index-constant for the cursor of a widget."""
INSERT = CURSOR
"""A index-constant alias for CURSOR."""
CURRENT = CURSOR
"""A index-constant alias for CURSOR."""
SELECTION = 's'
"""A index-constant for the selection-index of a widget."""
# set defaults
getFallbackDesign().selection_overlay = (45, 110, 235, 120)
"""Color to be overlayed by default for content that has been selected in a widget."""
[docs]class SelectionTextWidget(TextWidget):
    """
    Underlying class for widgets using selectable content.
    """
    def __init__(self, x, y, width, height, text="", font=getDefaultDesign().font, editable=False, validation_function=(lambda *x: True), selection_overlay=getDefaultDesign().selection_overlay):
        """
        Initialisation of a SelectionTextWidget.
        Args:
            inherit_doc:: arguments
            editable: A boolean indicating whether the widget's content is editable by the user.
                The default value is False, meaning it can not be edited by user-input.
            validation_function: A function that validates changed content.
                It will receive three arguments (the new content, the old content and the widget-object)
                and should return a boolean indicating whether the change is valid (True when valid).
                The old content can be None if it was not set before;
                the new content can be anything that is being passed to setText().
                The default value is a function that accepts every change.
            selection_overlay: A color-like object that can be interpreted as a color by pygame (such as a tuple with RGB values);
                this is used as an overlay for content that has been selected.
                The default value is the global default for the selection-color.
        """
        self.validation_function = validation_function
        super(SelectionTextWidget, self).__init__(x, y, width, height, text, font)
        self._cursor = 0
        self._selection_index = 0
        self.editable = editable
        self.selection_overlay = selection_overlay
[docs]    def setEditable(self, editable):
        """
        Set whether the widget's content is editable by the user (via user-input).
        Args:
            editable: A boolean indicating whether the widget's content is editable by the user.
        Returns:
            Itsself (the widget) for convenience.
        """
        self._editable = bool(editable)
        return self 
[docs]    def isEditable(self):
        """
        Return whether the widget's content is editable by the user (via user-input).
        Returns:
            A boolean indicating whether the widget's content is editable by the user.
        """
        return self._editable 
[docs]    def setValidation(self, validation_function):
        """
        Set the widget's validation-function.
        Args:
            validation_function: A function that validates changed content.
                It will receive three arguments (the new content, the old content and the widget-object)
                and should return a boolean indicating whether the change is valid (True when valid).
                The old content can be None if it was not set before; the new content can be anything that is being passed to setText().
        Returns:
            Itsself (the widget) for convenience.
        """
        self._validation_function = validation_function
        return self 
[docs]    def getValidation(self):
        """
        Return the widget's validation-function.
        Returns:
            A function that validates changed content.
            It will receive three arguments (the new content, the old content and the widget-object)
            and should return a boolean indicating whether the change is valid (True when valid).
            The old content can be None if it was not set before; the new content can be anything that is being passed to setText().
        """
        return self._validation_function 
[docs]    def setText(self, text, return_success_boolean=False):
        """
        Additionally validate the change of content.
        inherit_doc::
        """
        if self.validation_function and callable(self.validation_function) and self.validation_function(text, getattr(self, "text", None), self):
            super(SelectionTextWidget, self).setText(text)
            if return_success_boolean:
                return True
        if return_success_boolean:
            return False
        return self 
[docs]    def insert(self, index, text):
        """
        Insert a given text at the given index.
        Args:
            index: An integer (or known constant) representing the position the text should be insterted at.
            text: A string specifing the content to add to the content of the widget.
        Returns:
            A boolean indicating whether the change was successful.
        """
        index = self.getActualIndex(index)
        return self.setText(self.text[:index] + text + self.text[index:], True) 
[docs]    def delete(self, start, end):
        """
        Deletes the widget's content between the two given indices.
        Args:
            start: An integer representing the index from which the content should be deleted.
            end: An integer representing the index until which the content should be deleted.
        Returns:
            A boolean indicating whether the change was successful.
        """
        start, end = self._sort(start, end)
        return self.setText(self.text[:start] + self.text[end:], True) 
[docs]    def setSelectionOverlay(self, color):
        """
        Set the widget's color to overlay for content that has been selected.
        Args:
            color: A color-like object that can be interpreted as a color by pygame (such as a tuple with RGB values).
        Returns:
            Itsself (the widget) for convenience.
        """
        self._selection_overlay = color
        self.markDirty()
        return self 
[docs]    def getSelectionOverlay(self):
        """
        Return the widget's color to overlay for content that has been selected.
        Returns:
            A color-like object that represents the widget's color to overlay for content that has been selected.
        """
        return self._selection_overlay 
[docs]    def setCursor(self, index):
        """
        Set the widget's cursor-position.
        Args:
            index: An integer (or known constant) representing the index the cursor should be set to.
        Returns:
            Itsself (the widget) for convenience.
        """
        return self.setSelection(index, index) 
[docs]    def moveCursor(self, amount):
        """
        Move the widget's cursor-position by the given amount.
        Args:
            amount: An integer representing the amount the cursor should be moved by.
        Returns:
            Itsself (the widget) for convenience.
        """
        return self.setCursor(self.cursor + int(amount)) 
[docs]    def getCursor(self):
        """
        Return the widget's cursor-position.
        Returns:
            An integer representing the index the cursor is at.
        """
        return self._cursor 
[docs]    def setSelectionIndex(self, index):
        """
        Set the widget' selection-index.
        Args:
            index: An integer (or known constant) representing the index the selection-index should be set to.
        Returns:
            Itsself (the widget) for convenience.
        """
        return self.setSelection(index, CURSOR) 
[docs]    def moveSelectionIndex(self, amount):
        """
        Move the widget's cursor-position by the given amount.
        Args:
            amount: An integer representing the amount the cursor should be moved by.
        Returns:
            Itsself (the widget) for convenience.
        """
        return self.setSelectionIndex(self.selection_index + int(amount)) 
[docs]    def getSelectionIndex(self):
        """
        Return the widget' selection-index.
        Returns:
            An integer representing the index the selection-index is at.
        """
        return self._selection_index 
[docs]    def setSelection(self, selection_index, cursor):
        """
        Set the widget' selection between the given bounds.
        Args:
            selection_index: An integer (or known constant) representing the index the selection should start.
                This will be the position of the selection-index.
            cursor: An integer (or known constant) representing the index the selection should end.
                This will be the position of the cursor.
                The indices can actually be reversed (meaning the start-index is larger than the end-index)
                so that the cursor is at the start of the selection.
        Returns:
            Itsself (the widget) for convenience.
        """
        self._selection_index = self.getActualIndex(selection_index)
        self._cursor = self.getActualIndex(cursor)
        self.markDirty()
        return self 
[docs]    def getSelection(self):
        """
        Return the widget' selection-range.
        Returns:
            A tuple (start, end) with the start- and end-index of the selection.
            This is not the selected content, only the indices of the range!
        """
        return self._sort(self.selection_index, self.cursor) 
[docs]    def getActualIndex(self, index, constrain=True):
        """
        Return the actual index corresponding to the given representation.
        This converts known constants (e.g. END, CURSOR) to the corresponding integers.
        Args:
            index: An integer (or known constant) to be converted.
            constrain: A boolean indicating whether the given index should be constrained to
                valid indices for the content or not.
                The default value is True, meaning that the returned index is constrained.
        Returns:
            An integer representing the actual index the given value corresponds to.
        """
        if index == CURSOR:
            return self.cursor
        if index == END:
            return len(self.text)
        if index == SELECTION:
            return self.selection_index
        if constrain:
            return min(max(int(index), START), self.getActualIndex(END))
        return index 
    def _indexToPos(self, index):
        """
        Return the relative coordinate (x, y) corresponding to the given index.
        This is an internal function.
        Args:
            index: An integer (or known constant) to be converted.
        Returns:
            A pair of integers (x, y) representing a relative coordinate.
        """
        return self.font.size(self.text[:self.getActualIndex(index)])[0], 0
    def _posToIndex(self, x, y):
        """
        Return the index corresponding to the given relative coordinate (x, y).
        This is an internal function.
        Args:
            x: An integer representing a relative x-coordinate.
            y: An integer representing a relative y-coordinate.
        Returns:
            An integer representing the index corresponding to the given relative coordinate.
        """
        length = len(self.text)
        x = min(float(x), (self.font.size(self.text[:-1])[0]
                           + self.font.size(self.text[-1:])[0] * 1.5))
        index = 0
        n = 0
        if self.text:
            for n in range(max(min(int(x / (self.font.size(self.text)[0] / length)), length - 1), 0), 0, -1):
                if self.font.size(self.text[:n])[0] + self.font.size(self.text[n])[0] < x:
                    break
            for index in range(n, length):
                if self.font.size(self.text[:index])[0] + (self.font.size(self.text[index])[0] * 1.5) > x:
                    break
            else:
                index += 1
        return index
    def _sort(self, i, j, constrain=True):
        """
        Return the indices in ascending order.
        This is an internal function.
        Args:
            i: An integer (or known constant) to sort.
            j: Another integer (or known constant) to sort.
            constrain: A boolean indicating whether the given indices should be constrained to
                valid indices for the content or not.
                The default value is True, meaning that the returned index is constrained.
        Returns:
            A tuple (min, max) of the numbers which have been sorted.
        """
        i = self.getActualIndex(i, constrain)
        j = self.getActualIndex(j, constrain)
        if i > j:
            return j, i
        return i, j
[docs]    def update(self, *args):
        """
        Additionally handles the selection and deletion of content.
        inherit_doc::
        """
        if len(args) > 0 and self.isActive() and self.isFocused():
            event = args[0]
            if event.type == pygame.KEYDOWN and self.isEditable():
                if event.key in (pygame.K_BACKSPACE, pygame.K_DELETE):
                    if self.selection_index == self.cursor:
                        if event.key == pygame.K_DELETE:
                            self.delete(self.selection_index + 1, CURSOR)
                        else:
                            if self.delete(self.selection_index - 1, CURSOR):
                                self.moveCursor(-1)
                    else:
                        s, c = self._sort(SELECTION, CURSOR)
                        if self.delete(s, c):
                            self.setCursor(s)
            elif event.type == pygame.MOUSEMOTION:
                if (event.buttons[0] or event.buttons[2]) and self.rect.collidepoint(event.pos):
                    self.setSelection(SELECTION, self._posToIndex(event.pos[0] - self.rect.x, event.pos[1] - self.rect.y))
            elif event.type == pygame.MOUSEBUTTONDOWN:
                if event.button in (1, 3) and self.rect.collidepoint(event.pos):
                    self.setCursor(self._posToIndex(event.pos[0] - self.rect.x, event.pos[1] - self.rect.y))
        super(SelectionTextWidget, self).update(*args) 
    editable = property(lambda obj: obj.isEditable(), lambda obj, arg: obj.setEditable(arg), doc="""The widget' status as a boolean whether its content is editable by the user.""")
    validation_function = property(lambda obj: obj.getValidation(), lambda obj, arg: obj.setValidation(arg), doc="""The widget's function used for validating change of its content.""")
    selection_overlay = property(lambda obj: obj.getSelectionOverlay(), lambda obj, arg: obj.setSelectionOverlay(arg), doc="""The widget's color to overlay for content that has been selected.""")
    selection_index = property(lambda obj: obj.getSelectionIndex(), lambda obj, arg: obj.setSelectionIndex(arg), doc="""The widget's index representing an endpoint for the range of selected content.""")
    cursor = property(lambda obj: obj.getCursor(), lambda obj, arg: obj.setCursor(arg), doc="""The widget's position of the cursor as a index. This is another endpoint for the range of selected content.""")
    selection = property(lambda obj: obj.getSelection(), lambda obj, tuple: obj.setSelection(*tuple), doc="""The widget's indices spanning the range of selected content.""") 
# inherit docs from superclass
SelectionTextWidget = inherit_docstrings_from_superclass(SelectionTextWidget)