/* Copyright (c) 2013-2021 Jeffrey Pfau
 *
 * This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
#include <mgba/core/config.h>
#include <mgba/core/version.h>
#include <mgba/feature/updater.h>
#include <mgba-util/string.h>
#include <mgba-util/vfs.h>

#include <errno.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/stat.h>

#ifdef _WIN32
#include <direct.h>
#include <io.h>
#include <process.h>
#include <synchapi.h>

#define mkdir(X, Y) _mkdir(X)
#ifndef S_ISDIR
#define S_ISDIR(MODE) (((MODE) & _S_IFMT) == _S_IFDIR)
#endif
#elif defined(_POSIX_C_SOURCE)
#include <unistd.h>
#endif

#ifndef W_OK
#define W_OK 02
#endif

FILE* logfile;

bool rmdirRecursive(struct VDir* dir) {
	if (!dir) {
		return false;
	}
	bool ok = true;
	struct VDirEntry* vde;
	while ((vde = dir->listNext(dir))) {
		const char* name = vde->name(vde);
		if (strcmp(name, ".") == 0 || strcmp(name, "..") == 0) {
			continue;
		}
		switch (vde->type(vde)) {
		case VFS_DIRECTORY:
			fprintf(logfile, "cd   %s\n", name);
			if (!rmdirRecursive(dir->openDir(dir, name))) {
				ok = false;
			}
			fprintf(logfile, "cd   ..\n");
			// Fall through
		case VFS_FILE:
		case VFS_UNKNOWN:
			fprintf(logfile, "rm      %s\n", name);
			if (!dir->deleteFile(dir, name)) {
				fprintf(logfile, "error\n");
				ok = false;
			}
			break;
		}
	}
	dir->close(dir);
	return ok;
}

bool extractArchive(struct VDir* archive, const char* root, bool prefix) {
	char path[PATH_MAX] = {0};
	struct VDirEntry* vde;
	uint8_t block[8192];
	ssize_t size;
	while ((vde = archive->listNext(archive))) {
		struct VFile* vfIn;
		struct VFile* vfOut;
		const char* fname;
		if (prefix) {
			fname = strchr(vde->name(vde), '/');
			if (!fname) {
				continue;
			}
			snprintf(path, sizeof(path), "%s/%s", root, &fname[1]);
		} else {
			fname = vde->name(vde);
			snprintf(path, sizeof(path), "%s/%s", root, fname);
		}
		if (fname[0] == '.') {
			continue;
		}
		switch (vde->type(vde)) {
		case VFS_DIRECTORY:
			fprintf(logfile, "mkdir   %s\n", fname);
			if (mkdir(path, 0755) < 0) {
				bool redo = true;
				struct stat st;
				if (errno != EEXIST || stat(path, &st) < 0) {
					redo = false;
				} else if (!S_ISDIR(st.st_mode)) {
#ifdef _WIN32
					wchar_t wpath[MAX_PATH + 1];
					MultiByteToWideChar(CP_UTF8, 0, path, -1, wpath, MAX_PATH);
					DeleteFileW(wpath);
#else
					unlink(path);
#endif
					if (mkdir(path, 0755) < 0) {
						redo = false;
					}
				}
				if (!redo) {
					fprintf(logfile, "error   %i\n", errno);
					return false;
				}
			}
			if (!prefix) {
				struct VDir* subdir = archive->openDir(archive, fname);
				if (!subdir) {
					fprintf(logfile, "error\n");
					return false;
				}
				if (!extractArchive(subdir, path, false)) {
					subdir->close(subdir);
					return false;
				}
				subdir->close(subdir);
			}
			break;
		case VFS_FILE:
			fprintf(logfile, "extract %s\n", fname);
			vfIn = archive->openFile(archive, vde->name(vde), O_RDONLY);
			errno = 0;
			vfOut = VFileOpen(path, O_WRONLY | O_CREAT | O_TRUNC);
			if (!vfOut) {
				int error = errno;
				struct stat st;
				if (error == EISDIR || (stat(path, &st) >= 0 && S_ISDIR(st.st_mode))) {
					// Windows maps STATUS_FILE_IS_A_DIRECTORY to ERROR_ACCESS_DENIED,
					// which then gets mapped to EACCESS, because everything is awful
					fprintf(logfile, "rm -r   %s\n", path);
					if (!rmdirRecursive(VDirOpen(path))) {
						return false;
					}
#ifdef _WIN32
					wchar_t wpath[MAX_PATH + 1];
					MultiByteToWideChar(CP_UTF8, 0, path, -1, wpath, MAX_PATH);
					RemoveDirectoryW(wpath);
#else
					rmdir(path);
#endif
					vfOut = VFileOpen(path, O_WRONLY | O_CREAT | O_TRUNC);
				} else if (error == EACCES || error == ETXTBSY) {
#ifdef _WIN32
					Sleep(1000);
#else
					sleep(1);
#endif
					vfOut = VFileOpen(path, O_WRONLY | O_CREAT | O_TRUNC);
				}
			}
			if (!vfOut) {
				vfIn->close(vfIn);
				return false;
			}
			while ((size = vfIn->read(vfIn, block, sizeof(block))) > 0) {
				vfOut->write(vfOut, block, size);
			}
			vfOut->close(vfOut);
			vfIn->close(vfIn);
			if (size < 0) {
				return false;
			}
			break;
		case VFS_UNKNOWN:
			return false;
		}
	}
	return true;
}

int main(int argc, char* argv[]) {
	UNUSED(argc);
	UNUSED(argv);
	struct mCoreConfig config;
	char updateArchive[PATH_MAX] = {0};
	char bin[PATH_MAX] = {0};
	const char* root;
	int ok = 1;

	mCoreConfigDirectory(bin, sizeof(bin));
	strncat(bin, "/updater.log", sizeof(bin) - 1);
	logfile = fopen(bin, "w");

	mCoreConfigInit(&config, "updater");
	if (!mCoreConfigLoad(&config)) {
		fputs("Failed to load config\n", logfile);
	} else if (!mUpdateGetArchivePath(&config, updateArchive, sizeof(updateArchive)) || !(root = mUpdateGetRoot(&config))) {
		fputs("No pending update found\n", logfile);
	} else if (access(root, W_OK)) {
		fputs("Cannot write to update path\n", logfile);
	} else {
#ifdef __APPLE__
		char subdir[PATH_MAX];
		char devpath[PATH_MAX] = {0};
		bool needsUnmount = false;
#endif
		bool isPortable = mCoreConfigIsPortable();
		const char* extension = mUpdateGetArchiveExtension(&config);
		struct VDir* archive = NULL;
		bool prefix = true;
		if (strcmp(extension, "dmg") == 0) {
#ifdef __APPLE__
			char mountpoint[PATH_MAX];
			// Make a slightly random directory name for the updater mountpoint
			struct timeval t;
			gettimeofday(&t, NULL);
			int printed = snprintf(mountpoint, sizeof(mountpoint), "/Volumes/%s Updater %04X", projectName, (t.tv_usec >> 2) & 0xFFFF);

			// Fork hdiutil to mount it
			char* args[] = {"hdiutil", "attach", "-nobrowse", "-mountpoint", mountpoint, updateArchive, NULL};
			int fds[2];
			pipe(fds);
			pid_t pid = fork();
			if (pid == 0) {
				dup2(fds[1], STDOUT_FILENO);
				execvp("hdiutil", args);
				_exit(1);
			} else {
				// Parse out the disk ID so we can detach it when we're done
				char buffer[1024] = {0};
				ssize_t size;
				while ((size = read(fds[0], buffer, sizeof(buffer) - 1)) > 0) { // Leave the last byte null
					char* devinfo = strnstr(buffer, "\n/dev/disk", size);
					if (!devinfo) {
						continue;
					}
					char* devend = strpbrk(&devinfo[9], "s \t");
					if (!devend) {
						continue;
					}
					off_t diff = devend - devinfo - 1;
					memcpy(devpath, &devinfo[1], diff);
					break;
				}
				int retstat;
				wait4(pid, &retstat, 0, NULL);
			}
			snprintf(&mountpoint[printed], sizeof(mountpoint) - printed, "/%s.app", projectName);
			snprintf(subdir, sizeof(subdir), "%s/%s.app", root, projectName);
			root = subdir;
			archive = VDirOpen(mountpoint);
			prefix = false;
			needsUnmount = true;
#endif
		} else if (strcmp(extension, "appimage") != 0) {
			archive = VDirOpenArchive(updateArchive);
		}
		if (archive) {
			fputs("Extracting update\n", logfile);
			if (extractArchive(archive, root, prefix)) {
				ok = 0;
			} else {
				fputs("An error occurred\n", logfile);
			}
			archive->close(archive);
			unlink(updateArchive);
		}
#ifdef __linux__
		else if (strcmp(extension, "appimage") == 0) {
			const char* command = mUpdateGetCommand(&config);
			strlcpy(bin, command, sizeof(bin));
			if (rename(updateArchive, bin) < 0) {
				if (errno == EXDEV) {
					// Cross-dev, need to copy manually
					int infd = open(updateArchive, O_RDONLY);
					int outfd = -1;
					if (infd < 0) {
						ok = 2;
					} else {
						outfd = open(bin, O_CREAT | O_WRONLY | O_TRUNC, 0755);
					}
					if (outfd < 0) {
						ok = 2;
					} else {
						uint8_t buffer[2048];
						ssize_t size;
						while ((size = read(infd, buffer, sizeof(buffer))) > 0) {
							if (write(outfd, buffer, size) < size) {
								ok = 2;
								break;
							}
						}
						if (size < 0) {
							ok = 2;
						}
						close(outfd);
						close(infd);
					}
					if (ok == 2) {
						fputs("Cannot move update over old file\n", logfile);
					}
				} else {
					fputs("Cannot move update over old file\n", logfile);
				}
			} else {
				ok = 0;
			}
			if (ok == 0) {
				chmod(bin, 0755);
			}
		}
#endif
		else {
			fputs("Cannot open update archive\n", logfile);
		}
		if (ok == 0) {
			fputs("Complete", logfile);
			const char* command = mUpdateGetCommand(&config);
			strlcpy(bin, command, sizeof(bin));
			mUpdateDeregister(&config);
		}
#ifdef __APPLE__
		if (needsUnmount) {
			char* args[] = {"hdiutil", "detach", devpath, NULL};
			pid_t pid = vfork();
			if (pid == 0) {
				execvp("hdiutil", args);
				_exit(0);
			} else {
				int retstat;
				wait4(pid, &retstat, 0, NULL);
			}
		}
#endif
		if (!isPortable) {
			char portableIni[PATH_MAX] = {0};
			snprintf(portableIni, sizeof(portableIni), "%s/portable.ini", root);
			unlink(portableIni);
		}
	}
	mCoreConfigDeinit(&config);
	fclose(logfile);
	if (ok == 0) {
#ifdef _WIN32
		char qbin[PATH_MAX + 2] = {0};
		// Windows is a bad operating system
		snprintf(qbin, sizeof(qbin), "\"%s\"", bin);
		const char* argv[] = { qbin, NULL };
		_execv(bin, argv);
#elif defined(_POSIX_C_SOURCE) || defined(__APPLE__)
		char* const argv[] = { bin, NULL };
		execv(bin, argv);
#endif
	}
	return 1;
}

#ifdef _WIN32
#include <mgba-util/string.h>
#include <mgba-util/vector.h>

int wmain(int argc, wchar_t* argv[]) {
	struct StringList argv8;
	StringListInit(&argv8, argc);
	for (int i = 0; i < argc; ++i) {
		*StringListAppend(&argv8) = utf16to8((uint16_t*) argv[i], wcslen(argv[i]) * 2);
	}
	int ret = main(argc, StringListGetPointer(&argv8, 0));

	size_t i;
	for (i = 0; i < StringListSize(&argv8); ++i) {
		free(*StringListGetPointer(&argv8, i));
	}
	return ret;
}
#endif
