///////////////////////////////////////////////////////////////////////////////
// Name:        src/osx/cocoa/dnd.mm
// Purpose:     wxDropTarget, wxDropSource implementations
// Author:      Stefan Csomor
// Created:     1998-01-01
// Copyright:   (c) 1998 Stefan Csomor
// Licence:     wxWindows licence
///////////////////////////////////////////////////////////////////////////////

#include "wx/wxprec.h"

#if wxUSE_DRAG_AND_DROP || wxUSE_CLIPBOARD

#ifndef WX_PRECOMP
#include "wx/object.h"
#endif

#include "wx/dnd.h"
#include "wx/clipbrd.h"
#include "wx/filename.h"
#include "wx/recguard.h"

#ifndef WX_PRECOMP
    #include "wx/app.h"
    #include "wx/toplevel.h"
    #include "wx/gdicmn.h"
    #include "wx/wx.h"
#endif // WX_PRECOMP

#include "wx/evtloop.h"

#include "wx/osx/private.h"
#include "wx/osx/private/datatransfer.h"

wxOSXDataSinkItem::~wxOSXDataSinkItem()
{
}

void wxOSXDataSinkItem::SetFilename(const wxString& filename)
{
    wxCFRef<CFURLRef> url(wxOSXCreateURLFromFileSystemPath(filename));
    wxCFRef<CFDataRef> data(CFURLCreateData(nullptr,url,kCFStringEncodingUTF8,true));
    DoSetData( kUTTypeFileURL, data);
}

wxOSXDataSourceItem::~wxOSXDataSourceItem()
{
}

bool wxOSXDataSource::IsSupported(const wxDataFormat &dataFormat)
{
    wxCFMutableArrayRef<CFStringRef> typesarray;
    dataFormat.AddSupportedTypesForSetting(typesarray);
    return HasData(typesarray);
}

bool wxOSXDataSource::IsSupported(const wxDataObject &dataobj)
{
    wxCFMutableArrayRef<CFStringRef> typesarray;
    dataobj.AddSupportedTypes(typesarray, wxDataObjectBase::Direction::Get);
    return HasData(typesarray);
}

class WXDLLIMPEXP_CORE wxOSXPasteboardSinkItem : public wxOSXDataSinkItem
{
public:
    wxOSXPasteboardSinkItem(NSPasteboardItem* item): m_item(item)
    {
    }

    ~wxOSXPasteboardSinkItem()
    {
        [m_item release];
    }

    virtual void SetData(const wxDataFormat& format, const void *buf, size_t datasize) override
    {
        SetData( format.GetFormatId(), buf, datasize);
    }

    virtual void SetData(wxDataFormat::NativeFormat format, const void *buf, size_t datasize) override
    {
        wxCFRef<CFDataRef> data(CFDataCreate( kCFAllocatorDefault, (const UInt8*)buf, datasize ));
        DoSetData(format, data);
    }

    virtual void DoSetData(wxDataFormat::NativeFormat format, CFDataRef data) override
    {
        [m_item setData:(NSData*) data forType:(NSString*) format];
    }

    NSPasteboardItem* GetNative() { return m_item; }
private:
    NSPasteboardItem* m_item;
};

class WXDLLIMPEXP_CORE wxOSXPasteboardSourceItem : public wxOSXDataSourceItem
{
public:
    wxOSXPasteboardSourceItem(NSPasteboardItem* item, NSPasteboard* board): m_item(item), m_pasteboard(board)
    {
    }

    virtual wxDataFormat::NativeFormat AvailableType(CFArrayRef types) const override
    {
        return (wxDataFormat::NativeFormat)[m_item availableTypeFromArray:(NSArray*)types];
    }

    virtual bool GetData( const wxDataFormat& dataFormat, wxMemoryBuffer& target) override
    {
        return GetData(dataFormat.GetFormatId(), target);
    }


    virtual bool GetData( wxDataFormat::NativeFormat type, wxMemoryBuffer& target) override
    {
        bool success = false;

        target.Clear();

        CFDataRef flavorData = DoGetData( type );

        if ( flavorData )
        {
            CFIndex flavorDataSize = CFDataGetLength(flavorData);
            size_t allocatedSize = flavorDataSize+4;
            void * buf = target.GetWriteBuf(allocatedSize);
            if ( buf )
            {
                memset(buf, 0, allocatedSize);
                memcpy(buf, CFDataGetBytePtr(flavorData), flavorDataSize);
                target.UngetWriteBuf(flavorDataSize);
                success = true;
            }
        }
        return success;
    }

