/*
 *  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.
*/


#include "common.h"
#include "editor.h"
#include "utils.h"
#include "scintillaHeaders.h"
#include "messages.h"


#define MARKER_ERROR_ARROW       0
#define MARKER_ERROR_HIGHLIGHT   1


typedef struct EditorDataS {
	WindowDataT      windowData;
	GtkWidget        *boxForEditor;
	GtkWidget        *statusBar;
	GtkWidget        *editor;
	ScintillaObject  *sci;
	void             *pLexer;
	int               id;
	guint             statusBarId;
} EditorDataT;


static int _nextEditorId = 0;


static void     clearEditor(EditorDataT *self);
EVENT  void     editorEditorNotify(GtkWidget *sciWidget, gint ctrlID, struct SCNotification *notifyData, gpointer userData);
static void     loadEditor(EditorDataT *self);
static void     loadEditorConfig(char *lexer, EditorDataT *self);
EVENT  void     menuEditorEditCopy(GtkWidget *object, gpointer userData);
EVENT  void     menuEditorEditCut(GtkWidget *object, gpointer userData);
EVENT  void     menuEditorEditDelete(GtkWidget *object, gpointer userData);
EVENT  void     menuEditorEditPaste(GtkWidget *object, gpointer userData);
EVENT  void     menuEditorFileClose(GtkWidget *object, gpointer userData);
EVENT  void     menuEditorFileNew(GtkWidget *object, gpointer userData);
EVENT  void     menuEditorFileOpen(GtkWidget *object, gpointer userData);
EVENT  void     menuEditorFileSave(GtkWidget *object, gpointer userData);
EVENT  void     menuEditorFileSaveAs(GtkWidget *object, gpointer userData);
EVENT  void     menuEditorHelpEditor(GtkWidget *object, gpointer userData);
static void     status(EditorDataT *self, char *message);
EVENT  gboolean winEditorClose(GtkWidget *object, gpointer userData);
static void     winEditorDelete(gpointer userData);
static void     writeEditorConfig(char *lexer, EditorDataT *self);


static void clearEditor(EditorDataT *self) {
	(void)self;

	// Clear editor.
	SSM(SCI_CLEARALL, 0, 0);

	// Clear error markers.
	SSM(SCI_MARKERDELETEALL, MARKER_ERROR_ARROW, 0);
	SSM(SCI_MARKERDELETEALL, MARKER_ERROR_HIGHLIGHT, 0);
}


