/*
 * gaim-snpp Protocol Plugin
 *
 * Copyright (C) 2004-2008 Don Seiler <don@seiler.us>
 *
 * 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., 51 Franklin Street, Fifth Floor, Boston, MA  02111-1301  USA
 *
 */

/* If you can't figure out what this line is for, DON'T TOUCH IT. */
#include "../common/pp_internal.h"

#define _GNU_SOURCE

#include <stdio.h>
#include <string.h>

#ifndef _WIN32
# include <sys/socket.h>
#else
# include <libc_interface.h>
#endif

#include <sys/types.h>
#include <unistd.h>

#include <plugin.h>
#include <accountopt.h>
#include <prpl.h>
#include <conversation.h>
#include <notify.h>
#include <debug.h>
#include <blist.h>
#include <util.h>

#define SNPP_DEFAULT_SERVER "localhost"
#define SNPP_DEFAULT_PORT 444

#define SNPP_INITIAL_BUFSIZE 1024

enum state {
	CONN,
	PAGE,
	MESS,
	SEND,
	QUIT,
	LOGI
};

struct snpp_data {
	PurpleAccount *account;
	int fd;

	struct snpp_page *current_page;
};

struct snpp_page {
	char *pager;
	char *message;
	int state;
};

static PurplePlugin *_snpp_plugin = NULL;

static struct snpp_page *snpp_page_new()
{
	struct snpp_page *sp;

	purple_debug_info("snpp", "snpp_page_new\n");
	sp = g_new0(struct snpp_page, 1);
	sp->state = CONN;
	return sp;
};

static void snpp_page_destroy(struct snpp_page *sp)
{
	purple_debug_info("snpp", "snpp_page_destroy\n");

	if (sp->pager != NULL)
		g_free(sp->pager);

	if (sp->message != NULL)
		g_free(sp->message);

	g_free(sp);
	sp = NULL;
};

static void snpp_send(gint fd, const char *buf)
{
	purple_debug_info("snpp", "snpp_send\n");

	purple_debug_info("snpp", "snpp_send: sending %s\n", buf);
	if (!write(fd,buf,strlen(buf))) {
		purple_debug_warning("snpp", "snpp_send: Error sending message\n");
	}
};

static void snpp_reset(PurpleConnection *gc, struct snpp_data *sd)
{
	purple_debug_info("snpp", "snpp_reset\n");

	if (gc && gc->inpa)
		purple_input_remove(gc->inpa);

	if (sd && sd->fd)
		close(sd->fd);

	if (sd->current_page != NULL)
		snpp_page_destroy(sd->current_page);
}

static int snpp_cmd_logi(struct snpp_data *sd)
{
	char command[SNPP_INITIAL_BUFSIZE];
	const char *password;

	purple_debug_info("snpp", "snpp_cmd_logi\n");

	if ((password = purple_account_get_password(sd->account)) == NULL)
		password = "";

	/* If LOGI is unsupported, this should return 500 */
	sd->current_page->state = LOGI;
	g_snprintf(command, sizeof(command), "LOGI %s %s\n",
			purple_account_get_username(sd->account),
			password);

	snpp_send(sd->fd, command);

	return 0;
}

static int snpp_cmd_page(struct snpp_data *sd)
{
	char command[SNPP_INITIAL_BUFSIZE];

	purple_debug_info("snpp", "snpp_cmd_page\n");

	sd->current_page->state = PAGE;
	g_snprintf(command, sizeof(command), "PAGE %s\n", sd->current_page->pager);
	snpp_send(sd->fd, command);

	return 0;
};

static int snpp_cmd_mess(struct snpp_data *sd)
{
	char command[SNPP_INITIAL_BUFSIZE];

	purple_debug_info("snpp", "snpp_cmd_mess\n");

	sd->current_page->state = MESS;
	g_snprintf(command, sizeof(command), "MESS %s\n", sd->current_page->message);
	snpp_send(sd->fd, command);

	return 0;
};

static int snpp_cmd_send(struct snpp_data *sd)
{
	char command[SNPP_INITIAL_BUFSIZE];

	purple_debug_info("snpp", "snpp_cmd_send\n");

	sd->current_page->state = SEND;
	g_snprintf(command, sizeof(command), "SEND\n");
	snpp_send(sd->fd, command);

	return 0;
};

static int snpp_cmd_quit(struct snpp_data *sd)
{
	char command[SNPP_INITIAL_BUFSIZE];

	purple_debug_info("snpp", "snpp_cmd_quit\n");

	sd->current_page->state = QUIT;
	g_snprintf(command, sizeof(command), "QUIT\n");
	snpp_send(sd->fd, command);

	return 0;
};

