# -*- coding:utf-8 -*-

"""
This file is part of OpenSesame.

OpenSesame 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, either version 3 of the License, or
(at your option) any later version.

OpenSesame 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 OpenSesame.  If not, see <http://www.gnu.org/licenses/>.
"""
from libopensesame.py3compat import *
import os
from openexp._color.color import Color
from openexp._canvas._richtext.richtext import RichText
from openexp._canvas.canvas import Canvas
from openexp._coordinates.coordinates import Coordinates
from libqtopensesame.misc import drag_and_drop
from libqtopensesame.misc.config import cfg
from qtpy import QtWidgets, QtGui, QtCore
from libqtopensesame.misc.translate import translation_context
_ = translation_context(u'sketchpad', category=u'item')


class QtCanvas(Canvas, Coordinates):

    r"""A dummy Canvas that can be passed to the RichText element."""
    def __init__(self, experiment):

        Canvas.__init__(self, experiment)
        Coordinates.__init__(self)


class QtRichText(RichText):

    r"""Disables the pyqt initialization in the RichText element, because it
    is not necessary in the context of the GUI.
    """
    def _init_pyqt(self, exp):

        pass

    def _register_font(self, exp, font, fd=None):

        RichText._register_font(self, exp, font, QtGui.QFontDatabase())


