/*
 *  JoeyDev
 *  Copyright (C) 2018-2023 Scott Duensing <scott@kangaroopunch.com>
 *
 *  This software is provided 'as-is', without any express or implied
 *  warranty.  In no event will the authors be held liable for any damages
 *  arising from the use of this software.
 *
 *  Permission is granted to anyone to use this software for any purpose,
 *  including commercial applications, and to alter it and redistribute it
 *  freely, subject to the following restrictions:
 *
 *  1. The origin of this software must not be misrepresented; you must not
 *     claim that you wrote the original software. If you use this software
 *     in a product, an acknowledgment in the product documentation would be
 *     appreciated but is not required.
 *  2. Altered source versions must be plainly marked as such, and must not be
 *     misrepresented as being the original software.
 *  3. This notice may not be removed or altered from any source distribution.
*/


#pragma clang diagnostic push
//#pragma ide diagnostic ignored "EmptyDeclOrStmt"
#pragma ide diagnostic ignored "cppcoreguidelines-narrowing-conversions"
#pragma ide diagnostic ignored "hicpp-multiway-paths-covered"


#include <string.h>
#include <math.h>

#include "common.h"
#include "scintillaHeaders.h"
#include "vector.h"
#include "utils.h"
#include "draw.h"
#include "image.h"
#include "vecparse.h"
#include "color.h"
#include "palette.h"


#define RENDER_TIMEOUT  5       // In seconds

#define SSM(m, w, l) scintilla_send_message(self->sci, m, w, l)
#define MARGIN_SCRIPT_FOLD_INDEX 1
#define MARKER_ERROR_ARROW       0
#define MARKER_ERROR_HIGHLIGHT   1
#define PREVIEW_WIDTH            640
#define PREVIEW_HEIGHT           400


enum ClickStateE {
	CLICK_NONE = 0,
	CLICK_BOX_1,
	CLICK_BOX_2,
	CLICK_CIRCLE_1,
	CLICK_CIRCLE_2,
	CLICK_ELLIPSE_1,
	CLICK_ELLIPSE_2,
	CLICK_FILL_1,
	CLICK_LINE_1,
	CLICK_LINE_2,
	CLICK_LINE_X,
	CLICK_PLOT_1,
	CLICK_RECTANGLE_1,
	CLICK_RECTANGLE_2
};
typedef enum ClickStateE ClickStateT;


typedef struct VectorDataS {
	WindowDataT       windowData;
	GtkWidget        *drawVectorImage;
	GtkWidget        *boxVectorForEditor;
	GtkWidget        *editor;
	GtkWidget        *fileVectorTraceImage;
	GtkWidget        *statusBar;
	ScintillaObject  *sci;
	void             *pLexer;
	int               id;
	int               statusBarId;
	cairo_surface_t  *surface;
	cairo_surface_t  *scaled;
	cairo_surface_t  *target;
	cairo_surface_t  *trace;
	jlContextT       *jlc;
	double            traceImagePercent;
	float            *variables;
	char             *tracename;
	char             *buffer;
	int               bufferLength;
	PointT          **pointList;
	gboolean          allowRedraw;
	ClickStateT       clickState;
	PointT            clickTempPoint;
} VectorDataT;


static int _nextEditorId = 0;


static int      byte(VectorDataT *self, unsigned char byte);
EVENT  void     drawVectorImageClick(GtkWidget *object, GdkEventButton *event, gpointer userData);
EVENT  gboolean drawVectorImageDraw(GtkWidget *widget, cairo_t *cr, gpointer userData);
EVENT  gboolean drawVectorImageMotionEvent(GtkWidget *widget, GdkEventMotion *event, gpointer userData);
EVENT  void     editorVectorNotify(GtkWidget *sciWidget, gint ctrlID, struct SCNotification *notifyData, gpointer userData);
EVENT  void     fileVectorTraceImageFileSet(GtkWidget *object, gpointer userData);
static int      findClosestPoint(VectorDataT *self, int x, int y);
static int      getWord(VecByteCodeT *bytecode, int *index);
static void     insertCommand(VectorDataT *self, char *command);
static void     insertText(VectorDataT *self, char *text);
static void     loadTraceImage(VectorDataT *self, char *filename);
static void     loadVectorImage(VectorDataT *self);
EVENT  void     menuVectorEditCopy(GtkWidget *object, gpointer userData);
EVENT  void     menuVectorEditCut(GtkWidget *object, gpointer userData);
EVENT  void     menuVectorEditDelete(GtkWidget *object, gpointer userData);
EVENT  void     menuVectorEditPaste(GtkWidget *object, gpointer userData);
EVENT  void     menuVectorFileClose(GtkWidget *object, gpointer userData);
EVENT  void     menuVectorFileNew(GtkWidget *object, gpointer userData);
EVENT  void     menuVectorFileOpen(GtkWidget *object, gpointer userData);
EVENT  void     menuVectorFileSave(GtkWidget *object, gpointer userData);
EVENT  void     menuVectorFileSaveAs(GtkWidget *object, gpointer userData);
EVENT  void     menuVectorHelpVector(GtkWidget *object, gpointer userData);
static void     releasePointList(VectorDataT *self);
static void     renderBytecode(VecByteCodeT *bytecode, VectorDataT *self);
EVENT  void     scaleVectorTraceImageValueChanged(GtkWidget *object, gpointer userData);
static void     sortCoordinates(int *x1, int *y1, int *x2, int *y2);
static void     status(VectorDataT *self, char *message);
EVENT  void     toolBoxClicked(GtkToolButton *object, gpointer userData);
EVENT  void     toolCircleClicked(GtkToolButton *object, gpointer userData);
EVENT  void     toolColorClicked(GtkToolButton *object, gpointer userData);
EVENT  void     toolEllipseClicked(GtkToolButton *object, gpointer userData);
EVENT  void     toolFillClicked(GtkToolButton *object, gpointer userData);
EVENT  void     toolLineClicked(GtkToolButton *object, gpointer userData);
EVENT  void     toolPlotClicked(GtkToolButton *object, gpointer userData);
EVENT  void     toolRectangleClicked(GtkToolButton *object, gpointer userData);
static float    variable(VectorDataT *self, unsigned char byte);
static int      word(VectorDataT *self, unsigned short word);
EVENT  gboolean winVectorClose(GtkWidget *object, gpointer userData);
static void     winVectorDelete(gpointer userData);


static int byte(VectorDataT *self, unsigned char byte) {
	int value;
	// Decode our weird byte format, look up variables, and return sane numbers.
	// Is this a variable?
	if (byte & 0x80) {
		value = variable(self, byte);
	} else {
		// Bytes can't be negative in our world, so just return it.
		value = byte;
	}
	return value;
}


