/*
 *  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 "cppcoreguidelines-narrowing-conversions"


#include "common.h"
#include "project.h"
#include "cwalk.h"
#include "utils.h"
#include "http.h"
#include "ssh.h"


enum ProjectColumnsE {
	COL_FILENAME = 0,
	COL_COUNT
};
typedef enum ProjectColumnsE ProjectColumnsT;

enum ProjectSectionTypeE {
	SECTION_HEADER = 0,
	SECTION_CODE,
	SECTION_BITMAP,
	SECTION_STENCIL,
	SECTION_VECTOR,
	SECTION_SOUND,
	SECTION_MUSIC,
	SECTION_RAW_DATA,
	SECTION_COOKED_DATA,
	SECTION_COUNT
};
typedef enum ProjectSectionTypeE ProjectSectionTypeT;


typedef struct ArchS {
	char     *name;
	gboolean  selected;
} ArchT;

typedef struct TargetS {
	char   *name;
	char   *longName;
	ArchT **archs;
} TargetT;

typedef struct ProjectDataS {
	WindowDataT      windowData;
	GtkWidget       *treeProject;
	char            *configName;
	char            *buildHost;
	int              buildHTTPPort;
	int              buildSSHPort;
	char            *buildUser;
	char            *buildPassword;
	TargetT        **targets;
} ProjectDataT;

typedef struct SectionDataS {
	char *name;
	char *extension;
} SectionDataT;


static SectionDataT _sectionData[] = {
		{ "Header",   "*.h"   },
		{ "Code",     "*.c"   },
		{ "Bitmap",   "*.img" },
		{ "Stencil",  "*.stn" },
		{ "Vector",   "*.vic" },
		{ "Sound",    "*.snd" },
		{ "Music",    "*.mod" },
		{ "Raw Data",    NULL },
		{ "Cooked Data", NULL },
		{ NULL,          NULL }
};


#define BUILD_SETTINGS_RESPONSE_TEST  1


static void         addToTree(ProjectDataT *self, char *filename);
EVENT  void         buildTargetClicked(GtkButton *widget, gpointer userData);
static void         loadConfig(ProjectDataT *self);
static void         loadProject(ProjectDataT *self);
EVENT  void         menuProjectFileNew(GtkWidget *object, gpointer userData);
EVENT  void         menuProjectFileOpen(GtkWidget *object, gpointer userData);
EVENT  void         menuProjectFileSave(GtkWidget *object, gpointer userData);
EVENT  void         menuProjectFileSaveAs(GtkWidget *object, gpointer userData);
EVENT  void         menuProjectFileClose(GtkWidget *object, gpointer userData);
EVENT  void         menuProjectProjectAdd(GtkWidget *object, gpointer userData);
EVENT  void         menuProjectProjectRemove(GtkWidget *object, gpointer userData);
EVENT  void         menuProjectBuildSettings(GtkWidget *object, gpointer userData);
EVENT  void         menuProjectBuildTargets(GtkWidget *object, gpointer userData);
EVENT  void         menuProjectBuildBuild(GtkWidget *object, gpointer userData);
EVENT  void         menuProjectHelpProject(GtkWidget *object, gpointer userData);
static void         saveConfig(ProjectDataT *self);
static TargetT    **targetArrayCopy(TargetT **targets);
static void         targetArrayDelete(TargetT ***array);
EVENT  void         treeProjectRowActivated(GtkTreeView *treeView, GtkTreePath *path, GtkTreeViewColumn *column, gpointer userData);
static gboolean     updateBuildOptions(ProjectDataT *self);
EVENT  gboolean     winProjectClose(GtkWidget *object, gpointer userData);
static void         winProjectDelete(gpointer userData);


static void addToTree(ProjectDataT *self, char *filename) {
	ProjectSectionTypeT  section;
	char                *temp       = NULL;
	GtkTreeIter          iter;
	GtkTreeIter          child;
	GtkTreeModel        *model      = gtk_tree_view_get_model(GTK_TREE_VIEW(self->treeProject));
	ProjectSectionTypeT  foundAt    = SECTION_RAW_DATA;   // If it isn't a file we know, put it in "Raw Data".
	int                  fileLen;
	int                  extLen;

	// Is it long enough?
	if (strlen(filename) > 2) {
		// Is this already in the tree?  This is a pretty brain-dead test.
		gtk_tree_model_get_iter_first(model, &iter);
		do {
			if (gtk_tree_model_iter_children(model, &child, &iter)) {
				do {
					gtk_tree_model_get(model, &child, COL_FILENAME, &temp, -1);
					if (strcmp(temp, filename) == 0) {
						DEL(temp);  // This generates an unknown pointer warning in memwatch.  Pretty sure it's bogus.
						return;     // Silently fail to add on name collision.
					}
					DEL(temp);  // This generates an unknown pointer warning in memwatch.  Pretty sure it's bogus.
				} while (gtk_tree_model_iter_next(model, &child));
			}
		} while (gtk_tree_model_iter_next(model, &iter));

		// Find proper section.
		for (section = 0; section < SECTION_COUNT; section++) {
			if (_sectionData[section].extension != NULL) {
				// Compare last two bytes of filename with extension - it's enough to differentiate them and allows for ".c" and ".h".
				fileLen = strlen(filename) - 1;
				extLen  = strlen(_sectionData[section].extension) - 1;
				if (filename[fileLen - 1] == _sectionData[section].extension[extLen - 1] && filename[fileLen] == _sectionData[section].extension[extLen]) {
					foundAt = section;
					break;
				}
			}
		}
		DEL(temp);
		fileLen = 0;
		utilEnsureBufferSize((unsigned char **)&temp, &fileLen, 4);  // fileLen is just a throwaway here.
		snprintf(temp, 4, "%d", foundAt);
		gtk_tree_model_get_iter_from_string(model, &iter, temp);
		gtk_tree_store_append(GTK_TREE_STORE(model), &child, &iter);
		gtk_tree_store_set(GTK_TREE_STORE(model), &child, COL_FILENAME, filename, -1);
		DEL(temp);

		//***TODO*** Need to store extra RAW data somewhere.

		gtk_tree_view_expand_all(GTK_TREE_VIEW(self->treeProject));
	}
}


EVENT void buildTargetClicked(GtkButton *widget, gpointer userData) {
	ArchT *arch = (ArchT *)userData;

	arch->selected = gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(widget));
}


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

	self->configName = utilCreateString("%s%cjoeydev%cbuild.conf", g_get_user_config_dir(), UTIL_PATH_CHAR, UTIL_PATH_CHAR);
	if (utilFileExists(self->configName)) {
		in = fopen(self->configName, "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) {
				if (strlen(line) > 0) line[strlen(line) - 1] = 0;
				switch (count) {
					case 0:  // Version Number
					case 1:  // Separator line
						break;

					case 2: // Host
						DEL(self->buildHost);
						self->buildHost = strdup(line);
						break;

					case 3: // HTTP Port
						self->buildHTTPPort = atoi(line);
						break;

					case 4: // SSH Port
						self->buildSSHPort = atoi(line);
						break;

					case 5: // User
						DEL(self->buildUser);
						self->buildUser = strdup(line);
						break;

					case 6: // Password
						DEL(self->buildPassword);
						self->buildPassword = utilDeobfuscateASCII(line);
						break;

					default:  // Oops
						break;
				}
				count++;
			}
			fclose(in);
			DEL(line);
		} else {
			//***TODO*** Something bad happened.
		}
	} else {
		// No config file.  Set defaults.
		DEL(self->buildHost);
		DEL(self->buildUser);
		DEL(self->buildPassword);

		self->buildHost     = strdup("build.joeylib.com");
		self->buildHTTPPort = 6502;
		self->buildSSHPort  = 816;
		self->buildUser     = strdup(g_get_user_name());
		self->buildPassword = strdup("SuperSecret");

		saveConfig(self);
	}
}


static void loadProject(ProjectDataT *self) {
	FILE    *in    = NULL;
	char    *line  = NULL;
	size_t   len   = 0;
	char    *c     = NULL;
	TargetT *t     = NULL;
	int      i;
	int      j;
	char    *path  = NULL;

	in = fopen(self->windowData.filename, "rt");
	if (in != NULL) {
		// Set all our known archs to FALSE.
		for (i=0; i<arrlen(self->targets); i++) {
			for (j=0; j<arrlen(self->targets[i]->archs); j++) {
				self->targets[i]->archs[j]->selected = FALSE;
			}
		}
		// Find path of config file.
		path = strdup(self->windowData.filename);
		cwk_path_get_dirname(path, &i);
		if (i > 0) path[i] = 0;
		// Load config.
		utilEnsureBufferSize((unsigned char **)&line, (int *)&len, 1024);  // 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 'source' line?
			if (strcasecmp(c, "source") == 0) {
				c = utilGetToken(NULL, " ", "\"", "\"");
				utilDequote(c);
				cwk_path_get_relative(path, c, __utilFilenameBuffer, sizeof(__utilFilenameBuffer));
				if (__utilFilenameBuffer[0] == 0) {
					addToTree(self, c);
				} else {
					addToTree(self, __utilFilenameBuffer);
				}
			}
			// Is this a 'target' line?
			if (strcasecmp(c, "target") == 0) {
				c = utilGetToken(NULL, " ", "\"", "\"");
				utilDequote(c);
				// See if we know about this target.
				for (i=0; i<self->targets; i++) {
					if (strcasecmp(self->targets[i]->name, c) == 0) {
						t = self->targets[i];
						// We know this target.  Iterate over specified arches and turn them on if known.
						while (c != NULL) {
							c = utilGetToken(NULL, " ", "\"", "\"");
							if (c != NULL) {
								utilDequote(c);
								for (j=0; j<arrlen(t->archs); j++) {
									if (strcasecmp(t->archs[j]->name, c) == 0) {
										t->archs[j]->selected = TRUE;
										break;
									}
								}
							}
						}
						break;
					}
				}
			}
		}
		fclose(in);
		DEL(line);
		DEL(path);
		utilSetDirty((WindowDataT *)self, FALSE);  // Do again - loading text marks us dirty.
	} else {
		//***TODO*** Something bad happened.
	}
}


EVENT void menuProjectFileNew(GtkWidget *object, gpointer userData) {
	ProjectDataT        *self = (ProjectDataT *)userData;
	GtkTreeIter          iter;
	ProjectSectionTypeT  i;
	GtkTreeStore        *store;
	int                  x;
	int                  y;

	(void)object;

	// Reset project tree.
	gtk_tree_view_set_model(GTK_TREE_VIEW(self->treeProject), NULL);
	store = gtk_tree_store_new(1, G_TYPE_STRING);
	for (i=0; i<SECTION_COUNT; i++) {
		gtk_tree_store_append(store, &iter, NULL);
		gtk_tree_store_set(store, &iter, COL_FILENAME, _sectionData[i].name, -1);
	}
	gtk_tree_view_set_model(GTK_TREE_VIEW(self->treeProject), GTK_TREE_MODEL(store));
	g_object_unref(store);

	// Select every target and architecture.
	for (x=0; x<arrlen(self->targets); x++) {
		for (y=0; y<arrlen(self->targets[x]->archs); y++) {
			self->targets[x]->archs[y]->selected = TRUE;
		}
	}

	// Nuke filename & mark clean.
	DEL(self->windowData.filename);
	utilSetDirty(&self->windowData, FALSE);
}


EVENT void menuProjectFileOpen(GtkWidget *object, gpointer userData) {
	ProjectDataT *self = (ProjectDataT *)userData;

	if (utilFileOpen((WindowDataT *)userData, "*.joe", "Project")) {
		menuProjectFileNew(object, userData);
		loadProject(self);
	}
}


EVENT void menuProjectFileSave(GtkWidget *object, gpointer userData) {
	ProjectDataT *self        = (ProjectDataT *)userData;
	GtkTreeModel *model       = gtk_tree_view_get_model(GTK_TREE_VIEW(self->treeProject));
	FILE         *out         = NULL;
	GtkTreeIter   iter;
	GtkTreeIter   child;
	char         *temp        = NULL;
	TargetT      *t           = NULL;
	ArchT        *a           = NULL;
	gboolean      archWritten = FALSE;
	int           i;
	int           j;

	// 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) {
			menuProjectFileSaveAs(object, userData);
			return;
		}

		out = fopen(self->windowData.filename, "wt");
		if (out != NULL) {
			// Save!  Write out header.
			fprintf(out, "%s\n", PROJECT_VERSION);
			fprintf(out, "------------------------------------------------------------------------------\n");
			// Write out file list.
			gtk_tree_model_get_iter_first(model, &iter);
			do {
				if (gtk_tree_model_iter_children(model, &child, &iter)) {
					do {
						//***TODO*** For RAW we need to store function name somewhere.
						gtk_tree_model_get(model, &child, COL_FILENAME, &temp, -1);
						fprintf(out, "source \"%s\"\n", temp);
						DEL(temp);  // This generates an unknown pointer warning in memwatch.  Pretty sure it's bogus.
					} while (gtk_tree_model_iter_next(model, &child));
				}
			} while (gtk_tree_model_iter_next(model, &iter));
			// Write out desired targets.
			for (i=0; i<arrlen(self->targets); i++) {
				t = self->targets[i];
				archWritten = FALSE;
				for (j=0; j<arrlen(t->archs); j++) {
					a = t->archs[j];
					if (a->selected == TRUE) {
						if (archWritten == FALSE) {
							fprintf(out, "target %s", t->name);
							archWritten = TRUE;
						}
						fprintf(out, " %s", a->name);
					}
				}
				if (archWritten == TRUE) fprintf(out, "\n");
			}
			// Close file.
			fclose(out);
			// We're clean now.
			utilSetDirty((WindowDataT *)self, FALSE);
		} else {
			//***TODO*** Something bad happened.
		}
	}
}


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

	if (utilFileSaveAs((WindowDataT *)userData, "*.joe", "Project")) {
		menuProjectFileSave(object, (ProjectDataT *)userData);
	}
}


EVENT void menuProjectFileClose(GtkWidget *object, gpointer userData) {
	ProjectDataT *self = (ProjectDataT *)userData;

	(void)object;

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


EVENT void menuProjectProjectAdd(GtkWidget *object, gpointer userData) {
	ProjectDataT        *self    = (ProjectDataT *)userData;
	GtkWidget           *dialog;
	GtkFileFilter       *filter;
	ProjectSectionTypeT  section;
	char                *temp    = NULL;
	char                *path    = NULL;
	int                  x;

	(void)object;

	dialog = gtk_file_chooser_dialog_new("Add File to Project",
													 GTK_WINDOW(self->windowData.window),
													 GTK_FILE_CHOOSER_ACTION_OPEN,
													 "_Cancel", GTK_RESPONSE_CANCEL,
													 "_Open", GTK_RESPONSE_ACCEPT,
													 NULL
	);

	if (self->windowData.filename != NULL) {
		path = strdup(self->windowData.filename);
		cwk_path_get_dirname(path, &x);
		if (x > 0) path[x] = 0;
		gtk_file_chooser_set_current_folder(GTK_FILE_CHOOSER(dialog),path);
	}

	for (section=0; section<SECTION_COUNT; section++) {
		if (_sectionData[section].extension != NULL) {
			filter = gtk_file_filter_new();
			gtk_file_filter_set_name(filter, _sectionData[section].name);
			gtk_file_filter_add_pattern(filter,  _sectionData[section].extension);
			gtk_file_chooser_add_filter(GTK_FILE_CHOOSER(dialog), filter);
		}
	}
	filter = gtk_file_filter_new();
	gtk_file_filter_set_name(filter, _sectionData[SECTION_RAW_DATA].name);
	gtk_file_filter_add_pattern(filter,  "*");
	gtk_file_chooser_add_filter(GTK_FILE_CHOOSER(dialog), filter);

	if (gtk_dialog_run(GTK_DIALOG(dialog)) == GTK_RESPONSE_ACCEPT) {
		temp = gtk_file_chooser_get_filename(GTK_FILE_CHOOSER(dialog));
		if (path != NULL) {
			cwk_path_get_relative(path,temp, __utilFilenameBuffer, sizeof(__utilFilenameBuffer));
			DEL(temp);
			temp = strdup(__utilFilenameBuffer);
		}
		addToTree(self, temp);
		utilSetDirty((WindowDataT *)self, TRUE);
		DEL(temp);
	}

	gtk_widget_destroy(dialog);
}


EVENT void menuProjectProjectRemove(GtkWidget *object, gpointer userData) {
	ProjectDataT     *self      = (ProjectDataT *)userData;
	GtkTreeSelection *selection = gtk_tree_view_get_selection(GTK_TREE_VIEW(self->treeProject));
	gboolean          found     = FALSE;
	GtkWidget        *dialog;
	GtkTreeModel     *model;
	GtkTreeIter       iter;

	// Is anything selected?
	if (gtk_tree_selection_get_selected(selection, &model, &iter)) {
		// Are we on a child item?
		if (gtk_tree_store_iter_depth(GTK_TREE_STORE(model), &iter) > 0) {
			found = TRUE;
		}
	}

	if (found) {
		gtk_tree_store_remove(GTK_TREE_STORE(model), &iter);
		utilSetDirty((WindowDataT *)self, TRUE);
	} else {
		// Provide help.
		dialog = gtk_message_dialog_new(GTK_WINDOW(self->windowData.window),
													GTK_DIALOG_DESTROY_WITH_PARENT | GTK_DIALOG_MODAL,
													GTK_MESSAGE_INFO,
													GTK_BUTTONS_CLOSE,
													"Please select a file to remove.");
		gtk_dialog_run(GTK_DIALOG(dialog));
		gtk_widget_destroy(dialog);
	}
}


EVENT void menuProjectBuildSettings(GtkWidget *object, gpointer userData) {
	ProjectDataT *self = (ProjectDataT *)userData;
	GtkWidget    *dialog;
	GtkWidget    *dialogServerSettings;
	GtkWidget    *txtHost;
	GtkWidget    *txtHTTPPort;
	GtkWidget    *txtSSHPort;
	GtkWidget    *txtUser;
	GtkWidget    *txtPassword;
	GtkWidget    *btnTest;
	GtkWidget    *btnOkay;
	char         *widgetNames[] = {
			"dialogServerSettings",
			"txtHost",
			"txtHTTPPort",
			"txtSSHPort",
			"txtUser",
			"txtPassword",
			"btnTest",
			"btnOkay",
			NULL
	};
	GtkWidget   **widgets[] = {
			&dialogServerSettings,
			&txtHost,
			&txtHTTPPort,
			&txtSSHPort,
			&txtUser,
			&txtPassword,
			&btnTest,
			&btnOkay
	};
	char          temp[6];
	int           result  = 0;
	SSHT         *ssh     = NULL;
	char         *error   = NULL;

	(void)object;

	utilGetWidgetsFromMemory("/com/kangaroopunch/joeydev/BuildServer.glade", widgetNames, widgets, self);

	while (1) {
		gtk_entry_set_text(GTK_ENTRY(txtHost), self->buildHost);
		snprintf(temp, 6, "%d", self->buildHTTPPort);
		gtk_entry_set_text(GTK_ENTRY(txtHTTPPort), temp);
		snprintf(temp, 6, "%d", self->buildSSHPort);
		gtk_entry_set_text(GTK_ENTRY(txtSSHPort), temp);
		gtk_entry_set_text(GTK_ENTRY(txtUser), self->buildUser);
		gtk_entry_set_text(GTK_ENTRY(txtPassword), self->buildPassword);
		result = gtk_dialog_run(GTK_DIALOG(dialogServerSettings));
		if (result == BUILD_SETTINGS_RESPONSE_TEST || result == GTK_RESPONSE_OK) {
			// Save settings to disk.
			DEL(self->buildHost);
			DEL(self->buildUser);
			DEL(self->buildPassword);
			self->buildHost     = strdup(gtk_entry_get_text(GTK_ENTRY(txtHost)));
			self->buildHTTPPort = atoi(gtk_entry_get_text(GTK_ENTRY(txtHTTPPort)));
			self->buildSSHPort  = atoi(gtk_entry_get_text(GTK_ENTRY(txtSSHPort)));
			self->buildUser     = strdup(gtk_entry_get_text(GTK_ENTRY(txtUser)));
			self->buildPassword = strdup(gtk_entry_get_text(GTK_ENTRY(txtPassword)));
			saveConfig(self);

			if (result == BUILD_SETTINGS_RESPONSE_TEST) {
				gtk_widget_set_sensitive(btnTest, FALSE);
				gtk_widget_set_sensitive(btnOkay, FALSE);
				// Run server connection tests - HTTP.
				if (updateBuildOptions(self) == FALSE) {
					error = utilCreateString("Unable to connect to HTTP port.");
				}
				// Run server connection tests - SSH.
				ssh = sshConnect(self->buildHost, self->buildSSHPort, self->buildUser, self->buildPassword);
				if (ssh) {
					sshDisconnect(&ssh);
				} else {
					DEL(error);
					error = utilCreateString("Unable to connect to SSH port.");
				}
				// Did the tests pass?
				if (error == NULL) {
					dialog = gtk_message_dialog_new(GTK_WINDOW(dialogServerSettings),
															  GTK_DIALOG_DESTROY_WITH_PARENT | GTK_DIALOG_MODAL,
															  GTK_MESSAGE_INFO,
															  GTK_BUTTONS_CLOSE,
															  "Successful connection to build server!");
				} else {
					dialog = gtk_message_dialog_new(GTK_WINDOW(dialogServerSettings),
															  GTK_DIALOG_DESTROY_WITH_PARENT | GTK_DIALOG_MODAL,
															  GTK_MESSAGE_ERROR,
															  GTK_BUTTONS_CLOSE,
															  "%s", error);
				}
				gtk_dialog_run(GTK_DIALOG(dialog));
				gtk_widget_destroy(dialog);
				DEL(error);
				gtk_widget_set_sensitive(btnTest, TRUE);
				gtk_widget_set_sensitive(btnOkay, TRUE);
			} else {
				// Try to load the build options in case they didn't click TEST.
				updateBuildOptions(self);
				// Close dialog on OKAY but not TEST.
				break;
			}

		}
	}

	gtk_widget_destroy(dialogServerSettings);
}


EVENT void menuProjectBuildTargets(GtkWidget *object, gpointer userData) {
	ProjectDataT     *self         = (ProjectDataT *)userData;
	GtkWidget        *dialog;
	GtkWidget        *contentArea;
	GtkWidget        *grid;
	GtkWidget        *widget;
	TargetT          *t;
	ArchT            *a;
	char             *temp;
	int               i;
	int               j;
	int               result;
	TargetT         **backup;

	backup = targetArrayCopy(self->targets);

	grid = gtk_grid_new();
	gtk_grid_set_column_homogeneous(GTK_GRID(grid), FALSE);
	gtk_grid_set_row_homogeneous(GTK_GRID(grid), TRUE);
	gtk_grid_set_column_spacing(GTK_GRID(grid), 15);
	gtk_grid_set_row_spacing(GTK_GRID(grid), 5);
	for (i=0; i<arrlen(self->targets); i++) {
		t = self->targets[i];
		temp = utilCreateString("%s:", t->longName);
		widget = gtk_label_new(temp);
		DEL(temp);
		gtk_label_set_line_wrap(GTK_LABEL(widget), FALSE);
		gtk_label_set_xalign(GTK_LABEL(widget), 1.0);
		gtk_grid_attach(GTK_GRID(grid), widget, 0, i, 1, 1);
		for (j=0; j<arrlen(t->archs); j++) {
			a = t->archs[j];
			widget = gtk_check_button_new_with_label(a->name);
			gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(widget), a->selected);
			g_signal_connect(G_OBJECT(widget), "clicked", G_CALLBACK(buildTargetClicked), a);
			gtk_grid_attach(GTK_GRID(grid), widget, j + 1, i, 1, 1);
		}
	}

	dialog = gtk_dialog_new_with_buttons(
			"Targets",
			GTK_WINDOW(self->windowData.window),
			GTK_DIALOG_MODAL | GTK_DIALOG_DESTROY_WITH_PARENT,
			"_Cancel",
			GTK_RESPONSE_CANCEL,
			"_OK",
			GTK_RESPONSE_OK,
			NULL
	);

	contentArea = gtk_dialog_get_content_area(GTK_DIALOG(dialog));
	gtk_container_add(GTK_CONTAINER(contentArea), grid);

	gtk_widget_show_all(dialog);

	result = gtk_dialog_run(GTK_DIALOG(dialog));
	if (result != GTK_RESPONSE_OK) {
		targetArrayDelete(&self->targets);
		self->targets = targetArrayCopy(backup);
	}

	targetArrayDelete(&backup);

	gtk_widget_destroy(dialog);
}


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

}


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

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


static void saveConfig(ProjectDataT *self) {
	char *temp;
	FILE *out;

	// Make sure we have a config folder.
	temp = utilCreateString("%s%cjoeydev", g_get_user_config_dir(), UTIL_PATH_CHAR);
	utilMkDirP(temp, 0777);
	DEL(temp);

	// Save config.
	out = fopen(self->configName, "wt");
	if (out != NULL) {
		fprintf(out, "%s\n", BUILD_SETTINGS_VERSION);
		fprintf(out, "------------------------------------------------------------------------------\n");
		fprintf(out, "%s\n", self->buildHost);
		fprintf(out, "%d\n", self->buildHTTPPort);
		fprintf(out, "%d\n", self->buildSSHPort);
		fprintf(out, "%s\n", self->buildUser);
		temp = utilObfuscateASCII(self->buildPassword);
		fprintf(out, "%s\n", temp);
		DEL(temp);
		fclose(out);
	} else {
		//***TODO*** Something bad happened.
	}
}


EVENT void treeProjectRowActivated(GtkTreeView *treeView, GtkTreePath *path, GtkTreeViewColumn *column, gpointer userData) {
	ProjectDataT *self       = (ProjectDataT *)userData;
	GtkTreeModel *model      = gtk_tree_view_get_model(GTK_TREE_VIEW(self->treeProject));
	GtkTreeIter   iter;
	char         *name       = NULL;
	char         *pathString = NULL;

	pathString = gtk_tree_path_to_string(path);
	if (strstr(pathString, ":") != NULL) {
		gtk_tree_model_get_iter_from_string(model, &iter, pathString);
		gtk_tree_model_get(model, &iter, COL_FILENAME, &name, -1);

		debug("Double click!  [%s] [%s]\n", pathString, name);

		DEL(name);
	}
	DEL(pathString);
}


static gboolean updateBuildOptions(ProjectDataT *self) {
	char      *test   = NULL;
	char      *name   = NULL;
	char      *url    = NULL;
	gboolean   result = FALSE;
	FILE      *in     = NULL;
	char      *line   = NULL;
	size_t     len    = 0;
	char      *c      = NULL;
	TargetT   *t      = NULL;
	ArchT     *a      = NULL;
	TargetT  **backup = NULL;
	int        i;
	int        j;
	int        x;
	int        y;

	// This updates the build options file without clobbering one if it already exists and we fail to get a new one.

	test = utilCreateString("%s%cjoeydev%cjoeydev.temp", g_get_user_config_dir(), UTIL_PATH_CHAR, UTIL_PATH_CHAR);
	name = utilCreateString("%s%cjoeydev%cjoeydev.info", g_get_user_config_dir(), UTIL_PATH_CHAR, UTIL_PATH_CHAR);
	url  = utilCreateString("http://%s:%d/joeydev.info", self->buildHost, self->buildHTTPPort);

	httpDownload(httpWaitForDownloadCallback, test, url);
	if (httpWaitForDownload() == TRUE) {
		unlink(name);
		rename(test, name);
		result = TRUE;
	}

	// Unload current target listing.
	backup = targetArrayCopy(self->targets);
	targetArrayDelete(&self->targets);

	// If there's a list of targets on the disk, load them.
	if (utilFileExists(name) == TRUE) {
		in = fopen(name, "rt");
		if (in != NULL) {
			utilEnsureBufferSize(&line, &len, 1024);  // 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 'target' line?
				if (strcasecmp(c, "target") == 0) {
					t = NEW(TargetT);
					// Short name.
					c = utilGetToken(NULL, " ", "\"", "\"");
					utilDequote(c);
					t->name = strdup(c);
					// Long name.
					c = utilGetToken(NULL, " ", "\"", "\"");
					utilDequote(c);
					t->longName = strdup(c);
					// Architectures.
					do {
						c = utilGetToken(NULL, " ", "\"", "\"");
						if (c != NULL) {
							utilDequote(c);
							a = NEW(ArchT);
							a->name = strdup(c);
							a->selected = FALSE;
							arrput(t->archs, a);
						}
					} while (c != NULL);
					arrput(self->targets, t);
				}
			}
			fclose(in);
			DEL(line);
		}
	}

	// Merge any previously checked items into the new list.
	for (i=0; i<arrlen(backup); i++) {
		// Is this target in the new list?
		for (j=0; j<arrlen(self->targets); j++) {
			if (strcasecmp(backup[i]->name, self->targets[j]->name) == 0) {
				// This is our target.  Iterate over archs and enable any we had enabled before.
				for (x=0; x<arrlen(backup[i]->archs); x++) {
					for (y=0; x<arrlen(self->targets[j]->archs); y++) {
						if (strcasecmp(backup[i]->archs[x]->name, self->targets[j]->archs[y]->name) == 0) {
							self->targets[j]->archs[y]->selected = backup[i]->archs[x]->selected;
							break;
						}
					}
				}
				break;
			}
		}
	}

	targetArrayDelete(&backup);

	DEL(url);
	DEL(name);
	DEL(test);

	return result;
}


static TargetT **targetArrayCopy(TargetT **targets) {
	int               i;
	int               j;
	TargetT          *t      = NULL;
	ArchT            *a      = NULL;
	TargetT         **backup = NULL;

	for (i=0; i<arrlen(targets); i++) {
		t = NEW(TargetT);
		t->name     = strdup(targets[i]->name);
		t->longName = strdup(targets[i]->longName);
		for (j=0; j<arrlen(targets[i]->archs); j++) {
			a = NEW(ArchT);
			a->name     = strdup(targets[i]->archs[j]->name);
			a->selected = targets[i]->archs[j]->selected;
			arrput(t->archs, a);
		}
		arrput(backup, t);
	}

	return backup;
}


static void targetArrayDelete(TargetT ***array) {
	TargetT **targets = (TargetT **)*array;
	TargetT  *t       = NULL;
	ArchT    *a       = NULL;

	while (arrlen(targets) > 0) {
		t = targets[0];
		while (arrlen(t->archs) > 0) {
			a = t->archs[0];
			DEL(a->name);
			DEL(a);
			arrdel(t->archs, 0);
		}
		//arrfree(t->archs);
		DEL(t->name);
		DEL(t->longName);
		DEL(t);
		arrdel(targets, 0);
	}
	//arrfree(targets);
}


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

	(void)userData;

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

	winProjectDelete(self);
	return FALSE;
}


void winProjectCreate(void) {
	ProjectDataT  *self;
	char          *widgetNames[]  = {
			"winProject",
			"treeProject",
			NULL
	};
	GtkWidget    **widgets[]      = {
			NULL,
			NULL
	};
	GtkTreeViewColumn   *col;
	GtkCellRenderer     *renderer;

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

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

	// Register window.
	utilWindowRegister(self);

	// Build tree.
	col = gtk_tree_view_column_new();
	gtk_tree_view_append_column(GTK_TREE_VIEW(self->treeProject), col);
	renderer = gtk_cell_renderer_text_new();
	gtk_tree_view_column_pack_start(col, renderer, TRUE);
	gtk_tree_view_column_add_attribute(col, renderer, "text", COL_FILENAME);

	// Reset tree.
	menuProjectFileNew(self->windowData.window, self);

	// Load server settings.
	loadConfig(self);

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

	// Attempt to load build settings from build server.
	updateBuildOptions(self);

	//***DEBUG***
	self->windowData.filename = strdup("/home/scott/joeyapps/warehouse/Warehouse.joe");
	loadProject(self);
}


static void winProjectDelete(gpointer userData) {
	ProjectDataT *self = (ProjectDataT *)userData;

	utilWindowUnRegister(userData);

	targetArrayDelete(&self->targets);

	DEL(self->buildHost);
	DEL(self->buildUser);
	DEL(self->buildPassword);
	DEL(self->configName);

	DEL(self);
}


#pragma clang diagnostic pop