    virtual CFDataRef DoGetData(wxDataFormat::NativeFormat type) const override
    {
        // before a file promise can be resolved, we must pass a paste location
        if ( UTTypeConformsTo((CFStringRef)type, kPasteboardTypeFileURLPromise ) )
        {
            wxString tempdir = wxFileName::GetTempDir() + wxFILE_SEP_PATH + "wxtemp.XXXXXX";
            char* result = mkdtemp(const_cast<char*>(tempdir.fn_str().data()));

            wxCFRef<CFURLRef> dest(CFURLCreateFromFileSystemRepresentation(nullptr, (const UInt8*)result, strlen(result), true));
            PasteboardRef pboardRef = nullptr;
            PasteboardCreate((CFStringRef)[m_pasteboard name], &pboardRef);
            if (pboardRef != nullptr) {
                PasteboardSynchronize(pboardRef);
                PasteboardSetPasteLocation(pboardRef, (CFURLRef)dest);
                CFRelease(pboardRef);
            }
        }

        return (CFDataRef) [m_item dataForType:(NSString*) type];
    }

    NSPasteboardItem* GetNative() { return m_item; }
private:
    NSPasteboardItem* m_item;
    NSPasteboard* m_pasteboard;
};

wxOSXPasteboard::wxOSXPasteboard(OSXPasteboard native)
{
    m_pasteboard = native;
}

wxOSXPasteboard::~wxOSXPasteboard()
{
    DeleteSinkItems();
}

void wxOSXPasteboard::DeleteSinkItems()
{
    for ( wxVector<wxOSXDataSinkItem*>::iterator it = m_sinkItems.begin();
     it != m_sinkItems.end();
     ++it)
    {
        delete (*it);
    }
    m_sinkItems.clear();
}

// data sink methods

void wxOSXPasteboard::Clear()
{
    [m_pasteboard clearContents];
    DeleteSinkItems();
}

void wxOSXPasteboard::Flush()
{
    NSMutableArray* nsarray = [[NSMutableArray alloc] init];
    for ( wxVector<wxOSXDataSinkItem*>::iterator it = m_sinkItems.begin();
         it != m_sinkItems.end();
         ++it)
    {
        wxOSXPasteboardSinkItem* item = dynamic_cast<wxOSXPasteboardSinkItem*>(*it);
        [nsarray addObject:item->GetNative()];
        delete item;
    }
    m_sinkItems.clear();
    [m_pasteboard writeObjects:nsarray];
    [nsarray release];
}

wxOSXDataSinkItem* wxOSXPasteboard::CreateItem()
{
    NSPasteboardItem* nsitem = [[NSPasteboardItem alloc] init];
    wxOSXPasteboardSinkItem* item = new wxOSXPasteboardSinkItem(nsitem);
    m_sinkItems.push_back(item);

    return item;
}

// data source methods

bool wxOSXPasteboard::HasData(CFArrayRef types) const
{
    return [m_pasteboard canReadItemWithDataConformingToTypes:(NSArray*) types];
}

const wxOSXDataSourceItem* wxOSXPasteboard::GetItem(size_t pos) const
{
    return new wxOSXPasteboardSourceItem([[m_pasteboard pasteboardItems] objectAtIndex: pos], m_pasteboard);
}

// data source methods

wxOSXPasteboard* wxOSXPasteboard::GetGeneralClipboard()
{
    static wxOSXPasteboard clipboard( [NSPasteboard pasteboardWithName:NSGeneralPboard]);
    return &clipboard;
}

size_t wxOSXPasteboard::GetItemCount() const
{
    return [[m_pasteboard pasteboardItems] count];
}

#if wxUSE_DRAG_AND_DROP

static wxDropSource* gCurrentSource = nullptr;

wxDragResult NSDragOperationToWxDragResult(NSDragOperation code)
{
    switch (code)
    {
        case NSDragOperationGeneric:
            return wxDragCopy;
        case NSDragOperationCopy:
            return wxDragCopy;
        case NSDragOperationMove:
            return wxDragMove;
        case NSDragOperationLink:
            return wxDragLink;
        case NSDragOperationNone:
            return wxDragNone;
        case NSDragOperationDelete:
            return wxDragNone;
        default:
            wxFAIL_MSG("Unexpected result code");
    }
    return wxDragNone;
}

@interface DropSourceDelegate : NSObject<NSDraggingSource>
{
    BOOL dragFinished;
    int resultCode;
    wxDropSource* impl;

    // Flags for drag and drop operations (wxDrag_* ).
    int m_dragFlags;
}

- (void)setImplementation:(wxDropSource *)dropSource flags:(int)flags;
- (BOOL)finished;
- (NSDragOperation)code;
- (NSDragOperation)draggingSession:(nonnull NSDraggingSession *)session sourceOperationMaskForDraggingContext:(NSDraggingContext)context;
- (void)draggedImage:(NSImage *)anImage movedTo:(NSPoint)aPoint;
- (void)draggedImage:(NSImage *)anImage endedAt:(NSPoint)aPoint operation:(NSDragOperation)operation;
@end