EVENT void drawVectorImageClick(GtkWidget *object, GdkEventButton *event, gpointer userData) {
	VectorDataT *self       = (VectorDataT *)userData;
	char         temp[8];
	int          x          = (int)(event->x * 0.5);
	int          y          = (int)(event->y * 0.5);

	(void)object;

	if (event->type == GDK_BUTTON_PRESS) {

		if (event->button == 2 || event->button == 3) {
			if (self->clickState != CLICK_NONE) {
				// Cancel any in-progress drawing action.
				self->clickState = CLICK_NONE;
				status(self, "Tool canceled.");
			}
		}

		if (event->button == 1) {
			// Clamp coordinates, just in case.
			if (x < 0) x = 0;
			if (x > 319) x = 319;
			if (y < 0) y = 0;
			if (y > 199) y = 199;
			snprintf(temp, 8, "%d,%d", x, y);

			switch (self->clickState) {
				case CLICK_NONE:
					// Highlight the closest point in editor.
					findClosestPoint(self, x, y);
					break;

				case CLICK_BOX_1:
					insertText(self, temp);
					insertText(self, " to ");
					self->clickState++;
					status(self, "Box.  Select opposite corner.");
					break;

				case CLICK_BOX_2:
					insertText(self, temp);
					self->clickState = CLICK_NONE;
					status(self, "");
					break;

				case CLICK_CIRCLE_1:
					self->clickTempPoint.x = x;
					self->clickTempPoint.y = y;
					insertText(self, temp);
					insertText(self, " radius ");
					self->clickState++;
					status(self, "Circle.  Select radius.");
					break;

				case CLICK_CIRCLE_2:
					x = sqrt(pow(x - self->clickTempPoint.x, 2) + pow(y - self->clickTempPoint.y, 2));
					snprintf(temp, 8, "%d", x);
					insertText(self, temp);
					self->clickState = CLICK_NONE;
					status(self, "");
					break;

				case CLICK_ELLIPSE_1:
					insertText(self, temp);
					insertText(self, " to ");
					self->clickState++;
					status(self, "Ellipse.  Select opposite corner.");
					break;

				case CLICK_ELLIPSE_2:
					insertText(self, temp);
					self->clickState = CLICK_NONE;
					break;

				case CLICK_FILL_1:
					insertText(self, temp);
					self->clickState = CLICK_NONE;
					status(self, "");
					break;

				case CLICK_LINE_1:
					insertText(self, temp);
					insertText(self, " to ");
					self->clickState++;
					status(self, "Line.  Select next point.");
					break;

				case CLICK_LINE_2:
					insertText(self, temp);
					self->clickState++;
					status(self, "Line.  Select additional points, right-click to finish.");
					break;

				case CLICK_LINE_X:
					insertText(self, " to ");
					insertText(self, temp);
					break;

				case CLICK_PLOT_1:
					insertText(self, temp);
					self->clickState = CLICK_NONE;
					status(self, "");
					break;

				case CLICK_RECTANGLE_1:
					insertText(self, temp);
					insertText(self, " to ");
					self->clickState++;
					status(self, "Rectangle.  Select opposite corner.");
					break;

				case CLICK_RECTANGLE_2:
					insertText(self, temp);
					self->clickState = CLICK_NONE;
					status(self, "");
					break;
			}
		}
	}

	/*
	if (event->type == GDK_DOUBLE_BUTTON_PRESS) 	{
		snprintf(temp, 8, "%d,%d", x, y);
		SSM(SCI_ADDTEXT, strlen(temp), (sptr_t)temp);
		SSM(SCI_GRABFOCUS, 0, 0);
	}
	*/
}


EVENT gboolean drawVectorImageDraw(GtkWidget *widget, cairo_t *cr, gpointer userData) {
	VectorDataT     *self   = (VectorDataT *)userData;
	int              width  = cairo_image_surface_get_width(self->surface);
	int              height = cairo_image_surface_get_height(self->surface);
	cairo_t         *myCr;
	size_t           offset;
	unsigned char   *scaledPointer;
	unsigned char   *tracePointer;
	unsigned char   *targetPointer;

	(void)widget;
	(void)userData;

	// Copy JoeyLib output to a surface scaled to match the preview.
	jlContextFlush(self->jlc);
	cairo_surface_mark_dirty(self->surface);
	myCr = cairo_create(self->scaled);
	cairo_scale(myCr, (double)((double)PREVIEW_WIDTH / (double)width), (double)((double)PREVIEW_HEIGHT / (double)height));
	cairo_set_source_surface(myCr, self->surface, 0, 0);
	cairo_pattern_set_filter(cairo_get_source(myCr), CAIRO_FILTER_NEAREST);
	cairo_set_operator(myCr, CAIRO_OPERATOR_SOURCE);
	cairo_paint(myCr);
	cairo_destroy(myCr);

	// Get ready for more JoeyLib drawing.
	cairo_surface_flush(self->surface);

	cairo_save(cr);

	scaledPointer = cairo_image_surface_get_data(self->scaled);
	targetPointer = cairo_image_surface_get_data(self->target);
	if (self->trace != NULL) tracePointer = cairo_image_surface_get_data(self->trace);

	// Do our blending by hand.
	offset = 0;
	while (offset < PREVIEW_HEIGHT * PREVIEW_WIDTH * 4) {
		if (self->trace != NULL) {
			targetPointer[offset] = ((tracePointer[offset] * self->traceImagePercent) + (scaledPointer[offset] * (1.0 - self->traceImagePercent)));
		} else {
			targetPointer[offset] = scaledPointer[offset];
		}
		offset++;
	}

	cairo_surface_mark_dirty(self->target);

	cairo_set_source_surface(cr, self->target, 0, 0);
	cairo_pattern_set_filter(cairo_get_source(cr), CAIRO_FILTER_NEAREST);
	cairo_set_operator(cr, CAIRO_OPERATOR_SOURCE);
	cairo_paint(cr);

	cairo_restore(cr);

	return FALSE;
}


EVENT gboolean drawVectorImageMotionEvent(GtkWidget *widget, GdkEventMotion *event, gpointer userData) {
	VectorDataT *self = (VectorDataT *)userData;
	int x = (int)(event->x * 0.5);
	int y = (int)(event->y * 0.5);
	int temp;
	char buffer[8];

	(void)widget;

	// If we're in a drawing action, bail out of this.
	if (self->clickState != CLICK_NONE) return FALSE;

	// If they're holding the button down, update the point.
	if (event->state & GDK_BUTTON1_MASK) {

		// Did we find it in the text?
		temp = findClosestPoint(self, x, y);
		if (temp >= 0) {
			// Are these values sane?
			if (x < 0) x = 0;
			if (x > 319) x = 319;
			if (y < 0) y = 0;
			if (y > 199) y = 199;
			snprintf(buffer, 8, "%d,%d", x, y);
			SSM(SCI_REPLACETARGET, -1, (sptr_t)buffer);
		}
	}

	return FALSE;
}


