#include "CocoMM.h"

#ifdef GUI_COCOA

#define LLOG(x)  // LOG(x)

extern NSEvent *sCurrentMouseEvent__;

namespace Upp {

NSString *PasteboardType(const String& fmt)
{
	return decode(fmt, "text", NSPasteboardTypeString, "png", NSPasteboardTypePNG,
	                   "files", NSPasteboardTypeFileURL, "url", NSPasteboardTypeURL,
	                   "rtf", NSPasteboardTypeRTF,
	                   [NSString stringWithUTF8String:~fmt]);
}

bool IsStandardPasteboardType(NSString *type)
{
	return [type isEqual:NSPasteboardTypeString] || [type isEqual:NSPasteboardTypePNG] ||
	       [type isEqual:NSPasteboardTypeFileURL] || [type isEqual:NSPasteboardTypeURL] ||
	       [type isEqual:NSPasteboardTypeRTF];
}

NSPasteboard *Pasteboard(bool dnd = false)
{
	return dnd ? [NSPasteboard pasteboardWithName:NSPasteboardNameDrag] : [NSPasteboard generalPasteboard];
}

};

@interface CocoClipboardOwner : NSObject {
	@public
	Upp::VectorMap<Upp::String, Upp::ClipData> data;
	Upp::Ptr<Upp::Ctrl> source;
	bool dnd;
}
@end

@implementation CocoClipboardOwner
-(void)pasteboard:(NSPasteboard *)sender provideDataForType:(NSString *)type
{
	RLOG(Upp::ToString(type));
	
	Upp::GuiLock __;
	auto render = [&](const Upp::String& fmt) -> Upp::String {
		int q = data.Find(fmt);
		if(q < 0)
			return Upp::Null;
		return data[q].Render();
	};
	
	NSPasteboard *pasteboard = Upp::Pasteboard(dnd);
	if(Upp::IsStandardPasteboardType(type)) {
		RLOG("Standard type - clearning contents!");
		[pasteboard clearContents];
	}

	if([type isEqual:NSPasteboardTypeString]) {
		Upp::String raw = render("text");
		if(raw.GetCount() == 0 && source)
			raw = source->GetDropData("text");
	    [pasteboard setString:[NSString stringWithUTF8String:raw]
	                forType:type];
		return;
	}
	else if([type isEqual:NSPasteboardTypeFileURL]) {
		Upp::String raw = render("files");
		Upp::Value v = ParseJSON(raw);
		if(!IsValueArray(v))
			return;
		Upp::ValueArray va = v;

		NSMutableArray* array = [NSMutableArray array];
		for(int i = 0; i < va.GetCount(); ++i) {
			NSString *path = [NSString stringWithUTF8String:~va[i]];
			[array addObject:[NSURL fileURLWithPath:path]];
		}
		[pasteboard writeObjects:array];
		return;
	}

	bool is_png = [type isEqual:NSPasteboardTypePNG];
	bool is_rtf = [type isEqual:NSPasteboardTypeRTF];
	Upp::String fmt = is_png ? "png" : is_rtf ? "rtf" : Upp::ToString(type);
	
	Upp::String raw = render(fmt);
	if(raw.GetCount() == 0 && source)
		raw = source->GetDropData(fmt);
	[pasteboard setData:[NSData dataWithBytes:~raw length:raw.GetCount()] forType:type];
}

- (void)pasteboardChangedOwner:(NSPasteboard *)sender
{
	Upp::GuiLock __;
	LLOG("pasteboardCahandedOwner");
//	data.Clear();  // TODO: This seems to trigger on declareTypes despite owner being the same...
}

@end


