#include <string.h>

#include "irc.h"
#include "screen.h"
#include "edbox.h"
#include "numerics.h"

#include <atari.h>
#include "nio.h"
#include "config.h"
#include "indic8.h"
#include "complete.h"
#include "keytab.h"
#include "kgetc.h"
#include "streq.h"
#include "timers.h"
#include "memclear.h"

#ifndef VERSION
#define VERSION "?????"
#endif

#define MAX_MSG 512

char *msg_src, *msg_cmd, *msg_dest, *msg_text;
char *msg_args[MAX_MSG_ARGS];
char msg_argcount;

char irc_away = 0;
char bell_type;
char hide_motd;
char start_latch = 0;
char new_scr_status;
char need_rejoin;
char self_src;

static char msgbuf[MAX_MSG] = { 0 };
static int msgbuf_len = 0;

static char regged = 0, hilite = 0;
static char scr_prev = SCR_PRIV;

char numbuf[10];

char last_pm_nick[33];
char last_chan[33]; /* without a screen */

static int minutes, last_read_min;

/* see pollkbd.s */
extern void poll_keyboard(void);

/*
static void join_channel(void) {
	txbuf_set_str2("JOIN ", channel);
	txbuf_send();
}
*/

static void send_nick(void) {
	txbuf_set_str2("NICK ", config.nick);
	txbuf_send();
}

static void print_reason(void) {
	if(msg_text) {
		scr_print_active(": ");
		scr_print_active(msg_text);
	}
	scr_eol_active();
}

static void bold(void) {
	scr_putc_active('\x02');
}

static void hilite_bold(void) {
	if(hilite) bold();
}

static void do_chan_nick(void) {
	if(hilite) {
		bell();
		new_scr_status = SCR_HILITE;
	}
	hilite_bold();
	scr_putc_active('<');
	hilite_bold();
	scr_print_active(msg_src);
	if(scr_active == SCR_SERVER) {
		/* if we don't have a window for it */
		scr_putc_active('/');
		scr_print_active(msg_dest);
	}
	hilite_bold();
	scr_putc_active('>');
	hilite_bold();
	scr_putc_active(' ');
}

static void do_priv_nick(void) {
	if(msg_src) {
		scr_putc_active('*');
		scr_print_active(msg_src);
		scr_print_active("* ");
		new_scr_status = SCR_HILITE;
		bell();
	}
}

/* the ping calculation in jifsec.s relies on the assumption that
   nobody's ping time will ever be more than 9 minutes. anyone
   that lagged will have been disconnected from the server already.
   if this assumption turns out to be false, the ping time displayed
   will be wrong (modulo ~9 mins). I don't think it's ever going to be
   a real problem.
   Why do it this way? Because using longs instead of ints bloats the
   code by 778 bytes!
   note to self: using a div_t and div() seemed like it might make
   the compiled code smaller, but it grew by ~50 bytes. avoid.
 */
static void print_ping_time(char *p) {
	extern void calc_ping_time(char *p);

	scr_print_active("*** ");
	scr_print_active(msg_src);
	scr_print_active(" lag: ");
	calc_ping_time(p);
	scr_putc_active('s');
}

static void do_server_pong(void) {
	msg_src = "Server";
	// FIXME: what if the server doesn't :-quote the response?
	print_ping_time(msg_text);
	scr_eol_active();
}

static void print_ctcp(void) {
	scr_print_active("*** CTCP ");
}