EVENT void editorVectorNotify(GtkWidget *sciWidget, gint ctrlID, struct SCNotification *notifyData, gpointer userData) {

	VectorDataT   *self           = (VectorDataT *)userData;
	int            lineNumber     = (int)SSM(SCI_LINEFROMPOSITION, (uptr_t)notifyData->position, (sptr_t)0);
	int            length         = SSM(SCI_GETLENGTH, 0, 0);
	VecByteCodeT   byteCode;

	(void)sciWidget;
	(void)ctrlID;

	//debug("Notification %d\n", notifyData->modificationType);

	// Initialize bytecode data.
	byteCode.bytes      = NULL;
	byteCode.length     = 0;
	byteCode.bufferSize = 0;

	switch (notifyData->nmhdr.code) {
		case SCN_MODIFIED:
			if (notifyData->modificationType & SC_MOD_INSERTTEXT || notifyData->modificationType & SC_MOD_DELETETEXT) {
				// Is it safe to repaint now?
				if (self->allowRedraw) {
					// Allocate space to fetch code from editor.
					utilEnsureBufferSize((unsigned char **)&self->buffer, &self->bufferLength, length);
					// Clear error markers.
					SSM(SCI_MARKERDELETEALL, MARKER_ERROR_ARROW, 0);
					SSM(SCI_MARKERDELETEALL, MARKER_ERROR_HIGHLIGHT, 0);
					// Fetch code.
					SSM(SCI_GETTEXT, length, (sptr_t)self->buffer);
					// Parse code.
					lineNumber = vecparser(self->buffer, &byteCode);
					if (lineNumber >= 0) {
						// Mark lines that fail to parse.
						SSM(SCI_MARKERADD, lineNumber, MARKER_ERROR_ARROW);
						SSM(SCI_MARKERADD, lineNumber, MARKER_ERROR_HIGHLIGHT);
					} else {
#ifdef DEBUG_MODE
						FILE *out = fopen("bytecode.bin", "wb");
						fwrite(byteCode.bytes, byteCode.length, 1, out);
						fclose(out);
#endif
						// All good!
						renderBytecode(&byteCode, self);
					}
				}

				// Release bytecode.
				DEL(byteCode.bytes);
				// Mark text dirty.  SCN_SAVEPOINTLEFT isn't being reliable.
				utilSetDirty((WindowDataT *)self, TRUE);
			}
			break;

		case SCN_MARGINCLICK:
			switch (notifyData->margin) {
				case MARGIN_SCRIPT_FOLD_INDEX:
					SSM(SCI_TOGGLEFOLD, lineNumber, (sptr_t)0);
					break;
			}
			break;
	}
}


EVENT void fileVectorTraceImageFileSet(GtkWidget *object, gpointer userData) {
	VectorDataT *self = (VectorDataT *)userData;
	char        *temp = NULL;

	temp = (char *)gtk_file_chooser_get_filename(GTK_FILE_CHOOSER(object));
	loadTraceImage(self, temp);
	DEL(temp);

	status(self, "Trace image loaded.");

	utilSetDirty((WindowDataT *)self, TRUE);
}


static int findClosestPoint(VectorDataT *self, int x, int y) {
	int closest = -1;
	int distance = INT_MAX;
	int temp;
	int i;
	char buffer[8];

	// Find closest point to mouse pointer.
	for (i = 0; i < arrlen(self->pointList); i++) {
		temp = sqrt(pow(x - self->pointList[i]->x, 2) + pow(y - self->pointList[i]->y, 2));
		if (temp < distance) {
			closest = i;
			distance = temp;
		}
	}

	// Did we find a point to edit?
	if (closest >= 0) {
		// Find first occurrence of it in the text editor.
		snprintf(buffer, 8, "%d,%d", self->pointList[closest]->x, self->pointList[closest]->y);
		SSM(SCI_TARGETWHOLEDOCUMENT, 0, 0);
		SSM(SCI_SETSEARCHFLAGS, SCFIND_NONE, 0);
		temp = SSM(SCI_SEARCHINTARGET, strlen(buffer), (sptr_t)buffer);
		// Select the text.
		SSM(SCI_SETSEL, temp, temp + strlen(buffer));
		return temp;
	}

	return -1;
}

static int getWord(VecByteCodeT *bytecode, int *index) {
	int word;

	word =  bytecode->bytes[*index] << 8;
	*index = *index + 1;
	word += bytecode->bytes[*index];
	*index = *index + 1;

	return word;
}


static void insertCommand(VectorDataT *self, char *command) {
	int   pos;
	int   line;
	int   len;

	// Position Scintilla cursor at the start of the current line.
	pos  = SSM(SCI_GETCURRENTPOS, 0, 0);
	line = SSM(SCI_LINEFROMPOSITION, pos, 0);
	SSM(SCI_GOTOLINE, line, 0);

	// I'm using 'buffer' and don't want a redraw until I'm done.
	self->allowRedraw = FALSE;

	// Collect leading whitespace on this line.
	len = SSM(SCI_GETLINE, line, (sptr_t)NULL);
	utilEnsureBufferSize((unsigned char **)&self->buffer, &self->bufferLength, len + 1);
	SSM(SCI_GETLINE, line, (sptr_t)self->buffer);
	self->buffer[len + 1] = 0;
	pos = 0;
	while (self->buffer[pos] == ' ' || self->buffer[pos] == '\t') {
		pos++;
		if (pos > len - 1) {
			pos = 0;
			break;
		}
	}
	self->buffer[pos] = 0;

	// Insert CR on next line and leading whitespace of previous line.
	line++;
	SSM(SCI_GOTOLINE, line, 0);
	insertText(self, "\n");
	SSM(SCI_GOTOLINE, line, 0);
	insertText(self, self->buffer);
	insertText(self, command);
	insertText(self, " ");

	// Drawing is OK again.
	self->allowRedraw = TRUE;
}


static void insertText(VectorDataT *self, char *text) {
	int pos;

	pos = SSM(SCI_GETCURRENTPOS, 0, 0);
	SSM(SCI_INSERTTEXT, -1, (sptr_t)text);
	pos += strlen(text);
	SSM(SCI_GOTOPOS, pos, 0);
}


