/* ============================================================================
 * << File: gems_client.c >>
 * -------------------------
 *  Authors: Emiliano Castagnari (aka Torian) <ecastag@fi.uba.ar>
 *           Diego Essaya <dessaya@fi.uba.ar>
 *     Date: 29/12/03
 *
 *  Description:
 *    Every client-specific functions are implemented here.
 * ============================================================================
 * ============================================================================
 *
 * ChangeLog: View Changelog
 *
 * ============================================================================
 * Copyright (C) 2003, 2004 Diego Essaya, Emiliano Castagnari
 * 
 * This file is part of gems.
 * 
 * gems 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.
 *
 * gems 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 Library 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., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
 * ============================================================================
 */

#include <sys/socket.h>			/* socket(), connect() */
#include <arpa/inet.h>			/* inet_ntoa() */
#include <unistd.h>				/* write(), close(), ... */
#include <stdlib.h>				/* atoi()      */
#include <stdio.h>				/* fprintf()   */
#include <string.h>				/* strncmp()   */
#include <netdb.h>				/* gethostbyname() */
#include <termios.h>			/* tcsetattr(), tcgetattr() */
#include <errno.h>				/* Errors - errno variable */
#include <locale.h>				/* setlocale() */
#include <stdarg.h>				/* variable parameter functions */

#include "protocol.h"
#include "common.h"
#include "sighandlers.h"

#define	APPNAME	PROJECT_NAME"-client"

/* Accepted arguments: */
#define OPT_HELP		"-h"
#define OPT_VERSION		"-v"
#define OPT_IGNORE_WS	"-i"

/* Structure with client-relative information (sockets, file descriptors, 
 * options, etc), used in many functions */
typedef struct t_client_data
{
	struct termios *old_tty_settings; /* Old terminal configuration */

	int socket_fd;                    /* Connection socket FD */

	fd_set open_fds;                  /* List of open FDs, for select() */

	int server_cols;                  /* Server's terminal size */
	int server_rows;

	char alarm_enabled;               /* 1 = Play alarm when received ('\a') */
	char ignore_winsize;              /* 1 = Ignore server's terminal size   */
} t_client_data;


/* Print a message to stderr, prepending the "appname: " string */
void print_msg(char *msg, ...)
{
	static char string[255];	/* Container for the formatted message */
	va_list va;

	va_start(va, msg);
	vsprintf(string, msg, va);
	va_end(va);
	fprintf(stderr, "%s: %s\n", APPNAME, string);
}

/* Disables local echoing and canonical input for stdin (in order to be able
 * to read one character at a time, instead of one line). 
 * Returns the old terminal configuration, or NULL in case of error. */
struct termios *set_tty_settings()
{
	struct termios new_settings;
	static struct termios old_settings;

	if (tcgetattr(0, &old_settings) == -1)
	{
		perror(APPNAME);
		return NULL;
	}
	new_settings = old_settings;

	/* Disable echoing */
	new_settings.c_lflag &= (~ECHO);

	/* Disable canonical input, and set input buffer to 1 byte (1 char) */
	new_settings.c_lflag &= (~ICANON);
	new_settings.c_cc[VTIME] = 0;
	new_settings.c_cc[VMIN] = 1;

	if (tcsetattr(0, TCSANOW, &new_settings) == -1)
	{
		perror(APPNAME);
		return NULL;
	}

	return &old_settings;
}

/* Converts server's domain (obtained from program arguments) into its IP
 * address. Returns the structure with this info, ready to be used in
 * connect(), or NULL in case of error. */
struct sockaddr_in *get_server_address(char *host, int tcp_port)
{
	struct hostent *server;
	static struct sockaddr_in address;

	/* Get IP: */
	if (!(server = gethostbyname(host)) ||
	    !(server->h_addr))
	{
		print_msg(_("Host %s not found."), host);
		return NULL;
	}

	/* Type of IP: IPv4 */
	address.sin_family = AF_INET;
	/* Port: */
	address.sin_port = htons(tcp_port);
	/* IP address: */
	address.sin_addr.s_addr = ((struct in_addr *) (server->h_addr))->s_addr;

	return &address;
}


/* Establish connection with server. Returns the new socket, or -1 in case 
 * of error. */
int connect_with_server(char *ip_str, int port)
{
	struct sockaddr_in *address = NULL;	/* server's address  */
	int socket_fd = -1;

	/* Configure the struct address, to be passed to connect() */
	if ((address = get_server_address(ip_str, port)) == NULL)
		return -1;

	/* Create the socket: */
	if ((socket_fd = socket(AF_INET, SOCK_STREAM, 0)) == -1)
	{
		perror(APPNAME);
		return -1;
	}

	/* Connect to server: */
	if (connect(socket_fd, (struct sockaddr *) address, sizeof(*address)))
	{
		perror(APPNAME);
		return -1;
	}

	return socket_fd;
}