/* FIXME: this isn't very fast */
static void do_ctcp(char is_notice) {
	static char *p, *ctcp_type, *resp;

	resp = 0;
	ctcp_type = ++msg_text; /* skip leading ^A */

	if( (p = strchr(msg_text, '\x01')) ) {
		/* kill trailing ^A */
		*p = 0;
	}

	if( (p = strchr(msg_text, ' ')) ) {
		*p++ = 0;
	}

	if(is_notice) {
		/* NOTICEs are responses */
		if(p && streq_i(ctcp_type, "PING")) {
			print_ping_time(p);
		} else {
			print_ctcp();
			scr_print_active(ctcp_type);
			scr_print_active(" resp from ");
			scr_print_active(msg_src);
			if(p) {
				scr_print_active(": ");
				scr_print_active(p);
			}
		}
		scr_eol_active();
	} else {
		/* this is a PRIVMSG (aka a request) */
		if(streq_i(ctcp_type, "ACTION")) {
			scr_print_active("* ");
			scr_print_active(msg_src);
			scr_putc_active(' ');
			scr_print_active(p);
			scr_eol_active();
			return;
		}

		print_ctcp();
		scr_print_active(ctcp_type);
		scr_print_active(" req from ");
		scr_print_active(msg_src);
		scr_eol_active();

		if(streq_i(ctcp_type, "PING")) {
			resp = p;
		} else if(streq_i(ctcp_type, "CLIENTINFO")) {
			resp = "PING VERSION CLIENTINFO";
		} else if(streq_i(ctcp_type, "VERSION")) {
			resp = "FujiNetChat (" VERSION "; Atari 8-bit)";
		} else {
			/* unknown CTCP type, ignore */
			return;
		}

		txbuf_set_str3("NOTICE ", msg_src, " :\x01");
		txbuf_append_str(ctcp_type);
		txbuf_append_spc();
		txbuf_append_str(resp);
		txbuf_append_01();
		txbuf_send();
	}
}

static void do_privmsg(void) {
	/* TODO: this shouldn't be case-sensitive */
	/*
	if(strstr(msg_text, config.nick))
		hilite = 1;
	else
		hilite = 0;
		*/
	hilite = find_nick();

	if(*msg_text == '\x01') {
		do_ctcp(0);
		return;
	}

	if(*msg_dest == '#') {
		do_chan_nick();
		ind_act_chantext();
	} else {
		do_priv_nick();
		if(*msg_cmd == 'N')
			ind_act_notice();
		else
			ind_act_pm();
	}

	scr_print_active(msg_text);
	scr_eol_active();
}

static void do_notice(void) {
	if(*msg_text == '\x01') {
		do_ctcp(1);
	} else {
		scr_print_active("NOTICE ");
		do_privmsg();
	}
}

static void do_join(void) {
	ind_act_join();
	if(self_src) {
		scr_print_active("You");
	} else {
		scr_print_active("\x02=\x02");
		scr_print_active(msg_src);
	}
	scr_print_active(" joined ");
	scr_print_active(msg_dest);
	scr_eol_active();
}

static void do_nick(void) {
	/* Do not overwrite config.nick with bogus data! sometimes when
	   we get disconnected, upon reconnect we get a partial message.
	   if it's a NICK, missing its destination argument, we end up
	   blowing away config.nick, and subsequent reconnect attempts
	   fail with "USER: not enough parameters". This is purely
	   a band-aid; a proper solution involves rewriting parse_msg()
	   so it knows how many args each msg type needs, and refuses to
	   parse invalid ones. */
	if(!msg_dest || !*msg_dest || *msg_dest == ' ')
		return;

	// ind_act_none();
	if(self_src) {
		scr_print_active("You are ");
		strncpy(config.nick, msg_dest, 32);
	} else {
		scr_print_active(msg_src);
		scr_print_active(" is ");
	}
	scr_print_active("now known as ");
	scr_print_active(msg_dest);
	scr_eol_active();
}

static void do_quit(void) {
	ind_act_quit();
	scr_print_active(msg_src);
	scr_print_active(" has quit IRC");
	print_reason();
}

static void do_part(void) {
	ind_act_part();
	scr_print_active(msg_src);
	scr_print_active(" has left ");
	scr_print_active(msg_dest);
	print_reason();
}

static void do_topic(void) {
	scr_print_active(msg_src);
	scr_print_active(" sets the ");
	scr_print_active(msg_dest);
	scr_print_active(" topic to: ");
	scr_print_active(msg_text);
	scr_eol_active();
}

static void do_kick(void) {
	scr_print_active(msg_src);
	scr_print_active(" kicks ");
	scr_print_active(msg_args[1]);
	scr_print_active(" from ");
	scr_print_active(msg_dest);
	print_reason();
}

/* numerics call this with arg==1, since arg 0 is always our nick.
   other commands call with arg==0 to print everything.
 */
static void do_catchall(char arg) {
	if(msg_src) {
		scr_print_active(msg_src);
		scr_putc_active(' ');
	}

	scr_print_active(msg_cmd);

	for(; arg < msg_argcount; arg++) {
		scr_putc_active(' ');
		scr_print_active(msg_args[arg]);
	}

	if(msg_text) {
		scr_putc_active(' ');
		scr_print_active(msg_text);
	}
	scr_eol_active();
}

