/**
*   Copyright (C) 2007 by Marcelo Jorge Vieira (metal) <metal@alucinados.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.,
*  59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
*
* Public License can be found at http://www.gnu.org/copyleft/gpl.html
*
* @author Thadeu Cascardo <cascardo@minaslivre.org>
* @author Marcelo Jorge Vieira (metal) <metal@alucinados.com>
*
*/

#include <glib.h>
#include "velha.h"

gboolean myread (GIOChannel *, GIOCondition, gpointer);

typedef struct _client_t client_t;

struct _client_t
{
	char *table;
	gint game;
	char turn;
	GString *buffer;
	client_t *partner;
	GIOChannel *channel;
	guint watch;
};

GList *clients = NULL;

struct
{
	char *name;
	void (*func) (char*, int r, client_t*);
}
commands[] =
{
	{ "play", play_cb },
	{ "start", start_cb },
	{ "join", join_cb },
	{ "list", list_cb },
	{ "quit", quit_cb },
	{ NULL, NULL }
};


client_t *
client_new (int fd)
{
	client_t *client;

	client = g_slice_new (client_t);
	client->table = NULL;
	client->game = -1;
	client->buffer = g_string_sized_new (BUFSIZ);
	client->partner = NULL;
	client->channel = g_io_channel_unix_new (fd);
	g_io_channel_set_close_on_unref (client->channel, TRUE);
	g_io_channel_set_flags (client->channel, g_io_channel_get_flags (client->channel) | G_IO_FLAG_NONBLOCK, NULL);
	client->watch = g_io_add_watch (client->channel, G_IO_IN, myread, client);
	return client;
}

void client_destroy (client_t *client)
{
	clients = g_list_remove (clients, client);
	if (client->table)
		g_slice_free1 (9, client->table);
	if (client->buffer)
		g_string_free (client->buffer, TRUE);
	if (client->channel)
		g_io_channel_unref (client->channel);
	if (client->watch)
		g_source_remove (client->watch);
	g_slice_free (client_t, client);
}

void
client_write (client_t *client, GString *str)
{
	int fd;

	fd = g_io_channel_unix_get_fd (client->channel);
	write (fd, str->str, str->len);
}

void
cancel_game (client_t *client)
{
	clients = g_list_remove (clients, client);

	if (client->partner)
	{
		GString *str;;
		str = g_string_new ("CANCEL\r\n");
		client_write (client->partner, str);
		g_string_free (str, TRUE);
		client_destroy (client->partner);
		client->partner = NULL;
		client->table = NULL;
	}

	if (client->table)
	{
		g_free (client->table);
		client->table = NULL;
	}
	client->game = -1;
}

gint
client_compare (client_t * a, client_t * b)
{
	return a->game - b->game;
}

gboolean
new_game (client_t *client)
{
	GList *l;
	GList *last;
	client_t *other;
	gint prev = -1;

	if (clients == NULL)
	{
		client->game = 0;
		clients = g_list_prepend (clients, client);
		return TRUE;
	}

	for (l = g_list_first (clients); l != NULL; l = g_list_next (l))
	{
		other = l->data;
		if (other->game > prev + 1)
		{
			client->game = prev + 1;
			clients = g_list_insert_sorted (l, client, (GCompareFunc) client_compare);
			return TRUE;
		}
		prev = other->game;
		last = l;
	}

	if (l == NULL)
	{
		other = last->data;
		if (other->game == G_MAXINT)
			return FALSE;
		client->game = other->game + 1;
		clients = g_list_insert_sorted (last, client, (GCompareFunc) client_compare);
		return TRUE;
	}
	return FALSE;
}

void
start_cb (char *buffer, int len, client_t *client)
{
	if (client->game != -1)
	{
		cancel_game (client);
	}

	if (new_game (client) == FALSE)
	{
		cancel_game (client);
		client_destroy (client);
	}
	else
	{
		GString *str;
		str = g_string_sized_new (BUFSIZ);
		g_string_printf (str, "%d\r\n", client->game);
		client_write (client, str);
		g_string_free (str, TRUE);
	}
}

