/*************************************************************************
 * Copyright (c) 2011 AT&T Intellectual Property 
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v2.0
 * which accompanies this distribution, and is available at
 * https://www.eclipse.org/org/documents/epl-2.0/EPL-2.0.html
 *
 * Contributors: Details at https://graphviz.org
 *************************************************************************/

#include "config.h"

#include <stdlib.h>
#include <string.h>
#include <util/gv_math.h>

#ifdef __ENVIRONMENT_IPHONE_OS_VERSION_MIN_REQUIRED__
#if __ENVIRONMENT_IPHONE_OS_VERSION_MIN_REQUIRED__ >= 20000
#include <mach/mach_host.h>
#include <sys/mman.h>
#endif

#if __ENVIRONMENT_IPHONE_OS_VERSION_MIN_REQUIRED__ >= 40000
#include <ImageIO/ImageIO.h>
#endif
#endif

#include <gvc/gvplugin_device.h>
#include <gvc/gvplugin_render.h>
#include <cgraph/cgraph.h>

#include "gvplugin_quartz.h"

static CGFloat dashed[] = { 6.0 };
static CGFloat dotted[] = { 2.0, 6.0 };

static void quartzgen_begin_job(GVJ_t * job)
{
	switch (job->device.id) {
		case FORMAT_CGIMAGE:
			/* save the passed-in context in the window field, so we can create a CGContext in the context field later on */
			job->window = job->context;
			*((CGImageRef *) job->window) = NULL;
	}
	
	job->context = NULL;
}

static void quartzgen_end_job(GVJ_t * job)
{
    CGContextRef context = job->context;
	
#ifdef __ENVIRONMENT_IPHONE_OS_VERSION_MIN_REQUIRED__
#if __ENVIRONMENT_IPHONE_OS_VERSION_MIN_REQUIRED__ >= 20000
	void* context_data;
	size_t context_datalen;
	
	switch (job->device.id) {
			
		case FORMAT_PDF:
			context_data = NULL;
			context_datalen = 0;
			break;
			
		default:
			context_data = CGBitmapContextGetData(context);
			context_datalen = CGBitmapContextGetBytesPerRow(context) * CGBitmapContextGetHeight(context);
			break;
	}
#endif
#endif

	switch (job->device.id) {

	case FORMAT_PDF:
	    /* save the PDF */
	    CGPDFContextClose(context);
	    break;

	case FORMAT_CGIMAGE:
		/* create an image and save it where the window field is, which was set to the passed-in context at begin job */
		*((CGImageRef *) job->window) = CGBitmapContextCreateImage(context);
	    break;

#if (defined(__ENVIRONMENT_MAC_OS_X_VERSION_MIN_REQUIRED__) &&                 \
     __ENVIRONMENT_MAC_OS_X_VERSION_MIN_REQUIRED__ >= 1040) ||                 \
    (defined(__ENVIRONMENT_IPHONE_OS_VERSION_MIN_REQUIRED__) &&                \
     __ENVIRONMENT_IPHONE_OS_VERSION_MIN_REQUIRED__ >= 40000)
	default:		/* bitmap formats */
	    {
		/* create an image destination */
		CGDataConsumerRef data_consumer =
		    CGDataConsumerCreate(job,
					 &device_data_consumer_callbacks);
		CGImageDestinationRef image_destination =
		    CGImageDestinationCreateWithDataConsumer(data_consumer,
							     format_to_uti((format_type)job->device.id), 1, NULL);

		/* add the bitmap image to the destination and save it */
		CGImageRef image = CGBitmapContextCreateImage(context);
		CGImageDestinationAddImage(image_destination, image, NULL);
		CGImageDestinationFinalize(image_destination);

		/* clean up */
		if (image_destination)
		    CFRelease(image_destination);
		CGImageRelease(image);
		CGDataConsumerRelease(data_consumer);
	    }
	    break;
#endif
	}
	
	CGContextRelease(context);

#ifdef __ENVIRONMENT_IPHONE_OS_VERSION_MIN_REQUIRED__
#if __ENVIRONMENT_IPHONE_OS_VERSION_MIN_REQUIRED__ >= 20000
	if (context_data && context_datalen)
		munmap(context_data, context_datalen);
#endif
#endif
}

