/*
*
* This is `gramscii`, a simple tool to create ascii box graphs
*
*/

#include <stdio.h>
#include <stdlib.h>
#include <termios.h>
#include <unistd.h>
#include <signal.h>
#include <string.h>
#include <sys/ioctl.h>

#define MOVE   0x00
#define BOX    0x01
#define ARROW  0x02
#define TEXT   0x04
#define DEL    0x08
#define VIS    0x10

#define DIR_N  0x00
#define DIR_R  0x01
#define DIR_U  0x02
#define DIR_D  0x04
#define DIR_L  0x08

#define DIR_HOR (DIR_R | DIR_L)
#define DIR_VER (DIR_D | DIR_U)


#define NOFIX 0x0
#define FIX   0x1

#define BG        ' '
#define PTR       '+'
#define UND       '_'
#define ARR_L     '<'
#define ARR_R     '>'
#define ARR_U     '^'
#define ARR_D     'v'


#define HOME   0x01
#define END    0x02
#define MIDDLE 0x04

#define VIDEO_NRM 0
#define VIDEO_REV 7 

#define MIN(x,y)  (x) < (y) ? (x) : (y)
#define MAX(x,y)  (x) > (y) ? (x) : (y)

#define DEBUG 1

char **screen;
int WIDTH, HEIGHT;

int state;
int dir;
int x;
int y;
int step;
int force_new;
char cursor;
char corner;
char hlines[]  = {"-~=#@._ "};
char vlines[]  = {"|H#@:;i "};
char corners[] = {"+'H#@.\""};
char st_marks[] = {"+o-|<>^v"};
char end_marks[] = {">+o-|<^v"};

int hlines_sz= sizeof(hlines) -1;
int vlines_sz= sizeof(vlines) -1;
int corners_sz = sizeof(corners) -1;
int stmarks_sz = sizeof(st_marks) - 1;
int endmarks_sz = sizeof(st_marks) - 1;

int cur_hl, cur_vl, cur_corn, cur_start, cur_end;
char line_h;
char line_v;
char mark_st;
char mark_end;

char modified;
char fname[256];

char visual;

struct termios t1, t2, t3;


void cleanup(int s){

	printf("\033[;H\033[2J");
	tcsetattr(0, TCSANOW, &t1);
	exit(0);
}

/*** Status bar ***/

char* state_str(){
	switch(state){
		case MOVE:
			return "mov";
		case TEXT:
			return "txt";
		case BOX:
			return "box";
		case ARROW:
			return "arr";
		case DEL:
			return "del";
		case VIS:
			return "vis";
		default:
			return "ERR";
	}
	return "ERR";
}


void status_bar(){

	printf("\033[%d;1f\033[7m", HEIGHT+1);
	printf("%*s", WIDTH-1, "");
	printf("\033[%d;1f\033[7m", HEIGHT+1);
	printf(" x:%3d y:%3d -- MODE:%4s HL:%c VL:%c CN:%c SP:%c EP:%c %10s",
		x, y, state_str(), line_h, line_v, corner, mark_st, mark_end, "");
	if (!modified)
		printf(" [%s]", fname );
	else
		printf(" *%s*", fname );
#ifdef DEBUG
	printf("  '%d'  ", screen[y][x]);
#endif
	printf("\033[0m");
}

char get_key(char *msg){

	printf("\033[%d;1f\033[7m", HEIGHT+1);
	printf("%*s", WIDTH, "");
	printf("\033[%d;1f\033[7m", HEIGHT+1);
	printf("%s", msg);
	printf("\033[0m");
	return getchar();
}

void get_string(char *msg, char *s, int sz){

	printf("\033[%d;1f\033[7m", HEIGHT+1);
	printf("%*s", WIDTH, "");
	printf("\033[%d;1f\033[7m", HEIGHT+1);
	/* We must activate echo now */
	t3 = t2;
	t3.c_lflag |= (ECHO | ICANON);
	tcsetattr(0, TCSANOW, &t3);
	printf("%s", msg);
	printf("\033[0m");
	fgets(s, sz, stdin);
	s[strlen(s)-1] = '\0';
	tcsetattr(0, TCSANOW, &t2);
}