static void do_mode(void) {
	if(msg_src) {
		scr_print_active(msg_src);
		msg_src = 0; /* don't let do_catchall() print it again */
	} else {
		scr_print_active("Server");
	}
	scr_print_active(" sets mode:");
	do_catchall(0);
}

extern void permute_nick(void);

/* see: https://defs.ircdocs.horse/ */
static void do_forward_chan(void) {
	static char s;

	if(msg_argcount > 2 && msg_args[1][0] == '#' && msg_args[2][0] == '#') {
		s = scr_getbyname(msg_args[1]);
		if(s)
			strncpy(screens[s].title, msg_args[2], 31);
	}
	do_catchall(1);
}

static void do_numeric(void) {
	static unsigned int num;

	num = a2uint(msg_cmd);

	switch(num) {
		/* use the server's idea of what our nick is, in case it got
		   truncated. */
		case RPL_WELCOME:
			strcpy(config.nick, msg_args[0]);
			regged = 1;
			do_catchall(1);
			break;

			/*
		case RPL_WHOISCERTFP:
		case RPL_WHOISREGNICK:
		case RPL_WHOISUSER:
		case RPL_WHOISSERVER:
		case RPL_WHOISOPERATOR:
		case RPL_WHOISIDLE:
		case RPL_WHOISCHANNELS:
		case RPL_WHOISSPECIAL:
		case RPL_WHOISACCOUNT:
		case RPL_WHOISACTUALLY:
		case RPL_WHOISHOST:
		case RPL_WHOISMODES:
		case RPL_WHOISSECURE:
		case RPL_WHOWASUSER:
		These always go to the server window.
		*/

		case ERR_NICKNAMEINUSE:
			if(regged) {
				scr_activate(scr_current);
			} else {
				permute_nick();
				send_nick();
			}
			do_catchall(1);
			break;

		/* don't print these, just noise */
		case RPL_MOTDSTART:
			break;

		case RPL_MOTD:
			if(!hide_motd)
				do_catchall(1);
			break;

		/* don't print, but do trigger rejoin */
		case RPL_ENDOFMOTD:
		case ERR_NOMOTD:
			hide_motd = 0;
			if(need_rejoin) {
				cmd_rejoin_chans();
				if(screens[2].title[0] == '#')
					scr_display(2);
				need_rejoin = 0;
			}
			break;

		case RPL_NAMREPLY:
			scr_print_active(msg_args[2]);
			scr_print_active(" users: ");
			scr_print_active(msg_text);
			scr_eol_active();
			break;

		case RPL_ENDOFNAMES:
			/* don't print, just noise */
			break;

		case RPL_TOPIC:
			scr_print_active("Topic for ");
			scr_print_active(msg_args[1]);
			scr_print_active(": ");
			scr_print_active(msg_text);
			scr_eol_active();
			break;

		case RPL_TOPICWHOTIME:
			scr_print_active("Topic set by ");
			scr_print_active(msg_args[2]);
			scr_eol_active();
			break;

		case 470: /* aka ERR_LINKCHANNEL, for Unreal and Solanum ircds */
			do_forward_chan();
			break;

		case RPL_AWAY:
			scr_print_active(msg_args[1]);
			scr_print_active(" is away: ");
			scr_print_active(msg_text);
			scr_eol_active();
			break;

		default:
			if(num >= 400 && num < 600)
				scr_activate(scr_current);
			do_catchall(1);
			break;
	}
}

#if 0
static void invalid_msg(char type) {
	scr_print(SCR_SERVER, "??? unknown, type ");
	scr_putc(SCR_SERVER, type);
	scr_putc(SCR_SERVER, '\n');
}
#endif

static char cmd_is(char *cmd) {
	return streq_i(msg_cmd, cmd);
}