static void quartzgen_begin_page(GVJ_t * job)
{
    CGRect bounds = CGRectMake(0.0, 0.0, job->width, job->height);

    if (!job->context) {

	switch (job->device.id) {

	case FORMAT_PDF:
	    {
		/* create the auxiliary info for PDF content, author and title */
		const void *auxiliaryKeys[] = {
		    kCGPDFContextCreator,
		    kCGPDFContextTitle
		};
		const void *auxiliaryValues[] = {
		    CFStringCreateWithFormat(NULL, NULL,
					     CFSTR("%s %s"),
					     job->common->info[0],
					     job->common->info[1]),
		    job->obj->type ==
			ROOTGRAPH_OBJTYPE ?
			CFStringCreateWithCStringNoCopy(NULL,
						      agnameof(job->obj->u.g),
						      kCFStringEncodingUTF8,
						      kCFAllocatorNull)
			: CFSTR("")
		};
		CFDictionaryRef auxiliaryInfo =
		    CFDictionaryCreate(NULL,
				       auxiliaryKeys,
				       auxiliaryValues,
				       sizeof(auxiliaryValues) /
				       sizeof(auxiliaryValues[0]),
				       &kCFTypeDictionaryKeyCallBacks,
				       &kCFTypeDictionaryValueCallBacks);

		/* create a PDF for drawing into */
		CGDataConsumerRef data_consumer =
		    CGDataConsumerCreate(job,
					 &device_data_consumer_callbacks);
		job->context =
		    CGPDFContextCreate(data_consumer, &bounds,
				       auxiliaryInfo);

		/* clean up */
		CGDataConsumerRelease(data_consumer);
		CFRelease(auxiliaryInfo);
		for (size_t i = 0; i < sizeof(auxiliaryValues) / sizeof(auxiliaryValues[0]);
		     ++i)
		    CFRelease(auxiliaryValues[i]);
	    }
	    break;

	default:		/* bitmap formats */
	    {
		size_t bytes_per_row = job->width * BYTES_PER_PIXEL;
		// align up to a 16-byte boundary
		if (bytes_per_row % 16 != 0) {
		    bytes_per_row += 16 - (bytes_per_row % 16);
		}

		void *buffer = NULL;

#ifdef __ENVIRONMENT_IPHONE_OS_VERSION_MIN_REQUIRED__
#if __ENVIRONMENT_IPHONE_OS_VERSION_MIN_REQUIRED__ >= 20000

		/* iPhoneOS has no swap files for memory, so if we're short of memory we need to make our own temp scratch file to back it */

		size_t buffer_size = job->height * bytes_per_row;
		mach_msg_type_number_t vm_info_size = HOST_VM_INFO_COUNT;
		vm_statistics_data_t vm_info;

		if (host_statistics
		    (mach_host_self(), HOST_VM_INFO,
		     (host_info_t) & vm_info,
		     &vm_info_size) != KERN_SUCCESS
		    || buffer_size * 2 >
		    vm_info.free_count * vm_page_size) {
		    FILE *temp_file = tmpfile();
		    if (temp_file) {
			int temp_file_descriptor = fileno(temp_file);
			if (temp_file_descriptor >= 0
			    && ftruncate(temp_file_descriptor,
					 buffer_size) == 0) {
			    buffer = mmap(NULL, buffer_size, PROT_READ | PROT_WRITE,
			                  MAP_FILE | MAP_PRIVATE, temp_file_descriptor, 0);
			    if (buffer == MAP_FAILED)
				buffer = NULL;
			}
			fclose(temp_file);
		    }
		}
		if (buffer == NULL) {
		    buffer = mmap(NULL, buffer_size, PROT_READ | PROT_WRITE,
				  MAP_ANON | MAP_PRIVATE, -1, 0);
		    if (buffer == MAP_FAILED) {
			buffer = NULL;
		    }
		}
#endif
#endif

		/* create a true color bitmap for drawing into */
		CGColorSpaceRef color_space =
		    CGColorSpaceCreateDeviceRGB();
		job->context = CGBitmapContextCreate(buffer,	/* data: MacOSX lets system allocate, iPhoneOS use manual memory mapping */
						     job->width,	/* width in pixels */
						     job->height,	/* height in pixels */
						     BITS_PER_COMPONENT,	/* bits per component */
						     bytes_per_row,	/* bytes per row: align to 16 byte boundary */
						     color_space,	/* color space: device RGB */
						     kCGImageAlphaPremultipliedFirst	/* bitmap info: premul ARGB has best support in OS X */
		    );
		job->imagedata = CGBitmapContextGetData(job->context);

		/* clean up */
		CGColorSpaceRelease(color_space);
	    }
	    break;
	}

    }

    /* start the page (if this is a paged context) and graphics state */
    CGContextRef context = job->context;
    CGContextBeginPage(context, &bounds);
    CGContextSaveGState(context);
    /* CGContextSetMiterLimit(context, 1.0); */
    /* CGContextSetLineJoin(context, kCGLineJoinBevel); */

    /* set up the context transformation */
    CGContextScaleCTM(context, job->scale.x, job->scale.y);
    CGContextRotateCTM(context, job->rotation * M_PI / 180.0);
    CGContextTranslateCTM(context, job->translation.x, job->translation.y);
}

