/*
 * Copyright (c) 2016-2018  Moddable Tech, Inc.
 *
 *   This file is part of the Moddable SDK Runtime.
 * 
 *   The Moddable SDK Runtime is free software: you can redistribute it and/or modify
 *   it under the terms of the GNU Lesser General Public License as published by
 *   the Free Software Foundation, either version 3 of the License, or
 *   (at your option) any later version.
 * 
 *   The Moddable SDK Runtime 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 Lesser General Public License for more details.
 * 
 *   You should have received a copy of the GNU Lesser General Public License
 *   along with the Moddable SDK Runtime.  If not, see <http://www.gnu.org/licenses/>.
 *
 */

#include "piuPC.h"
#include <dlfcn.h>
#include "screen.h"

typedef struct PiuScreenStruct PiuScreenRecord, *PiuScreen;
typedef struct PiuScreenMessageStruct  PiuScreenMessageRecord, *PiuScreenMessage;

@interface TouchFinger : NSObject {
	id identity;
	NSPoint point;
}
@property (retain) id identity;
@property (assign) NSPoint point;
@end

@implementation TouchFinger
@synthesize identity;
@synthesize point;
- (void)dealloc {
    [identity release];
    [super dealloc];
}
@end

@interface NSPiuScreenView : NSView {
	PiuScreen* piuScreen;
	void* library;
	txScreen* screen;
    NSTimeInterval time;
	NSTimer *timer;
    NSImage *touchImage;
	id *touches;
	BOOL touching;
}
@property (assign) PiuScreen* piuScreen;
@property (assign) void* library;
@property (assign) txScreen *screen;
@property (assign) NSTimeInterval time;
@property (assign) NSTimer *timer;
@property (retain) NSImage *touchImage;
@property (assign) id *touches;
@property (assign) BOOL touching;
- (void)launchMachine:(NSString*)string;
- (void)quitMachine;
@end

static void fxScreenAbort(txScreen* screen);
static void fxScreenBufferChanged(txScreen* screen);
static void fxScreenFormatChanged(txScreen* screen);
static void fxScreenPost(txScreen* screen, char* message, int size);
static void fxScreenStart(txScreen* screen, double interval);
static void fxScreenStop(txScreen* screen);

struct PiuScreenStruct {
	PiuHandlePart;
	PiuIdlePart;
	PiuBehaviorPart;
	PiuContentPart;
    NSPiuClipView *nsClipView;
    NSPiuScreenView *nsScreenView;
    txScreen* screen;
	PiuRectangleRecord hole;
};

struct PiuScreenMessageStruct {
	void* buffer;
	int size;
};

