/*
 * Purple-Schedule - Schedule reminders/pounces at specified times.
 * Copyright (C) 2006-2008
 *
 * 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.
 */

#include "schedule.h"

#include <notify.h>
#include <xmlnode.h>
#include <util.h>

#include <stdarg.h>
#include <stdlib.h>
#include <string.h>

static GList *schedules;
static int timeout;

static void save_cb();
static void parse_schedule(xmlnode *node);
static void parse_when(PurpleSchedule *schedule, xmlnode *when);
static void parse_action(PurpleSchedule *schedule, xmlnode *action);
static int sort_schedules(gconstpointer a, gconstpointer b);
static void purple_schedules_load();

#define TIMEOUT	60		/* update every 60 seconds */

#define MINUTE	60
#define HOUR	(MINUTE * 60)
#define	DAY		(HOUR * 24)
#define	YEAR	(DAY * 365)
#define	WEEK	(DAY * 7)

/*static int months[] = {-1, */

/* I think this is going to be rather ugly */

static int
days_in_month(int month, int year)
{
	int days[] = {31, -1, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};

	if (month == 1)
	{
		if (year %400 == 0)
			return 29;
		if (year % 100 == 0)
			return 28;
		if (year % 4 == 0)
			return 29;
		return 28;
	}
	else
		return days[month];
}

static time_t
get_next(PurpleSchedule *s)
{
	struct
	{
		int mins[61];
		int hrs[25];
		int dates[32];
		int months[13];
		int years[3];
	} p;
	int i;
	int y, mo, d, h, mi;
	struct tm *tm, test;
	time_t tme;
	int first = 0;

	memset(&p, -1, sizeof(p));
	time(&tme);
	tm = localtime(&tme);

#define DECIDE(prop, st, count, offset) \
	do { \
		if (prop == -1) {  \
			first = 1; \
			for (i = 0; i < count; i++)  { \
				st[i] = ((first ? 0 : offset) + i) % count;  \
			} \
		} else  \
			st[0] = prop; \
	} while (0)

	DECIDE(s->minute, p.mins, 60, tm->tm_min); /* Minute */

	DECIDE(s->hour, p.hrs, 24, tm->tm_hour); /* Hour */

	DECIDE(s->d.date, p.dates, 31, tm->tm_mday); /* Date */

	DECIDE(s->month, p.months, 12, tm->tm_mon); /* Month */

	if (s->year == -1) {
		p.years[0] = tm->tm_year;
		p.years[1] = p.years[0] + 1;
	} else
		p.years[0] = s->year;

	test = *tm;

	for (y = 0; p.years[y] != -1; y++) {
		test.tm_year = p.years[y];
		for (mo = 0; p.months[mo] != -1; mo++) {
			test.tm_mon = p.months[mo];
			for (d = 0; p.dates[d] != -1; d++) {
				test.tm_mday = p.dates[d] + 1;     /* XXX: +1 is necessary */
				if (test.tm_mday > days_in_month(test.tm_mon, test.tm_year + 1900))
					continue;
				for (h = 0; p.hrs[h] != -1; h++) {
					test.tm_hour = p.hrs[h];
					for (mi = 0; p.mins[mi] != -1; mi++) {
						time_t then;

						test.tm_min = p.mins[mi];

						then = mktime(&test);
						if (then > tme)
							return then;
					}
				}
			}
		}
	}
	return -1;
}

static void
calculate_timestamp_for_schedule(PurpleSchedule *schedule)
{
	schedule->timestamp = get_next(schedule);
}

void
purple_schedule_reschedule(PurpleSchedule *schedule)
{
	calculate_timestamp_for_schedule(schedule);
	if (schedule->timestamp < time(NULL))
	{
		purple_debug_warning("purple-schedule", "schedule \"%s\" will not be executed (%s)\n", schedule->name,
					purple_date_format_full(localtime(&schedule->timestamp)));
		schedule->timestamp = 0;
	}
	else
	{
		purple_debug_info("purple-schedule", "schedule \"%s\" will be executed at: %s\n", schedule->name,
					purple_date_format_full(localtime(&schedule->timestamp)));
	}
}