int is_yes(char c){
	return c=='y' ? 1 : c == 'Y'? 1 : 0;
}

/*** Screen management ***/

void show_cursor(){
	printf("\033[%d;%df", y+1, x+1);
}

void set_cur(char c){
	screen[y][x] = c;
}

void set_xy(int x, int y, char c){
	/* FIXME: check if x and y are valid!!!! */
	screen[y][x] = c;
}

void draw_xy(int x, int y, char c){
	/* FIXME: check if x and y are valid!!!! */
	printf("\033[%d;%df",y+1,x+1);
	putchar(c);
}

void update_current(){
	printf("\033[%d'%df",y+1,x+1);
	putchar(screen[y][x]);
}

void erase_line(char *s){
	while(*s){
		*s = BG;
		s++;
	}
}

void erase_box(int x1, int y1, char c){
	int x_incr, y_incr, i; 

	x_incr = x1 < x? +1: -1;
	y_incr = y1 < y? +1: -1;
	do{
		i = y1;
		do{
			set_xy(x1, i, c);
		} while(i != y && (1 | (i += y_incr)));
	} while(x1 != x && (1 | (x1 += x_incr)));
	
}

void erase_screen(){
	int i;
	for(i=0;i<HEIGHT; i++)
		erase_line(screen[i]);
}

void check_bound(){
	if (x<0) x=0;
	else if (x>=WIDTH) x = WIDTH-1;
	if (y<0) y=0;
	else if (y>=HEIGHT) y = HEIGHT -1;
}

void reset_styles(){

	cur_corn = 0;
	corner = corners[0];
	cur_hl = cur_vl = 0;
	cur_start = cur_end = 0;
	line_h = hlines[cur_hl];
	line_v = vlines[cur_vl];
	mark_st = st_marks[cur_start];
	mark_end = end_marks[cur_end];
}

void redraw(){
	int i;

	printf("\033[2J\033[1;1H");
	for (i=0;i<HEIGHT;i++){
		fprintf(stdout,"%s\n",screen[i]);
	}
	status_bar();
	show_cursor();
}

void go_to(int where){
	switch(where){
		case HOME:
			x = y = 0;
			break;
		case END:
			x = WIDTH-1;
			y = HEIGHT-1;
			break;
		case MIDDLE:
			x = WIDTH/2;
			y = HEIGHT/2;
			break;
	}
	check_bound();
	show_cursor();
}

void handle_goto(){
	char c;	
	c=getchar();
	switch(c){
		case 'h':
			dir = DIR_L;
			x = 0;
			break;
		case 'l':
			dir = DIR_R;
			x = WIDTH - 1;
			break;
		case 'j':
			dir = DIR_D;
			y = HEIGHT - 1;
			break;
		case 'k':
			dir = DIR_U;
			y = 0;
			break;
		case 'g':
			dir = DIR_N;
			go_to(HOME);
			break;
		case 'G':
			dir = DIR_N;
			go_to(END);
			break;
		case 'm':
			dir = DIR_N;
			go_to(MIDDLE);
			break;
	}
	check_bound();
	show_cursor();
}

int move_around(char c){

	switch(c){
		case 'H': step = 5;/** FALLTHROUGH **/
		case 'h':
			dir = DIR_L;
			x -= step;
			break;
		case 'J': step = 5;/** FALLTHROUGH **/
		case 'j':
			dir = DIR_D;
			y += step;
			break;
		case 'K': step = 5;/** FALLTHROUGH **/
		case 'k':
			dir = DIR_U;
			y -= step;
			break;
		case 'L': step = 5;/** FALLTHROUGH **/
		case 'l':
			dir = DIR_R;
			x += step;
			break;
		case 'g':
			handle_goto();
			break;
		default:
			return 0;
	}
	return c;
}

