# (C) Copyright 2004-2023 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 various editors for a drag-and-drop editor,
    for the wxPython user interface toolkit. A drag-and-drop editor represents
    its value as a simple image which, depending upon the editor style, can be
    a drag source only, a drop target only, or both a drag source and a drop
    target.
"""


import wx
import numpy

from pickle import load

from traits.api import Bool

from pyface.wx.drag_and_drop import (
    PythonDropSource,
    PythonDropTarget,
    clipboard,
)


try:
    from apptools.io import File
except ImportError:
    File = None

try:
    from apptools.naming.api import Binding
except ImportError:
    Binding = None

from pyface.image_resource import ImageResource

from .editor import Editor


# The image to use when the editor accepts files:
file_image = ImageResource("file").create_image()

# The image to use when the editor accepts objects:
object_image = ImageResource("object").create_image()

# The image to use when the editor is disabled:
inactive_image = ImageResource("inactive").create_image()

# String types:
string_type = (str, str)

# -------------------------------------------------------------------------
#  'SimpleEditor' class:
# -------------------------------------------------------------------------


class SimpleEditor(Editor):
    """Simply style of editor for a drag-and-drop editor, which is both a drag
    source and a drop target.
    """

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

    #: Is the editor a drop target?
    drop_target = Bool(True)

    #: Is the editor a drag source?
    drag_source = Bool(True)

    def init(self, parent):
        """Finishes initializing the editor by creating the underlying toolkit
        widget.
        """
        # Determine the drag/drop type:
        value = self.value
        self._is_list = isinstance(value, list)
        self._is_file = isinstance(value, string_type) or (
            self._is_list
            and (len(value) > 0)
            and isinstance(value[0], string_type)
        )

        # Get the right image to use:
        image = self.factory.image
        if image is not None:
            image = image.create_image()
            disabled_image = self.factory.disabled_image
            if disabled_image is not None:
                disabled_image = disabled_image.create_image()
        else:
            disabled_image = inactive_image
            image = object_image
            if self._is_file:
                image = file_image

        self._image = image.ConvertToBitmap()
        if disabled_image is not None:
            self._disabled_image = disabled_image.ConvertToBitmap()
        else:
            data = numpy.reshape(
                numpy.fromstring(image.GetData(), numpy.uint8), (-1, 3)
            ) * numpy.array([[0.297, 0.589, 0.114]])
            g = data[:, 0] + data[:, 1] + data[:, 2]
            data[:, 0] = data[:, 1] = data[:, 2] = g
            image.SetData(numpy.ravel(data.astype(numpy.uint8)).tostring())
            image.SetMaskColour(0, 0, 0)
            self._disabled_image = image.ConvertToBitmap()

        # Create the control and set up the event handlers:
        self.control = control = wx.Window(
            parent, -1, size=wx.Size(image.GetWidth(), image.GetHeight())
        )
        self.set_tooltip()

        if self.drop_target:
            control.SetDropTarget(PythonDropTarget(self))

        control.Bind(wx.EVT_LEFT_DOWN, self._left_down)
        control.Bind(wx.EVT_LEFT_UP, self._left_up)
        control.Bind(wx.EVT_MOTION, self._mouse_move)
        control.Bind(wx.EVT_PAINT, self._on_paint)

    def dispose(self):
        """Disposes of the contents of an editor."""
        control = self.control
        control.Unbind(wx.EVT_LEFT_DOWN)
        control.Unbind(wx.EVT_LEFT_UP)
        control.Unbind(wx.EVT_MOTION)
        control.Unbind(wx.EVT_PAINT)

        super().dispose()

    def update_editor(self):
        """Updates the editor when the object trait changes externally to the
        editor.
        """
        return

    # -- Private Methods ------------------------------------------------------

    def _get_drag_data(self, data):
        """Returns the processed version of a drag request's data."""
        if isinstance(data, list):

            if Binding is not None and isinstance(data[0], Binding):
                data = [item.obj for item in data]

            if File is not None and isinstance(data[0], File):
                data = [item.absolute_path for item in data]
                if not self._is_file:
                    result = []
                    for file in data:
                        item = self._unpickle(file)
                        if item is not None:
                            result.append(item)
                    data = result

        else:
            if Binding is not None and isinstance(data, Binding):
                data = data.obj

            if File is not None and isinstance(data, File):
                data = data.absolute_path
                if not self._is_file:
                    object = self._unpickle(data)
                    if object is not None:
                        data = object

        return data

    def _unpickle(self, file_name):
        """Returns the unpickled version of a specified file (if possible)."""
        with open(file_name, "rb") as fh:
            try:
                object = load(fh)
            except Exception:
                object = None

        return object

    # -- wxPython Event Handlers ----------------------------------------------

    def _on_paint(self, event):
        """Called when the control needs repainting."""
        image = self._image
        control = self.control
        if not control.IsEnabled():
            image = self._disabled_image

        wdx, wdy = control.GetClientSize()
        wx.PaintDC(control).DrawBitmap(
            image,
            (wdx - image.GetWidth()) // 2,
            (wdy - image.GetHeight()) // 2,
            True,
        )

    def _left_down(self, event):
        """Handles the left mouse button being pressed."""
        if self.control.IsEnabled() and self.drag_source:
            self._x, self._y = event.GetX(), event.GetY()
            self.control.CaptureMouse()

        event.Skip()

    def _left_up(self, event):
        """Handles the left mouse button being released."""
        if self._x is not None:
            self._x = None
            self.control.ReleaseMouse()

        event.Skip()

    def _mouse_move(self, event):
        """Handles the mouse being moved."""
        if self._x is not None:
            if (
                abs(self._x - event.GetX()) + abs(self._y - event.GetY())
            ) >= 3:
                self.control.ReleaseMouse()
                self._x = None
                if self._is_file:
                    FileDropSource(self.control, self.value)
                else:
                    PythonDropSource(self.control, self.value)

        event.Skip()

    # ----- Drag and drop event handlers: -------------------------------------

    def wx_dropped_on(self, x, y, data, drag_result):
        """Handles a Python object being dropped on the tree."""
        try:
            self.value = self._get_drag_data(data)
            return drag_result
        except:
            return wx.DragNone

    def wx_drag_over(self, x, y, data, drag_result):
        """Handles a Python object being dragged over the tree."""
        try:
            self.object.base_trait(self.name).validate(
                self.object, self.name, self._get_drag_data(data)
            )
            return drag_result
        except:
            return wx.DragNone


class CustomEditor(SimpleEditor):
    """Custom style of drag-and-drop editor, which is not a drag source."""

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

    #: Is the editor a drag source? This value overrides the default.
    drag_source = False


class ReadonlyEditor(SimpleEditor):
    """Read-only style of drag-and-drop editor, which is not a drop target."""

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

    #: Is the editor a drop target? This value overrides the default.
    drop_target = False


class FileDropSource(wx.DropSource):
    """Represents a draggable file."""

    def __init__(self, source, files):
        """Initializes the object."""
        self.handler = None
        self.allow_move = True

        # Put the data to be dragged on the clipboard:
        clipboard.data = files
        clipboard.source = source
        clipboard.drop_source = self

        data_object = wx.FileDataObject()
        if isinstance(files, string_type):
            files = [files]

        for file in files:
            data_object.AddFile(file)

        # Create the drop source and begin the drag and drop operation:
        super().__init__(source)
        self.SetData(data_object)
        self.result = self.DoDragDrop(True)

    def on_dropped(self, drag_result):
        """Called when the data has been dropped."""
        return
