/*
  ==============================================================================

   This file is part of the JUCE library.
   Copyright (c) 2020 - Raw Material Software Limited

   JUCE is an open source library subject to commercial or open-source
   licensing.

   By using JUCE, you agree to the terms of both the JUCE 6 End-User License
   Agreement and JUCE Privacy Policy (both effective as of the 16th June 2020).

   End User License Agreement: www.juce.com/juce-6-licence
   Privacy Policy: www.juce.com/juce-privacy-policy

   Or: You may also use this code under the terms of the GPL v3 (see
   www.gnu.org/licenses).

   JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
   EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
   DISCLAIMED.

  ==============================================================================
*/

namespace juce
{

extern void* createDraggingHandCursor();
extern ComponentPeer* getPeerFor (::Window);

//==============================================================================
class X11DragState
{
public:
    X11DragState() = default;

    //==============================================================================
    bool isDragging() const noexcept
    {
        return dragging;
    }

    //==============================================================================
    void handleExternalSelectionClear()
    {
        if (dragging)
            externalResetDragAndDrop();
    }

    void handleExternalSelectionRequest (const XEvent& evt)
    {
        auto targetType = evt.xselectionrequest.target;

        XEvent s;
        s.xselection.type      = SelectionNotify;
        s.xselection.requestor = evt.xselectionrequest.requestor;
        s.xselection.selection = evt.xselectionrequest.selection;
        s.xselection.target    = targetType;
        s.xselection.property  = None;
        s.xselection.time      = evt.xselectionrequest.time;

        auto* display = getDisplay();

        if (allowedTypes.contains (targetType))
        {
            s.xselection.property = evt.xselectionrequest.property;

            X11Symbols::getInstance()->xChangeProperty (display, evt.xselectionrequest.requestor, evt.xselectionrequest.property,
                                                        targetType, 8, PropModeReplace,
                                                        reinterpret_cast<const unsigned char*> (textOrFiles.toRawUTF8()),
                                                        (int) textOrFiles.getNumBytesAsUTF8());
        }

        X11Symbols::getInstance()->xSendEvent (display, evt.xselectionrequest.requestor, True, 0, &s);
    }

    void handleExternalDragAndDropStatus (const XClientMessageEvent& clientMsg)
    {
        if (expectingStatus)
        {
            expectingStatus = false;
            canDrop         = false;
            silentRect      = {};

            const auto& atoms = getAtoms();

            if ((clientMsg.data.l[1] & 1) != 0
                && ((Atom) clientMsg.data.l[4] == atoms.XdndActionCopy
                    || (Atom) clientMsg.data.l[4] == atoms.XdndActionPrivate))
            {
                if ((clientMsg.data.l[1] & 2) == 0) // target requests silent rectangle
                    silentRect.setBounds ((int) clientMsg.data.l[2] >> 16, (int) clientMsg.data.l[2] & 0xffff,
                                          (int) clientMsg.data.l[3] >> 16, (int) clientMsg.data.l[3] & 0xffff);

                canDrop = true;
            }
        }
    }

    void handleExternalDragButtonReleaseEvent()
    {
        if (dragging)
            X11Symbols::getInstance()->xUngrabPointer (getDisplay(), CurrentTime);

        if (canDrop)
        {
            sendExternalDragAndDropDrop();
        }
        else
        {
            sendExternalDragAndDropLeave();
            externalResetDragAndDrop();
        }
    }

    void handleExternalDragMotionNotify()
    {
        auto* display = getDisplay();

        auto newTargetWindow = externalFindDragTargetWindow (X11Symbols::getInstance()
                                                               ->xRootWindow (display,
                                                                              X11Symbols::getInstance()->xDefaultScreen (display)));

        if (targetWindow != newTargetWindow)
        {
            if (targetWindow != None)
                sendExternalDragAndDropLeave();

            canDrop = false;
            silentRect = {};

            if (newTargetWindow == None)
                return;

            xdndVersion = getDnDVersionForWindow (newTargetWindow);

            if (xdndVersion == -1)
                return;

            targetWindow = newTargetWindow;
            sendExternalDragAndDropEnter();
        }

        if (! expectingStatus)
            sendExternalDragAndDropPosition();
    }