EVENT void editorEditorNotify(GtkWidget *sciWidget, gint ctrlID, struct SCNotification *notifyData, gpointer userData) {
	EditorDataT *self       = (EditorDataT *)userData;
	int          lineNumber = (int)SSM(SCI_LINEFROMPOSITION, (uptr_t)notifyData->position, (sptr_t)0);

	(void)sciWidget;
	(void)ctrlID;

	switch (notifyData->nmhdr.code) {
		case SCN_MODIFIED:
			if (notifyData->modificationType & SC_MOD_INSERTTEXT || notifyData->modificationType & SC_MOD_DELETETEXT) {
				// 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;

	}
}


static void loadEditor(EditorDataT *self) {
	FILE    *in     = NULL;
	char    *line   = NULL;
	size_t   len    = 0;

	in = fopen(self->windowData.filename, "rt");
	if (in != NULL) {
		utilEnsureBufferSize((unsigned char **)&line, (int *)&len, 1024);  // Not technically needed, but fixes a pointer warning from memmaker.
		while (getline(&line, &len, in) != -1) {
			SSM(SCI_ADDTEXT, strlen(line), (sptr_t)line);
		}
		fclose(in);
		DEL(line);
	}

	// Do again - loading text marks us dirty.
	utilSetDirty((WindowDataT *)self, FALSE);
}


static void loadEditorConfig(char *lexer, EditorDataT *self) {
	char         *config = NULL;
	FILE         *in     = NULL;
	char         *line   = NULL;
	size_t        len    = 0;
	char         *c      = NULL;
	char         *type   = NULL;
	char         *name   = NULL;
	unsigned int  number;
	unsigned int  integer;

	config = utilCreateString("%s%cjoeydev%ceditor-%s.conf", g_get_user_config_dir(), UTIL_PATH_CHAR, UTIL_PATH_CHAR, lexer);
	if (!utilFileExists(config)) {
		// Nothing to load.
		DEL(config);
		return;
	}

	in = fopen(config, "rt");
	DEL(config);

	if (in) {
		// Load config.
		utilEnsureBufferSize((unsigned char **)&line, (int *)&len, 4096);  // Not technically needed, but fixes a pointer warning from memmaker.
		while (getline(&line, &len, in) != -1) {
			if (strlen(line) > 0) line[strlen(line) - 1] = 0;
			c = utilGetToken(line, " ", "\"", "\"");
			utilDequote(c);

			// Is this a 'style' line?
			if (strcasecmp(c, "style") == 0) {
				// style number "tags" "description" ... (style keywords)
				c = utilGetToken(NULL, " ", "\"", "\"");
				number = strtol(c, NULL, 10);
				utilGetToken(NULL, " ", "\"", "\"");
				utilGetToken(NULL, " ", "\"", "\"");
				c = utilGetToken(NULL, " ", "\"", "\"");
				do {
					if (strcmp(c, "fore") == 0) {
						c = utilGetToken(NULL, " ", "\"", "\"");
						integer = strtol(c, NULL, 16);
						SSM(SCI_STYLESETFORE, number, integer);
					}
					if (strcmp(c, "back") == 0) {
						c = utilGetToken(NULL, " ", "\"", "\"");
						integer = strtol(c, NULL, 16);
						SSM(SCI_STYLESETBACK, number, integer);
					}
					if (strcmp(c, "bold") == 0) {
						SSM(SCI_STYLESETBOLD, number, 1);
					}
					if (strcmp(c, "italic") == 0) {
						SSM(SCI_STYLESETITALIC, number, 1);
					}
					c = utilGetToken(NULL, " ", "\"", "\"");
				} while (c != NULL);
				continue;
			}

			// Is this a 'property' line?
			if (strcasecmp(c, "property") == 0) {
				// property type name "description" value
				c = utilGetToken(NULL, " ", "\"", "\"");
				type = strdup(c);
				c = utilGetToken(NULL, " ", "\"", "\"");
				name = strdup(c);
				utilGetToken(NULL, " ", "\"", "\"");
				c = utilGetToken(NULL, " ", "\"", "\"");
				utilDequote(c);
				SSM(SCI_SETPROPERTY, (sptr_t)name, (sptr_t)c);
				DEL(name);
				DEL(type);
				continue;
			}

			// Is this a 'keywords' line?
			if (strcasecmp(c, "keywords") == 0) {
				// keywords number "description" wordlist
				c = utilGetToken(NULL, " ", "\"", "\"");
				number = strtol(c, NULL, 10);
				utilGetToken(NULL, " ", "\"", "\"");
				c = utilGetToken(NULL, "\n", "\"", "\"");
				if (c && strlen(c) > 0) SSM(SCI_SETKEYWORDS, number, (sptr_t)c);
				continue;
			}
		}
		DEL(line);
		fclose(in);
	}

}


EVENT void menuEditorEditCopy(GtkWidget *object, gpointer userData) {
	EditorDataT *self = (EditorDataT *)userData;

	(void)object;

	SSM(SCI_COPY, 0, 0);
}


EVENT void menuEditorEditCut(GtkWidget *object, gpointer userData) {
	EditorDataT *self = (EditorDataT *)userData;

	(void)object;

	SSM(SCI_CUT, 0, 0);
}


EVENT void menuEditorEditDelete(GtkWidget *object, gpointer userData) {
	EditorDataT *self = (EditorDataT *)userData;

	(void)object;

	SSM(SCI_CLEAR, 0, 0);
}


EVENT void menuEditorEditPaste(GtkWidget *object, gpointer userData) {
	EditorDataT *self = (EditorDataT *)userData;

	(void)object;

	SSM(SCI_PASTE, 0, 0);
}


EVENT void menuEditorFileClose(GtkWidget *object, gpointer userData) {
	EditorDataT *self = (EditorDataT *)userData;

	(void)object;

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


EVENT void menuEditorFileNew(GtkWidget *object, gpointer userData) {
	EditorDataT *self = (EditorDataT *)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.");
	}

	clearEditor(self);

	// Clear any filename.
	DEL(self->windowData.filename);

	// Mark clean.
	utilSetDirty((WindowDataT *)self, FALSE);
}


EVENT void menuEditorFileOpen(GtkWidget *object, gpointer userData) {
	EditorDataT   *self   = (EditorDataT *)userData;

	(void)object;

	if (utilFileOpen((WindowDataT *)userData, "*.*", "Code")) {
		clearEditor(self);
		loadEditor(self);
	}
}


EVENT void menuEditorFileSave(GtkWidget *object, gpointer userData) {
	EditorDataT *self   = (EditorDataT *)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) {
			menuEditorFileSaveAs(object, userData);
			return;
		}

		// Allocate space to fetch code from editor.
		code = (char *)malloc(length + 1);
		if (!code) {
			message(MSG_SEVERE, "Unable to allocate code buffer during editor save!");
			return;
		}

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

		out = fopen(self->windowData.filename, "wt");
		if (out != NULL) {
			// Save!
			fprintf(out, "%s\n", code);
			fclose(out);
			status(self, "Saved.");
			// We're clean now.
			utilSetDirty((WindowDataT *)self, FALSE);
		} else {
			message(MSG_SEVERE, "Unknown error attempting to save %s!", self->windowData.filename);
		}

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


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

	(void)object;

	if (utilFileSaveAs((WindowDataT *)userData, "*.*", "Code")) {
		menuEditorFileSave(object, (EditorDataT *)userData);
	}
}


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

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


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


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

	(void)userData;

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

	winEditorDelete(self);
	return FALSE;
}