static void loadTraceImage(VectorDataT *self, char *filename) {
	int              x;
	int              y;
	int              n;
	size_t           offset;
	unsigned char   *image;
	unsigned char   *pixels;
	cairo_surface_t *temp;
	cairo_t         *cr;

	DEL(self->tracename);
	self->tracename = strdup(filename);
	gtk_file_chooser_set_filename(GTK_FILE_CHOOSER(self->fileVectorTraceImage), filename);

	image = stbi_load(self->tracename, &x, &y, &n, 4);  // Cairo is always 32 bit even with no alpha.  Derp.

	if (image != NULL) {
		// Create Cairo surface the same size as the loaded image.
		temp = cairo_image_surface_create(CAIRO_FORMAT_RGB24, x, y);

		// Copy loaded pixels into Cairo surface.  OF COURSE the pixel order is backwards in Cairo.
		cairo_surface_flush(temp);
		pixels = cairo_image_surface_get_data(temp);
		offset = 0;
		while (offset < x * y * 4 - 4) {  // Not sure why we need the -4 but it keeps us from going one byte off the end of pixels[].
			pixels[offset + 2] = image[offset    ];  // R
			pixels[offset + 1] = image[offset + 1];  // G
			pixels[offset    ] = image[offset + 2];  // B
			pixels[offset + 4] = image[offset + 3];  // A
			offset += 4;
		}
		cairo_surface_mark_dirty(temp);

		// Release loaded image.
		stbi_image_free(image);

		// (Re)create the trace surface we're actually going to display.
		if (self->trace != NULL) cairo_surface_destroy(self->trace);
		self->trace = cairo_image_surface_create(CAIRO_FORMAT_RGB24, PREVIEW_WIDTH, PREVIEW_HEIGHT);

		// Create Cairo context.
		cr = cairo_create(self->trace);

		// Copy and resize image in surface to our trace surface.
		cairo_scale(cr, (double)((double)PREVIEW_WIDTH / (double)x), (double)((double)PREVIEW_HEIGHT / (double)y));
		cairo_set_source_surface(cr, temp, 0, 0);
		cairo_pattern_set_filter(cairo_get_source(cr), CAIRO_FILTER_NEAREST);
		cairo_set_operator(cr, CAIRO_OPERATOR_SOURCE);
		cairo_paint(cr);

		// Clean up.
		cairo_destroy(cr);
		cairo_surface_destroy(temp);
	}
}


static void loadVectorImage(VectorDataT *self) {
	FILE          *in     = NULL;
	char          *line   = NULL;
	size_t         len    = 0;
	size_t         count  = 0;

	in = fopen(self->windowData.filename, "rt");
	if (in != NULL) {
		self->buffer[0] = 0;
		while (getline(&line, &len, in) != -1) {
			switch (count) {
				case 0:  // Version Number
					break;

				case 1: // Trace image name
					line[strlen(line) - 1] = 0;
					if (utilFileExists(line)) {
						loadTraceImage(self, line);
					}
					break;

				case 2: // Separator line
					break;

				default:  // Code for editor
					utilEnsureBufferSize((unsigned char **)&self->buffer, &self->bufferLength, strlen(self->buffer) + strlen(line));
					strcat(self->buffer, line);
					break;
			}
			count++;
		}
		fclose(in);
		DEL(line);
		SSM(SCI_ADDTEXT, strlen(self->buffer), (sptr_t)self->buffer);
		//SSM(SCI_CONVERTEOLS, SC_EOL_CR, 0);
		status(self, "Image loaded.");
		utilSetDirty((WindowDataT *)self, FALSE);  // Do again - loading text marks us dirty.
	} else {
		//***TODO*** Something bad happened.
	}

}


EVENT void menuVectorEditCopy(GtkWidget *object, gpointer userData) {
	VectorDataT *self = (VectorDataT *)userData;

	(void)object;

	SSM(SCI_COPY, 0, 0);
}


EVENT void menuVectorEditCut(GtkWidget *object, gpointer userData) {
	VectorDataT *self = (VectorDataT *)userData;

	(void)object;

	SSM(SCI_CUT, 0, 0);
}


EVENT void menuVectorEditDelete(GtkWidget *object, gpointer userData) {
	VectorDataT *self = (VectorDataT *)userData;

	(void)object;

	SSM(SCI_CLEAR, 0, 0);
}


EVENT void menuVectorEditPaste(GtkWidget *object, gpointer userData) {
	VectorDataT *self = (VectorDataT *)userData;

	(void)object;

	SSM(SCI_PASTE, 0, 0);
}


EVENT void menuVectorFileClose(GtkWidget *object, gpointer userData) {
	VectorDataT *self = (VectorDataT *)userData;

	(void)object;

	gtk_window_close(GTK_WINDOW(self->windowData.window));
}


EVENT void menuVectorFileNew(GtkWidget *object, gpointer userData) {
	VectorDataT *self = (VectorDataT *)userData;

	(void)object;

	if (self->windowData.isDirty == TRUE) {
		if (!utilQuestionDialog(self->windowData.window, "New", "You have unsaved changes.  Start new?")) {
			return;
		}
		status(self, "New image.");
	}

	// Clear editor.
	SSM(SCI_CLEARALL, 0, 0);
	// Clear error markers.
	SSM(SCI_MARKERDELETEALL, MARKER_ERROR_ARROW, 0);
	SSM(SCI_MARKERDELETEALL, MARKER_ERROR_HIGHLIGHT, 0);
	// Reset JoeyLib drawing state.
	jlPaletteDefault(self->jlc);
	jlDrawColorSet(self->jlc, 0);
	jlDrawClear(self->jlc);
	jlDrawColorSet(self->jlc, 15);
	// Destroy trace image, if any.
	if (self->trace != NULL) {
		cairo_surface_destroy(self->trace);
		self->trace = NULL;
		gtk_file_chooser_unselect_all(GTK_FILE_CHOOSER(self->fileVectorTraceImage));
	}
	// Clear any filename.
	DEL(self->windowData.filename);
	// Refresh widget.
	gtk_widget_queue_draw(self->drawVectorImage);
	// Mark clean.
	utilSetDirty((WindowDataT *)self, FALSE);
}


EVENT void menuVectorFileOpen(GtkWidget *object, gpointer userData) {
	VectorDataT   *self   = (VectorDataT *)userData;

	(void)object;

	if (utilFileOpen((WindowDataT *)userData, "*.vic", "Image")) {
		loadVectorImage(self);
	}
}