namespace Upp {
	
CocoClipboardOwner *ClipboardOwner(bool dnd = false)
{
	GuiLock __;
	static CocoClipboardOwner *general = [[CocoClipboardOwner alloc] init];
	static CocoClipboardOwner *drag = [[CocoClipboardOwner alloc] init];
	general->dnd = false;
	drag->dnd = true;
	return dnd ? drag : general;
}

void ClearClipboard(bool dnd)
{
	GuiLock __;
	[Pasteboard(dnd) clearContents];
	ClipboardOwner()->data.Clear();
}

void ClearClipboard()
{
	ClearClipboard(false);
}

NSMutableSet *PasteboardTypes(const Vector<String>& fmt)
{
	NSMutableSet *types = [[[NSMutableSet alloc] init] autorelease];
	for(auto id : fmt)
		[types addObject:PasteboardType(id)];
	return types;
}

void AppendClipboard(bool dnd, const char *format, const Value& value, String (*render)(const Value& data))
{
	GuiLock __;

	auto& data = ClipboardOwner(dnd)->data;

	for(String fmt : Split(format, ';'))
		data.GetAdd(fmt) = ClipData(value, render);

	AutoreleasePool ___;

	[Pasteboard(dnd) declareTypes:[PasteboardTypes(data.GetKeys()) allObjects]
	                        owner:ClipboardOwner(dnd)];
}

Index<String> drop_formats;

void Ctrl::RegisterCocoaDropFormats()
{
	AutoreleasePool ___;
	NSView *nsview = (NSView *)GetNSView();
	[nsview registerForDraggedTypes:[PasteboardTypes(drop_formats.GetKeys()) allObjects]];
}

void Ctrl::RegisterDropFormats(const char *formats)
{
	int n = drop_formats.GetCount();
	for(String fmt : Split(formats, ';'))
		drop_formats.FindAdd(fmt);
	if(drop_formats.GetCount() != n)
		for(Ctrl *q : Ctrl::GetTopCtrls())
			q->RegisterCocoaDropFormats();
}

INITBLOCK {
	Ctrl::RegisterDropFormats("text;png;image;rtf;files;url");
}

void AppendClipboard(const char *format, const Value& value, String (*render)(const Value& data))
{
	AppendClipboard(false, format, value, render);
}

void AppendClipboard(const char *format, const String& data)
{
	GuiLock __;
	AppendClipboard(format, Value(data), NULL);
}

void AppendClipboard(const char *format, const byte *data, int length)
{
	GuiLock __;
	AppendClipboard(format, String(data, length));
}

bool IsFormatAvailable(NSPasteboard *pasteboard, const char *fmt)
{
	return [pasteboard availableTypeFromArray:[NSArray arrayWithObjects:PasteboardType(fmt), nil]];
}

String ReadFormat(NSPasteboard *pasteboard, const char *fmt)
{
	if(bool is_files = String(fmt) == "files"; is_files || String(fmt) == "url") {
		JsonArray array;
		
		NSArray *urls = [pasteboard readObjectsForClasses:@[[NSURL class]] options:nil];
		for (NSURL *url : urls) {
			array << String(is_files ? [url.path UTF8String] : [url.absoluteString UTF8String]);
		}
		return ~array;
	}
	
	NSData *data = [pasteboard dataForType:PasteboardType(fmt)];
	return String((const char *)[data bytes], [data length]);
}

bool PasteClip::IsAvailable(const char *fmt) const
{
	return nspasteboard ? IsFormatAvailable((NSPasteboard *)nspasteboard, fmt)
	                    : IsClipboardAvailable(fmt);
}

String PasteClip::Get(const char *fmt) const
{
	return nspasteboard ? ReadFormat((NSPasteboard *)nspasteboard, fmt)
	                    : ReadClipboard(fmt);
}

void PasteClip::GuiPlatformConstruct()
{
	nspasteboard = NULL;
}

bool IsClipboardAvailable(const char *fmt)
{
	return IsFormatAvailable([NSPasteboard generalPasteboard], fmt);
}

String ReadClipboard(const char *fmt)
{
	return ReadFormat([NSPasteboard generalPasteboard], fmt);
}


bool IsClipboardAvailableText()
{
	return IsClipboardAvailable("text");
}

String ReadClipboardText()
{
	if(IsClipboardAvailableText())
		return ReadClipboard("text");
	return Null;
}

WString ReadClipboardUnicodeText()
{
	return ReadClipboardText().ToWString();
}

void AppendClipboardText(const String& s)
{
	AppendClipboard("text", s);
	// TODO Remove:
//	CFRef<CFStringRef> cs = CFStringCreateWithCString(NULL, (const char *)~s.ToString(), kCFStringEncodingUTF8);
  //  [[NSPasteboard generalPasteboard] setString:(NSString *)~cs forType:NSPasteboardTypeString];
}

void AppendClipboardUnicodeText(const WString& s)
{
	AppendClipboardText(s.ToString());
}

const char *ClipFmtsText()
{
	return "text";
}

String GetString(PasteClip& clip)
{
	GuiLock __;
	return clip.Get("text");
}

WString GetWString(PasteClip& clip)
{
	GuiLock __;
	return GetString(clip).ToWString();
}

bool AcceptText(PasteClip& clip)
{
	return clip.Accept(ClipFmtsText());
}

void Append(VectorMap<String, ClipData>& data, const String& text)
{
	data.GetAdd("text", ClipData(text, NULL));
}

void Append(VectorMap<String, ClipData>& data, const WString& text)
{
	data.GetAdd("text", ClipData(text.ToString(), NULL));
}

String GetTextClip(const WString& text, const String& fmt)
{
	return text.ToString();
}

String GetTextClip(const String& text, const String& fmt)
{
	return text;
}

const char *ClipFmtsImage()
{
	return "image;png";
}

bool AcceptImage(PasteClip& clip)
{
	GuiLock __;
	return clip.Accept(ClipFmtsImage());
}

Image GetImage(PasteClip& clip)
{
	GuiLock __;
	if(clip.Accept("image")) {
		Image m;
		LoadFromString(m, ~clip);
		if(!m.IsEmpty())
			return m;
	}
	if(clip.Accept("png"))
		return PNGRaster().LoadString(~clip);
	return Null;
}

Image ReadClipboardImage()
{
	GuiLock __;
	PasteClip d = Ctrl::Clipboard();
	return GetImage(d);
}

String sPng(const Value& image)
{
	if(IsNull(image))
		return Null;
	return PNGEncoder().SaveString(image);
}

String sImage(const Value& image)
{
	Image img = image;
	return StoreAsString(img);
}

String GetImageClip(const Image& img, const String& fmt)
{
	GuiLock __;
	if(img.IsEmpty()) return Null;
	if(fmt == "image")
		return sImage(img);
	if(fmt == "png")
		return sPng(img);
	return Null;
}

void Append(VectorMap<String, ClipData>& data, const Image& img)
{
	data.GetAdd("image") = ClipData(img, sImage);
}

void AppendClipboardImage(const Image& img)
{
	GuiLock __;
	if(img.IsEmpty())
		return;
	AppendClipboard("image", img, sImage);
	AppendClipboard("png", img, sPng);
}

bool AcceptFiles(PasteClip& clip)
{
	if(clip.Accept("files;url")) {
		clip.SetAction(DND_COPY);
		return true;
	}
	return false;
}

bool IsAvailableFiles(PasteClip& clip)
{
	return clip.IsAvailable("files;url");
}

Vector<String> GetFiles(PasteClip& clip)
{
	GuiLock __;
	bool is_files = clip.IsAvailable("files");
	bool is_url = clip.IsAvailable("url");
	if(!is_files && !is_url) {
		return {};
	}
	
	Value v = ParseJSON(clip.Get(is_files ? "files" : "url"));
	if(!IsValueArray(v))
		return {};
		
	ValueArray va = v;
	Vector<String> f;
	for (int i = 0; i < va.GetCount(); ++i) {
		f.Add(va[i]);
	}
	return f;
}

void AppendFiles(VectorMap<String, ClipData>& clip, const Vector<String>& files)
{
	JsonArray array;
	for(auto f : files) {
		array << f;
	}
	clip.GetAdd("files") = ~array;
}

Ctrl * Ctrl::GetDragAndDropSource()
{
	return ClipboardOwner(true)->source;
}

};