int progr_x(int dir){
	return dir == DIR_L ? -1 : dir == DIR_R ? 1: 0;
}


int progr_y(int dir){
	return dir == DIR_U ? -1 : dir == DIR_D ? 1: 0;
}

void set_video(int v){
	printf("\033[%dm", v);
}

/*** Lines and markers ***/

void toggle_hline(){

	cur_hl = (cur_hl + 1) % hlines_sz;
	line_h = hlines[cur_hl];

}

void toggle_corner(){

	cur_corn = (cur_corn + 1 ) % corners_sz;
	corner = corners[cur_corn];

}

void toggle_vline(){

	cur_vl = (cur_vl + 1) % vlines_sz;
	line_v = vlines[cur_vl];

}

void toggle_st_mark(){

	cur_start = (cur_start + 1 ) % stmarks_sz;
	mark_st = st_marks[cur_start];
}

void toggle_end_mark(){

	cur_end = (cur_end+ 1 ) % endmarks_sz;
	mark_end = end_marks[cur_end];
}

int change_style(char c){
	switch(c){
		case '-':
			toggle_hline();
			break;
		case '|':
			toggle_vline();
			break;
		case '+':
			toggle_corner();
			break;
		case '<':
			toggle_st_mark();
			break;
		case '>':
			toggle_end_mark();
			break;
		case '.':
			reset_styles();
			break;
		default: 
			return 0;
	}
	return c;
}




/*****  text, box, arrows  *****/

void get_text(){
	char c;
	int orig_x = x;

	redraw();
	while((c=getchar())!=EOF && c != 27){
		if(c=='\n'){
			set_cur(BG);
			y += 1;
			x = orig_x;
		}
		else {
			set_cur(c);
			update_current();
			modified = 1;
			x += 1;
			if (x >= WIDTH)
				x = orig_x;
		}
		check_bound();
		status_bar();
		show_cursor();
	}
	state=MOVE;
}

void draw_box(int x1, int y1, int fix){

	int xmin, ymin, xmax, ymax;
	int i;
	void (*f)(int, int, char);

	if (fix == FIX)
		f = set_xy;
	else
		f = draw_xy;

	xmin = MIN(x, x1);
	xmax = MAX(x, x1);
	ymin = MIN(y, y1);
	ymax = MAX(y, y1);

	for(i=xmin+1; i<=xmax; i++){
		f(i, ymin, line_h);
		f(i, ymax, line_h);
	}
	for(i=ymin+1; i<=ymax; i++){
		f(xmin, i, line_v);
		f(xmax, i, line_v);
	}
	f(xmin, ymin, corner);
	f(xmin, ymax, corner);
	f(xmax, ymin, corner);
	f(xmax, ymax, corner);
	show_cursor();
}

void get_box(){
	char c;
	int orig_x=x, orig_y=y;
	redraw();
	step = 1;
	draw_box(x,y,NOFIX);
	while((c=getchar())!=EOF && c != 27 && c!= 'b' && c != '\n'){
		if (change_style(c))
			goto update_box;
		if (!move_around(c)) 
			continue;
		check_bound();
		redraw();
		step = 1;
update_box:
		draw_box(orig_x, orig_y, NOFIX);
		status_bar();
		show_cursor();
	}
	if (c == 'b' || c == '\n'){
		draw_box(orig_x, orig_y, FIX);
		modified = 1;
	}
	redraw();
	state = MOVE;
}