EVENT void menuVectorFileSave(GtkWidget *object, gpointer userData) {
	VectorDataT *self   = (VectorDataT *)userData;
	int          length = SSM(SCI_GETLENGTH, 0, 0);
	FILE        *out    = NULL;
	char        *code   = NULL;

	// Do we need to save?
	if (self->windowData.isDirty == TRUE) {

		// Do we have a filename?  If not, kick 'em to SaveAs.
		if (self->windowData.filename == NULL) {
			menuVectorFileSaveAs(object, userData);
			return;
		}

		// Allocate space to fetch code from editor.
		code = (char *)malloc(length + 1);
		if (!code) {
			//***TODO*** Something bad happened.
			return;
		}

		// Fetch code.
		SSM(SCI_GETTEXT, length, (sptr_t)code);

		out = fopen(self->windowData.filename, "wt");
		if (out != NULL) {
			// Save!
			fprintf(out, "%s\n", VICTOR_VERSION);
			fprintf(out, "%s\n", (self->tracename != NULL ? self->tracename : ""));
			fprintf(out, "------------------------------------------------------------------------------\n");
			fprintf(out, "%s\n", code);
			fclose(out);
			status(self, "Saved.");
			// We're clean now.
			utilSetDirty((WindowDataT *)self, FALSE);
		} else {
			//***TODO*** Something bad happened.
		}

		// Release code.
		DEL(code);
	}
}


EVENT void menuVectorFileSaveAs(GtkWidget *object, gpointer userData) {

	(void)object;

	if (utilFileSaveAs((WindowDataT *)userData, "*.vic", "Image")) {
		menuVectorFileSave(object, (VectorDataT *)userData);
	}
}


EVENT void menuVectorHelpVector(GtkWidget *object, gpointer userData) {
	(void)object;
	(void)userData;

	gtk_show_uri_on_window(NULL, "https://skunkworks.kangaroopunch.com/skunkworks/joeydev/-/wikis/Victor-Vector-Editor", GDK_CURRENT_TIME, NULL);
}


static void releasePointList(VectorDataT *self) {
	PointT *temp;

	while (arrlen(self->pointList) > 0) {
		temp = self->pointList[0];
		arrdel(self->pointList, 0);
		DEL(temp);
	}
}