    void handleDragAndDropPosition (const XClientMessageEvent& clientMsg, ComponentPeer* peer)
    {
        if (dragAndDropSourceWindow == 0)
            return;

        dragAndDropSourceWindow = (::Window) clientMsg.data.l[0];

        if (windowH == 0)
            windowH = (::Window) peer->getNativeHandle();

        auto dropPos = Desktop::getInstance().getDisplays().physicalToLogical (Point<int> ((int) clientMsg.data.l[2] >> 16,
                                                                                           (int) clientMsg.data.l[2] & 0xffff));
        dropPos -= peer->getBounds().getPosition();

        const auto& atoms = getAtoms();

        auto targetAction = atoms.XdndActionCopy;

        for (int i = numElementsInArray (atoms.allowedActions); --i >= 0;)
        {
            if ((Atom) clientMsg.data.l[4] == atoms.allowedActions[i])
            {
                targetAction = atoms.allowedActions[i];
                break;
            }
        }

        sendDragAndDropStatus (true, targetAction);

        if (dragInfo.position != dropPos)
        {
            dragInfo.position = dropPos;

            if (dragInfo.isEmpty())
                updateDraggedFileList (clientMsg, (::Window) peer->getNativeHandle());

            if (! dragInfo.isEmpty())
                peer->handleDragMove (dragInfo);
        }
    }

    void handleDragAndDropDrop (const XClientMessageEvent& clientMsg, ComponentPeer* peer)
    {
        if (dragInfo.isEmpty())
        {
            // no data, transaction finished in handleDragAndDropSelection()
            finishAfterDropDataReceived = true;
            updateDraggedFileList (clientMsg, (::Window) peer->getNativeHandle());
        }
        else
        {
            handleDragAndDropDataReceived();  // data was already received
        }
    }

    void handleDragAndDropEnter (const XClientMessageEvent& clientMsg, ComponentPeer* peer)
    {
        dragInfo.clear();
        srcMimeTypeAtomList.clear();

        dragAndDropCurrentMimeType = 0;
        auto dndCurrentVersion = (static_cast<unsigned long> (clientMsg.data.l[1]) & 0xff000000) >> 24;

        if (dndCurrentVersion < 3 || dndCurrentVersion > XWindowSystemUtilities::Atoms::DndVersion)
        {
            dragAndDropSourceWindow = 0;
            return;
        }

        const auto& atoms = getAtoms();

        dragAndDropSourceWindow = (::Window) clientMsg.data.l[0];

        if ((clientMsg.data.l[1] & 1) != 0)
        {
            XWindowSystemUtilities::ScopedXLock xLock;
            XWindowSystemUtilities::GetXProperty prop (dragAndDropSourceWindow, atoms.XdndTypeList, 0, 0x8000000L, false, XA_ATOM);

            if (prop.success && prop.actualType == XA_ATOM && prop.actualFormat == 32 && prop.numItems != 0)
            {
                auto* types = prop.data;

                for (unsigned long i = 0; i < prop.numItems; ++i)
                {
                    unsigned long type;
                    memcpy (&type, types, sizeof (unsigned long));

                    if (type != None)
                        srcMimeTypeAtomList.add (type);

                    types += sizeof (unsigned long);
                }
            }
        }

        if (srcMimeTypeAtomList.isEmpty())
        {
            for (int i = 2; i < 5; ++i)
                if (clientMsg.data.l[i] != None)
                    srcMimeTypeAtomList.add ((unsigned long) clientMsg.data.l[i]);

            if (srcMimeTypeAtomList.isEmpty())
            {
                dragAndDropSourceWindow = 0;
                return;
            }
        }

        for (int i = 0; i < srcMimeTypeAtomList.size() && dragAndDropCurrentMimeType == 0; ++i)
            for (int j = 0; j < numElementsInArray (atoms.allowedMimeTypes); ++j)
                if (srcMimeTypeAtomList[i] == atoms.allowedMimeTypes[j])
                    dragAndDropCurrentMimeType = atoms.allowedMimeTypes[j];

        handleDragAndDropPosition (clientMsg, peer);
    }

    void handleDragAndDropExit()
    {
        if (auto* peer = getPeerFor (windowH))
            peer->handleDragExit (dragInfo);
    }

    void handleDragAndDropSelection (const XEvent& evt)
    {
        dragInfo.clear();

        if (evt.xselection.property != None)
        {
            StringArray lines;

            {
                MemoryBlock dropData;

                for (;;)
                {
                    XWindowSystemUtilities::GetXProperty prop (evt.xany.window, evt.xselection.property,
                                       (long) (dropData.getSize() / 4), 65536, false, AnyPropertyType);

                    if (! prop.success)
                        break;

                    dropData.append (prop.data, (size_t) (prop.actualFormat / 8) * prop.numItems);

                    if (prop.bytesLeft <= 0)
                        break;
                }

                lines.addLines (dropData.toString());
            }

            if (XWindowSystemUtilities::Atoms::isMimeTypeFile (getDisplay(), dragAndDropCurrentMimeType))
            {
                for (const auto& line : lines)
                {
                    const auto escaped = line.replace ("+", "%2B").replace ("file://", String(), true);
                    dragInfo.files.add (URL::removeEscapeChars (escaped));
                }

                dragInfo.files.trim();
                dragInfo.files.removeEmptyStrings();
            }
            else
            {
                dragInfo.text = lines.joinIntoString ("\n");
            }

            if (finishAfterDropDataReceived)
                handleDragAndDropDataReceived();
        }
    }