/* Once the connection has been established, this function starts 
 * communication with the server, following the protocol specifications.
 * Saves the server's terminal size in *server_cols and *server_rows. */
fbool_t start_comm(t_client_data *data)
{
	char *version;
	fbool_t r;

	/* Server sends its version: */
	if ((r = get_peer_version(data->socket_fd, &version)) != TRUE)
	{
		/* If the server sends DISCONNECT instead of its version, it means
		 * that the connection cannot be established, because the server is
		 * saturated. */
		if (r == FALSE) 
			print_msg(_("Server does not accept any more connections."));
		return r;
	}
	/* Compare versions: */
	if (strncmp(version, PROTOCOL_VERSION, strlen(PROTOCOL_VERSION)))
	{
		print_msg(_("Incompatible server version."));
		if ((r = send_disconnect(data->socket_fd) != TRUE)) return r;
		return FALSE;
	}
	if ((r = send_ack(data->socket_fd) != TRUE)) return r;

	/* Now the client must send its version: */
	if ((r = send_version(data->socket_fd)) != TRUE) return r;
	if ((r = get_ack(data->socket_fd)) != TRUE)
	{
		if (r == FALSE) print_msg(
				_("Connection refused by server - Incompatible versions."));
		return r;
	}

	/* Server sends terminal size: */
	if (get_winsize(data->socket_fd, 
		&(data->server_cols), &(data->server_rows)) != TRUE)
	{
		/* Local size too small */
		if (! data->ignore_winsize) {
			print_msg(_("Terminal size too small (minimum required: %dx%d)."),  
					data->server_cols, data->server_rows);
			if ((r = send_disconnect(data->socket_fd) != TRUE)) return r;
			return FALSE;
		}
	}
	if ((r = send_ack(data->socket_fd) != TRUE)) return r;

	return TRUE;
}


/* Writes data received from server into stdout. 
 * If data->alarm_enabled == 0, characteres '\a' are also filtered. */
void do_output(char *buffer, int len, t_client_data *data)
{
	int i;

	if (data->alarm_enabled)
		write(STDOUT_FILENO, buffer, len);
	else
	{
		for (i = 0; i < len; i++)
			if (buffer[i] != '\a')
				write(STDOUT_FILENO, buffer + i, 1);
	}
}


/* Gets a complete message from the server, and processes it. 
 * Returns EXIT_OK if the server closed the connection, and CONTINUE if no
 * errors occured. */
status_t do_server_input(t_client_data *data)
{
	t_gems_msg msg;
	fbool_t r;

	r = get_msg(data->socket_fd, &msg);

	if (r == FALSE)
	{
		fputc('\n', stderr);
		print_msg(_("End of transmission."));
		/* putchar('\r'); */ /* Just in case */
		return EXIT_OK;
	}
	else if (r == FAIL) return EXIT_FAIL;

	switch (msg.type)
	{
	case GEMS_DATA:		/* Data to be written to stdout */
		do_output(msg.data, msg.len, data);
		break;
	case GEMS_WINSIZE:	/* remote terminal size has changed */
		get_cols_rows(&msg, &(data->server_cols), &(data->server_rows));
		if (! data->ignore_winsize)
		{
			if (compare_winsize(data->server_cols, data->server_rows, 
					APPNAME) != TRUE) 
				return EXIT_FAIL;
		}
		break;
	default:			/* unknown message type */
		return EXIT_FAIL;
	}

	return CONTINUE;
}


/* Prints a short help message */
void print_help()
{
	printf(_("Usage: %s [%s] HOST [PORT]\n"), APPNAME, OPT_IGNORE_WS);
	printf("       "APPNAME" "OPT_HELP" | "OPT_VERSION"\n");
}


/* Prints complete help message. */
void long_help()
{
	printf(_("Usage: %s [%s] HOST [PORT]\n"), APPNAME, OPT_IGNORE_WS);
	printf("       "APPNAME" "OPT_HELP" | "OPT_VERSION"\n");
	putchar('\n');
	printf(_("Arguments:\n"));
	printf(_("  HOST\t\tServer's IP address or domain.\n"));
	printf(_("  PORT\t\tServer's TCP port. Default: %d.\n"),
			DEFAULT_TCP_PORT);
	printf(_("  %s\t\tIgnore server's terminal size.\n"), OPT_IGNORE_WS);
	printf(_("  %s\t\tShow this help message.\n"), OPT_HELP);
	printf(_("  %s\t\tShow version information.\n"), OPT_VERSION);
}


/* Parses arguments received from command line. Saves server's domain and
 * port in *addr and *port. */