static void quartzgen_end_page(GVJ_t * job)
{
    /* end the page (if this is a paged context) and graphics state */
    CGContextRef context = job->context;
    CGContextRestoreGState(context);
    CGContextEndPage(context);
}

static void quartzgen_begin_anchor(GVJ_t * job, char *url, char *tooltip,
				   char *target, char *id)
{
    (void)tooltip;
    (void)target;
    (void)id;

    pointf *url_map = job->obj->url_map_p;
    if (url && url_map) {
	/* set up the hyperlink to the given url */
	CGContextRef context = job->context;
	CFURLRef uri = CFURLCreateWithBytes(NULL, (const UInt8 *)url,
				 strlen(url), kCFStringEncodingUTF8, NULL);
	CGPDFContextSetURLForRect(context, uri,
				  /* need to reverse the CTM on the area to get it to work */
				  CGRectApplyAffineTransform(CGRectMake
							     (url_map[0].x,
							      url_map[0].y,
							      url_map[1].
							      x -
							      url_map[0].x,
							      url_map[1].
							      y -
							      url_map[0].
							      y),
							     CGContextGetCTM
							     (context))
	    );

	/* clean up */
	CFRelease(uri);
    }
}

static void quartzgen_path(GVJ_t * job, int filled)
{
    CGContextRef context = job->context;

    /* set up colors */
    if (filled)
	CGContextSetRGBFillColor(context, job->obj->fillcolor.u.RGBA[0],
				 job->obj->fillcolor.u.RGBA[1],
				 job->obj->fillcolor.u.RGBA[2],
				 job->obj->fillcolor.u.RGBA[3]);
    CGContextSetRGBStrokeColor(context, job->obj->pencolor.u.RGBA[0],
			       job->obj->pencolor.u.RGBA[1],
			       job->obj->pencolor.u.RGBA[2],
			       job->obj->pencolor.u.RGBA[3]);

    /* set up line style */
    const CGFloat *segments;
    size_t segment_count;
    switch (job->obj->pen) {
    case PEN_DASHED:
	segments = dashed;
	segment_count = sizeof(dashed) / sizeof(CGFloat);
	break;
    case PEN_DOTTED:
	segments = dotted;
	segment_count = sizeof(dotted) / sizeof(CGFloat);
	break;
    default:
	segments = NULL;
	segment_count = 0;
	break;
    }
    CGContextSetLineDash(context, 0.0, segments, segment_count);

    /* set up line width */
    CGContextSetLineWidth(context, job->obj->penwidth);	// *job->scale.x);

    /* draw the path */
    CGContextDrawPath(context, filled ? kCGPathFillStroke : kCGPathStroke);
}

static void quartzgen_textspan(GVJ_t * job, pointf p, textspan_t * span)
{
    CGContextRef context = job->context;

    /* adjust text position */
    switch (span->just) {
    case 'r':
	p.x -= span->size.x;
	break;
    case 'l':
	p.x -= 0.0;
	break;
    case 'n':
    default:
	p.x -= span->size.x / 2.0;
	break;
    }
    p.y += span->yoffset_centerline;

    void *layout;
    if (span->free_layout == &quartz_free_layout)
	layout = span->layout;
    else
	layout =
	    quartz_new_layout(span->font->name, span->font->size, span->str);

    CGContextSetRGBFillColor(context, job->obj->pencolor.u.RGBA[0],
			     job->obj->pencolor.u.RGBA[1],
			     job->obj->pencolor.u.RGBA[2],
			     job->obj->pencolor.u.RGBA[3]);
    quartz_draw_layout(layout, context, CGPointMake(p.x, p.y));

    if (span->free_layout != &quartz_free_layout)
	quartz_free_layout(layout);
}