static void renderBytecode(VecByteCodeT *bytecode, VectorDataT *self) {
	int        x1;
	int        y1;
	int        x2;
	int        y2;
	int        count;
	int        i;
	float      f1;
	float      f2;
	time_t     startTime;
	GtkWidget *dialog;
	int        index     = 0;
	int       *stack     = NULL;
	PointT    *p         = NULL;

#define GET_BYTE          (bytecode->bytes[index++])
#define GET_WORD          getWord(bytecode, &index)
#define ADD_POINT(px,py)  p=NEW(PointT); p->x=(px); p->y=(py); arrput(self->pointList, p)

	debug("-----------------------------------\n");

	// Clear collected points array.
	releasePointList(self);

	// Reset JoeyLib draw state.
	jlPaletteDefault(self->jlc);
	jlDrawColorSet(self->jlc, 0);
	jlDrawClear(self->jlc);
	jlDrawColorSet(self->jlc, 15);
	jlUtilRandomSeedSet(0);

	startTime = time(NULL);
	self->variables = NULL;

	while (index < bytecode->length && difftime(time(NULL), startTime) < RENDER_TIMEOUT) {
		switch (bytecode->bytes[index++]) {

			case PARSE_NONE:
				// Won't happen, but silences an error.
				break;

			case PARSE_BOX:
				x1 = word(self, GET_WORD);
				y1 = word(self, GET_WORD);
				x2 = word(self, GET_WORD);
				y2 = word(self, GET_WORD);
				sortCoordinates(&x1, &y1, &x2, &y2);
				debug("Box %d,%d to %d,%d\n", x1, y1, x2, y2);
				jlDrawBoxFilled(self->jlc, x1, y1, x2, y2);
				ADD_POINT(x1, y1);
				ADD_POINT(x2, y2);
				break;

			case PARSE_CALL:
				x1 = word(self, GET_WORD);
				arrput(stack, index);
				index = x1;
				debug("Call %d\n", index);
				break;

			case PARSE_CIRCLE:
				y2 = word(self, GET_WORD);
				x1 = word(self, GET_WORD);
				y1 = word(self, GET_WORD);
				debug("Circle %d at %d,%d\n", y2, x1, y1);
				jlDrawCircle(self->jlc, x1, y1, y2);
				ADD_POINT(x1, y1);
				break;

			case PARSE_CLEAR:
				debug("Clear\n");
				jlDrawClear(self->jlc);
				break;

			case PARSE_COLOR:
				x1 = byte(self, GET_BYTE);
				debug("Color %d\n", x1);
				jlDrawColorSet(self->jlc, x1);
				break;

			case PARSE_COMMENT:
				// Won't happen.  Nothing to do!
				break;

			case PARSE_ELLIPSE:
				x1 = word(self, GET_WORD);
				y1 = word(self, GET_WORD);
				x2 = word(self, GET_WORD);
				y2 = word(self, GET_WORD);
				sortCoordinates(&x1, &y1, &x2, &y2);
				debug("Ellipse %d,%d to %d,%d\n", x1, y1, x2, y2);
				jlDrawEllipse(self->jlc, x1, y1, x2, y2);
				ADD_POINT(x1, y1);
				ADD_POINT(x2, y2);
				break;

			case PARSE_FILL:
				x1 = word(self, GET_WORD);
				y1 = word(self, GET_WORD);
				x2 = byte(self, GET_BYTE);
				if (x2 > 15) {
					debug("Fill %d,%d\n", x1, y1);
					jlDrawFill(self->jlc, x1, y1);
				} else {
					debug("Fill %d,%d to %d\n", x1, y1, x2);
					jlDrawFillTo(self->jlc, x1, y1, x2);
				}
				ADD_POINT(x1, y1);
				break;

			case PARSE_GOTO:
				index = word(self, GET_WORD);
				debug("Goto %d\n", index);
				break;

			case PARSE_IF:
				x1 = word(self, GET_WORD); // arg1
				y1 = byte(self, GET_BYTE); // compare
				x2 = word(self, GET_WORD); // arg2
				debug("If %d ", x1);
				y2 = -1;
				switch (y1) {
					case 0:  // ==
						debug("==");
						if (x1 == x2) y2 = 1;
						break;

					case 1:  // !=
						debug("!=");
						if (x1 != x2) y2 = 1;
						break;

					case 2:  // <
						debug("<");
						if (x1 < x2) y2 = 1;
						break;

					case 3:  // >
						debug(">");
						if (x1 > x2) y2 = 1;
						break;

					case 4:  // <=
						debug("<=");
						if (x1 <= x2) y2 = 1;
						break;

					case 5:  // >=
						debug(">=");
						if (x1 >= x2) y2 = 1;
						break;
				}
				debug(" %d ", x2);
				x1 = byte(self, GET_BYTE); // goto/call
				x2 = word(self, GET_WORD); // label
				debug(" %s %d ", (x1 == 0 ? "Goto" : "Call"), x2);
				if (y2 > 0) {
					if (x1 == 1) arrput(stack, index);
					index = x2;
					debug("(true)");
				}
				debug("\n");
				break;

			case PARSE_LABEL:
				break;

			case PARSE_LINE:
				count = word(self, GET_WORD);
				x1    = word(self, GET_WORD);
				y1    = word(self, GET_WORD);
				ADD_POINT(x1, y1);
				debug("Line %d,%d", x1, y1);
				for (i=0; i<count - 1; i++) {
					x2 = word(self, GET_WORD);
					y2 = word(self, GET_WORD);
					debug(" to %d,%d", x2, y2);
					jlDrawLine(self->jlc, x1, y1, x2, y2);
					ADD_POINT(x2, y2);
					x1 = x2;
					y1 = y2;
				}
				debug("\n");
				break;

			case PARSE_MATH:
				x1 = GET_BYTE;  // var index
				y1 = GET_BYTE;  // operand
				x2 = GET_WORD;  // Possible variable
				if (x2 & 0x8000) {
					f2 = variable(self, x2);
				} else {
					f2 = x2;
				}
				x1 &= 0x7f;     // Clear variable flag.
				f1 = variable(self, x1);
				debug("Math:  %d: ", x1);
				switch (y1) {
					case MATH_NONE:
						// Well, none!
						break;

					case MATH_ASSIGN:
						f1 = f2;
						debug("= %f", f2);
						break;

					case MATH_ADD:
						f1 += f2;
						debug("%f + %f", f1, f2);
						break;

					case MATH_SUBTRACT:
						f1 -= f2;
						debug("%f - %f", f1, f2);
						break;

					case MATH_MULTIPLY:
						f1 *= f2;
						debug("%f * %f", f1, f2);
						break;

					case MATH_DIVIDE:
						f1 /= f2;
						debug("%f / %f", f1, f2);
						break;

					case MATH_RND:
						f1 = abs(jlUtilRandom());
						f1 = (int)f1 % (int)f2;
						debug("%f rnd %f", f1, f2);
						break;

					case MATH_MOD:
						f1 = modff(f1, &f2);
						debug("%f mod %f", f1, f2);
						break;

					case MATH_POW:
						f1 = powf(f1, f2);
						debug("%f pow %f", f1, f2);
						break;

					case MATH_SQRT:
						f1 = sqrtf(f2);
						debug("%f sqrt %f", f1, f2);
						break;

					case MATH_ABS:
						f1 = fabsf(f2);
						debug("%f abs %f", f1, f2);
						break;

					case MATH_COS:
						f1 = cosf(f2);
						debug("%f cos %f", f1, f2);
						break;

					case MATH_SIN:
						f1 = sinf(f2);
						debug("%f sin %f", f1, f2);
						break;

					case MATH_TAN:
						f1 = tanf(f2);
						debug("%f tan %f", f1, f2);
						break;
				}
				debug("\n");
				// Make sure we have enough slots for this variable.
				while (arrlen(self->variables) <= x1) {
					arrput(self->variables, 0.0f);
				}
				self->variables[x1] = f1;
				break;

			case PARSE_PALETTE:
				x1 = byte(self, GET_BYTE);
				x2 = byte(self, GET_BYTE);
				y1 = byte(self, GET_BYTE);
				y2 = byte(self, GET_BYTE);
				debug("Palette %d as %d,%d,%d\n", x1, x2, y1, y2);
				jlPaletteSet(self->jlc, x1, x2, y1, y2);
				break;

			case PARSE_PLOT:
				x1 = word(self, GET_WORD);
				y1 = word(self, GET_WORD);
				debug("Plot %d,%d\n", x1, y1);
				jlDrawPixelSet(self->jlc, x1, y1);
				ADD_POINT(x1, y1);
				break;

			case PARSE_RECTANGLE:
				x1 = word(self, GET_WORD);
				y1 = word(self, GET_WORD);
				x2 = word(self, GET_WORD);
				y2 = word(self, GET_WORD);
				sortCoordinates(&x1, &y1, &x2, &y2);
				debug("Rectangle %d,%d to %d,%d\n", x1, y1, x2, y2);
				jlDrawBox(self->jlc, x1, y1, x2, y2);
				ADD_POINT(x1, y1);
				ADD_POINT(x2, y2);
				break;

			case PARSE_RETURN:
				index = arrpop(stack);
				debug("Return %d\n", index);
				break;

		} // switch
	} // while

	debug("Points: %ld\n", arrlen(self->pointList));

	// Clear variables.
	ARRFREE(self->variables);

	// Clear stack.
	ARRFREE(stack);

	// Refresh widget.
	gtk_widget_queue_draw(self->drawVectorImage);

	// Did execution time out?
	if (index < bytecode->length) {
		dialog = gtk_message_dialog_new(
				GTK_WINDOW(self->windowData.window),
				GTK_DIALOG_DESTROY_WITH_PARENT | GTK_DIALOG_MODAL,
				GTK_MESSAGE_WARNING,
				GTK_BUTTONS_OK,
				"Rendering is taking too long!  Stopped.\n\n(Did you create an infinite loop?)");
		gtk_window_set_title(GTK_WINDOW(dialog), "Notice");
		gtk_dialog_run(GTK_DIALOG(dialog));
		gtk_widget_destroy(dialog);
	}
}


EVENT void scaleVectorTraceImageValueChanged(GtkWidget *object, gpointer userData) {
	VectorDataT *self = (VectorDataT *)userData;

	(void)object;

	self->traceImagePercent = gtk_range_get_value(GTK_RANGE(object)) * 0.01;

	// Refresh widget.
	gtk_widget_queue_draw(self->drawVectorImage);
}


static void sortCoordinates(int *x1, int *y1, int *x2, int *y2) {
	int temp;

	if (*x2 < *x1) {
		temp = *x1;
		*x1  = *x2;
		*x2  = temp;
	}

	if (*y2 < *y1) {
		temp = *y1;
		*y1  = *y2;
		*y2  = temp;
	}
}


static void status(VectorDataT *self, char *message) {
	gtk_statusbar_remove_all(GTK_STATUSBAR(self->statusBar), self->statusBarId);
	gtk_statusbar_push(GTK_STATUSBAR(self->statusBar), self->statusBarId, message);
}


EVENT void toolBoxClicked(GtkToolButton *object, gpointer userData) {
	VectorDataT *self = (VectorDataT *)userData;

	(void)object;

	insertCommand(self, "box");
	self->clickState = CLICK_BOX_1;
	status(self, "Box.  Select first corner.");
}


EVENT void toolCircleClicked(GtkToolButton *object, gpointer userData) {
	VectorDataT *self = (VectorDataT *)userData;

	(void)object;

	insertCommand(self, "circle");
	self->clickState = CLICK_CIRCLE_1;
	status(self, "Circle.  Select center.");
}