void draw_arrow(int x, int y, char *a, int a_len, int fix){

	int i, j, cur_dir;
	char line;
	void (*f)(int, int, char);


	if (fix == FIX)
		f = set_xy;
	else
		f = draw_xy;

	f(x,y,mark_st);
	if (!a_len){
		show_cursor();
		return;
	}
	cur_dir=DIR_N;
	for (i=0; i<a_len; i+=2){
		if (i>0) {
			/* If we are switching between horizontal and vertical, put a "corner" */
			if (((cur_dir & DIR_HOR) && (a[i] & DIR_VER)) ||
			    ((cur_dir & DIR_VER) && (a[i] & DIR_HOR))){
				f(x,y,corner);
				show_cursor();
			}
		}
		for(j=0; j<a[i+1]; j++){
			line = (a[i] & DIR_L) || (a[i] & DIR_R) ? line_h : line_v;
			x += progr_x(a[i]);
			y += progr_y(a[i]);
			f(x, y, line);
		}
		/* f(x,y,mark_end);*/
		cur_dir = a[i];
	}
	f(x,y,mark_end);
	show_cursor();
}

void get_arrow(){

	char c;
	int orig_x=x, orig_y=y, arrow_len;
	static char *arrow = NULL;
	static int arrow_sz;

	if (!arrow){
		arrow_sz = 100;
		arrow = malloc(arrow_sz * sizeof(char));
	}
	arrow_len = 0;
	dir = DIR_N;

	redraw();
	step = 1;
	draw_arrow(x,y, arrow, 0, NOFIX);
	while((c=getchar())!=EOF && c != 27 && c!= 'a' && c != '\n'){
		if (change_style(c))
			goto update_arrow;
		if (!move_around(c))
			continue;
		check_bound();
		/* FIXME: if we are out of bound, do nothing? */
		if (arrow_len == arrow_sz){
			arrow_sz *=2;
			arrow = realloc(arrow, arrow_sz * sizeof(char));
		}
		arrow[arrow_len++] = dir;
		arrow[arrow_len++] = step;
		redraw();
		step = 1;
update_arrow:
		draw_arrow(orig_x, orig_y, arrow, arrow_len, NOFIX);
		status_bar();
		show_cursor();
	}
	if (c == 'a' || c == '\n'){
		draw_arrow(orig_x, orig_y, arrow, arrow_len, FIX);
		modified = 1;
	}
	redraw();
	state = MOVE;
}


void do_delete(int x1, int y1){
	int i;
	switch(dir){
		case DIR_R:
			for(i=x1; i<=x; i++) set_xy(i,y,BG);
			break;
		case DIR_L:
			for(i=x1; i>=x; i--) set_xy(i,y,BG);
			break;
		case DIR_U:
			for(i=y1; i>=y; i--) set_xy(x,i,BG);
			break;
		case DIR_D:
			for(i=y1; i<=y; i++) set_xy(x,i,BG);
			break;
	}
}


void delete(){
	char c;
	int orig_x = x, orig_y = y;
	status_bar();
	show_cursor();
	while((c=getchar())!=EOF && c!=27 && c!= 'x' && c != '\n'){
		if (!move_around(c)) continue;
		check_bound();
		do_delete(orig_x, orig_y);
		step = 1;
		modified = 1;
		orig_x = x;
		orig_y = y;
		redraw();
		status_bar();
		show_cursor();
	}
	state = MOVE;
}

/*** File management ***/

void write_file(){
	FILE *f;
	int i;

	if (!fname[0] || force_new){
		get_string("Write to: ", fname, 255);
		if (f=fopen(fname, "r")){
			if (!is_yes(get_key("File exists. Overwrite [y/n]?")) ){
				fclose(f);
				return;
			}
			fclose(f);
		}
	}
	if((f=fopen(fname, "w"))==NULL){
		get_key("Error opening file.");
		return;
	}
	for (i=0; i<HEIGHT; i++){
		fprintf(f, "%s\n", screen[i]);
	}
	fclose(f);
	modified = 0;
	get_key("File saved.");
}

void check_modified(){

	if (modified){
		if (!is_yes(get_key("Unsaved changes. Write to file [y/n]?")) ){
			return;
		}
		write_file(0);
	}
}