static void quartzgen_ellipse(GVJ_t * job, pointf * A, int filled)
{
    /* convert ellipse into the current path */
    CGContextRef context = job->context;
    double dx = A[1].x - A[0].x;
    double dy = A[1].y - A[0].y;
    CGContextAddEllipseInRect(context,
			      CGRectMake(A[0].x - dx, A[0].y - dy,
					 dx * 2.0, dy * 2.0));

    /* draw the ellipse */
    quartzgen_path(job, filled);
}

static void quartzgen_polygon(GVJ_t *job, pointf *A, size_t n, int filled) {
    /* convert polygon into the current path */
    CGContextRef context = job->context;
    CGContextMoveToPoint(context, A[0].x, A[0].y);
    for (size_t i = 1; i < n; ++i)
	CGContextAddLineToPoint(context, A[i].x, A[i].y);
    CGContextClosePath(context);

    /* draw the ellipse */
    quartzgen_path(job, filled);
}

static void quartzgen_bezier(GVJ_t *job, pointf *A, size_t n, int filled) {
    /* convert Bézier into the current path */
    CGContextRef context = job->context;
    CGContextMoveToPoint(context, A[0].x, A[0].y);
    for (size_t i = 1; i < n; i += 3)
	CGContextAddCurveToPoint(context, A[i].x, A[i].y, A[i + 1].x,
				 A[i + 1].y, A[i + 2].x, A[i + 2].y);

    /* draw the ellipse */
    quartzgen_path(job, filled);
}

static void quartzgen_polyline(GVJ_t *job, pointf *A, size_t n) {
    /* convert polyline into the current path */
    CGContextRef context = job->context;
    CGContextMoveToPoint(context, A[0].x, A[0].y);
    for (size_t i = 1; i < n; ++i)
	CGContextAddLineToPoint(context, A[i].x, A[i].y);

    /* draw the ellipse */
    quartzgen_path(job, 0);
}

static gvrender_engine_t quartzgen_engine = {
    quartzgen_begin_job,
    quartzgen_end_job,
    0,				/* quartzgen_begin_graph */
    0,				/* quartzgen_end_graph */
    0,				/* quartzgen_begin_layer */
    0,				/* quartzgen_end_layer */
    quartzgen_begin_page,
    quartzgen_end_page,
    0,				/* quartzgen_begin_cluster */
    0,				/* quartzgen_end_cluster */
    0,				/* quartzgen_begin_nodes */
    0,				/* quartzgen_end_nodes */
    0,				/* quartzgen_begin_edges */
    0,				/* quartzgen_end_edges */
    0,				/* quartzgen_begin_node */
    0,				/* quartzgen_end_node */
    0,				/* quartzgen_begin_edge */
    0,				/* quartzgen_end_edge */
    quartzgen_begin_anchor,
    0,				/* quartzgen_end_anchor */
    0,				/* quartzgen_begin_label */
    0,				/* quartzgen_end_label */
    quartzgen_textspan,
    0,
    quartzgen_ellipse,
    quartzgen_polygon,
    quartzgen_bezier,
    quartzgen_polyline,
    0,				/* quartzgen_comment */
    0,				/* quartzgen_library_shape */
};

static gvrender_features_t render_features_quartz = {
    GVRENDER_DOES_MAPS | GVRENDER_DOES_MAP_RECTANGLE | GVRENDER_DOES_TRANSFORM,	/* flags */
    4.,				/* default pad - graph units */
    NULL,			/* knowncolors */
    0,				/* sizeof knowncolors */
    RGBA_DOUBLE			/* color_type */
};

#if (defined(__ENVIRONMENT_MAC_OS_X_VERSION_MIN_REQUIRED__) &&                 \
     __ENVIRONMENT_MAC_OS_X_VERSION_MIN_REQUIRED__ >= 1040) ||                 \
    (defined(__ENVIRONMENT_IPHONE_OS_VERSION_MIN_REQUIRED__) &&                \
     __ENVIRONMENT_IPHONE_OS_VERSION_MIN_REQUIRED__ >= 20000)