static int
sort_schedules(gconstpointer a, gconstpointer b)
{
	const PurpleSchedule *sa = a, *sb = b;

	if (sa->timestamp < sb->timestamp)
		return -1;
	else if (sa->timestamp == sb->timestamp)
		return 0;
	else
		return 1;
}

static void
recalculate_next_slots()
{
	GList *iter;

	for (iter = schedules; iter; iter = iter->next)
	{
		purple_schedule_reschedule(iter->data);
	}

	schedules = g_list_sort(schedules, sort_schedules);
}

PurpleSchedule *purple_schedule_new()
{
	PurpleSchedule *sch = g_new0(PurpleSchedule, 1);

	return sch;
}

void purple_schedule_add_action(PurpleSchedule *schedule, ScheduleActionType type, ...)
{
	ScheduleAction *action;
	va_list vargs;

	action = g_new0(ScheduleAction, 1);
	action->type = type;

	va_start(vargs, type);
	switch (type)
	{
		case SCHEDULE_ACTION_POPUP:
			action->d.popup_message = g_strdup(va_arg(vargs, char *));
			break;
		case SCHEDULE_ACTION_CONV:
			action->d.send.message = g_strdup(va_arg(vargs, char*));
			action->d.send.who = g_strdup(va_arg(vargs, char *));
			action->d.send.account = va_arg(vargs, PurpleAccount*);
			break;
		case SCHEDULE_ACTION_STATUS:
			action->d.status_title = g_strdup(va_arg(vargs, char *));
			break;
		default:
			g_free(action);
			g_return_if_reached();
	}
	va_end(vargs);
	schedule->actions = g_list_append(schedule->actions, action);
	save_cb();
}

void purple_schedule_remove_action(PurpleSchedule *schedule, ScheduleActionType type)
{
	GList *iter;

	for (iter = schedule->actions; iter; iter = iter->next)
	{
		ScheduleAction *action = iter->data;
		if (action->type == type)
		{
			purple_schedule_action_destroy(action);
			schedule->actions = g_list_delete_link(schedule->actions, iter);
			break;
		}
	}
}

void purple_schedule_action_activate(ScheduleAction *action)
{
	PurpleConversation *conv;

	switch (action->type)
	{
		case SCHEDULE_ACTION_POPUP:
			purple_notify_message(action, PURPLE_NOTIFY_MSG_INFO, _("Schedule"), action->d.popup_message,
							NULL, NULL, NULL);
			break;
		case SCHEDULE_ACTION_CONV:
			conv = purple_conversation_new(PURPLE_CONV_TYPE_IM, action->d.send.account, action->d.send.who);
			purple_conv_im_send_with_flags(PURPLE_CONV_IM(conv), action->d.send.message, 0);
			break;
		default:
			purple_debug_warning("purple-schedule", "unimplemented action\n");
			break;
	}
}

void purple_schedule_activate_actions(PurpleSchedule *sch)
{
	GList *iter;

	for (iter = sch->actions; iter; iter = iter->next)
	{
		purple_schedule_action_activate(iter->data);
	}
}

void purple_schedule_action_destroy(ScheduleAction *action)
{
	switch (action->type)
	{
		case SCHEDULE_ACTION_POPUP:
			g_free(action->d.popup_message);
			break;
		case SCHEDULE_ACTION_CONV:
			g_free(action->d.send.message);
			g_free(action->d.send.who);
			break;
		case SCHEDULE_ACTION_STATUS:
			g_free(action->d.status_title);
			break;
		default:
			purple_debug_warning("purple-schedule", "unknown action type\n");
			break;
	}
	g_free(action);
	purple_notify_close_with_handle(action);
}