void select_screen(void) {
	char s;
	if(!msg_dest) {
		s = SCR_SERVER;
	} else if(*msg_dest == '#') {
		s = scr_getbyname(msg_dest);
		if(!s) {
			strncpy(last_chan, msg_dest, 32);
			comp_add_pm_chan(last_chan);
			s = SCR_SERVER;
		}
	} else {
		s = scr_getbyname(msg_src);
		if(!s) {
			if(cmd_is("PRIVMSG")) { /* or maybe NOTICE? */
				strncpy(last_pm_nick, msg_src, 32);
				comp_add_pm_nick(last_pm_nick);
				s = SCR_PRIV;
			} else {
				s = SCR_SERVER;
			}
		}
	}
	scr_activate(s);
}

static void dispatch_msg(void) {
	self_src = streq_i(config.nick, msg_src);

	/* at this point, we know the message source and destination, so: */
	/* FIXME: maybe we know... */
	select_screen();
	new_scr_status = SCR_OTHER;

	if(cmd_is("PRIVMSG")) {
		new_scr_status = SCR_ACTIVE;
		do_privmsg();
	} else if(cmd_is("NOTICE")) {
		do_notice();
	} else if(cmd_is("JOIN")) {
		do_join();
	} else if(cmd_is("NICK")) {
		do_nick();
	} else if(cmd_is("QUIT")) {
		do_quit();
	} else if(cmd_is("PART")) {
		do_part();
	} else if(cmd_is("TOPIC")) {
		do_topic();
	} else if(cmd_is("KICK")) {
		do_kick();
	} else if(cmd_is("MODE")) {
		do_mode();
	} else if(cmd_is("PONG")) {
		if(*msg_text == 'A') {
			return; /* do not set screen status */
		} else {
			do_server_pong();
		}
	} else if(isnum(msg_cmd[0])) {
		do_numeric();
	} else {
		do_catchall(0);
	}

	if(scr_active != scr_current) {
		if(screens[scr_active].status < new_scr_status) {
			screens[scr_active].status = new_scr_status;
			scr_show_status(scr_current);
		}
	}
}

/* msgbuf contains a complete message from the server, whose
   length is msgbuf_len. The EOL that was at the end of the message
   is *not* present here (been replaced with a null already). */
static void parse_msg(void) {
	char *p;

	/* ignore empty message */
	if(!*msgbuf) return;

	msg_cmd = msg_text = msg_src = msg_dest = 0;
	memclear(msg_args, sizeof(msg_args));

	/* first token is either the source (with a :) or a command (without) */
	if(*msgbuf == ':') {
		msg_src = msgbuf + 1; /* generally :irc.example.com or :nick!user@host */
		msg_cmd = nextarg(msgbuf);
	} else {
		msg_src = 0; /* no source supplied */
		msg_cmd = msgbuf;
	}
	p = nextarg(msg_cmd);

	#if 0
	if(!msg_cmd) {
		invalid_msg('2');
		return;
	}
	#endif

	/* special case for ping, treat as 1 arg, even if it has space and no : */
	if(cmd_is("PING")) {
		txbuf_set_str2("PONG ", msg_cmd + 6);
		txbuf_send();
		return;
	}

	for(msg_argcount = 0; msg_argcount < MAX_MSG_ARGS; msg_argcount++) {
		if(!p) break;

		if(*p == ':') {
			msg_text = p + 1;
			break;
		}

		msg_args[msg_argcount] = p;

		/* if any arg is a channel name, use it for the dest */
		if(*p == '#')
			msg_dest = p;

		p = nextarg(p);
	}

	if(!msg_dest) {
		if(msg_argcount)
			msg_dest = msg_args[0];
		else if(msg_text)
			msg_dest = msg_text;
	}

	if(msg_src) {
		if((p = strchr(msg_src, '!'))) {
			*p = '\0';
		} else if(strchr(msg_src, '.')) {
			msg_src = 0;
		}
	}

	dispatch_msg();
}


/* we read via block I/O, so split into lines and process each one
   separately. */
static void irc_split_Lines(void) {
	int i;
	char *p = rx_buf;

	for(i = 0; i < rxbuflen; i++) {
		/* skip ASCII \r character */
		if(*p != 0x0d) {
			if(*p == 0x0a) {
				/* got ASCII \n */
				msgbuf[msgbuf_len] = '\0';
				parse_msg();
				msgbuf_len = 0;
			} else {
				msgbuf[msgbuf_len++] = *p;
			}
		}
		p++;
	}
}

void print_errnum(void) {
	extern unsigned char err;
	scr_print_current("Error #");
	scr_cur_printnum(err);
}