    void externalResetDragAndDrop()
    {
        if (dragging)
        {
            XWindowSystemUtilities::ScopedXLock xLock;
            X11Symbols::getInstance()->xUngrabPointer (getDisplay(), CurrentTime);
        }

        if (completionCallback != nullptr)
            completionCallback();
    }

    bool externalDragInit (::Window window, bool text, const String& str, std::function<void()>&& cb)
    {
        windowH            = window;
        isText             = text;
        textOrFiles        = str;
        targetWindow       = windowH;
        completionCallback = std::move (cb);

        auto* display = getDisplay();

        allowedTypes.add (XWindowSystemUtilities::Atoms::getCreating (display, isText ? "text/plain" : "text/uri-list"));

        auto pointerGrabMask = (unsigned int) (Button1MotionMask | ButtonReleaseMask);

        XWindowSystemUtilities::ScopedXLock xLock;

        if (X11Symbols::getInstance()->xGrabPointer (display, windowH, True, pointerGrabMask,
                                                     GrabModeAsync, GrabModeAsync, None, None, CurrentTime) == GrabSuccess)
        {
            const auto& atoms = getAtoms();

            // No other method of changing the pointer seems to work, this call is needed from this very context
            X11Symbols::getInstance()->xChangeActivePointerGrab (display, pointerGrabMask, (Cursor) createDraggingHandCursor(), CurrentTime);

            X11Symbols::getInstance()->xSetSelectionOwner (display, atoms.XdndSelection, windowH, CurrentTime);

            // save the available types to XdndTypeList
            X11Symbols::getInstance()->xChangeProperty (display, windowH, atoms.XdndTypeList, XA_ATOM, 32, PropModeReplace,
                                                        reinterpret_cast<const unsigned char*> (allowedTypes.getRawDataPointer()), allowedTypes.size());

            dragging = true;
            xdndVersion = getDnDVersionForWindow (targetWindow);

            sendExternalDragAndDropEnter();
            handleExternalDragMotionNotify();

            return true;
        }

        return false;
    }

private:
    //==============================================================================
    const XWindowSystemUtilities::Atoms& getAtoms() const noexcept  { return XWindowSystem::getInstance()->getAtoms(); }
    ::Display* getDisplay() const noexcept                          { return XWindowSystem::getInstance()->getDisplay(); }

    //==============================================================================
    void sendDragAndDropMessage (XClientMessageEvent& msg)
    {
        auto* display = getDisplay();

        msg.type      = ClientMessage;
        msg.display   = display;
        msg.window    = dragAndDropSourceWindow;
        msg.format    = 32;
        msg.data.l[0] = (long) windowH;

        XWindowSystemUtilities::ScopedXLock xLock;
        X11Symbols::getInstance()->xSendEvent (display, dragAndDropSourceWindow, False, 0, (XEvent*) &msg);
    }

    bool sendExternalDragAndDropMessage (XClientMessageEvent& msg)
    {
        auto* display = getDisplay();

        msg.type      = ClientMessage;
        msg.display   = display;
        msg.window    = targetWindow;
        msg.format    = 32;
        msg.data.l[0] = (long) windowH;

        XWindowSystemUtilities::ScopedXLock xLock;
        return X11Symbols::getInstance()->xSendEvent (display, targetWindow, False, 0, (XEvent*) &msg) != 0;
    }

    void sendExternalDragAndDropDrop()
    {
        XClientMessageEvent msg;
        zerostruct (msg);

        msg.message_type = getAtoms().XdndDrop;
        msg.data.l[2] = CurrentTime;

        sendExternalDragAndDropMessage (msg);
    }

    void sendExternalDragAndDropEnter()
    {
        XClientMessageEvent msg;
        zerostruct (msg);

        msg.message_type = getAtoms().XdndEnter;
        msg.data.l[1] = (xdndVersion << 24);

        for (int i = 0; i < 3; ++i)
            msg.data.l[i + 2] = (long) allowedTypes[i];

        sendExternalDragAndDropMessage (msg);
    }