void purple_schedule_destroy(PurpleSchedule *schedule)
{
	while (schedule->actions)
	{
		ScheduleAction *action = schedule->actions->data;
		purple_schedule_action_destroy(action);
		schedule->actions = g_list_delete_link(schedule->actions, schedule->actions);
	}
	g_free(schedule);

	schedules = g_list_remove(schedules, schedule);
}

GList *purple_schedules_get_all()
{
	return schedules;
}


void purple_schedules_add(PurpleSchedule *schedule)
{
	schedules = g_list_append(schedules, schedule);
}

static gboolean
check_and_execute(gpointer null)
{
	PurpleSchedule *schedule;
	GList *iter = schedules;

	if (iter == NULL)
		return TRUE;
	schedule = iter->data;
	while (schedule->timestamp && schedule->timestamp < time(NULL))
	{
		purple_schedule_activate_actions(schedule);
		purple_schedule_reschedule(schedule);
		iter = iter->next;
		if (iter == NULL)
			break;
		schedule = iter->data;
	}
	schedules = g_list_sort(schedules, sort_schedules);
	return TRUE;
}

void purple_schedule_init()
{
	purple_schedules_load();
	recalculate_next_slots();
	timeout = g_timeout_add(10000, (GSourceFunc)check_and_execute, NULL);
}

void purple_schedule_uninit()
{
	g_source_remove(timeout);
	save_cb();
	while (schedules)
	{
		purple_schedule_destroy(schedules->data);
	}
}

void purple_schedules_sync()
{
	save_cb();
}

/**
 * Read from XML
 */
static void
purple_schedules_load()
{
	xmlnode *purple, *schedule;

	purple = purple_util_read_xml_from_file("schedules.xml", _("list of schedules"));
	if (purple == NULL)
		return;

	schedule = xmlnode_get_child(purple, "schedules");
	if (schedule)
	{
		xmlnode *sch;
		for (sch = xmlnode_get_child(schedule, "schedule"); sch;
					sch = xmlnode_get_next_twin(sch))
		{
			parse_schedule(sch);
		}
	}

	xmlnode_free(purple);
}

static void
parse_schedule(xmlnode *node)
{
	PurpleSchedule *schedule;
	xmlnode *child;
	const char *name;

	child = xmlnode_get_child(node, "when");
	name = xmlnode_get_attrib(node, "name");

	if (name == NULL || child == NULL)
	{
		return;
	}

	schedule = purple_schedule_new();
	schedule->name = g_strdup(name);

	schedules = g_list_append(schedules, schedule);

	parse_when(schedule, child);

	for (child = xmlnode_get_child(node, "action"); child; child = xmlnode_get_next_twin(child))
	{
		parse_action(schedule, child);
	}
}

static void
parse_when(PurpleSchedule *schedule, xmlnode *when)
{
	int type = atoi(xmlnode_get_attrib(when, "type"));

	schedule->type = type;
	if (type == PURPLE_SCHEDULE_TYPE_DATE)
		schedule->d.date = atoi(xmlnode_get_attrib(when, "date"));
	else
		schedule->d.day = atoi(xmlnode_get_attrib(when, "day"));

	schedule->month = atoi(xmlnode_get_attrib(when, "month"));
	schedule->year = atoi(xmlnode_get_attrib(when, "year"));
	schedule->hour = atoi(xmlnode_get_attrib(when, "hour"));
	schedule->minute = atoi(xmlnode_get_attrib(when, "minute"));
}

static void
parse_action(PurpleSchedule *schedule, xmlnode *action)
{
	int type = atoi(xmlnode_get_attrib(action, "type"));
	xmlnode *data = xmlnode_get_child(action, "data"), *account, *message;
	char *tmp;

	switch (type)
	{
		case SCHEDULE_ACTION_POPUP:
			tmp = xmlnode_get_data(data);
			purple_schedule_add_action(schedule, type, tmp);
			g_free(tmp);
			break;
		case SCHEDULE_ACTION_CONV:
			account = xmlnode_get_child(data, "account");
			message = xmlnode_get_child(data, "message");
			tmp = xmlnode_get_data(message);
			purple_schedule_add_action(schedule, type, tmp,
						xmlnode_get_attrib(account, "who"),
						purple_accounts_find(xmlnode_get_attrib(account, "name"),
									xmlnode_get_attrib(account, "prpl")));
			g_free(tmp);
			break;
		case SCHEDULE_ACTION_STATUS:
			tmp = xmlnode_get_data(action);
			purple_schedule_add_action(schedule, type, tmp);
			g_free(tmp);
			break;
		default:
			g_return_if_reached();
	}
}