static gvdevice_features_t device_features_quartz = {
    GVDEVICE_BINARY_FORMAT | GVDEVICE_DOES_TRUECOLOR,	/* flags */
    {0., 0.},			/* default margin - points */
    {0., 0.},			/* default page width, height - points */
    {72., 72.}			/* dpi */
};
#endif

static gvdevice_features_t device_features_quartz_paged = {
    GVDEVICE_DOES_PAGES | GVDEVICE_BINARY_FORMAT | GVDEVICE_DOES_TRUECOLOR | GVRENDER_NO_WHITE_BG,	/* flags */
    {36., 36.},			/* default margin - points */
    {0., 0.},			/* default page width, height - points */
    {72., 72.}			/* dpi */
};

gvplugin_installed_t gvrender_quartz_types[] = {
    {0, "quartz", 1, &quartzgen_engine, &render_features_quartz},
    {0, NULL, 0, NULL, NULL}
};

gvplugin_installed_t gvdevice_quartz_types[] = {
    {FORMAT_PDF, "pdf:quartz", 8, NULL, &device_features_quartz_paged},
#if (defined(__ENVIRONMENT_MAC_OS_X_VERSION_MIN_REQUIRED__) &&                 \
     __ENVIRONMENT_MAC_OS_X_VERSION_MIN_REQUIRED__ >= 1040) ||                 \
    (defined(__ENVIRONMENT_IPHONE_OS_VERSION_MIN_REQUIRED__) &&                \
     __ENVIRONMENT_IPHONE_OS_VERSION_MIN_REQUIRED__ >= 20000)
    {FORMAT_CGIMAGE, "cgimage:quartz", 8, NULL, &device_features_quartz},
#endif
#if (defined(__ENVIRONMENT_MAC_OS_X_VERSION_MIN_REQUIRED__) &&                 \
     __ENVIRONMENT_MAC_OS_X_VERSION_MIN_REQUIRED__ >= 1040) ||                 \
    (defined(__ENVIRONMENT_IPHONE_OS_VERSION_MIN_REQUIRED__) &&                \
     __ENVIRONMENT_IPHONE_OS_VERSION_MIN_REQUIRED__ >= 40000)
    {FORMAT_BMP, "bmp:quartz", 8, NULL, &device_features_quartz},
    {FORMAT_GIF, "gif:quartz", 8, NULL, &device_features_quartz},
    {FORMAT_ICO, "ico:quartz", 8, NULL, &device_features_quartz},
    {FORMAT_JPEG, "jpe:quartz", 8, NULL, &device_features_quartz},
    {FORMAT_JPEG, "jpeg:quartz", 8, NULL, &device_features_quartz},
    {FORMAT_JPEG, "jpg:quartz", 8, NULL, &device_features_quartz},
    {FORMAT_JPEG2000, "jp2:quartz", 8, NULL, &device_features_quartz},
    {FORMAT_PNG, "png:quartz", 8, NULL, &device_features_quartz},
    {FORMAT_TIFF, "tif:quartz", 8, NULL, &device_features_quartz},
    {FORMAT_TIFF, "tiff:quartz", 8, NULL, &device_features_quartz},
    {FORMAT_TGA, "tga:quartz", 8, NULL, &device_features_quartz},
#endif
#ifdef __ENVIRONMENT_MAC_OS_X_VERSION_MIN_REQUIRED__
#if __ENVIRONMENT_MAC_OS_X_VERSION_MIN_REQUIRED__ >= 1040
    {FORMAT_EXR, "exr:quartz", 8, NULL, &device_features_quartz},
    {FORMAT_ICNS, "icns:quartz", 8, NULL, &device_features_quartz},
    {FORMAT_PICT, "pct:quartz", 8, NULL, &device_features_quartz},
    {FORMAT_PICT, "pict:quartz", 8, NULL, &device_features_quartz},
    {FORMAT_PSD, "psd:quartz", 8, NULL, &device_features_quartz},
    {FORMAT_SGI, "sgi:quartz", 8, NULL, &device_features_quartz},
#endif
#endif
    {0, NULL, 0, NULL, NULL}
};