@implementation NSPiuScreenView
@synthesize piuScreen;
@synthesize library;
@synthesize screen;
@synthesize time;
@synthesize timer;
@synthesize touchImage;
@synthesize touches;
@synthesize touching;
- (void)dealloc {
	if (library)
    	dlclose(library);
    [touchImage release];
    [super dealloc];
}
- (void)drawRect:(NSRect)rect {
	CGRect bounds = NSRectToCGRect(self.bounds);
	CGContextRef context = (CGContextRef)[[NSGraphicsContext currentContext] CGContext];
    CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB();
	CGDataProviderRef provider = CGDataProviderCreateWithData(nil, screen->buffer, screen->width * screen->height * screenBytesPerPixel, nil);
    CGImageRef image = CGImageCreate(screen->width, screen->height, 8, 32, screen->width * screenBytesPerPixel, colorSpace, kCGBitmapByteOrder32Big | kCGImageAlphaNoneSkipLast, provider, nil, NO, kCGRenderingIntentDefault);
	if (PiuRectangleIsEmpty(&(*piuScreen)->hole)) {
		CGContextDrawImage(context, bounds, image);
	}
	else {
        PiuRectangleRecord s, r;
        PiuRectangleSet(&s, 0, 0, screen->width, screen->height);
        if (PiuRectangleIntersect(&r, &s, &(*piuScreen)->hole)) {
        	if (!PiuRectangleIsEqual(&r, &s)) {
// 				r.y = s.height - (r.y + r.height);
				if (r.x > 0) {
					CGRect clip = CGRectMake(0, 0, r.x, s.height);
					CGImageRef part = CGImageCreateWithImageInRect(image, clip);
					CGContextDrawImage(context, CGRectOffset(clip, bounds.origin.x, bounds.origin.y), part);
					CGImageRelease(part);
				}
				if (r.y > 0) {
					CGRect clip = CGRectMake(r.x, 0, r.width, r.y);
					CGImageRef part = CGImageCreateWithImageInRect(image, clip);
					CGContextDrawImage(context, CGRectOffset(clip, bounds.origin.x, bounds.origin.y + bounds.size.height - r.y), part);
					CGImageRelease(part);
				}
				if (r.y + r.height < s.height) {
					CGRect clip = CGRectMake(r.x, r.y + r.height, r.width, s.height - (r.y + r.height));
					CGImageRef part = CGImageCreateWithImageInRect(image, clip);
					CGContextDrawImage(context, CGRectOffset(clip, bounds.origin.x, bounds.origin.y - (r.y + r.height)), part);
					CGImageRelease(part);
				}
				if (r.x + r.width < s.width) {
					CGRect clip = CGRectMake(r.x + r.width, 0, s.width - (r.x + r.width), s.height);
					CGImageRef part = CGImageCreateWithImageInRect(image, clip);
					CGContextDrawImage(context, CGRectOffset(clip, bounds.origin.x, bounds.origin.y), part);
					CGImageRelease(part);
				}
			}
		}
		else {
			CGContextDrawImage(context, bounds, image);
		}
	}
	CGImageRelease(image);
	CGDataProviderRelease(provider);
	CGColorSpaceRelease(colorSpace);
	if (touching) {
		int i;
		for (i = 0; i < 10;  i++) {
			TouchFinger *finger = touches[i];
			if (finger) {
				[touchImage drawAtPoint:finger.point fromRect:NSZeroRect operation:NSCompositingOperationSourceOver fraction:1.0];
			}
		}
	}
}
- (void)launchMachine:(NSString*)path {
	NSString *info = nil;
	txScreenLaunchProc launch;
	self.library = dlopen([path UTF8String], RTLD_NOW | RTLD_LOCAL);
	if (!self.library) {
		info = [NSString stringWithFormat:@"%s", dlerror()];
		goto bail;
	}
	launch = (txScreenLaunchProc)dlsym(self.library, "fxScreenLaunch");
	if (!launch) {
		info = [NSString stringWithFormat:@"%s", dlerror()];
		goto bail;
	}
	(*launch)(self.screen);
	return;
bail:
	if (self.library) {
		dlclose(self.library);
		self.library = nil;
	}
	NSAlert *alert = [[[NSAlert alloc] init] autorelease];
	[alert setMessageText:[NSString stringWithFormat:@"Cannot open \"%@\"", path]];
	if (info)
		[alert setInformativeText:info];
	[alert runModal];
}
- (void)messageToApplication:(NSObject *)object {
	if (self.screen->invoke) {
		NSData* data = (NSData*)object;
		PiuScreenMessage message = (PiuScreenMessage)[data bytes];
		(*self.screen->invoke)(self.screen, message->buffer, message->size);
		free(message->buffer);
	}
}
- (void)messageToHost:(NSObject *)object {
	NSData* data = (NSData*)object;
	PiuScreenMessage message = (PiuScreenMessage)[data bytes];
	if ((*piuScreen)->behavior) {
		xsBeginHost((*piuScreen)->the);
		xsVars(3);
		xsVar(0) = xsReference((*piuScreen)->behavior);
		if (xsFindResult(xsVar(0), xsID_onMessage)) {
			xsVar(1) = xsReference((*piuScreen)->reference);
			if (message->size > 0)
				xsVar(2) = xsArrayBuffer(message->buffer, message->size);
			else
				xsVar(2) = xsString(message->buffer);
			(void)xsCallFunction2(xsResult, xsVar(0), xsVar(1), xsVar(2));
		}
		xsEndHost((*piuScreen)->the);
	}
	free(message->buffer);
}
- (void)mouseDown:(NSEvent *)event {
	if (touching)
		return;
	NSTimeInterval when = 1000 * (time + [event timestamp]);
	NSPoint point = [event locationInWindow];
	point = [self convertPoint:point fromView:nil];
	point.y = [self frame].size.height - point.y;
	if (self.screen->touch) 
		(*self.screen->touch)(self.screen, touchEventBeganKind, 0, point.x, point.y, when);
}
- (void)mouseDragged:(NSEvent *)event {
	if (touching)
		return;
	NSTimeInterval when = 1000 * (time + [event timestamp]);
	NSPoint point = [event locationInWindow];
	point = [self convertPoint:point fromView:nil];
	point.y = [self frame].size.height - point.y;
	if (self.screen->touch) 
		(*self.screen->touch)(self.screen, touchEventMovedKind, 0, point.x, point.y, when);
}
- (void)mouseUp:(NSEvent *)event {
	if (touching)
		return;
	NSTimeInterval when = 1000 * (time + [event timestamp]);
	NSPoint point = [event locationInWindow];
	point = [self convertPoint:point fromView:nil];
	point.y = [self frame].size.height - point.y;
	if (self.screen->touch) 
		(*self.screen->touch)(self.screen, touchEventEndedKind, 0, point.x, point.y, when);
}
- (void)quitMachine {
	if (self.screen->quit) 
		(*self.screen->quit)(self.screen);
	if (self.library) {
    	dlclose(self.library);
    	self.library = nil;
    }
    [self display];
}
- (void)timerCallback:(NSTimer*)theTimer {
	if (self.screen->idle) 
		(*self.screen->idle)(self.screen);
}
- (void)touchesBeganWithEvent:(NSEvent *)event {
	if (!touching)
		return;
    NSSet *set = [event touchesMatchingPhase:NSTouchPhaseBegan inView:nil];
    NSEnumerator *enumerator = [set objectEnumerator];
    NSTouch* touch;
    int i;
	NSSize size = [self frame].size;
	NSTimeInterval when = 1000 * (time + [event timestamp]);
	while ((touch = [enumerator nextObject])) {
		NSPoint point = touch.normalizedPosition;
		point.x *= size.width;
		point.y *= size.height;
		for (i = 0; i < 10;  i++) {
			if (touches[i] == nil) {
				TouchFinger *finger = [TouchFinger alloc];
				finger.identity = touch.identity;
				finger.point = point;
    			touches[i] = [finger retain];
  				break;
			}
		}
		if (self.screen->touch) 
			(*self.screen->touch)(self.screen, touchEventBeganKind, i, point.x, size.height - point.y, when);
	}  
}
- (void)touchesCancelledWithEvent:(NSEvent *)event {
	if (!touching)
		return;
    NSSet *set = [event touchesMatchingPhase:NSTouchPhaseCancelled inView:nil];
    NSEnumerator *enumerator = [set objectEnumerator];
    NSTouch* touch;
    int i;
	NSSize size = [self frame].size;
	NSTimeInterval when = 1000 * (time + [event timestamp]);
	while ((touch = [enumerator nextObject])) {
		NSPoint point = touch.normalizedPosition;
		point.x *= size.width;
		point.y *= size.height;
		for (i = 0; i < 10;  i++) {
			TouchFinger *finger = touches[i];
    		if (finger && ([finger.identity isEqual:touch.identity])) {
    			[finger release];
				touches[i] = nil;
   				break;
			}
		}
		if (self.screen->touch) 
			(*self.screen->touch)(self.screen, touchEventCancelledKind, i, point.x, size.height - point.y, when);
	}
}
- (void)touchesEndedWithEvent:(NSEvent *)event {
	if (!touching)
		return;
    NSSet *set = [event touchesMatchingPhase:NSTouchPhaseEnded inView:nil];
    NSEnumerator *enumerator = [set objectEnumerator];
    NSTouch* touch;
    int i;
	NSSize size = [self frame].size;
	NSTimeInterval when = 1000 * (time + [event timestamp]);
	while ((touch = [enumerator nextObject])) {
		NSPoint point = touch.normalizedPosition;
		point.x *= size.width;
		point.y *= size.height;
		for (i = 0; i < 10;  i++) {
			TouchFinger *finger = touches[i];
    		if (finger && ([finger.identity isEqual:touch.identity])) {
    			[finger release];
				touches[i] = nil;
    			break;
			}
		}
		if (self.screen->touch) 
			(*self.screen->touch)(self.screen, touchEventEndedKind, i, point.x, size.height - point.y, when);
	}
}
- (void)touchesMovedWithEvent:(NSEvent *)event {
	if (!touching)
		return;
    NSSet *set = [event touchesMatchingPhase:NSTouchPhaseMoved inView:nil];
    NSEnumerator *enumerator = [set objectEnumerator];
    NSTouch* touch;
    int i;
	NSSize size = [self frame].size;
	NSTimeInterval when = 1000 * (time + [event timestamp]);
	while ((touch = [enumerator nextObject])) {
		NSPoint point = touch.normalizedPosition;
		point.x *= size.width;
		point.y *= size.height;
		for (i = 0; i < 10;  i++) {
			TouchFinger *finger = touches[i];
    		if (finger && ([finger.identity isEqual:touch.identity])) {
				finger.point = point;
   				break;
			}
		}
		if (self.screen->touch) 
			(*self.screen->touch)(self.screen, touchEventMovedKind, i, point.x, size.height - point.y, when);
	}
}
@end