/**
 * Write into XML
 */

static void
xmlnode_set_attrib_int(xmlnode *node, const char *name, int value)
{
	char *v = g_strdup_printf("%d", value);
	xmlnode_set_attrib(node, name, v);
	g_free(v);
}

static xmlnode*
action_to_xmlnode(ScheduleAction *action)
{
	xmlnode *node, *child, *gchild;

	node = xmlnode_new("action");
	xmlnode_set_attrib_int(node, "type", action->type);

	child = xmlnode_new_child(node, "data");

	switch (action->type)
	{
		case SCHEDULE_ACTION_POPUP:
			/* XXX: need to do funky string-operations? */
			xmlnode_insert_data(child, action->d.popup_message, -1);
			break;
		case SCHEDULE_ACTION_CONV:
			gchild = xmlnode_new_child(child, "account");
			xmlnode_set_attrib(gchild, "prpl", purple_account_get_protocol_id(action->d.send.account));
			xmlnode_set_attrib(gchild, "name", purple_account_get_username(action->d.send.account));
			xmlnode_set_attrib(gchild, "who", action->d.send.who);

			gchild = xmlnode_new_child(child, "message");
			xmlnode_insert_data(gchild, action->d.send.message, -1);
			break;
		default:
			purple_debug_warning("purple-schedule", "unknown action type\n");
			break;
	}

	return node;
}

static xmlnode*
when_to_xmlnode(PurpleSchedule *schedule)
{
	xmlnode *node;

	node = xmlnode_new("when");

	xmlnode_set_attrib_int(node, "type", schedule->type);

	switch (schedule->type)
	{
		case PURPLE_SCHEDULE_TYPE_DATE:
			xmlnode_set_attrib_int(node, "date", schedule->d.date);
			break;
		case PURPLE_SCHEDULE_TYPE_DAY:
			xmlnode_set_attrib_int(node, "day", schedule->d.day);
			break;
	}

	xmlnode_set_attrib_int(node, "month", schedule->month);

	xmlnode_set_attrib_int(node, "year", schedule->year);

	xmlnode_set_attrib_int(node, "hour", schedule->hour);

	xmlnode_set_attrib_int(node, "minute", schedule->minute);

	return node;
}

static xmlnode*
schedule_to_xmlnode(PurpleSchedule *schedule)
{
	xmlnode *node, *child;
	GList *iter;

	node = xmlnode_new("schedule");
	xmlnode_set_attrib(node, "name", schedule->name);

	child = when_to_xmlnode(schedule);
	xmlnode_insert_child(node, child);

	for (iter = schedule->actions; iter; iter = iter->next)
	{
		xmlnode_insert_child(node, action_to_xmlnode(iter->data));
	}

	return node;
}

static xmlnode*
schedules_to_xmlnode()
{
	GList *iter;
	xmlnode *node, *child;

	node = xmlnode_new("purple-schedule");
	xmlnode_set_attrib(node, "version", PP_VERSION);

	child = xmlnode_new_child(node, "schedules");

	for (iter = schedules; iter; iter = iter->next)
	{
		PurpleSchedule *schedule = iter->data;

		xmlnode_insert_child(child, schedule_to_xmlnode(schedule));
	}

	return node;
}

static void
save_cb()
{
	xmlnode *node;
	char *data;

	node = schedules_to_xmlnode();
	data = xmlnode_to_formatted_str(node, NULL);
	purple_util_write_data_to_file("schedules.xml", data, -1);
	g_free(data);
	xmlnode_free(node);
}