void winEditorCreate(char *filename) {
	EditorDataT  *self;
	char         *widgetNames[]  = {
			"winEditor",
			"boxEditorForEditor",
			"statusEditor",
			NULL
	};
	GtkWidget   **widgets[]      = {
			NULL,
			NULL,
			NULL
	};

	//***TODO*** Be sure this is something we can edit as text.

	// Set up instance data.
	self = NEW(EditorDataT);
	self->windowData.closeWindow = winEditorClose;

	// Load widgets from XML.
	widgets[0] = &self->windowData.window;
	widgets[1] = &self->boxForEditor;
	widgets[2] = &self->statusBar;
	utilGetWidgetsFromMemory("/com/kangaroopunch/joeydev/Editor.glade", widgetNames, widgets, self);

	// Register window.
	utilWindowRegister(self);

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

	// 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->boxForEditor), self->editor, TRUE, TRUE, 0, GTK_PACK_START);
	gtk_container_add(GTK_CONTAINER(self->boxForEditor), 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);

	SSM(SCI_SETFOLDMARGINCOLOUR, 1, 0x222222);
	SSM(SCI_SETFOLDMARGINHICOLOUR, 1, 0x222222);
	SSM(SCI_SETMARGINTYPEN, MARGIN_SCRIPT_FOLD_INDEX, SC_MARGIN_SYMBOL);
	SSM(SCI_SETMARGINMASKN, MARGIN_SCRIPT_FOLD_INDEX, SC_MASK_FOLDERS);
	SSM(SCI_SETMARGINWIDTHN, MARGIN_SCRIPT_FOLD_INDEX, 20);
	SSM(SCI_SETMARGINSENSITIVEN, MARGIN_SCRIPT_FOLD_INDEX, 1);
	SSM(SCI_MARKERDEFINE, SC_MARKNUM_FOLDEROPEN, SC_MARK_BOXMINUS);
	SSM(SCI_MARKERDEFINE, SC_MARKNUM_FOLDER, SC_MARK_BOXPLUS);
	SSM(SCI_MARKERDEFINE, SC_MARKNUM_FOLDERSUB, SC_MARK_VLINE);
	SSM(SCI_MARKERDEFINE, SC_MARKNUM_FOLDERTAIL, SC_MARK_LCORNER);
	SSM(SCI_MARKERDEFINE, SC_MARKNUM_FOLDEREND, SC_MARK_BOXPLUSCONNECTED);
	SSM(SCI_MARKERDEFINE, SC_MARKNUM_FOLDEROPENMID, SC_MARK_BOXMINUSCONNECTED);
	SSM(SCI_MARKERDEFINE, SC_MARKNUM_FOLDERMIDTAIL, SC_MARK_TCORNER);
	SSM(SCI_SETFOLDFLAGS, SC_FOLDFLAG_LINEAFTER_CONTRACTED , 0);

	// 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

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

	writeEditorConfig("cpp", self);
	loadEditorConfig("cpp", self);

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

	// Show window.
	gtk_widget_show_all(self->windowData.window);

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


static void winEditorDelete(gpointer userData) {
	EditorDataT *self = (EditorDataT *)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(editorEditorNotify), self);

	utilWindowUnRegister(userData);

	DEL(self);
}