static void PiuScreenBind(void* it, PiuApplication* application, PiuView* view);
static void PiuScreenDelete(void* it);
static void PiuScreenDictionary(xsMachine* the, void* it);
static void PiuScreenMark(xsMachine* the, void* it, xsMarkRoot markRoot);
static void PiuScreenUnbind(void* it, PiuApplication* application, PiuView* view);
static void PiuScreenMessageBuild(PiuScreenMessage message, char* buffer, int size);

const PiuDispatchRecord ICACHE_FLASH_ATTR PiuScreenDispatchRecord = {
	"Screen",
	PiuScreenBind,
	PiuContentCascade,
	PiuContentDraw,
	PiuContentFitHorizontally,
	PiuContentFitVertically,
	PiuContentHit,
	PiuContentIdle,
	PiuContentInvalidate,
	PiuContentMeasureHorizontally,
	PiuContentMeasureVertically,
	PiuContentPlace,
	NULL,
	NULL,
	PiuContentReflow,
	PiuContentShowing,
	PiuContentShown,
	PiuContentSync,
	PiuScreenUnbind,
	PiuContentUpdate
};

const xsHostHooks ICACHE_FLASH_ATTR PiuScreenHooks = {
	PiuScreenDelete,
	PiuScreenMark,
	NULL
};