@implementation DropSourceDelegate

- (id)init
{
    if ( self = [super init] )
    {
        dragFinished = NO;
        resultCode = NSDragOperationNone;
        impl = nullptr;
        m_dragFlags = wxDrag_CopyOnly;
    }
    return self;
}

- (void)setImplementation:(wxDropSource *)dropSource flags:(int)flags
{
    impl = dropSource;
    m_dragFlags = flags;
}

- (BOOL)finished
{
    return dragFinished;
}

- (NSDragOperation)code
{
    return resultCode;
}

- (NSDragOperation)draggingSession:(nonnull NSDraggingSession *)session sourceOperationMaskForDraggingContext:(NSDraggingContext)context
{
    wxUnusedVar(session);
    wxUnusedVar(context);

    NSDragOperation allowedDragOperations = NSDragOperationEvery;

    // NSDragOperationGeneric also makes a drag to the trash possible
    // resulting in something we don't support (NSDragOperationDelete)

    allowedDragOperations &= ~(NSDragOperationDelete | NSDragOperationGeneric);

    if (m_dragFlags == wxDrag_CopyOnly)
    {
        allowedDragOperations &= ~NSDragOperationMove;
    }

    // we might adapt flags here in the future
    // context can be NSDraggingContextOutsideApplication or NSDraggingContextWithinApplication

    return allowedDragOperations;
}

- (void)draggedImage:(NSImage *)anImage movedTo:(NSPoint)aPoint
{
    wxUnusedVar( anImage );
    wxUnusedVar( aPoint );

    bool optionDown = GetCurrentKeyModifiers() & optionKey;
    wxDragResult result = optionDown ? wxDragCopy : wxDragMove;

    if (wxDropSource* source = impl)
    {
        if (!source->GiveFeedback(result))
        {
            wxStockCursor cursorID = wxCURSOR_NONE;

            switch (result)
            {
                case wxDragCopy:
                    cursorID = wxCURSOR_COPY_ARROW;
                    break;

                case wxDragMove:
                    cursorID = wxCURSOR_ARROW;
                    break;

                case wxDragNone:
                    cursorID = wxCURSOR_NO_ENTRY;
                    break;

                case wxDragError:
                case wxDragLink:
                case wxDragCancel:
                default:
                    // put these here to make gcc happy
                    ;
            }

            if (cursorID != wxCURSOR_NONE)
            {
                // TODO under 10.6 the os itself deals with the cursor, remove if things
                // work properly everywhere
#if 0
                wxCursor cursor( cursorID );
                cursor.MacInstall();
#endif
            }
        }
    }
}

- (void)draggedImage:(NSImage *)anImage endedAt:(NSPoint)aPoint operation:(NSDragOperation)operation
{
    wxUnusedVar( anImage );
    wxUnusedVar( aPoint );

    resultCode = operation;
    dragFinished = YES;
}

@end

wxDropTarget::wxDropTarget( wxDataObject *data )
            : wxDropTargetBase( data )
{

}

//-------------------------------------------------------------------------
// wxDropSource
//-------------------------------------------------------------------------

wxDropSource::wxDropSource(wxWindow *win,
                           const wxCursorBundle& cursorCopy,
                           const wxCursorBundle& cursorMove,
                           const wxCursorBundle& cursorStop)
            : wxDropSourceBase(cursorCopy, cursorMove, cursorStop)
{
    m_window = win;
}

wxDropSource::wxDropSource(wxDataObject& data,
                           wxWindow *win,
                           const wxCursorBundle& cursorCopy,
                           const wxCursorBundle& cursorMove,
                           const wxCursorBundle& cursorStop)
            : wxDropSource(win, cursorCopy, cursorMove, cursorStop)
{
    SetData( data );
}

wxDropSource* wxDropSource::GetCurrentDropSource()
{
    return gCurrentSource;
}

#if __MAC_OS_X_VERSION_MAX_ALLOWED < MAC_OS_X_VERSION_10_13
typedef NSString* NSPasteboardType;
#endif

@interface wxPasteBoardWriter : NSObject<NSPasteboardWriting>
{
    wxDataObject* m_data;
}

- (id) initWithDataObject:(wxDataObject*) obj;
@end

@implementation wxPasteBoardWriter

- (id) initWithDataObject:(wxDataObject*) obj
{
    m_data = obj;
    return self;
}