static void snpp_callback(gpointer data, gint source, PurpleInputCondition cond)
{
	PurpleConnection *gc;
	struct snpp_data *sd;
	int len;
	char buf[SNPP_INITIAL_BUFSIZE];
	char *retcode = NULL;
	PurpleConversation *conv;

	purple_debug_info("snpp", "snpp_callback\n");

	gc = data;
	sd = gc->proto_data;

	if ((len = read(sd->fd, buf, SNPP_INITIAL_BUFSIZE - 1)) < 0) {
		purple_debug_warning("snpp", "snpp_callback: Read error\n");
		/* purple_connection_error(gc, _("Read error")); */
		return;
	} else if (len == 0) {
		/* Remote closed the connection, probably */
		return;
	}

	buf[len] = '\0';
	purple_debug_info("snpp", "snpp_callback: Recv: %s\n", buf);
	retcode = g_strndup(buf,3);

	if (sd->current_page != NULL) {
		purple_debug_info("snpp", "snpp_callback: Current page found.\n");
		/*
		 * Evaluate state and return code and call appropriate function
		 * to faciliate processing of pages.
		 */

		switch (sd->current_page->state) {
		case CONN:
			purple_debug_info("snpp", "snpp_callback: State is CONN, return code was %s\n", retcode);
			if (!g_ascii_strcasecmp(retcode,"220"))
				snpp_cmd_logi(sd);
			else {
				purple_notify_error(gc, NULL, buf, NULL);
				snpp_reset(gc, sd);
			}
			break;

		case LOGI:
			purple_debug_info("snpp", "snpp_callback: State is LOGI, return code was %s\n", retcode);
			/* If LOGI is unsupported, server should return 500
			   XXX 230 is crutch for HylaFAX breaking the protocol */
			if (!g_ascii_strcasecmp(retcode,"250")
					|| !g_ascii_strcasecmp(retcode, "500")
					|| !g_ascii_strcasecmp(retcode, "230"))
				snpp_cmd_page(sd);
			else {
				purple_notify_error(gc, NULL, buf, NULL);
				snpp_reset(gc, sd);
			}
			break;

		case PAGE:
			purple_debug_info("snpp", "snpp_callback: State is PAGE, return code was %s\n", retcode);
			if (!g_ascii_strcasecmp(retcode,"250"))
				snpp_cmd_mess(sd);
			else {
				purple_notify_error(gc, NULL, buf, NULL);
				snpp_reset(gc, sd);
			}
			break;

		case MESS:
			purple_debug_info("snpp", "snpp_callback: State is MESS, return code was %s\n", retcode);
			if (!g_ascii_strcasecmp(retcode,"250"))
				snpp_cmd_send(sd);
			else {
				purple_notify_error(gc, NULL, buf, NULL);
				snpp_reset(gc, sd);
			}
			break;

		case SEND:
			purple_debug_info("snpp", "snpp_callback: State is SEND, return code was %s\n", retcode);
			if (!g_ascii_strcasecmp(retcode,"250")
				|| !g_ascii_strcasecmp(retcode,"860")
				|| !g_ascii_strcasecmp(retcode,"960")) {
				/* Print status message (buf) to window */
				if ((conv = purple_find_conversation_with_account(PURPLE_CONV_TYPE_IM, sd->current_page->pager, sd->account))) {
					purple_conversation_write(conv, NULL, buf, PURPLE_MESSAGE_SYSTEM, time(NULL));
				}
				snpp_cmd_quit(sd);
			} else {
				purple_notify_error(gc, NULL, buf, NULL);
				snpp_reset(gc, sd);
			}
			break;

		case QUIT:
			/* Not sure if we should ever get here */
			purple_debug_info("snpp", "snpp_callback: State is QUIT, return code was %s\n", retcode);

			if (g_ascii_strcasecmp(retcode,"221"))
				purple_debug_info("snpp", "snpp_callback: Return code of 221 expected, not received\n");

			snpp_reset(gc, sd);
			break;

		default:
			purple_debug_info("snpp", "snpp_callback: current_page in unknown state\n");
			purple_notify_error(gc, NULL, buf, NULL);
		}

	} else {
		purple_debug_info("snpp", "snpp_callback: No current page to process\n");
	}

	g_free(retcode);
};