void PiuScreenBind(void* it, PiuApplication* application, PiuView* view)
{
	PiuScreen* self = it;
	PiuContentBind(it, application, view);
	
    NSPiuScreenView *screenView = [[NSPiuScreenView alloc] init];
	screenView.piuScreen = self;

	NSPiuClipView *clipView = [[NSPiuClipView alloc] init];
    [clipView setDocumentView:screenView];
    [clipView setDrawsBackground:NO];
	clipView.piuContent = (PiuContent*)self;
	
	(*self)->nsClipView = clipView;
	(*self)->nsScreenView = screenView;
	
	PiuDimension width = (*self)->coordinates.width;
	PiuDimension height = (*self)->coordinates.height;
    txScreen* screen = malloc(sizeof(txScreen) - 1 + (width * height * 4));
    memset(screen, 0, sizeof(txScreen) - 1 + (width * height * 4));
    screen->view = screenView;
    screen->abort = fxScreenAbort;
    screen->bufferChanged = fxScreenBufferChanged;
    screen->formatChanged = fxScreenFormatChanged;
    screen->post = fxScreenPost;
    screen->start = fxScreenStart;
    screen->stop = fxScreenStop;
	mxCreateMutex(&screen->workersMutex);
    screen->width = width;
    screen->height = height;
    (*self)->screen = screen;
    
    screenView.screen = screen;
    
    NSProcessInfo* processInfo = [NSProcessInfo processInfo];
    NSTimeInterval time = [processInfo systemUptime];
    NSDate* date = [NSDate dateWithTimeIntervalSinceNow:-time];
    screenView.time = [date timeIntervalSince1970];
    screenView.timer = nil;
  
    screenView.touchImage = [NSImage imageNamed:@"fingerprint"];
	screenView.touches = (id *)malloc(10 * sizeof(id));
    int i;
    for (i = 0; i < 10; i++)
		screenView.touches[i] = nil;
	 screenView.touching = NO;
	 
    [(*view)->nsView addSubview:clipView];
}

