/*
   atariserver.C - demo implementation of an Atari SIO server

   Copyright (C) 2002,2003 Matthias Reichl <hias@horus.com>

   This program is free software; you can redistribute it and/or modify
   it under the terms of the GNU General Public License as published by
   the Free Software Foundation; either version 2 of the License, or
   (at your option) any later version.

   This program is distributed in the hope that it will be useful,
   but WITHOUT ANY WARRANTY; without even the implied warranty of
   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
   GNU General Public License for more details.

   You should have received a copy of the GNU General Public License
   along with this program; if not, write to the Free Software
   Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
*/

#include <stdio.h>
#include <stdlib.h>
#include <termios.h>
#include <fcntl.h>
#include <unistd.h>
#include <limits.h>
#include <string.h>

#include <signal.h>
#include <sched.h>
#include <sys/mman.h>
#include <readline/readline.h>
#include <readline/history.h>
#include <ctype.h>

#include "AtrSIOHandler.h"
#include "AtrMemoryImage.h"
#include "SIOManager.h"
#include "DiskManager.h"
#include "SIOTracer.h"

#define VERSION_STRING "0.10 alpha"

static struct termios orig_stdin_tio, noncanon_stdin_tio;

/* set stdin to non-canonical input mode */
static int init_noncanon_stdin_tio()
{
	if (tcgetattr(STDIN_FILENO, &orig_stdin_tio) < 0) {
		printf("tcgetattr error\n");
		return 1;
	}
	if (tcgetattr(STDIN_FILENO, &noncanon_stdin_tio) < 0) {
		printf("tcgetattr error\n");
		return 1;
	}
	noncanon_stdin_tio.c_lflag &= ~(ICANON | ECHO);

	return 0;
}

static int set_noncanon_stdin_mode()
{
	if (tcsetattr(STDIN_FILENO, TCSAFLUSH, &noncanon_stdin_tio) < 0) {
		printf("tcsetattr error\n");
		return 1;
	}
	return 0;
}

static int restore_stdin_mode()
{
	if (tcsetattr(STDIN_FILENO, TCSAFLUSH, &orig_stdin_tio) < 0) {
		printf("tcsetattr error\n");
		return 1;
	}
	return 0;
}

void my_sig_handler(int sig)
{
	switch (sig) {
	case SIGALRM:
		printf("got SIGALARM\n");
		goto quit;
	case SIGINT:
		goto quit;
	quit:
		printf("good bye!\n");
		restore_stdin_mode();
		exit(1);
	default:
		break;
	}
}

// use some space on the stack, so that later we won't run into
// problems with stack space (as a side effect of mlockall)

static void reserve_stack_memory()
{
#define RESERVE_STACK_SIZE 100000
	char dummy_string[RESERVE_STACK_SIZE];
	int i;
	for (i=0;i<RESERVE_STACK_SIZE;i++) {
		dummy_string[i]=0;
	}
}

static void set_realtime_scheduling(int priority)
{
	struct sched_param sp;
	pid_t myPid;

	memset(&sp, 0, sizeof(struct sched_param));
	sp.sched_priority = sched_get_priority_max(SCHED_RR) - priority;
	myPid = getpid();

	if (sched_setscheduler(myPid, SCHED_RR, &sp) == 0) {
		printf("activated realtime scheduling\n");

/*
#ifdef ATARI_DEBUG
		printf("the server will automatically terminate in 5 minutes!\n");
		signal(SIGALRM, my_sig_handler);
		alarm(300);
#endif
*/
	} else {
		printf("could not set realtime scheduling! please run as root!\n");
	}

	reserve_stack_memory();
	if (mlockall(MCL_CURRENT) == 0) {
		printf("mlockall succeeded\n");
	} else {
		printf("mlockall failed\n");
	}

	// drop root privileges - set eid to real id
	seteuid(getuid());
	setegid(getgid());
}

static int trace_level = 0;
static bool auto_status_update = true;