static void start_minute_timer() {
	OS.cdtmf4 = 0xff;
	OS.cdtmv4 = timers.one_sec;
}

static char service_minute_timer() {
	signed int idle;

	if(OS.cdtmf4) return 1; /* timer hath not expired */

	minutes++;
	start_minute_timer();
	idle = minutes - last_read_min;

	/* if idle is 1, that doesn't really mean a full minute of
	   idle time: last_read_min might have gotten set just before
		minutes updated. think of it as rounding up. */
	if(idle < 0) {
		/* timer rolled over, reset and ignore */
		last_read_min = minutes;
		return 1;
	} else if(idle < 2) {
		/* idle between 1 and 60 sec, no problem. */
		return 1;
	} else if(idle == 2) {
		/* we've been idle between 61 and 120 seconds.
		   generate some traffic. */
		txbuf_send_str("PING A");
		return 1;
	} else {
		/* idle >=121 sec, already sent a ping, got nothing back.
		   we timed out. */
		scr_print_current("Server timed out\n");
		return 0;
	}
}

char irc_read(void) {
	if(!trip) return 1;

	last_read_min = minutes;
	err = nstatus();

	if(err != 1) {
		scr_display(SCR_SERVER);
		ind_net_down();
		if(err == 136) {
			scr_print_current("Disconnected");
		} else {
			print_errnum();
		}
		scr_eol_current();
		bell();
		return 0;
	}

	// Get # of bytes waiting, no more than size of rx_buf
	rxbuflen = (OS.dvstat[1] << 8) | OS.dvstat[0];

	if(rxbuflen > MAX_MSG)
		rxbuflen = MAX_MSG;

	ind_net_rx();
	if(rxbuflen > 0) {
      err = nread_rxbuf();
		if(err != 1) {
			ind_net_down();
			print_errnum();
			return 0;
		}

		trip = 0;
		PIA.pactl |= 1; // Flag interrupt as serviced, ready for next one.

		irc_split_Lines();
	}

	ind_net_idle();
	return 1;
}

/* modern.ircdocs.horse say to do this IMMEDIATELY upon TCP
   connection, without waiting for anything from the server. */
void irc_register(void) {
	if(regged)
		return;

	/* 2nd arg: local (UNIX) username, just use the nick */
	/* 3rd arg: "real" name */
	txbuf_set_str3("USER ", config.nick, " 0 * :");
	txbuf_append_str(config.real_name);
	txbuf_send();

	send_nick();
}

static void scrollback() {
	char c;
	scr_scrollback();
	while(!keypress())
		irc_read();
	c = kgetc();
	while(c == '-') {
		scr_scrollback();
		while(!keypress())
			irc_read();
		c = kgetc();
	}
	scr_end_scrollback();
}

static void hunt_screen(signed char dir) {
	signed char s = scr_current;

	do {
		s += dir;
		if(s < 0)
			s = MAX_SCREENS - 1;
		if(s == MAX_SCREENS)
			s = 0;
	} while(screens[s].status == SCR_UNUSED);

	scr_display(s);
}

static char *get_cur(void) {
	if((scr_current > 1) && (screens[scr_current].title[0] == '#'))
		return screens[scr_current].title;
	else
		return 0;
}

static char *get_cur_chan(void) {
	if(scr_current == SCR_SERVER && last_chan[0])
		return last_chan;
	else return get_cur();
}

static char *get_cur_nick(void) {
	if(scr_current == SCR_PRIV && last_pm_nick[0])
		return last_pm_nick;
	else return get_cur();
}

static void send2_with_space(char *s1, char *s2) {
	txbuf_set_str3(s1, " ", s2);
	txbuf_send();
}

static void send_cur_nick_cmd(char *cmd) {
	char *nick;

	if(!(nick = get_cur_nick()))
		return;

	send2_with_space(cmd, nick);
}

static void send_cur_chan_cmd(char *cmd) {
	char *chan;

	if(!(chan = get_cur_chan()))
		return;

	send2_with_space(cmd, chan);
}

/*
static char cur_is_chan(void) {
	return (scr_current > 2 && scr_names[scr_current][0] == '#');
}

static char cur_is_query(void) {
	return (scr_current > 2 && !cur_is_chan());
}
*/