void PiuScreenDictionary(xsMachine* the, void* it) 
{
}

void PiuScreenDelete(void* it) 
{
}

void PiuScreenMark(xsMachine* the, void* it, xsMarkRoot markRoot)
{
	PiuContentMark(the, it, markRoot);
}

void PiuScreenUnbind(void* it, PiuApplication* application, PiuView* view)
{
	PiuScreen* self = it;
    [(*self)->nsClipView removeFromSuperview];
	mxDeleteMutex(&((*self)->screen->workersMutex));
	free((*self)->screen);
	(*self)->screen = NULL;
    [(*self)->nsClipView release];
	(*self)->nsClipView = NULL;
    [(*self)->nsScreenView release];
	(*self)->nsScreenView = NULL;
	PiuContentUnbind(it, application, view);
}

void PiuScreenMessageBuild(PiuScreenMessage message, char* buffer, int size) 
{
	message->size = size;
	if (size < 0)
		message->buffer = buffer;
	else {
		if (!size)
			size = strlen(buffer) + 1;
		message->buffer = malloc(size);
		if (message->buffer)
			memcpy(message->buffer, buffer, size);
	}
}
void PiuScreen_create(xsMachine* the)
{
	PiuScreen* self;
	xsVars(2);
	xsSetHostChunk(xsThis, NULL, sizeof(PiuScreenRecord));
	self = PIU(Screen, xsThis);
	(*self)->the = the;
	(*self)->reference = xsToReference(xsThis);
	xsSetHostHooks(xsThis, (xsHostHooks*)&PiuScreenHooks);
	(*self)->dispatch = (PiuDispatch)&PiuScreenDispatchRecord;
	(*self)->flags = piuVisible;
	PiuContentDictionary(the, self);
	PiuScreenDictionary(the, self);
	PiuBehaviorOnCreate(self);
}

void PiuScreen_get_hole(xsMachine* the)
{
	PiuScreen* self = PIU(Screen, xsThis);
	xsResult = xsNewObject();
	xsDefine(xsResult, xsID_x, xsPiuCoordinate((*self)->hole.x), xsDefault);
	xsDefine(xsResult, xsID_y, xsPiuCoordinate((*self)->hole.y), xsDefault);
	xsDefine(xsResult, xsID_width, xsPiuDimension((*self)->hole.width), xsDefault);
	xsDefine(xsResult, xsID_height, xsPiuDimension((*self)->hole.height), xsDefault);
}