static void print_status(const DiskManager& manager)
{
	printf("+----------------------+------------------------------------------------------+\n");
	printf("|  atariserver status  | ");

     	printf("high speed = ");
	if (manager.GetUseHighSpeed()) {
		printf("on ");
	} else {
		printf("off");
	}

	printf("   auto update = ");
	if (auto_status_update) {
		printf("on ");
	} else {
		printf("off");
	}

	printf("   trace = %d     |", trace_level);

	printf("\n");

	printf("+-----+---+---+--------+------------------------------------------------------+\n");
	printf("| DRV | C | P |  size  | filename                                             |\n");
	printf("+-----+---+---+--------+------------------------------------------------------+\n");
	int i;
	int maxlen=53;
	for (i=DiskManager::eMinDriveNumber;i<=DiskManager::eMaxDriveNumber;i++) {
		printf("| D%d: | ",i);
		if (manager.DriveInUse(i)) {
			if (manager.DriveIsChanged(i)) {
				printf("C | ");
			} else {
				printf("  | ");
			}

			if (manager.DriveIsWriteProtected(i)) {
				printf("P | ");
			} else {
				printf("  | ");
			}

			printf("%5dk | ", (manager.GetDriveImageSize(i)+512) / 1024);
			const char * f = manager.GetImageFilename(i);
			if (f) {
				int len=strlen(f);
				if (len<=maxlen) {
					printf("%s", f);
					while (len++<maxlen) {
						printf(" ");
					}
					printf("|");
				} else {
					/* try to shorten the filename */
					const char *p=f+len-maxlen+3;
					len=strlen(p)+3;
					while (*p && *p!='/') {
						p++; len--;
					}
					if (*p) {
						printf("...%s",p);
						while (len++<maxlen) {
							printf(" ");
						}
						printf("|");
					} else {
						printf("%s",f);
					}
				}
			} else {
				printf("                                                     |");
			}
		} else {
			printf("- | - | ------ | <empty>                                              |");
		}
		printf("\n");
	}
	printf("+-----+---+---+--------+------------------------------------------------------+\n");
}

static void print_auto_status(const DiskManager& manager)
{
	if (auto_status_update) {
		print_status(manager);
	}
}

/* menu helper routines */

static int input_char(const char* allowed)
{
	while (1) {
		int c = getchar();
		if (c==EOF || c==27 || c=='\a') {
			return -1;
		}
		if (strchr(allowed,c)) {
			return c;
		}
	}
}

static void print_aborted()
{
	printf("[aborted]\n");
}

static void print_ok()
{
	printf(" - OK\n");
}

static void print_error()
{
	printf(" - ERROR!\n");
}

static int input_drive_number()
/* returns:
 * -1 = abort
 * 1..8 = drive number
 */
{
	static char * driveChars = 0;

	if (driveChars == 0) {
		int len = DiskManager::eMaxDriveNumber-DiskManager::eMinDriveNumber+2;
		driveChars = (char*) malloc(len);
		driveChars[len-1] = 0;
		for (int i=DiskManager::eMinDriveNumber;i<=DiskManager::eMaxDriveNumber;i++) {
			driveChars[i-DiskManager::eMinDriveNumber] = '0'+i;
		};
	}
	int chr = input_char(driveChars);
       
	if (chr > 0) {
		printf("%c", chr);
		fflush(stdout);
		return chr - '0';
	} else {
		print_aborted();
		return chr;
	}
}

static void print_drive_number_prompt(const char* prompt)
{
	printf("%s [ %d-%d ESC ] > ",prompt,DiskManager::eMinDriveNumber, DiskManager::eMaxDriveNumber);
	fflush(stdout);
}

static int input_used_drive_number(const DiskManager& manager, char *prompt)
{
	print_drive_number_prompt(prompt);

	int d = input_drive_number();

	if (d>0 && !manager.DriveInUse(d)) {
		printf(" - no disk in drive D%d:!\n",d);
		return -1;
	} else {
		return d;
	}
}

static int input_unused_drive_number(const DiskManager& manager, char *prompt)
{
	print_drive_number_prompt(prompt);

	int d = input_drive_number();

	if (d>0 && manager.DriveInUse(d)) {
		printf(" - there's a disk in drive D%d:!\n",d);
		return -1;
	} else {
		return d;
	}
}

static char * readline_default_text = 0;

static int readline_startup_hook()
{
	if (readline_default_text) {
		rl_insert_text (readline_default_text);
		free(readline_default_text);
		readline_default_text = 0;
		rl_startup_hook = (rl_hook_func_t *)NULL;
	}
	return 0;
}