static void writeEditorConfig(char *lexer, EditorDataT *self) {
	char *name        = NULL;
	char *tags        = NULL;
	char *description = NULL;
	char *config      = NULL;
	FILE *out         = NULL;
	int   result;
	int   size;
	int   x;

	config = utilCreateString("%s%cjoeydev%ceditor-%s.conf", g_get_user_config_dir(), UTIL_PATH_CHAR, UTIL_PATH_CHAR, lexer);
	if (utilFileExists(config)) {
		// Don't clobber existing files.
		DEL(config);
		return;
	}

	out = fopen(config, "wt");
	DEL(config);

	if (out) {
		// Header
		fprintf(out, "%s\n", LEXER_VERSION);
		fprintf(out, "------------------------------------------------------------------------------\n");

		// Styles
		result = SSM(SCI_GETNAMEDSTYLES, 0, 0);
		if (result > 0) {
			for (x = 0; x < result; x++) {
				size = SSM(SCI_NAMEOFSTYLE, x, 0);
				name = (char *)malloc(size + 1);
				SSM(SCI_NAMEOFSTYLE, x, (sptr_t)name);
				size = SSM(SCI_TAGSOFSTYLE, x, 0);
				tags = (char *)malloc(size + 1);
				SSM(SCI_TAGSOFSTYLE, x, (sptr_t)tags);
				size = SSM(SCI_DESCRIPTIONOFSTYLE, x, 0);
				description = (char *)malloc(size + 1);
				SSM(SCI_DESCRIPTIONOFSTYLE, x, (sptr_t)description);
				if (strlen(tags) > 0) {
					fprintf(out, "style %d \"%s\" \"%s\" fore 0xffffff\n", x, tags, description);
				}
				DEL(description);
				DEL(tags);
				DEL(name);
			}
		}
		fprintf(out, "\n");

		// Properties
		size = SSM(SCI_PROPERTYNAMES, 0, 0);
		name = (char *)malloc(size + 1);
		SSM(SCI_PROPERTYNAMES, 0, (sptr_t)name);
		tags = strtok(name, "\n");
		do {
			size = SSM(SCI_DESCRIBEPROPERTY, (sptr_t)tags, 0);
			description = (char *)malloc(size + 1);
			SSM(SCI_DESCRIBEPROPERTY, (sptr_t)tags, (sptr_t)description);
			result = SSM(SCI_PROPERTYTYPE, (sptr_t)tags, 0);
			fprintf(out, "property ");
			switch (result) {
				case SC_TYPE_BOOLEAN:
					fprintf(out, "boolean");
					size = SSM(SCI_GETPROPERTYINT, (sptr_t)tags, 0);
					config = utilCreateString("%d", size);
					break;

				case SC_TYPE_STRING:
					fprintf(out, "string");
					//***TODO*** This doesn't appear to be working.
					size = SSM(SCI_GETPROPERTY, (sptr_t)tags, 0);
					config = (char *)malloc(size + 1);
					SSM(SCI_GETPROPERTY, (sptr_t)tags, (sptr_t)config);
					break;

					case SC_TYPE_INTEGER:
					fprintf(out, "integer");
					size = SSM(SCI_GETPROPERTYINT, (sptr_t)tags, 0);
					config = utilCreateString("%d", size);
					break;

				default:
					message(MSG_SEVERE, "Unknown property type saving editor config!");
					break;
			}
			fprintf(out, " %s \"%s\" %s\n", tags, description, config);
			DEL(config);
			DEL(description);
			tags = strtok(NULL, "\n");
		} while (tags != NULL);
		DEL(name);
		fprintf(out, "\n");

		// Keyword Sets
		size = SSM(SCI_DESCRIBEKEYWORDSETS, 0, 0);
		name = (char *)malloc(size + 1);
		SSM(SCI_DESCRIBEKEYWORDSETS, 0, (sptr_t)name);
		tags = strtok(name, "\n");
		size = 0;
		do {
			fprintf(out, "keywords %d \"%s\"\n", size, tags);
			size++;
			tags = strtok(NULL, "\n");
		} while (tags != NULL);
		DEL(name);

		fclose(out);
	}
}