void PiuScreen_set_hole(xsMachine* the)
{
	PiuScreen* self = PIU(Screen, xsThis);
	xsIntegerValue value;
	if (xsFindInteger(xsArg(0), xsID_x, &value)) {
		(*self)->hole.x = value;
	}
	if (xsFindInteger(xsArg(0), xsID_y, &value)) {
		(*self)->hole.y = value;
	}
	if (xsFindInteger(xsArg(0), xsID_width, &value)) {
		(*self)->hole.width = value;
	}
	if (xsFindInteger(xsArg(0), xsID_height, &value)) {
		(*self)->hole.height = value;
	}
	PiuContentInvalidate(self, NULL);
	[(*self)->nsScreenView display];
}

void PiuScreen_get_running(xsMachine* the)
{
	PiuScreen* self = PIU(Screen, xsThis);
	xsResult = (*self)->nsScreenView.library ? xsTrue : xsFalse;
}
	
void PiuScreen_launch(xsMachine* the)
{
	PiuScreen* self = PIU(Screen, xsThis);
	xsStringValue path = xsToString(xsArg(0));
    [(*self)->nsScreenView launchMachine:[NSString stringWithUTF8String:path]];
}
	
void PiuScreen_postMessage(xsMachine* the)
{
	PiuScreen* self = PIU(Screen, xsThis);
	PiuScreenMessageRecord message;
	if (xsIsInstanceOf(xsArg(0), xsArrayBufferPrototype)) {
		int size = (int)xsGetArrayBufferLength(xsArg(0));
		PiuScreenMessageBuild(&message, xsToArrayBuffer(xsArg(0)), size);
	}
	else {
		xsStringValue string = xsToString(xsArg(0));
		PiuScreenMessageBuild(&message, string, 0);
	}
	if (message.buffer) {
		NSData* data = [NSData dataWithBytes:&message length:sizeof(PiuScreenMessageRecord)];
		[(*self)->nsScreenView performSelectorOnMainThread:@selector(messageToApplication:) withObject:data waitUntilDone:NO];
	}
}

void PiuScreen_quit(xsMachine* the)
{
	PiuScreen* self = PIU(Screen, xsThis);
    [(*self)->nsScreenView quitMachine];
}

void fxScreenAbort(txScreen* screen)
{
	NSPiuScreenView *screenView = screen->view;
    [screenView performSelectorOnMainThread:@selector(quitMachine) withObject:nil waitUntilDone:NO];
}

void fxScreenBufferChanged(txScreen* screen)
{
	NSPiuScreenView *screenView = screen->view;
	[screenView display];
}

void fxScreenFormatChanged(txScreen* screen)
{
}

void fxScreenPost(txScreen* screen, char* buffer, int size)
{
	NSPiuScreenView* screenView = screen->view;
	PiuScreenMessageRecord message;
	PiuScreenMessageBuild(&message, buffer, size);
	if (message.buffer) {
		NSData* data = [NSData dataWithBytes:&message length:sizeof(PiuScreenMessageRecord)];
		[screenView performSelectorOnMainThread:@selector(messageToHost:) withObject:data waitUntilDone:NO];
	}
}

void fxScreenStart(txScreen* screen, double interval)
{
	NSPiuScreenView *screenView = screen->view;
	screenView.timer = [NSTimer scheduledTimerWithTimeInterval:interval/1000 target:screenView selector:@selector(timerCallback:) userInfo:nil repeats:YES];
}

void fxScreenStop(txScreen* screen)
{
	NSPiuScreenView *screenView = screen->view;
	if (screenView.timer)
		[screenView.timer invalidate];
	screenView.timer = nil;
}

extern void PiuScreenWorkerCreateAux(xsMachine* the, txScreen* screen);

void PiuScreenWorkerCreate(xsMachine* the)
{
	PiuScreen* self = PIU(Screen, xsArg(1));
	PiuScreenWorkerCreateAux(the, (*self)->screen);
}