EVENT void toolColorClicked(GtkToolButton *object, gpointer userData) {
	VectorDataT   *self          = (VectorDataT *)userData;
	GtkWidget     *colorDialog;
	GtkWidget     *paletteDialog;
	ColorT         colors[16];
	char           temp[32];
	int            response;
	int            selected;

	(void)object;

	// Load the current JoeyLib palette into the dialog.
	for (selected=0; selected<16; selected++) {
		jlPaletteGet(self->jlc, selected, &colors[selected].red, &colors[selected].green, &colors[selected].blue);
	}

	// Build the dialog.
	colorDialog = color_dialog_new(self->windowData.window, "Select Color", colors);

	// Run the dialog.
	do {
		response = gtk_dialog_run(GTK_DIALOG(colorDialog));
		selected = color_dialog_get_selected(colorDialog);
	} while (selected == -1 && response != GTK_RESPONSE_CANCEL);

	// They want to edit a color.
	if (response == COLOR_RESPONSE_EDIT) {
		paletteDialog = palette_dialog_new(colorDialog, "Edit Palette Entry", colors[selected].red, colors[selected].green, colors[selected].blue);
		if (gtk_dialog_run(GTK_DIALOG(paletteDialog)) == GTK_RESPONSE_OK) {
			palette_dialog_get_color(paletteDialog, &colors[selected].red, &colors[selected].green, &colors[selected].blue);
			// Write our new palette entry into the editor.
			snprintf(temp, 32, "%d as %d,%d,%d", selected, colors[selected].red, colors[selected].green, colors[selected].blue);
			insertCommand(self, "palette");
			insertText(self, temp);
		}
		gtk_widget_destroy(paletteDialog);
	}

	// They selected a color.
	if (response == GTK_RESPONSE_OK) {
		if (selected >= 0) {
			// Write our selected color into the editor.
			snprintf(temp, 4, " %d", selected);
			insertCommand(self, "color");
			insertText(self, temp);
		}
	}

	gtk_widget_destroy(colorDialog);
}


EVENT void toolEllipseClicked(GtkToolButton *object, gpointer userData) {
	VectorDataT *self = (VectorDataT *)userData;

	(void)object;

	insertCommand(self, "ellipse");
	self->clickState = CLICK_ELLIPSE_1;
	status(self, "Ellipse.  Select first corner.");
}


EVENT void toolFillClicked(GtkToolButton *object, gpointer userData) {
	VectorDataT *self = (VectorDataT *)userData;

	(void)object;

	insertCommand(self, "fill");
	self->clickState = CLICK_FILL_1;
	status(self, "Fill.  Select fill location.");
}


EVENT void toolLineClicked(GtkToolButton *object, gpointer userData) {
	VectorDataT *self = (VectorDataT *)userData;

	(void)object;

	insertCommand(self, "line");
	self->clickState = CLICK_LINE_1;
	status(self, "Line.  Select first point.");
}


EVENT void toolPlotClicked(GtkToolButton *object, gpointer userData) {
	VectorDataT *self = (VectorDataT *)userData;

	(void)object;

	insertCommand(self, "plot");
	self->clickState = CLICK_PLOT_1;
	status(self, "Plot.  Select location.");
}


EVENT void toolRectangleClicked(GtkToolButton *object, gpointer userData) {
	VectorDataT *self = (VectorDataT *)userData;

	(void)object;

	insertCommand(self, "rectangle");
	self->clickState = CLICK_RECTANGLE_1;
	status(self, "Rectangle.  Select first corner.");
}


static float variable(VectorDataT *self, unsigned char byte) {
	int value;

	// Clear possibly set variable flag.
	byte &= 0x7f;

	// Look up variable and return value.
	if (byte < arrlen(self->variables)) {
		value = self->variables[byte];
	} else {
		// We don't know this variable yet.  Be like BASIC.
		value = 0;
	}
	return value;
}


static int word(VectorDataT *self, unsigned short word) {
	int value;
	// Decode our weird word format, look up variables, and return sane numbers.
	// Is this a variable?
	if (word & 0x8000) {
		value = variable(self, word);
	} else {
		// Is it negative?
		if (word & 0x4000) {
			// Clear the negative flag and actually negate it.
			value = 0 - (word & 0xbfff);
		} else {
			// Just a boring number, copy it out.
			value = word;
		}
	}
	return value;
}


EVENT gboolean winVectorClose(GtkWidget *object, gpointer userData) {
	// userData is not reliable due to menuVectorFileClose and util indirectly calling us.
	VectorDataT *self = (VectorDataT *)utilGetWindowData(object);

	(void)userData;

	if (self->windowData.isDirty == TRUE) {
		if (utilQuestionDialog(self->windowData.window, "Exit", "You have unsaved changes.  Exit?")) {
			winVectorDelete(self);
			return FALSE;
		}
		return TRUE;
	}

	winVectorDelete(self);
	return FALSE;
}