void
quit_cb (char *buffer, int len, client_t *client)
{
	cancel_game (client);
	client_destroy (client);
}

void
win_game (client_t *client)
{
	GString *str;

	str = g_string_new ("200 WIN\r\n");
	client_write (client, str);
	str = g_string_assign (str, "200 LOOSE\r\n");
	client_write (client->partner, str);
	g_string_free (str, TRUE);
	cancel_game (client);
	client_destroy (client);
}

void tie_game (client_t *client)
{
	GString *str;

	str = g_string_new ("200 TIE\r\n");
	client_write (client, str);
	client_write (client->partner, str);
	g_string_free (str, TRUE);
	cancel_game (client);
	client_destroy (client);
}

void
check_game (client_t *client, int pos)
{
	int line;
	int column;

	line = pos % 3;
	column = pos / 3;

	if ((client->table[INDEX(column, 0)] == client->table[INDEX(column, 1)] &&
			client->table[INDEX(column, 0)] == client->table[INDEX(column, 2)]) ||
		(client->table[INDEX(0, line)] == client->table[INDEX(1, line)] &&
			client->table[INDEX(0, line)] == client->table[INDEX(2, line)]) ||
		((pos == 4 || pos % 4 == 2) &&
			client->table[INDEX(0,2)] == client->table[INDEX(1,1)] &&
			client->table[INDEX(0,2)] == client->table[INDEX(2,0)]) ||
		((pos == 4 || pos % 4 == 0) &&
			client->table[INDEX(2,2)] == client->table[INDEX(1,1)] &&
			client->table[INDEX(2,2)] == client->table[INDEX(0,0)]))
	{
		win_game (client);
	}
	else if (memchr (client->table, 0, 9) == NULL)
	{
		tie_game (client);
	}
}

void
play_cb (char *buffer, int len, client_t *client)
{
	char *p;
	char *end;
	int pos;
	GString *str;

	if (client->partner == NULL)
	{
		GString *str;
		str = g_string_new ("400 Wait for your partner\r\n");
		client_write (client, str);
		g_string_free (str, TRUE);
		return;
	}

	if (client->turn == 0)
	{
		GString *str;
		str = g_string_new ("400 It is not your turn! Duh!\r\n");
		client_write (client, str);
		g_string_free (str, TRUE);
		return;
	}
	buffer[len - 1] = 0;
	errno = 0;
	p = buffer + sizeof ("play");
	pos = strtol (p, &end, 0);

	if (end == p || errno != 0 || pos < 0 || pos > 8)
	{
		GString *str;
		str = g_string_new ("400 Invalid message format\r\n");
		client_write (client, str);
		g_string_free (str, TRUE);
		return;
	}

	if (client->table[pos] != 0)
	{
		GString *str;
		str = g_string_new ("400 Already played. Choose another.\r\n");
		client_write (client, str);
		g_string_free (str, TRUE);
		return;
	}

	client->table[pos] = client->turn;
	client->partner->turn = client->turn == 1 ? 2 : 1;
	client->turn = 0;
	str = g_string_sized_new (BUFSIZ);
	g_string_printf (str, "PLAY %d\r\n", pos);
	client_write (client->partner, str);
	g_string_printf (str, "200 OK\r\n");
	client_write (client, str);
	g_string_free (str, TRUE);
	check_game (client, pos);
}

void
join_cb (char *buffer, int len, client_t *client)
{
	char *p;
	char *end;
	gint game;
	GList *l;
	client_t *other;
	GString *str;

	if (client->game)
		cancel_game (client);

	buffer[len - 1] = 0;
	errno = 0;
	p = buffer + sizeof ("join");
	game = strtol (p, &end, 0);

	if (end == p || errno != 0 || game < 0 || game > G_MAXINT)
	{
		str = g_string_new ("400 Invalid message format\r\n");
		client_write (client, str);
		g_string_free (str, TRUE);
		return;
	}

	client->game = game;
	l = g_list_find_custom (clients, client, (GCompareFunc) client_compare);

	if (l == NULL)
	{
		str = g_string_new ("404 Game not found\r\n");
		client_write (client, str);
		g_string_free (str, TRUE);
		return;
	}

	other = l->data;

	if (other->partner)
	{
		str = g_string_new ("403 Game already started! Go away!\r\n");
		client_write (client, str);
		g_string_free (str, TRUE);
		return;
	}

	other->partner = client;
	client->partner = other;
	client->table = other->table = g_slice_alloc0 (9);
	client->turn = 1;
	other->turn = 0;
	str = g_string_new ("200 OK\r\n");
	client_write (client, str);
	g_string_free (str, TRUE);
}