class SketchpadCanvas(QtWidgets.QGraphicsScene):

    r"""A partial implementation of a canvas, so that sketchpad elements can
    draw to the canvas just as in runtime.
    """
    def __init__(self, sketchpad):
        r"""constructor.

        Parameters
        ----------
        sketchpad
            A sketchpad object.
        """
        self.sketchpad = sketchpad
        self.background_color = sketchpad.var.background
        self.placeholder_color = cfg.sketchpad_placeholder_color
        self.grid = 32
        super().__init__(self.sketchpad.main_window)
        self.indicator = None

    @property
    def selected_element_tool(self):

        return self.sketchpad.sketchpad_widget.selected_element_tool

    def dragMoveEvent(self, e):
        r"""Accepts drag-moves to implement moving items.

        Parameters
        ----------
        e : QGraphicsSceneDragDropEvent
        """
        data = drag_and_drop.receive(e)
        if data[u'type'] != u'sketchpad-element-move':
            e.ignore()
            return
        ex, ey = self.cursor_pos(e, grid=True)
        dx = ex-self.start_move_pos[0]
        dy = ey-self.start_move_pos[1]
        self.start_move_pos = ex, ey
        if abs(dx) < self.grid and abs(dy) < self.grid:
            e.accept()
            return
        self.sketchpad.move_elements(self.sketchpad.selected_elements(),
                                     dx=dx, dy=dy)
        self.sketchpad.draw()
        e.accept()

    def wheelEvent(self, e):
        r"""Scrolls the graphics view based on a wheel event (i.e. mouse-scroll
        event.

        Parameters
        ----------
        e : QGraphicsViewWheelEvent
        """
        if not QtCore.Qt.ControlModifier & e.modifiers():
            return
        self.sketchpad.zoom_diff(e.delta())

    def mouseDoubleClickEvent(self, e):
        r"""Processes mouse-press events, to handle element selection.

        Parameters
        ----------
        e : QGraphicsSceneMouseEvent
        """
        if not QtCore.Qt.LeftButton & e.button():
            # Only accept right clicks
            return
        # Select the pointed-at element
        element = self.element_at(e.scenePos())
        if element is None:
            return
        element.show_edit_dialog()

    def mousePressEvent(self, e):
        """
        desc:
                Processes mouse-press events. Mouse presses can have a number of
                effects.

                - All selected elements are deselected unless Control is pressed
                - Left clicks:
                        - If the pointer tool is selected, clicking an element selects
                          it.
                        - If an element tool is selected, clicking gives the element
                          tool the opporunity (to start a) drawing operation.
                - Right clicks:
                        - If an element is selected, a context menu is shown.

        arguments:
                e:
                        type:	QMouseEvent
        """
        # When control is not pressed, we deselect all currently selected
        # elements first
        if not QtCore.Qt.ControlModifier & e.modifiers():
            for element in self.sketchpad.elements:
                element.select(False)
        element = self.element_at(e.scenePos())
        if QtCore.Qt.RightButton & e.button() and element is not None:
            # The right buttons pops up a context menu.
            element.show_context_menu(e.screenPos())
        if QtCore.Qt.LeftButton & e.button():
            # The left button selects and drags.
            if self.selected_element_tool is not None:
                # When in pointer-tool mode, mouse clicks create new elements.
                self.sketchpad.add_element(
                    self.selected_element_tool.mouse_press(self.sketchpad,
                                                           self.cursor_pos(e)))
            elif element is None:
                self.sketchpad.select_pointer_tool()
            else:
                element.select()
                self.sketchpad.show_element_settings(element)
                x, y = self.cursor_pos(e, grid=False)
                self.start_move_pos = x, y
                data = {
                    u'type': u'sketchpad-element-move',
                    u'from_x': x,
                    u'from_y': y
                }
                drag_and_drop.send(e.widget(), data)

    def mouseReleaseEvent(self, e):
        r"""Mouse-release events give elements the opportunity to finish a
        drawing operation.

        Parameters
        ----------
        e : QMouseEvent
        """
        if self.selected_element_tool is not None:
            self.sketchpad.add_element(
                self.selected_element_tool.mouse_release(self.sketchpad,
                                                         self.cursor_pos(e)))

    def mouseMoveEvent(self, e):
        r"""Processes mouse-move events, to handle highlighting and cursor-
        position display.

        Parameters
        ----------
        e : QMouseEvent
        """
        cursor_pos = self.cursor_pos(e)
        self.sketchpad.set_cursor_pos(cursor_pos)
        for element in self.sketchpad.elements:
            element.highlight(False)
        # Only highlight elements if the pointer tool is selected
        if self.selected_element_tool is None:
            element = self.element_at(e.scenePos())
            if element is not None:
                element.highlight()
        else:
            self.selected_element_tool.mouse_move(self.sketchpad, cursor_pos)

    def keyPressEvent(self, e):
        r"""Processes key-press events, to handle deletion, movement, etc.

        Parameters
        ----------
        e : QKeyEvent
        """
        if e.key() == QtCore.Qt.Key_Delete:
            self.sketchpad.remove_elements(self.sketchpad.selected_elements())
        elif e.key() == QtCore.Qt.Key_Up:
            self.sketchpad.move_elements(self.sketchpad.selected_elements(),
                                         dy=-self.grid)
        elif e.key() == QtCore.Qt.Key_Down:
            self.sketchpad.move_elements(self.sketchpad.selected_elements(),
                                         dy=self.grid)
        elif e.key() == QtCore.Qt.Key_Left:
            self.sketchpad.move_elements(self.sketchpad.selected_elements(),
                                         dx=-self.grid)
        elif e.key() == QtCore.Qt.Key_Right:
            self.sketchpad.move_elements(self.sketchpad.selected_elements(),
                                         dx=self.grid)
        else:
            super(sketchpad_canvas, self).keyPressEvent(e)
            return
        self.sketchpad.draw()

    def element_at(self, pos):
        r"""Gets the element at a specific position.

        Parameters
        ----------
        pos
            An (x,y) tuple.

        Returns
        -------
        A sketchpad element (base_element) or None of no element was at pos.
        """
        # First try to see if there's an exact position match ...
        graphics_item = self.itemAt(pos, QtGui.QTransform())
        # ... if not, try if there's an element that encompasses the position.
        if graphics_item is None:
            for item in self.items():
                if item.boundingRect().contains(pos):
                    graphics_item = item
        # ... else don't find an element.
        if graphics_item is None:
            return
        # If the item is part of a group, we want the group, not the invdividual
        # item, because the group has the element property.
        if graphics_item.group() is not None:
            graphics_item = graphics_item.group()
        try:
            return graphics_item.element
        except AttributeError:  # Appears to happen in rare race conditions
            return

    def cursor_pos(self, e, grid=True):
        r"""Gets the position of the mouse cursor.

        Parameters
        ----------
        e : QMouseEvent
        grid : bool, optional
            Indicates whether the cursor should be locked to the grid.

        Returns
        -------
        An (x, y) tuple with the the coordinates of the mouse cursor.
        """
        pos = e.scenePos().toPoint()
        if grid:
            x = pos.x() + 0.5 * self.grid
            y = pos.y() + 0.5 * self.grid
            x = x - x % self.grid
            y = y - y % self.grid
        else:
            x = pos.x()
            y = pos.y()
        return x, y

    def notify(self, msg):
        r"""Adds a notification message."""
        self.notifications.append(msg)

    def is_var(self, val):
        r"""Determines whether a value is a string containing variable
        references (i.e. "[my_var]").

        Parameters
        ----------
        val
            A value.

        Returns
        -------
        True if val contains variable references, False otherwise.
        """
        if not isinstance(val, str):
            return False
        return self.sketchpad.syntax.contains_variables(val)

    def _pixmap(self, fname):
        r"""Safely returns a QPixmap.

        Returns
        -------
        A QPixmap object.
        """
        if not os.path.exists(fname):
            self.notify(
                _(u'Image name "%s" is unknown or variably defined, using fallback image')
                % fname)
            return self.sketchpad.theme.qpixmap(u'dialog-question')
        return QtGui.QPixmap(fname)

    def _pen(self, color, penwidth, alpha=255):

        color = self._color(color)
        color.setAlpha(alpha)
        p = QtGui.QPen(color)
        p.setWidth(self._penwidth(penwidth))
        return p

    def _brush(self, color):
        r"""Safely returns a QBrush.

        Returns
        -------
        A QBrush object.
        """
        return QtGui.QBrush(self._color(color))

    def _penwidth(self, penwidth):
        r"""Safely returns a penwidth.

        Returns
        -------
        An int penwidth.
        """
        if type(penwidth) not in (int, float):
            self.notify(
                _(u'Penwidth "%s" is unknown or variably defined, using 1') % penwidth)
            return 1
        return penwidth

    def _scale(self, scale):
        r"""Safely returns a scale.

        Returns
        -------
        An float scale.
        """
        if not isinstance(scale, (int, float)):
            self.notify(
                _(u'Scale "%s" is unknown or variably defined, using 1')
                % scale
            )
            return 1
        return scale

    def _rotation(self, rotation):
        r"""Safely returns a rotation.

        Returns
        -------
        A rotation.
        """
        if rotation is None:
            return 0
        if not isinstance(rotation, (int, float)):
            self.notify(
                _('Rotation "%s" is unknown or variably defined, using 0')
                % rotation
            )
            return 0
        return rotation

    def _point(self, i, x, y, center, scale):
        r"""Safely returns a QPoint.

        Parameters
        ----------
        i
            A QGraphicsItem.
        x
            An X coordinate.
        y
            A Y coordinate.
        center
            A boolean indicating whether the point should reflect the center of
            i (True), or the top-left (False).
        scale
            A scaling factor.

        Returns
        -------
        A QPoint object.
        """
        x = self._x(x)
        y = self._y(y)
        scale = self._scale(scale)
        if center:
            r = i.boundingRect()
            x -= r.width() * scale/2
            y -= r.height() * scale/2
        return QtCore.QPoint(x, y)

    def _color(self, _color):
        r"""Safely returns a QColor.

        Returns
        -------
        A QColor object.
        """
        if isinstance(_color, QtGui.QColor):
            return _color
        try:
            hexcolor = Color(self.sketchpad.experiment, _color).hexcolor
        except:
            self.notify(
                _(u'Color "%s" is unknown or variably defined, using placeholder color')
                % _color)
            return QtGui.QColor(self.placeholder_color)
        return QtGui.QColor(hexcolor)

    def _fill(self, fill):
        r"""Safely returns a fill value.

        Returns
        -------
        A fill value (bool).
        """
        if isinstance(fill, str):
            self.notify(
                _(u'Fill "%s" is unknown or variably defined, assuming filled')
                % fill)
            return True
        return bool(fill)

    def _x(self, x):
        r"""Safely returns an X coordinate.

        Returns
        -------
        An X coordinate.
        """
        if type(x) not in (int, float):
            self.notify(
                _('X coordinate "%s" is unknown or variably defined, using display center')
                % x)
            return 0
        return x

    def _y(self, y):
        r"""Safely returns a Y coordinate.

        Returns
        -------
        A Y coordinate.
        """
        if type(y) not in (int, float):
            self.notify(
                _('Y coordinate "%s" is unknown or variably defined, using display center')
                % y)
            return 0
        return y

    def _r(self, r):
        r"""Safely returns a radius

        Returns
        -------
        A radius.
        """
        if type(r) not in (int, float):
            self.notify(
                _('Radius "%s" is unknown or variably defined, using 50') % r)
            return 50
        return r

    def _p(self, r):
        r"""Safely returns a proportion

        Returns
        -------
        A radius.
        """
        if type(r) not in (int, float):
            self.notify(
                _('Proportion "%s" is unknown or variably defined, using .5')
                % r)
            return .5
        return r

    def _w(self, x):
        r"""Safely returns a width.

        Returns
        -------
        A width.
        """
        if type(x) not in (int, float):
            self.notify(
                _('Width "%s" is unknown or variably defined, using 100') % x)
            return 100
        return x

    def _h(self, y):
        r"""Safely returns a height.

        Returns
        -------
        A height.
        """
        if type(y) not in (int, float):
            self.notify(
                _('Height "%s" is unknown or variably defined, using 100') % y)
            return 100
        return y

    def _font_size(self, font_size):
        r"""Safely returns a font_size.

        Returns
        -------
        A font size.
        """
        if not isinstance(font_size, int) or font_size < 0:
            self.notify(_('Font size "%s" is invalid or variably defined, using 18')
                        % font_size)
            return 18
        return font_size

    def drawBackground(self, painter, rect):
        r"""Draws the background and the grid."""
        xc = int(self.xcenter())
        yc = int(self.ycenter())
        w = 2*xc
        h = 2*yc
        painter.fillRect(QtCore.QRect(-xc, -yc, w, h), self._color(
            self.background_color))
        painter.setPen(self._pen(cfg.sketchpad_grid_color,
                                 cfg.sketchpad_grid_thickness_thin,
                                 cfg.sketchpad_grid_opacity))
        painter.drawRect(QtCore.QRect(-xc, -yc, w, h))
        # Draw all lines except for the center ones, because they should be
        # thicker
        if self.grid > 1:
            for x in range(0, xc+1, self.grid):
                painter.drawLine(x, -yc, x, yc)
            for x in range(-self.grid, -xc-1, -self.grid):
                painter.drawLine(x, -yc, x, yc)
            for y in range(0, yc+1, self.grid):
                painter.drawLine(-xc, y, xc, y)
            for y in range(-self.grid, -yc-1, -self.grid):
                painter.drawLine(-xc, y, xc, y)
        # Draw thicker central lines
        painter.setPen(self._pen(cfg.sketchpad_grid_color,
                                 cfg.sketchpad_grid_thickness_thick,
                                 cfg.sketchpad_grid_opacity))
        painter.drawLine(-xc, 0, xc, 0)
        painter.drawLine(0, -yc, 0, yc)

    def clear(self):
        """Mimics canvas api. See openexp._canvas.canvas."""
        super(sketchpad_canvas, self).clear()
        self.notifications = []
        self.elements = []
        self.background_color = self.sketchpad.var.background

    def xcenter(self):
        """Mimics canvas api. See openexp._canvas.canvas."""
        return self.sketchpad.var.get(u'width') // 2

    def ycenter(self):
        """Mimics canvas api. See openexp._canvas.canvas."""
        return self.sketchpad.var.get(u'height') // 2

    def text(self, text, center=True, x=None, y=None, max_width=None,
             **properties):
        """Mimics canvas api. See openexp._canvas.canvas."""
        if u'color' in properties:
            properties[u'color'] = self._color(properties[u'color']).name()
        if u'font_size' in properties:
            properties[u'font_size'] = self._font_size(
                properties[u'font_size'])
        x = self._x(x)
        y = self._x(y)
        # We set the color backend to a dummy value. This avoids PyGame from
        # being unnecessarily loaded in the legacy color backend. Then we
        # restore the backend, also catching the case in which no color backend
        # was set.
        color_backend = (
            self.sketchpad.experiment.var.color_backend
            if u'color_backend' in self.sketchpad.experiment.var
            else None
        )
        self.sketchpad.experiment.var.color_backend = 'color'
        t = QtRichText(
            QtCanvas(self.sketchpad.experiment),
            text,
            x=x, y=y, center=center, max_width=max_width,
            **properties
        )
        # Restore the color backend so that we don't change the experiment
        if color_backend is None:
            del self.sketchpad.experiment.var.color_backend
        else:
            self.sketchpad.experiment.var.color_backend = color_backend
        im = t._to_pil()
        qim = QtGui.QImage(
            im.tobytes('raw', 'BGRA'),
            im.size[0], im.size[1],
            QtGui.QImage.Format_ARGB32
        )
        pixmap = QtGui.QPixmap.fromImage(qim)
        i = self.addPixmap(pixmap)
        c = i.boundingRect().center()
        dx = c.x()
        dy = c.y()
        if center:
            x -= dx
            y -= dy
        i.setPos(x, y)
        return i

    def line(self, sx, sy, ex, ey, color=None, penwidth=None, add=True):
        """Mimics canvas api. See openexp._canvas.canvas."""
        i = QtWidgets.QGraphicsLineItem(self._x(sx), self._y(sy), self._x(ex),
                                        self._y(ey))
        i.setPen(self._pen(color, penwidth))
        if add:
            self.addItem(i)
        return i

    def arrow(self, sx, sy, ex, ey, body_length=0.8, body_width=.5,
              head_width=30, color=None, fill=False, penwidth=None, add=True):
        """Mimics canvas api. See openexp._canvas.canvas."""
        from openexp._canvas._arrow.arrow import Arrow

        shape = Arrow._shape(self._x(sx), self._y(sy), self._x(ex),
                             self._y(ey), body_length=self._p(body_length),
                             body_width=self._p(body_width),
                             head_width=self._w(head_width))
        color = self._color(color)
        if fill:
            pen = self._pen(color, 1)
            brush = self._brush(color)
        else:
            pen = self._pen(color, penwidth)
            brush = QtGui.QBrush()
        polygon = QtGui.QPolygonF()
        for point in shape:
            polygon << QtCore.QPointF(point[0], point[1])
        i = QtWidgets.QGraphicsPolygonItem(polygon)
        i.setPen(pen)
        i.setBrush(brush)
        if add:
            self.addItem(i)
        return i

    def rect(self, x, y, w, h, fill=False, color=None, penwidth=None):
        """Mimics canvas api. See openexp._canvas.canvas."""
        color = self._color(color)
        if self._fill(fill):
            pen = self._pen(color, 1)
            brush = self._brush(color)
        else:
            pen = self._pen(color, penwidth)
            brush = QtGui.QBrush()
        i = self.addRect(self._x(x), self._y(y), self._w(w), self._h(h),
                         pen=pen, brush=brush)
        return i

    def ellipse(self, x, y, w, h, fill=False, color=None, penwidth=None,
                add=True):
        """Mimics canvas api. See openexp._canvas.canvas."""
        color = self._color(color)
        if fill:
            pen = self._pen(color, 1)
            brush = self._brush(color)
        else:
            pen = self._pen(color, penwidth)
            brush = QtGui.QBrush()
        i = QtWidgets.QGraphicsEllipseItem(self._x(x), self._y(y), self._w(w),
                                           self._h(h))
        i.setPen(pen)
        i.setBrush(brush)
        if add:
            self.addItem(i)
        return i

    def circle(self, x, y, r, fill=False, color=None, penwidth=None):
        """Mimics canvas api. See openexp._canvas.canvas."""
        r = self._r(r)
        x = self._x(x) - r
        y = self._y(y) - r
        i = self.ellipse(x, y, 2*r, 2*r, fill=fill, color=color,
                         penwidth=penwidth)
        return i

    def image(self, image, center=True, x=None, y=None, scale=None,
              rotation=None):
        """Mimics canvas api. See openexp._canvas.canvas."""
        if not isinstance(image, QtGui.QPixmap):
            image = self._pixmap(image)
        i = self.addPixmap(image)
        s = self._scale(scale)
        x = self._x(x)
        y = self._x(y)
        r = self._rotation(rotation)
        c = i.boundingRect().center()
        dx = c.x()*s
        dy = c.y()*s
        i.setTransform(
            QtGui.QTransform()
            .translate(dx, dy)
            .rotate(r)
            .translate(-dx, -dy)
            .scale(s, s)
        )
        if center:
            x -= dx
            y -= dy
        i.setPos(x, y)
        return i

    def gabor(self, x, y, *arglist, **kwdict):
        """Mimics canvas api. See openexp._canvas.canvas."""
        from openexp.canvas import gabor_file
        try:
            image = gabor_file(*arglist, **kwdict)
        except:
            self.notify(
                _(u'Some properties of a Gabor patch are unknown or variably defined, using fallback image'))
            image = self.sketchpad.theme.qpixmap(u'dialog-question')
        return self.image(image, x=x, y=y, scale=1)

    def noise_patch(self, x, y, *arglist, **kwdict):
        """Mimics canvas api. See openexp._canvas.canvas."""
        from openexp.canvas import noise_file
        try:
            image = noise_file(*arglist, **kwdict)
        except:
            self.notify(
                _(u'Some properties of a noise patch are unknown or variably defined, using fallback image'))
            image = self.sketchpad.theme.qpixmap(u'dialog-question')
        return self.image(image, x=x, y=y, scale=1)

    def fixdot(self, x=None, y=None, color=None, style=u'default'):
        """Mimics canvas api. See openexp._canvas.canvas."""
        color = self._color(color)
        x = self._x(x)
        y = self._y(y)
        group = QtWidgets.QGraphicsItemGroup()
        h = 2
        known = True
        if style == u'default':
            style = u'open-medium'
        if u'large' in style:
            s = 16
        elif u'medium' in style:
            s = 8
        elif u'small' in style:
            s = 4
        else:
            known = False
            s = 8
        if u'open' in style:
            i = self.ellipse(x-s, y-s, 2*s, 2*s, fill=True, color=color,
                             add=False)
            group.addToGroup(i)
            i = self.ellipse(x-h, y-h, 2*h, 2*h, fill=True,
                             color=self.background_color, add=False)
            group.addToGroup(i)
        elif u'filled' in style:
            i = self.ellipse(x-s, y-s, 2*s, 2*s, fill=True, color=color,
                             add=True)
            group.addToGroup(i)
        elif u'cross' in style:
            i = self.line(x, y-s, x, y+s, color=color, add=False, penwidth=1)
            group.addToGroup(i)
            i = self.line(x-s, y, x+s, y, color=color, add=False, penwidth=1)
            group.addToGroup(i)
        else:
            i = self.ellipse(x-s, y-s, 2*s, 2*s, fill=True, color=color,
                             add=True)
            group.addToGroup(i)
            known = False
        self.addItem(group)
        if not known:
            self.notify(_(u'Fixdot style "%s" is unknown or variably defined')
                        % style)
        return group


# Alias for backwards compatibility
sketchpad_canvas = SketchpadCanvas