void winVectorCreate(char *filename) {
	VectorDataT      *self;
	char             *widgetNames[]  = {
			"winVector",
			"boxVectorForEditor",
			"drawVectorImage",
			"fileVectorTraceImage",
			"status",
			NULL
	};
	GtkWidget       **widgets[]      = {
			NULL,
			NULL,
			NULL,
			NULL,
			NULL
	};

	// Set up instance data.
	self = NEW(VectorDataT);
	self->windowData.closeWindow = winVectorClose;
	self->allowRedraw            = TRUE;

	// Set up working buffer.
	utilEnsureBufferSize((unsigned char **)&self->buffer, &self->bufferLength, 1024);

	// Load widgets from XML.
	widgets[0] = &self->windowData.window;
	widgets[1] = &self->boxVectorForEditor;
	widgets[2] = &self->drawVectorImage;
	widgets[3] = &self->fileVectorTraceImage;
	widgets[4] = &self->statusBar;
	utilGetWidgetsFromMemory("/com/kangaroopunch/joeydev/Vector.glade", widgetNames, widgets, self);

	// Get status bar context ID.
	self->statusBarId = gtk_statusbar_get_context_id(GTK_STATUSBAR(self->statusBar), "JoeyDev");

	// Add our custom icons.
	gtk_icon_theme_add_resource_path(gtk_icon_theme_get_for_screen(gdk_screen_get_default()), "/com/kangaroopunch/joeydev/icons");

	// Add missing event to drawVectorImage
	gtk_widget_add_events(self->drawVectorImage, GDK_BUTTON_PRESS_MASK | GDK_POINTER_MOTION_MASK);
	g_signal_connect(G_OBJECT(self->drawVectorImage), "button-press-event", G_CALLBACK(drawVectorImageClick), self);

	// Create Scintilla editor.
	self->editor = scintilla_new();
	self->sci    = SCINTILLA(self->editor);
	self->id     = _nextEditorId++;
	scintilla_set_id(self->sci, self->id);
	gtk_widget_set_halign(self->editor, GTK_ALIGN_FILL);
	gtk_widget_set_valign(self->editor, GTK_ALIGN_FILL);
	gtk_widget_set_hexpand(self->editor, TRUE);
	gtk_widget_set_vexpand(self->editor, TRUE);
	gtk_box_set_child_packing(GTK_BOX(self->boxVectorForEditor), self->editor, TRUE, TRUE, 0, GTK_PACK_START);
	gtk_container_add(GTK_CONTAINER(self->boxVectorForEditor), self->editor);

	// Configure editor.
	SSM(SCI_SETCODEPAGE, SC_CP_UTF8, 0);
	SSM(SCI_SETIMEINTERACTION, SC_IME_WINDOWED, 0);
	SSM(SCI_STYLESETCHARACTERSET, STYLE_DEFAULT, SC_CHARSET_DEFAULT);
	SSM(SCI_STYLESETFONT, STYLE_DEFAULT, (sptr_t)"Monospace");
	SSM(SCI_STYLESETSIZEFRACTIONAL, STYLE_DEFAULT, 11 * SC_FONT_SIZE_MULTIPLIER);
	SSM(SCI_STYLESETFORE, STYLE_DEFAULT, 0xFFFFFF);
	SSM(SCI_STYLESETBACK, STYLE_DEFAULT, 0);
	SSM(SCI_STYLECLEARALL, 0, 0);
	SSM(SCI_SETTABWIDTH, 3, 0);
	//SSM(SCI_SETEOLMODE, SC_EOL_CR, 0);
	SSM(SCI_SETMARGINWIDTHN, 0, (int)SSM(SCI_TEXTWIDTH, STYLE_LINENUMBER, (sptr_t)"_99999"));
	SSM(SCI_SETMARGINWIDTHN, 1, 16);
	SSM(SCI_SETWRAPMODE, SC_WRAP_NONE, 0);
	SSM(SCI_SETCARETSTYLE, CARETSTYLE_BLOCK | CARETSTYLE_OVERSTRIKE_BLOCK, 0);
	SSM(SCI_SETCARETFORE, 0x00ffff, 0);
	SSM(SCI_STYLESETBACK, STYLE_LINENUMBER, 0x222222);

	/*
	// Add "indent" (no language) lexer for basic folding support.
	self->pLexer = CreateLexer("cpp");
	SSM(SCI_SETILEXER, 0, (sptr_t)self->pLexer);

	// Syntax highlighting.  Not really C but it works.
	SSM(SCI_SETKEYWORDS, 0, (sptr_t)"at call circle color end fill include line to sub var");
	SSM(SCI_STYLESETFORE, SCE_CAML_KEYWORD, 0x777777);  // Not right - trying to find the property that is making keywords blue!
	SSM(SCI_STYLESETFORE, SCE_C_PREPROCESSOR, 0x0080ff);
	SSM(SCI_STYLESETFORE, SCE_C_COMMENT, 0x00FF00);
	SSM(SCI_STYLESETFORE, SCE_C_COMMENTLINE, 0x00FF00);
	SSM(SCI_STYLESETFORE, SCE_C_NUMBER, 0xFFFF00);
	SSM(SCI_STYLESETFORE, SCE_C_WORD, 0xFF0000);
	SSM(SCI_STYLESETFORE, SCE_C_STRING, 0xFF00FF);
	SSM(SCI_STYLESETBOLD, SCE_C_OPERATOR, 1);
	 */

	// Margin markers.
	SSM(SCI_MARKERDEFINE, MARKER_ERROR_ARROW, SC_MARK_SHORTARROW);  // Error
	SSM(SCI_MARKERSETBACK, MARKER_ERROR_ARROW, 255 | (0 << 8) | (0 << 16));  // RGB
	SSM(SCI_MARKERDEFINE, MARKER_ERROR_HIGHLIGHT, SC_MARK_BACKGROUND);  // Error
	SSM(SCI_MARKERSETBACK, MARKER_ERROR_HIGHLIGHT, 127 | (0 << 8) | (0 << 16));  // RGB

	// Debug
	/*
	SSM(SCI_INSERTTEXT, 0, (sptr_t)
			"goto red\n\n"
			"yellow:\n"
			"\t%yellow = %red\n"
			"\t%yellow - 1\n"
			"\tcolor %yellow\n"
			"\tline 27,22 to 289,24 to 297,169 to 27,166 to 27,23\n"
			"\tcolor 11\n"
			"\tcircle 50 at 159,95\n"
			"\tgoto end\n\n"
			"red:\n"
			"\t%red = 15\n"
			"\tpalette %red as 15,0,0\n"
			"\tcolor %red\n"
			"\tbox 0,0 to 319,199\n"
			"\tgoto yellow\n\n"
			"end:\n"
	);
	 */

	// Connect editor to our code.
	g_signal_connect(G_OBJECT(self->editor), "sci-notify", G_CALLBACK(editorVectorNotify), self);

	// Create our drawing surface and context.
	self->surface           = cairo_image_surface_create(CAIRO_FORMAT_RGB24, 320, 200);
	self->scaled            = cairo_image_surface_create(CAIRO_FORMAT_RGB24, PREVIEW_WIDTH, PREVIEW_HEIGHT);
	self->trace             = NULL;
	self->target            = cairo_image_surface_create(CAIRO_FORMAT_RGB24, PREVIEW_WIDTH, PREVIEW_HEIGHT);
	cairo_surface_flush(self->surface);
	self->jlc               = jlContextNew(cairo_image_surface_get_data(self->surface));
	self->traceImagePercent = 50  * 0.01;

	// Register window & show it.
	utilWindowRegister(self);
	gtk_widget_show_all(self->windowData.window);

	status(self, "Welcome to the Victor Vector Editor!");

	if (filename != NULL) {
		self->windowData.filename = strdup(filename);
		loadVectorImage(self);
	}
}


static void winVectorDelete(gpointer userData) {
	VectorDataT *self = (VectorDataT *)userData;

	// Scintilla keeps sending events after we delete things it expects to still exist.  Prevent that.
	g_signal_handlers_disconnect_by_func(G_OBJECT(self->editor), G_CALLBACK(editorVectorNotify), self);

	utilWindowUnRegister(userData);

	jlContextDel(&self->jlc);
	cairo_surface_destroy(self->surface);
	cairo_surface_destroy(self->scaled);
	cairo_surface_destroy(self->target);
	if (self->trace != NULL) cairo_surface_destroy(self->trace);
	releasePointList(self);
	DEL(self->tracename);
	DEL(self->buffer);

	DEL(self);
}


#pragma clang diagnostic pop