static void snpp_connect_cb(gpointer data, gint source, const gchar *error_message)
{
	PurpleConnection *gc;
	struct snpp_data *sd;
	GList *connections;

	purple_debug_info("snpp", "snpp_connect_cb\n");

	gc = data;
	sd = gc->proto_data;
	connections = purple_connections_get_all();

	if (source < 0)
		return;

	if (!g_list_find(connections, gc)) {
		close(source);
		return;
	}

	sd->fd = source;

	gc->inpa = purple_input_add(sd->fd, PURPLE_INPUT_READ, snpp_callback, gc);
}

static void snpp_connect(PurpleConnection *gc)
{
	PurpleProxyConnectData *ppcd = NULL;

	purple_debug_info("snpp", "snpp_connect\n");

	ppcd =	purple_proxy_connect(gc->account, gc->account,
			purple_account_get_string(gc->account, "server", SNPP_DEFAULT_SERVER),
			purple_account_get_int(gc->account, "port", SNPP_DEFAULT_PORT),
			snpp_connect_cb,
			gc);

	if (!ppcd || !gc->account->gc) {
		purple_connection_error(gc, _("Couldn't connect to SNPP server"));
		return;
	}
};


static int snpp_process(PurpleConnection *gc, struct snpp_data *sd)
{
	purple_debug_info("snpp", "snpp_process\n");

	if (sd->current_page->message && (strlen(sd->current_page->message) > 0)) {
		/* Just completed proxy_connect, ready to send data to server */
		purple_debug_info("snpp", "snpp_page: Sending SNPP Request:\n\n%s\n\n", sd->current_page->message);

		/* Get ball rolling, snpp_callback will take over */
		snpp_connect(gc);
	}

	return 1;
};




static int snpp_send_im(PurpleConnection *gc,
						const char *who,
						const char *what,
						PurpleMessageFlags flags)
{
	struct snpp_data *sd;
	struct snpp_page *sp;

	purple_debug_info("snpp", "snpp_send_im\n");
	sd = gc->proto_data;
	sp = snpp_page_new();

	sp->pager = g_strdup(who);
	sp->message = g_strdup(what);

	sd->current_page = sp;
	snpp_process(gc, sd);

	return 1;
};


static const char *snpp_icon(PurpleAccount *a, PurpleBuddy *b)
{
	/* purple_debug_info("snpp", "snpp_icon\n"); */
	return "snpp";
}

static void fake_buddy_signons(PurpleAccount *account)
{
	PurpleBlistNode *node = purple_get_blist()->root;

	while (node != NULL)
	{
		if (PURPLE_BLIST_NODE_IS_BUDDY(node))
		{
			PurpleBuddy *buddy = (PurpleBuddy *)node;
			if (buddy->account == account)
				purple_prpl_got_user_status(account, buddy->name, "available", NULL);
		}

		node = purple_blist_node_next(node, FALSE);
	}

}

static void snpp_login(PurpleAccount *account)
{
	PurpleConnection *gc;
	struct snpp_data *sd;

	purple_debug_info("snpp", "snpp_login\n");

	gc = purple_account_get_connection(account);
	gc->proto_data = sd = g_new0(struct snpp_data, 1);
	sd->account = account;

	purple_connection_set_state(gc, PURPLE_CONNECTED);

	fake_buddy_signons(account);
};


static void snpp_close(PurpleConnection *gc)
{
	struct snpp_data *sd;
	purple_debug_info("snpp", "snpp_close\n");

	sd = gc->proto_data;

	if (sd == NULL)
		return;

	snpp_reset(gc, sd);
	g_free(sd);
};

static void snpp_add_buddy(PurpleConnection *gc, PurpleBuddy *b, PurpleGroup *group)
{
	purple_debug_info("snpp", "snpp_add_buddy\n");
	purple_prpl_got_user_status(purple_connection_get_account(gc), b->name, "available", NULL);
};

static void snpp_remove_buddy(PurpleConnection *gc, PurpleBuddy *b, PurpleGroup *group)
{
	purple_debug_info("snpp", "snpp_remove_buddy\n");
};

static GList *snpp_status_types(PurpleAccount *account)
{
	PurpleStatusType *status;
	GList *types = NULL;

	status = purple_status_type_new_full(PURPLE_STATUS_OFFLINE,
									   NULL, NULL, FALSE, TRUE, FALSE);
	types = g_list_append(types, status);

	status = purple_status_type_new_full(PURPLE_STATUS_AVAILABLE,
									   NULL, NULL, FALSE, TRUE, FALSE);
	types = g_list_append(types, status);

	return types;
}