/* count backwards here, because the [server] screen is 0, it's
   the least interesting one. */
char find_scr_with_status(char status) {
	signed char i;

	for(i = MAX_SCREENS - 1; i != -1; i--) {
		if(screens[i].status == status) {
			return i;
		}
	}
	return 0xff;
}

void switch_to_active() {
	char i;

	i = find_scr_with_status(SCR_HILITE);
	if(i == 0xff)
		i = find_scr_with_status(SCR_ACTIVE);
	if(i == 0xff)
		i = find_scr_with_status(SCR_OTHER);
	if(i != 0xff) {
		scr_prev = scr_current;
		scr_display(i);
	}

}

void list_screens(void) {
	char i;

	scr_activate(scr_current);
	for(i = 0; i < MAX_SCREENS; i++) {
		bold();
		scr_putc_active(i + '1');
		bold();
		scr_putc_active(':');
		if(screens[i].status == SCR_UNUSED)
			scr_print_active("<unused>");
		else
			scr_print_active(screens[i].title);
		scr_eol_active();
	}
}

void join_last_invite(void) {
	// TODO!
}

void ui_ping(void) {
	char *nick;

	if(!(nick = get_cur_nick()))
		return;

	cmd_ctcp_ping(nick);
}

static void toggle_edbox_only(void) {
	if(OS.sdlst == edbox_only_dlist)
		scr_end_scrollback();
	else
		OS.sdlst = edbox_only_dlist;
}

void start_keystroke(char c) {
	char s;

	start_latch = 0;
	if(c == CH_ESC) return;

	if(c >= '1' && c <= '7') {
		s = c - '1';
		if(s != scr_current) {
			if(screens[s].status != SCR_UNUSED) {
				scr_prev = scr_current;
				scr_display(s);
			}
		}
		return;
	}

	switch(lcase(c)) {
		case XCH_UP:
		case '-':
			scrollback();
			return;
		case 0x18: /* ^X */
			send_cur_chan_cmd("PART");
			/* fall thru */
		case 'x':
			scr_prev = SCR_PRIV;
			scr_destroy(scr_current);
			return;
		case XCH_LEFT:
		case '+':
			scr_prev = scr_current;
			hunt_screen(-1);
			return;
		case XCH_RIGHT:
		case '*':
			scr_prev = scr_current;
			hunt_screen(1);
			return;
		case XCH_TAB:
			c = scr_current;
			scr_display(scr_prev);
			scr_prev = c;
			return;
		case 'q':
			if(scr_current == SCR_PRIV && *last_pm_nick) {
				scr_prev = scr_current;
				scr_create(last_pm_nick);
				*last_pm_nick = 0;
			} else if(scr_current == SCR_SERVER && *last_chan) {
				scr_prev = scr_current;
				scr_create(last_chan);
				*last_chan = 0;
			}
			return;
		case 'a':
			switch_to_active();
			return;
		case 'l':
			list_screens();
			return;
		case 'j':
			join_last_invite();
			return;
		case 'w':
			send_cur_nick_cmd("WHOIS");
			return;
		case 'n':
			send_cur_chan_cmd("NAMES");
			return;
		case 'p':
			ui_ping();
			return;
		case 't':
			send_cur_chan_cmd("TOPIC");
			break;
		case 'e':
			toggle_edbox_only();
			break;
		case 0x04: /* ^D */
			fn_disconnect(); /* testing only! */
			break;
		case 's':
			edbox_hide();
			/* fall thru */
		default:
			break;
	}
}

/* only exits on error (e.g. connection closed, which might be via /QUIT). */
void irc_loop(void) {
	/* this stuff happens on every connect. */
	hide_motd = config.hide_motd;
	msgbuf[0] = msgbuf_len = regged = irc_away = minutes = 0;
	need_rejoin = 1;
	start_minute_timer();

	while(1) {
		ind_check_timer();
		if(config.atract_away) {
			if(!irc_away && (OS.atract & 0x80)) {
				irc_away = 1;
				txbuf_send_str("AWAY :ATRACT mode");
			}
		}
		if(!irc_read() || !service_minute_timer()) {
			return;
		}
		OS.cdtmv3 = 0;
		do {
			poll_keyboard();
		} while(OS.cdtmv3);
	}
}