- (void) clearDataObject
{
    m_data = nullptr;
}
- (nullable id)pasteboardPropertyListForType:(nonnull NSPasteboardType)type
{
    if ( m_data )
    {
        wxDataFormat format((wxDataFormat::NativeFormat) type);
        size_t size = m_data->GetDataSize(format);
        CFMutableDataRef data = CFDataCreateMutable(kCFAllocatorDefault,size );
        m_data->GetDataHere(format, CFDataGetMutableBytePtr(data));
        CFDataSetLength(data, size);
        return (id) data;
    }
    return nil;
}

- (nonnull NSArray<NSPasteboardType> *)writableTypesForPasteboard:(nonnull NSPasteboard *)pasteboard
{
    wxUnusedVar(pasteboard);
    wxCFMutableArrayRef<CFStringRef> typesarray;
    if ( m_data )
        m_data->AddSupportedTypes(typesarray, wxDataObjectBase::Direction::Get);
    return (NSArray<NSPasteboardType>*) typesarray.autorelease();
}

@end

wxDragResult wxDropSource::DoDragDrop(int flags)
{
    static wxRecursionGuardFlag s_inDragDrop = 0;

    wxASSERT_MSG( m_data, wxT("Drop source: no data") );

    wxDragResult result = wxDragNone;
    if ((m_data == nullptr) || (m_data->GetFormatCount() == 0))
        return result;

    wxRecursionGuard guard(s_inDragDrop);
    if (guard.IsInside())
        return wxDragNone;

    NSView* view = m_window->GetPeer()->GetWXWidget();
    if (view)
    {
        NSEvent* theEvent = (NSEvent*)wxTheApp->MacGetCurrentEvent();

        // Drag and drop operation can be started from an external mouse event
        // handler, this happens at least when using CEF, so synthesize the
        // mouse event if we don't have one.
        if (theEvent == nil)
        {
            NSPoint mouse_location = [NSEvent mouseLocation];
            theEvent = [NSEvent mouseEventWithType:NSEventTypeLeftMouseDragged 
                location:mouse_location
                modifierFlags:0
                timestamp: 0
                windowNumber: [NSWindow windowNumberAtPoint:mouse_location belowWindowWithWindowNumber:0]
                context:nil
                eventNumber: 0
                clickCount: 0
                pressure: 1.0];
        }

        gCurrentSource = this;

        DropSourceDelegate* delegate = [[DropSourceDelegate alloc] init];
        [delegate setImplementation:this flags:flags];

        // add a dummy square as dragged image for the moment,
        // TODO: proper drag image for data
        NSSize sz = NSMakeSize(16,16);
        NSRect fillRect = NSMakeRect(0, 0, 16, 16);
        NSImage* image = [[NSImage alloc] initWithSize: sz];

        [image lockFocus];

        [[[NSColor whiteColor] colorWithAlphaComponent:0.8] set];
        NSRectFill(fillRect);
        [[NSColor blackColor] set];
        NSFrameRectWithWidthUsingOperation(fillRect, 1, NSCompositeDestinationOver);

        [image unlockFocus];

        NSPoint down = [theEvent locationInWindow];
        NSPoint p = [view convertPoint:down fromView:nil];

        wxPasteBoardWriter* writer = [[wxPasteBoardWriter alloc] initWithDataObject:m_data];
        wxCFMutableArrayRef<NSDraggingItem*> items;
        NSDraggingItem* item = [[NSDraggingItem alloc] initWithPasteboardWriter:writer];
        [item setDraggingFrame:NSMakeRect(p.x, p.y, 16, 16) contents:image];
        items.push_back(item);
        [view beginDraggingSessionWithItems:items event:theEvent source:delegate];

        wxEventLoopBase * const loop = wxEventLoop::GetActive();
        while ( ![delegate finished] )
            loop->Dispatch();

        result = NSDragOperationToWxDragResult([delegate code]);
        [delegate release];
        [image release];
        [writer clearDataObject];
        [writer release];

        wxWindow* mouseUpTarget = wxWindow::GetCapture();

        if ( mouseUpTarget == nullptr )
        {
            mouseUpTarget = m_window;
        }

        if ( mouseUpTarget != nullptr )
        {
            const auto wxpeer = (wxWidgetCocoaImpl*)mouseUpTarget->GetPeer();
            for ( auto wxevent : wxpeer->TranslateMouseEvent(theEvent) )
            {
                if ( wxevent.GetEventType() == wxEVT_LEFT_DOWN )
                {
                    wxevent.SetEventType(wxEVT_LEFT_UP);

                    mouseUpTarget->HandleWindowEvent(wxevent);
                    break;
                }
            }
        }

        gCurrentSource = nullptr;
    }

    return result;
}

#endif // wxUSE_DRAG_AND_DROP
#endif // wxUSE_DRAG_AND_DROP || wxUSE_CLIPBOARD