void
list_cb (char *buffer, int len, client_t* client)
{
	GString *str;
	GList *l;
	client_t *other;

	str = g_string_sized_new (BUFSIZ);
	g_string_append (str, "START\r\n");

	for (l = g_list_first (clients); l != NULL; l = g_list_next (l))
	{
		other = l->data;
		g_string_append_printf (str, "%d\r\n", other->game);
	}

	g_string_append (str, "END\r\n");
	client_write (client, str);
	g_string_free (str, TRUE);
}

gboolean
myboo (client_t *client)
{
	int n;
	char *p;
	char *buffer;
	int i;

	for (n = 0, p = client->buffer->str; n < client->buffer->len; n++, p++)
	{
		if (*p == '\n' || (*p == '\r' && ++n < client->buffer->len && *(++p) == '\n'))
			break;
	}

	if (n == client->buffer->len)
	{
		//g_debug ("Time to learn how to count, cascardo.");
		return FALSE;
	}

	buffer = g_strndup (client->buffer->str, n + 1);
	g_string_erase (client->buffer, 0, n + 1);
	g_debug ("Command %s received", buffer);

	for (i = 0; commands[i].name != NULL; i++)
	{
		//g_debug ("%d %s", i, commands[i].name);
		if (g_ascii_strncasecmp (commands[i].name, buffer, strlen (commands[i].name)) == 0)
		{
			//g_debug ("%s running", commands[i].name);
			commands[i].func (buffer, n + 1, client);
			break;
		}
	}

	g_free (buffer);
	return TRUE;
}

gboolean
myread (GIOChannel *channel, GIOCondition cond, gpointer data)
{
	client_t *client;
	int fd;
	char buffer[BUFSIZ];
	int r;

	//g_debug ("New data");
	client = data;
	fd = g_io_channel_unix_get_fd (channel);
	r = read (fd, buffer, BUFSIZ);

	if ((r < 0 && errno != EAGAIN) || r == 0)
	{
		g_debug ("Error or finished connection.");
		cancel_game (client);
		client_destroy (client);
		return FALSE;
	}

	g_string_append_len (client->buffer, buffer, r);

	while (myboo (client));

	return TRUE;
}

gboolean
myaccept (GIOChannel *channel, GIOCondition cond, gpointer data)
{
	int fd;
	int newfd;
	client_t *client;

	fd = g_io_channel_unix_get_fd (channel);
	newfd = accept (fd, NULL, 0);
	client = client_new (newfd);

	return TRUE;
}

int main (int argc, char **argv)
{
	int fd;
	GIOChannel *channel;
	struct sockaddr_in saddr;
	int reuseaddr ;

	fd = socket (PF_INET, SOCK_STREAM, 0);
	saddr.sin_family = AF_INET;
	saddr.sin_port = htons (5555);
	saddr.sin_addr.s_addr = htonl (INADDR_LOOPBACK);
	reuseaddr = 1;
	setsockopt (fd, SOL_SOCKET, SO_REUSEADDR, &reuseaddr, sizeof (reuseaddr));
	bind (fd, (struct sockaddr*) &saddr, sizeof (struct sockaddr_in));
	listen (fd, 5);
	channel = g_io_channel_unix_new (fd);
	g_io_add_watch (channel, G_IO_IN, myaccept, NULL);
	g_main_loop_run (g_main_loop_new (g_main_context_default (), TRUE));

	return 0;
}