void load_file(){

	char newfname[256];
	FILE *f;
	int i;

	get_string("Load file: ", newfname, 255);
	if ((f=fopen(newfname, "r")) != NULL){
		i = 0;
		while((fgets(screen[i], WIDTH+2, f)) != NULL && i<HEIGHT)
			screen[i++][WIDTH-1]='\0';
		for(;i<HEIGHT; i++){
			erase_line(screen[i]);
		}
		fclose(f);
	}
	strcpy(fname, newfname);
	modified=0;
	redraw();
}

void new_file(){
	check_modified();
	erase_screen();
	go_to(MIDDLE);
	redraw();
	fname[0] = '\0';
	modified=0;
}

/*** Visual ***/


void visual_box(){
	int orig_x =x, orig_y = y;
	char c, f = BG;

	redraw();
	step = 1;
	set_video(VIDEO_REV);
	draw_box(x,y,NOFIX);
	while((c=getchar())!=EOF && c != 27 && c!= 'v' && c != '\n'){
		if (!move_around(c)) switch(c){
			case 'f':/* fill */
				f = get_key("fill char: "); /** FALLTHROUGH **/
			case 'x':/* erase */
				erase_box(orig_x, orig_y, f);
				modified = 1;
				goto vis_exit;
				break;
		} 
		check_bound();
		set_video(VIDEO_NRM);
		redraw();
		step = 1;
		f = BG;
		set_video(VIDEO_REV);
		draw_box(orig_x, orig_y, NOFIX);
		status_bar();
		show_cursor();
	}
vis_exit:
	set_video(VIDEO_NRM);
	redraw();
	state = MOVE;
}

/*** Initialisation ***/

void init_screen(){
	int i;
	struct winsize wsz;
	
	if (!ioctl(STDIN_FILENO, TIOCGWINSZ, &wsz)){
		WIDTH=wsz.ws_col - 2;
		HEIGHT=wsz.ws_row - 1;
	}
	else {
		WIDTH=80;
		HEIGHT=24;
	}
	screen = malloc(HEIGHT * sizeof(char *));
	if (screen == NULL){
		perror("allocating screen");
		exit(1);
	}
	for (i=0; i<HEIGHT; i++){
		screen[i] = malloc((WIDTH+1) * sizeof(char));
		if (screen[i] == NULL){
			perror("allocating screen[i]");
			exit(1);
		}
		memset(screen[i], ' ', WIDTH * sizeof(char));
		screen[i][WIDTH]='\0';
	}
	reset_styles();
}


void init(){

	signal(SIGHUP, cleanup);
	signal(SIGINT, cleanup);
	signal(SIGTERM, cleanup);
	signal(SIGQUIT, cleanup);

	tcgetattr(0, &t1);
	t2 = t1;
	t2.c_lflag &= ~(ICANON | ECHO);
	tcsetattr(0, TCSANOW, &t2);

	init_screen();
	x = 0;
	y = 0;
	modified = 0;
	fname[0] = '\0';
	redraw();
}


/*** Commands ***/

void commands(){

	char c;
	while((c=getchar())!=EOF){
		if (!change_style(c) && !move_around(c)){
			switch(c){
				case 'i':
					state = TEXT;
					get_text();
					break;
				case 'R':
					redraw();
					break;
				case 'b':
					state = BOX;
					get_box();
					break;
				case 'a':
					state = ARROW;
					get_arrow();
					break;
				case 'W':
					force_new = 1;/** FALLTHROUGH **/
				case 'w':
					write_file();
					break;
				case 'e':
					check_modified();/** FALLTHROUGH **/
				case 'E':
					load_file();
					break;
				case 'N':
					new_file();
					break;
				case 'x':
					state = DEL;
					delete();
					break;
				case 'v':
					state = VIS;
					visual_box();
					break;
				case 'q':
					check_modified();/** FALLTHROUGH **/
				case 'Q':
					cleanup(0);
					exit(0);
					break;
			}
		}
		check_bound();
		status_bar();
		show_cursor();
		step = 1;
		force_new = 0;
	}

}


int main(int argc, char *argv[]){

	init();

	commands();
	cleanup(0);
}