    void sendExternalDragAndDropPosition()
    {
        XClientMessageEvent msg;
        zerostruct (msg);

        const auto& atoms = getAtoms();

        msg.message_type = atoms.XdndPosition;

        auto mousePos = Desktop::getInstance().getMousePosition();

        if (silentRect.contains (mousePos)) // we've been asked to keep silent
            return;

        mousePos = Desktop::getInstance().getDisplays().logicalToPhysical (mousePos);

        msg.data.l[1] = 0;
        msg.data.l[2] = (mousePos.x << 16) | mousePos.y;
        msg.data.l[3] = CurrentTime;
        msg.data.l[4] = (long) atoms.XdndActionCopy; // this is all JUCE currently supports

        expectingStatus = sendExternalDragAndDropMessage (msg);
    }

    void sendDragAndDropStatus (bool acceptDrop, Atom dropAction)
    {
        XClientMessageEvent msg;
        zerostruct (msg);

        msg.message_type = getAtoms().XdndStatus;
        msg.data.l[1]    = (acceptDrop ? 1 : 0) | 2; // 2 indicates that we want to receive position messages
        msg.data.l[4]    = (long) dropAction;

        sendDragAndDropMessage (msg);
    }

    void sendExternalDragAndDropLeave()
    {
        XClientMessageEvent msg;
        zerostruct (msg);

        msg.message_type = getAtoms().XdndLeave;
        sendExternalDragAndDropMessage (msg);
    }

    void sendDragAndDropFinish()
    {
        XClientMessageEvent msg;
        zerostruct (msg);

        msg.message_type = getAtoms().XdndFinished;
        sendDragAndDropMessage (msg);
    }

    void updateDraggedFileList (const XClientMessageEvent& clientMsg, ::Window requestor)
    {
        jassert (dragInfo.isEmpty());

        if (dragAndDropSourceWindow != None && dragAndDropCurrentMimeType != None)
        {
            auto* display = getDisplay();

            XWindowSystemUtilities::ScopedXLock xLock;
            X11Symbols::getInstance()->xConvertSelection (display, getAtoms().XdndSelection, dragAndDropCurrentMimeType,
                                                          XWindowSystemUtilities::Atoms::getCreating (display, "JXSelectionWindowProperty"),
                                                          requestor, (::Time) clientMsg.data.l[2]);
        }
    }

    bool isWindowDnDAware (::Window w) const
    {
        int numProperties = 0;
        auto* properties = X11Symbols::getInstance()->xListProperties (getDisplay(), w, &numProperties);

        bool dndAwarePropFound = false;

        for (int i = 0; i < numProperties; ++i)
            if (properties[i] == getAtoms().XdndAware)
                dndAwarePropFound = true;

        if (properties != nullptr)
            X11Symbols::getInstance()->xFree (properties);

        return dndAwarePropFound;
    }

    int getDnDVersionForWindow (::Window target)
    {
        XWindowSystemUtilities::GetXProperty prop (target, getAtoms().XdndAware, 0, 2, false, AnyPropertyType);

        if (prop.success && prop.data != None && prop.actualFormat == 32 && prop.numItems == 1)
            return jmin ((int) prop.data[0], (int) XWindowSystemUtilities::Atoms::DndVersion);

        return -1;
    }

    ::Window externalFindDragTargetWindow (::Window target)
    {
        if (target == None)
            return None;

        if (isWindowDnDAware (target))
            return target;

        ::Window child, phonyWin;
        int phony;
        unsigned int uphony;

        X11Symbols::getInstance()->xQueryPointer (getDisplay(), target, &phonyWin, &child, &phony, &phony, &phony, &phony, &uphony);

        return externalFindDragTargetWindow (child);
    }

    void handleDragAndDropDataReceived()
    {
        ComponentPeer::DragInfo dragInfoCopy (dragInfo);

        sendDragAndDropFinish();

        if (! dragInfoCopy.isEmpty())
            if (auto* peer = getPeerFor (windowH))
                peer->handleDragDrop (dragInfoCopy);
    }

    //==============================================================================
    ::Window windowH = 0, targetWindow = 0, dragAndDropSourceWindow = 0;

    int xdndVersion = -1;
    bool isText = false, dragging = false, expectingStatus = false, canDrop = false, finishAfterDropDataReceived = false;

    Atom dragAndDropCurrentMimeType;
    Array<Atom> allowedTypes, srcMimeTypeAtomList;

    ComponentPeer::DragInfo dragInfo;
    Rectangle<int> silentRect;
    String textOrFiles;

    std::function<void()> completionCallback = nullptr;

    //==============================================================================
    JUCE_LEAK_DETECTOR (X11DragState)
};

} // namespace juce