static PurplePluginProtocolInfo prpl_info =
{
	OPT_PROTO_PASSWORD_OPTIONAL,	/* options */
	NULL,							/* user_splits */
	NULL,							/* protocol_options */
	NO_BUDDY_ICONS,					/* icon_spec */
	snpp_icon,						/* list_icon */
	NULL,							/* list_emblems */
	NULL,							/* status_text */
	NULL,							/* tooltip_text */
	snpp_status_types,				/* status_types */
	NULL,							/* blist_node_menu */
	NULL,							/* chat_info */
	NULL,							/* chat_info_defaults */
	snpp_login,						/* login */
	snpp_close,						/* close */
	snpp_send_im,					/* send_im */
	NULL,							/* set_info */
	NULL,							/* send_typing */
	NULL,							/* get_info */
	NULL,							/* set_away */
	NULL,							/* set_idle */
	NULL,							/* change_password */
	snpp_add_buddy,					/* add_buddy */
	NULL,							/* add_buddies */
	snpp_remove_buddy,				/* remove_buddy */
	NULL,							/* remove_buddies */
	NULL,							/* add_permit */
	NULL,							/* add_deny */
	NULL,							/* rem_permit */
	NULL,							/* rem_deny */
	NULL,							/* set_permit_deny */
	NULL,							/* warn */
	NULL,							/* join_chat */
	NULL,							/* reject_chat */
	NULL,							/* chat_invite */
	NULL,							/* chat_leave */
	NULL,							/* chat_whisper */
	NULL,							/* chat_send */
	NULL,							/* keepalive */
	NULL,							/* register_user */
	NULL,							/* get_cb_info */
	NULL,							/* get_cb_away */
	NULL,							/* alias_buddy */
	NULL,							/* group_buddy */
	NULL,							/* rename_group */
	NULL,							/* buddy_free */
	NULL,							/* convo_closed */
	NULL,							/* normalize */
	NULL,							/* set_buddy_icon */
	NULL,							/* remove_group */
	NULL,							/* get_cb_real_name */
	NULL,							/* set_chat_topic */
	NULL,							/* find_blist_chat */
	NULL,							/* roomlist_get_list */
	NULL,							/* roomlist_cancel */
	NULL,							/* roomlist_expand_catagory */
	NULL,							/* can_receive_file */
	NULL,							/* send_file */
	NULL,							/* new_xfer */
	NULL,							/* offline_message */
	NULL,							/* whiteboard_prpl_ops */
	NULL,							/* send_raw */
	NULL,							/* roomlist_room_serialize */
	NULL,							/* unregister_user */
	NULL,							/* send_attention */
	NULL,							/* get_attention_types */
	sizeof(PurplePluginProtocolInfo),/* struct_size */
	NULL,							/* get_account_text_table */
	NULL,							/* initiate_media */
	NULL,							/* get_media_caps */
	NULL,							/* get_moods */
	NULL,							/* set_public_alias */
	NULL							/* get_public_alias */
};


static PurplePluginInfo info =
{
	PURPLE_PLUGIN_MAGIC,			/* magic */
	PURPLE_MAJOR_VERSION,			/* purple major */
	PURPLE_MINOR_VERSION,			/* purple minor */
	PURPLE_PLUGIN_PROTOCOL,			/* type */
	NULL,							/* ui_requirement */
	0,								/* flags */
	NULL,							/* dependencies */
	PURPLE_PRIORITY_DEFAULT,		/* priority */
	"prpl-snpp",					/* id */
	NULL,							/* name */
	PP_VERSION,						/* version */
	NULL,							/* summary */
	NULL,							/* description */
	"Don Seiler <don@seiler.us>",	/* author */
	PP_WEBSITE,						/* homepage */
	NULL,							/* load */
	NULL,							/* unload */
	NULL,							/* destroy */
	NULL,							/* ui_info */
	&prpl_info,						/* extra_info */
	NULL,							/* prefs_info */
	NULL,							/* actions */
	NULL,							/* reserved */
	NULL,							/* reserved */
	NULL,							/* reserved */
	NULL							/* reserved */
};


static void _init_plugin(PurplePlugin *plugin)
{
	PurpleAccountOption *option;
	option = purple_account_option_string_new(_("Server"), "server", SNPP_DEFAULT_SERVER);
	prpl_info.protocol_options = g_list_append(prpl_info.protocol_options, option);

	option = purple_account_option_int_new(_("Port"), "port", SNPP_DEFAULT_PORT);
	prpl_info.protocol_options = g_list_append(prpl_info.protocol_options, option);

	_snpp_plugin = plugin;

	info.name = _("SNPP");
	info.summary = _("SNPP Plugin");
	info.description =
		_("Allows libpurple to send messages over the Simple Network Paging Protocol (SNPP).");
};

PURPLE_INIT_PLUGIN(snpp, _init_plugin, info);