@interface DNDSource : NSObject
{
	@public
	int actions;
	int result;
}
@end

@implementation DNDSource
- (NSDragOperation)draggingSourceOperationMaskForLocal:(BOOL)isLocal
{
    return actions;
}

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

@end

namespace Upp {

bool Ctrl::local_dnd_copy;

int Ctrl::DoDragAndDrop(const char *fmts, const Image& sample, dword actions,
                        const VectorMap<String, ClipData>& data)
{
	ASSERT_(sCurrentMouseEvent__, "Drag can only start within LeftDrag!");
	if(!sCurrentMouseEvent__)
		return DND_NONE;
	NSWindow *nswindow = (NSWindow *)GetTopCtrl()->GetNSWindow();
	ASSERT_(nswindow, "Ctrl is not in open window");
	if(!nswindow)
		return DND_NONE;
	NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];

	ClearClipboard(true);
	ClipboardOwner(true)->source = this;
	for(int i = 0; i < data.GetCount(); i++) {
		RegisterDropFormats(data.GetKey(i));
		AppendClipboard(true, data.GetKey(i), data[i].data, data[i].render);
	}
	for(String fmt : Split(fmts, ';')) // GetDropData formats
		AppendClipboard(true, fmt, String(), NULL);
	
	CGImageRef cgimg = createCGImage(Nvl(sample, DrawImg::DefaultDragImage()));

	Size isz = sample.GetSize();
	NSSize size;
	double scale = 1.0 / DPI(1);
	size.width = scale * isz.cx;
	size.height = scale * isz.cy;

	NSImage *nsimg = [[[NSImage alloc] initWithCGImage:cgimg size:size] autorelease];

	static DNDSource *src = [[DNDSource alloc] init];
	
	src->actions = 0;
	if(actions & DND_COPY)
		src->actions |= NSDragOperationCopy;
	if(actions & DND_MOVE)
		src->actions |= NSDragOperationMove;
	
	NSPoint p = [sCurrentMouseEvent__ locationInWindow];
	p.y -= size.height;
	
	local_dnd_copy = false; // macos does not have ability to change action in performDragOperation

	[nswindow dragImage:nsimg
	                 at:p
	             offset:NSMakeSize(0, 0)
	              event:sCurrentMouseEvent__
	         pasteboard:Pasteboard(true)
	             source:src
	          slideBack:YES];

	ClipboardOwner(true)->source = NULL;

	[pool release];
    CGImageRelease(cgimg);
    ReleaseCapture();
    
    if(local_dnd_copy) // action was local and changed to copy in DragAndDrop
        return DND_COPY;
    
	return decode(src->result, NSDragOperationCopy, DND_COPY,
	                           NSDragOperationMove, DND_MOVE,
	                           DND_NONE);
}

void Ctrl::SetSelectionSource(const char *fmts) {}

Image MakeDragImage(const Image& arrow, Image sample);

Image MakeDragImage(const Image& arrow, const Image& arrow98, Image sample)
{
	return MakeDragImage(arrow, sample);
}

};

#endif