static void set_readline_default_text(const char* text)
{
	if (readline_default_text) {
		free(readline_default_text);
		readline_default_text=0;
	}
	if (text && *text) {
		readline_default_text = strdup(text);
		rl_startup_hook = readline_startup_hook;
	}
}

static char * my_readline(const char* prompt)
{
	restore_stdin_mode();

	const char * break_tmp = rl_basic_word_break_characters;
	int tmp_app = rl_completion_append_character;
	rl_basic_word_break_characters = "\n";
	rl_completion_append_character = 0;

 	char * filename = readline(prompt);

	rl_basic_word_break_characters = break_tmp;
	rl_completion_append_character = tmp_app;

	set_noncanon_stdin_mode();

	int l = strlen(filename);
	while (l>0 && isspace(filename[l-1])) {
			filename[l---1] = 0;
	}
	return filename;
}

static void set_trace(int level) {
	SIOTracer* tracer = SIOTracer::GetInstance();
	tracer->SetTraceCommands( level >= 1 );
	tracer->SetTraceCommandResults( level >= 2 );
	tracer->SetTraceDataBlocks( level >= 3 );
}

int main(int argc, char**argv)
{
	DiskManager manager;

	int i, drive, ret;

	bool write_protect_next = false;

	printf("atariserver V%s\n(c) 2002, 2003 by Matthias Reichl <hias@horus.com>\n\n",VERSION_STRING);

	set_realtime_scheduling(0); // also drops root privileges

	drive = 1; // default: drive D1:

	for (i=1;i<argc;i++) {
		int len=strlen(argv[i]);
		if (len == 0) {
			printf("illegal argv!\n");
			exit(1);
		}
		if (argv[i][0]=='-') {
			switch(argv[i][1]) {
			case '1':
			case '2':
			case '3':
			case '4':
			case '5':
			case '6':
			case '7':
			case '8':
				if (len != 2) {
					goto illegal_option;
				}
				drive = argv[i][1] - '0';
				break;
			case 't':
				if (len != 2) {
					goto illegal_option;
				}
				trace_level++;
				break;
			case 's':
				manager.SetUseHighSpeed(false);
				printf("disabling high-speed SIO\n");
				break;
			case 'a':
				auto_status_update = false;
				break;
			case 'p':
				write_protect_next = true;
				break;
			case 'h':
				goto usage;
			default:
illegal_option:
				printf("illegal option \"%s\"\n", argv[i]);
				goto usage;
			}
		} else {
			if (manager.DriveNumberOK(drive)) {
				if (manager.DriveInUse(drive)) {
					printf("drive D%d: already assigned!\n", drive);
					goto usage;
				}
				printf("loading \"%s\" into D%d:\n", argv[i], drive);
				if (manager.LoadAtrMemoryImage(drive, argv[i])) {
					printf("cannot load \"%s\"\n", argv[i]);
					return 1;
				}
				if (write_protect_next) {
					if (manager.SetWriteProtectImage(drive, true)) {
						printf("warning: write protecting D%d: failed!\n", drive);
					}
					write_protect_next = false;
				}

				add_history(argv[i]);
				drive++;
			} else {
				printf("too many images - there is no drive D%d:\n",drive);
				goto usage;
			}
		}
	}

	if (init_noncanon_stdin_tio()) {
		printf("couldn't init non-canonical stdin mode\n");
		return 1;
	}
	signal(SIGINT, my_sig_handler);
	if (set_noncanon_stdin_mode()) {
		printf("couldn't set stdin to non-canonical mode\n");
	}

	set_trace(trace_level);

	printf("atariserver is up and running...\n\n");
	print_auto_status(manager);
	printf("\npress 'h' for help\n");

	fflush(stdout);

	{
		bool running = true;
	
		while (running) {
			ret = manager.DoServing(fileno(stdin));
			if (ret==0) {
				unsigned char c;
				int bytes = read(fileno(stdin),&c,1);
	
				if (bytes==1) {
					switch(c) {
					case 'q':
						printf("good bye!\n");
						running = false;
						break;
					case ' ':
						print_status(manager);
						break;
					case 'h':
						printf("\n");
						printf("+-----------------------------------+\n");
						printf("|         AtariServer help:         |\n");
						printf("+-----------------------------------+\n");
						printf("| h - help                          |\n");
						printf("| q - quit                          |\n");
						printf("| <space> - status                  |\n");
						printf("| c - create image                  |\n");
						printf("| l - load image                    |\n");
						printf("| w - write image to file           |\n");
						printf("| W - write back all changed images |\n");
						printf("| u - unload image                  |\n");
						printf("| p - write protect disk            |\n");
						printf("| P - unprotect disk                |\n");
						printf("| x - exchange drives               |\n");
						printf("| s - high speed SIO on/off         |\n");
						printf("| a - auto status update on/off     |\n");
						printf("| t - set SIO trace level           |\n");
						printf("+-----------------------------------+\n");
						printf("\n");
						break;
					case 'W':
						printf("starting to write back changed images\n");
						fflush(stdout);
						if (manager.WriteBackImagesIfChanged() == 0) {
							printf("writing back changed images finished\n");
						} else {
							printf("error writing back changed images!\n");
						}
						break;
					case 'p':
						{
							int d = input_used_drive_number(manager, "write protect disk");
							if (d > 0) {
								if (manager.SetWriteProtectImage(d,true) == 0) {
									print_ok();
									print_auto_status(manager);
								} else {
									print_error();
								}
							}
						}
						break;
					case 'P':
						{
							int d = input_used_drive_number(manager,"unprotect disk");
							if (d > 0) {
								if (manager.SetWriteProtectImage(d,false) == 0) {
									print_ok();
									print_auto_status(manager);
								} else {
									print_error();
								}
							}
						}
						break;

					case 'x':
						{
							int d1,d2;
							print_drive_number_prompt("exchange drive");

							d1 = input_drive_number();
							if (d1 > 0) {
								printf(" with ");
								fflush(stdout);
								d2 = input_drive_number();
								if (d2 > 0) {
									if (d1 != d2) {
										if (manager.ExchangeDrives(d1,d2) == 0) {
											print_ok();
											print_auto_status(manager);
										} else {
											print_error();
										}
									} else {
										printf(" - ignoring\n");
									}
								} else {
									print_aborted();
								}
							}
						}
						break;
					case 'u':
						{
							int d = input_used_drive_number(manager, "unload drive");
							if (d > 0) {
								if (manager.UnloadAtrImage(d) == 0) {
									print_ok();
									print_auto_status(manager);
								} else {
									print_error();
								}
							}
						}
						break;
					case 't':
						{
							printf("set SIO trace level [ 0-3 ESC ] > ");
							fflush(stdout);
							int d = input_char("0123");
							if (d >=0) {
									printf("%c",d);
									trace_level = d - '0';
									set_trace(trace_level);
									print_ok();
									print_auto_status(manager);
							} else {
									print_aborted();
							}
						}
						break;
					case 'l':
						{
							int d = input_unused_drive_number(manager, "load drive");
							if (d > 0) {
								printf("\n");
							  	char * filename = my_readline("filename > ");

							  	if (filename && *filename) {
									add_history(filename);

									if (manager.LoadAtrMemoryImage(d, filename) == 0) {
										printf("loaded \"%s\" into D%d:\n", filename, d);
										print_auto_status(manager);
									} else {
										printf("error loading \"%s\"!\n",filename);
									}
								} else {
									print_aborted();
								}
								if (filename) {
									free (filename);
								}
							}
						}
						break;
					case 'w':
						{
							int d = input_used_drive_number(manager, "write drive");
							if (d > 0) {
								set_readline_default_text(manager.GetImageFilename(d));

								printf("\n");
							  	char * filename = my_readline("filename > ");

								if (filename && *filename) {
									add_history(filename);

									if (manager.WriteAtrMemoryImage(d, filename) == 0) {
										printf("wrote D%d: to \"%s\"\n", d, filename);
										print_auto_status(manager);
									} else {
										printf("error writing \"%s\"!\n",filename);
									}
								} else {
									print_aborted();
								}
								if (filename) {
									free(filename);
								}
							}
						}
						break;
					case 'c':
						{
							int d = input_unused_drive_number(manager, "create image in drive");
							if (d>0) {
								printf("\nsize: [ 1=90k 2=130k 3=180k 4=360k S=other(SD) D=other(DD) ESC ] > ");
								fflush(stdout);
								int s = input_char("1234sSdD");
								if (s>=0) {
									printf("%c\n",s);
									switch (s) {
									case '1':
										if (manager.CreateAtrMemoryImage(d, e90kDisk) == 0) {
											goto create_ok;
										} else {
											goto create_error;
										}
										break;
									case '2':
										if (manager.CreateAtrMemoryImage(d, e130kDisk) == 0) {
											goto create_ok;
										} else {
											goto create_error;
										}
										break;
									case '3':
										if (manager.CreateAtrMemoryImage(d, e180kDisk) == 0) {
											goto create_ok;
										} else {
											goto create_error;
										}
										break;
									case '4':
										if (manager.CreateAtrMemoryImage(d, e360kDisk) == 0) {
											goto create_ok;
										} else {
											goto create_error;
										}
										break;
									case 's':
									case 'S':
									case 'd':
									case 'D':
										{
											char * sectors = my_readline("number of sectors: > ");

											if (sectors && *sectors) {
												int numSec = atoi(sectors);
												if (numSec < 1 || numSec > 65535) {
													printf("illegal number of sectors %d - aborted\n",numSec);
												} else {
													int ret;
													if (toupper(s)=='S') {
														ret = manager.CreateAtrMemoryImage(d, e128BytesPerSector, numSec);
													} else {
														ret = manager.CreateAtrMemoryImage(d, e256BytesPerSector, numSec);
													}
													if (ret == 0) {
														goto create_ok;
													} else {
														goto create_error;
													}
												}
											} else {
												print_aborted();
											}
										}
										break;
									create_ok:
										printf("created disk in D%d:\n",d);
										print_auto_status(manager);
										break;
									create_error:
										printf("error creating empty disk in D%d: !\n",d);
										break;
									}

								} else {
									print_aborted();
								}
							}
						}
						break;
					case 's':
						{
							printf("enable high speed: [ Y N ESC ] > ");
							fflush(stdout);
							int sel = input_char("yYnN");
							if (sel >= 0) {
								printf("%c", toupper(sel));
								if (toupper(sel) == 'Y') {
									if (manager.SetUseHighSpeed(true) == 0) {
										print_ok();
									} else {
										print_error();
									}
								} else {
									if (manager.SetUseHighSpeed(false) == 0) {
										print_ok();
									} else {
										print_error();
									}
								}
								print_auto_status(manager);
							} else {
								print_aborted();
							}
						}
						break;
					case 'a':
						{
							printf("enable auto status update: [ Y N ESC ] > ");
							fflush(stdout);
							int sel = input_char("yYnN");
							if (sel >= 0) {
								printf("%c", toupper(sel));
								if (toupper(sel) == 'Y') {
									auto_status_update = true;
								} else {
									auto_status_update = false;
								}
								print_ok();
								print_auto_status(manager);
							} else {
								print_aborted();
							}
						}
						break;
					default:
						printf("illegal command ");
						if (c>=32 && c<=126) {
							printf("'%c'",c);
						} else {
							printf("0x%02x",c);
						}
						printf(" - press 'h' for help\n");
						break;
					}
					fflush(stdout);
					fflush(stderr);
				} else {
					printf("read(STDIN) returned %d\n", bytes);
				}
			} else { // ret == -1
				printf("SIO manager returned error: %d\n",ret);
			}
		}
	}

	restore_stdin_mode();
	return 0;

usage:
	printf("usage: [-h] [-a] [-t] [-s] [ [-1] [-p] filename [ [-2] [-p] filename ...] ]\n");
	printf("-h          display help\n");
	printf("-a          disable auto status update\n");
	printf("-p          write protect the next image\n");
	printf("-s          slow mode - disable highspeed SIO\n");
	printf("-t          increase SIO trace level (default:0, max:3)\n");
	printf("-1..-8      set current drive number (default: 1)\n");
	printf("<filename>  load <filename> into current drive number, and then\n");
	printf("            increment drive number by one\n");
	return 1;
}