status_t parse_args(int argc, char *argv[], char **addr, int *port,
					t_client_data *data)
{
	int i;
	char addr_received = FALSE;
	char port_received = FALSE;

	for (i = 1; i < argc; i++)
	{
		/* ¿Option '-h' received? */
		if (!strcmp(argv[i], OPT_HELP))
		{
			long_help();
			return EXIT_OK;
		}

		/* ¿Option '-v' received? */
		if (!strcmp(argv[i], OPT_VERSION))
		{
			PRINT_VERSION(APPNAME)
			return EXIT_OK;
		}

		/* ¿Option '-i' received? */
		if (!strcmp(argv[i], OPT_IGNORE_WS))
		{
			data->ignore_winsize = TRUE;
			continue;
		}

		if (! addr_received)
		{
			*addr = argv[i];
			addr_received = TRUE;
			continue;
		}

		if (! port_received)
		{
			*port = atoi(argv[2]);
			port_received = TRUE;
			continue;
		}

		/* Syntax error: */
		print_help();
		return EXIT_FAIL;
	}

	if (! addr_received)
	{
		/* Syntax error: */
		print_help();
		return EXIT_FAIL;
	}

	return CONTINUE;
}


/* Reads one char from stdin and makes the appropriate action. Returns EXIT_OK
 * if 'q' is received; CONTINUE otherwise. Beeps if the pressed key has no
 * action. */
status_t parse_key(t_client_data *data)
{
	char c;
	const char alarm_char = '\a';

	c = getchar();

	if (c == 'q')
	{
		fprintf(stderr, _("\n%s terminated.\n"), APPNAME);
		return EXIT_OK;
	}

	if (c == 'a') data->alarm_enabled = !(data->alarm_enabled);
	else write(STDOUT_FILENO, &alarm_char, 1);

	return CONTINUE;
}


/* Initializes the client, including connection with server. */
status_t init_client(int argc, char **argv, t_client_data *data)
{
	char *server_addr;					/* Server's domain */
	int server_port = DEFAULT_TCP_PORT;	/* Server's port   */

	status_t r;

	/* Initialize client information that has not yet been specified,
	 * in case of error (see finish_client() ) */
	data->socket_fd = -1;
	data->old_tty_settings = NULL;
	data->alarm_enabled = TRUE;
	data->ignore_winsize = FALSE;

	/* Configure locale: */
	setlocale(LC_ALL, "");
	bindtextdomain("gems", LOCALEDIR);
	textdomain("gems");

	/* Process received arguments: */
	if ((r = parse_args(argc, argv, &server_addr, &server_port, data)) !=
			CONTINUE)
		return r;

	/* Configure terminal: */
	if (!(data->old_tty_settings = set_tty_settings())) return EXIT_FAIL;

	/* Initialize connection: */
	if ((data->socket_fd = connect_with_server(server_addr, server_port)) == -1)
		return EXIT_FAIL;

	/* Start communication: */
	if (start_comm(data) != TRUE) return EXIT_FAIL;

	/* Configure open_fds for calling select(). The fds to be checked are
	 * stdin and the socket. */
	FD_ZERO(&(data->open_fds));
	FD_SET(STDIN_FILENO, &(data->open_fds));
	FD_SET(data->socket_fd, &(data->open_fds));

	/* Configure signal handlers: */
	install_sighandlers();

	/* Connection succeded: */
	print_msg(_("Connection established -- Press 'q' to exit."));

	return CONTINUE;
}


/* Makes all necessary steps to finish the program in a clean way. */
void finish_client(t_client_data *data)
{
	/* Close socket: */
	if (data->socket_fd > 0) close(data->socket_fd);

	/* Restore old terminal configuration: */
	if (data->old_tty_settings) tcsetattr(0, TCSANOW, data->old_tty_settings);
}


int main(int argc, char *argv[])
{
	t_client_data data;     /* Client data to be used in all functions */
	fd_set rfds;			/* temporary fd_set for select()           */
	status_t status;		/* Client status                           */

	status = init_client(argc, argv, &data);

	/* Main loop: */
	for ( ; status == CONTINUE ; )
	{
		/* Wait until data received, or signal raised: */
		rfds = data.open_fds;
		if (select(data.socket_fd + 1, &rfds, NULL, NULL, NULL) == -1)
			if (errno != EINTR) status = EXIT_FAIL;  /* error */

		if (sigint_raised) status = EXIT_OK; /* CTRL-C pressed */
		else if (winsize_changed)
		{
			/* Local terminal size changed: */
			if (compare_winsize(data.server_cols, data.server_rows, 
					APPNAME) != TRUE)
				status = EXIT_FAIL;
			winsize_changed = 0;
		}
		else if (FD_ISSET(STDIN_FILENO, &rfds))	
			/* Data received from stdin (keyboard) */
			status = parse_key(&data);
		else if (FD_ISSET(data.socket_fd, &rfds))	
			/* Data received from server: */
			status = do_server_input(&data);
	}

	finish_client(&data);
	return status;
}

/* vim: set ts=4 sw=4: */
