/********************************************************************\
  * BitlBee -- An IRC to other IM-networks gateway                     *
  *                                                                    *
  * Copyright 2002-2013 Wilmer van der Gaast and others                *
  \********************************************************************/

/* Account management functions                                         */

/*
  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 with
  the Debian GNU/Linux distribution in /usr/share/common-licenses/GPL;
  if not, write to the Free Software Foundation, Inc., 51 Franklin St.,
  Fifth Floor, Boston, MA  02110-1301  USA
*/

#define BITLBEE_CORE
#include "bitlbee.h"
#include "account.h"

static const char* account_protocols_local[] = {
	"gg", "whatsapp", NULL
};

static char *set_eval_nick_source(set_t *set, char *value);

account_t *account_add(bee_t *bee, struct prpl *prpl, char *user, char *pass)
{
	account_t *a;
	set_t *s;
	char tag[strlen(prpl->name) + 10];

	if (bee->accounts) {
		for (a = bee->accounts; a->next; a = a->next) {
			;
		}
		a = a->next = g_new0(account_t, 1);
	} else {
		bee->accounts = a = g_new0(account_t, 1);
	}

	a->prpl = prpl;
	a->user = g_strdup(user);
	a->pass = g_strdup(pass);
	a->auto_connect = 1;
	a->bee = bee;

	s = set_add(&a->set, "auto_connect", "true", set_eval_account, a);
	s->flags |= SET_NOSAVE;

	s = set_add(&a->set, "auto_reconnect", "true", set_eval_bool, a);

	s = set_add(&a->set, "handle_unknown", NULL, NULL, a);
	s->flags |= SET_NULL_OK;

	s = set_add(&a->set, "nick_format", NULL, NULL, a);
	s->flags |= SET_NULL_OK;

	s = set_add(&a->set, "nick_source", "handle", set_eval_nick_source, a);
	s->flags |= SET_NOSAVE; /* Just for bw compatibility! */

	s = set_add(&a->set, "password", NULL, set_eval_account, a);
	s->flags |= SET_NOSAVE | SET_NULL_OK | SET_PASSWORD | ACC_SET_LOCKABLE;

	s = set_add(&a->set, "tag", NULL, set_eval_account, a);
	s->flags |= SET_NOSAVE;

	s = set_add(&a->set, "username", NULL, set_eval_account, a);
	s->flags |= SET_NOSAVE | ACC_SET_OFFLINE_ONLY | ACC_SET_LOCKABLE;
	set_setstr(&a->set, "username", user);

	if (prpl == &protocol_missing) {
		s = set_add(&a->set, "server", NULL, set_eval_account, a);
		s->flags |= SET_NOSAVE | SET_HIDDEN | ACC_SET_OFFLINE_ONLY | ACC_SET_ONLINE_ONLY;
	}

	/* Hardcode some more clever tag guesses. */
	strcpy(tag, prpl->name);
	if (strcmp(prpl->name, "jabber") == 0) {
		if (strstr(a->user, "@gmail.com") ||
		    strstr(a->user, "@googlemail.com")) {
			strcpy(tag, "gtalk");
		}
	}

	if (account_by_tag(bee, tag)) {
		char *numpos = tag + strlen(tag);
		int i;

		for (i = 2; i < 10000; i++) {
			sprintf(numpos, "%d", i);
			if (!account_by_tag(bee, tag)) {
				break;
			}
		}
	}
	set_setstr(&a->set, "tag", tag);

	a->nicks = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, g_free);

	/* This function adds some more settings (and might want to do more
	   things that have to be done now, although I can't think of anything. */
	if (prpl->init) {
		prpl->init(a);
	}

	s = set_add(&a->set, "away", NULL, set_eval_account, a);
	s->flags |= SET_NULL_OK;

	if (a->flags & ACC_FLAG_STATUS_MESSAGE) {
		s = set_add(&a->set, "status", NULL, set_eval_account, a);
		s->flags |= SET_NULL_OK;
	}

	return a;
}

char *set_eval_account(set_t *set, char *value)
{
	account_t *acc = set->data;

	/* Double-check: We refuse to edit on-line accounts. */
	if (set->flags & ACC_SET_OFFLINE_ONLY && acc->ic) {
		return SET_INVALID;
	}

	if (strcmp(set->key, "server") == 0) {
		g_free(acc->server);
		if (value && *value) {
			acc->server = g_strdup(value);
			return value;
		} else {
			acc->server = g_strdup(set->def);
			return g_strdup(set->def);
		}
	} else if (strcmp(set->key, "username") == 0) {
		g_free(acc->user);
		acc->user = g_strdup(value);
		return value;
	} else if (strcmp(set->key, "password") == 0) {
		/* set -del allows /oper to be used to change the password or,
		   iff oauth is enabled, reset the oauth credential magic.
		*/
		if (!value) {
			if (set_getbool(&(acc->set), "oauth")) {
				value = "";
			} else {
				value = PASSWORD_PENDING;
				((irc_t *) acc->bee->ui_data)->status |= OPER_HACK_ACCOUNT_PASSWORD;
				irc_rootmsg((irc_t *) acc->bee->ui_data, "You may now use /OPER to set the password");
			}
		}

		g_free(acc->pass);
		acc->pass = g_strdup(value);
		return NULL;    /* password shouldn't be visible in plaintext! */
	} else if (strcmp(set->key, "tag") == 0) {
		account_t *oa;

		/* Enforce uniqueness. */
		if ((oa = account_by_tag(acc->bee, value)) && oa != acc) {
			return SET_INVALID;
		}

		g_free(acc->tag);
		acc->tag = g_strdup(value);
		return value;
	} else if (strcmp(set->key, "auto_connect") == 0) {
		if (!is_bool(value)) {
			return SET_INVALID;
		}

		acc->auto_connect = bool2int(value);
		return value;
	} else if (strcmp(set->key, "away") == 0 ||
	           strcmp(set->key, "status") == 0) {
		if (acc->ic && acc->ic->flags & OPT_LOGGED_IN) {
			/* If we're currently on-line, set the var now already
			   (bit of a hack) and send an update. */
			g_free(set->value);
			set->value = g_strdup(value);

			imc_away_send_update(acc->ic);
		}

		return value;
	}

	return SET_INVALID;
}

/* For bw compatibility, have this write-only setting. */
static char *set_eval_nick_source(set_t *set, char *value)
{
	account_t *a = set->data;

	if (strcmp(value, "full_name") == 0) {
		set_setstr(&a->set, "nick_format", "%full_name");
	} else if (strcmp(value, "first_name") == 0) {
		set_setstr(&a->set, "nick_format", "%first_name");
	} else {
		set_setstr(&a->set, "nick_format", "%-@nick");
	}

	return value;
}

account_t *account_get(bee_t *bee, const char *id)
{
	account_t *a, *ret = NULL;
	char *handle, *s;
	int nr;

	/* Tags get priority above anything else. */
	if ((a = account_by_tag(bee, id))) {
		return a;
	}

	/* This checks if the id string ends with (...) */
	if ((handle = strchr(id, '(')) && (s = strchr(handle, ')')) && s[1] == 0) {
		struct prpl *proto;

		*s = *handle = 0;
		handle++;

		if ((proto = find_protocol(id))) {
			for (a = bee->accounts; a; a = a->next) {
				if (a->prpl == proto &&
				    a->prpl->handle_cmp(handle, a->user) == 0) {
					ret = a;
				}
			}
		}

		/* Restore the string. */
		handle--;
		*handle = '(';
		*s = ')';

		if (ret) {
			return ret;
		}
	}

	if (sscanf(id, "%d", &nr) == 1 && nr < 1000) {
		for (a = bee->accounts; a; a = a->next) {
			if ((nr--) == 0) {
				return(a);
			}
		}

		return(NULL);
	}

	for (a = bee->accounts; a; a = a->next) {
		if (g_strcasecmp(id, a->prpl->name) == 0) {
			if (!ret) {
				ret = a;
			} else {
				return(NULL);   /* We don't want to match more than one... */
			}
		} else if (strstr(a->user, id)) {
			if (!ret) {
				ret = a;
			} else {
				return(NULL);
			}
		}
	}

	return(ret);
}

account_t *account_by_tag(bee_t *bee, const char *tag)
{
	account_t *a;

	for (a = bee->accounts; a; a = a->next) {
		if (a->tag && g_strcasecmp(tag, a->tag) == 0) {
			return a;
		}
	}

	return NULL;
}

void account_del(bee_t *bee, account_t *acc)
{
	account_t *a, *l = NULL;

	if (acc->ic) {
		/* Caller should have checked, accounts still in use can't be deleted. */
		return;
	}

	for (a = bee->accounts; a; a = (l = a)->next) {
		if (a == acc) {
			if (l) {
				l->next = a->next;
			} else {
				bee->accounts = a->next;
			}

			/** FIXME
			for( c = bee->chatrooms; c; c = nc )
			{
			        nc = c->next;
			        if( acc == c->acc )
			                chat_del( bee, c );
			}
			*/

			while (a->set) {
				set_del(&a->set, a->set->key);
			}

			g_hash_table_destroy(a->nicks);

			g_free(a->tag);
			g_free(a->user);
			g_free(a->pass);
			g_free(a->server);
			if (a->reconnect) {     /* This prevents any reconnect still queued to happen */
				cancel_auto_reconnect(a);
			}
			g_free(a);

			break;
		}
	}
}

static gboolean account_on_timeout(gpointer d, gint fd, b_input_condition cond);

void account_on(bee_t *bee, account_t *a)
{
	if (a->ic) {
		/* Trying to enable an already-enabled account */
		return;
	}

	cancel_auto_reconnect(a);

	a->reconnect = 0;
	a->prpl->login(a);

	if (a->ic && !(a->ic->flags & (OPT_SLOW_LOGIN | OPT_LOGGED_IN))) {
		a->ic->keepalive = b_timeout_add(120000, account_on_timeout, a->ic);
	}
}

void account_off(bee_t *bee, account_t *a)
{
	imc_logout(a->ic, FALSE);
	a->ic = NULL;
	if (a->reconnect) {
		/* Shouldn't happen */
		cancel_auto_reconnect(a);
	}
}

static gboolean account_on_timeout(gpointer d, gint fd, b_input_condition cond)
{
	struct im_connection *ic = d;

	if (!(ic->flags & (OPT_SLOW_LOGIN | OPT_LOGGED_IN))) {
		imcb_error(ic, "Connection timeout");
		imc_logout(ic, TRUE);
	}

	return FALSE;
}

struct account_reconnect_delay {
	int start;
	char op;
	int step;
	int max;
};

int account_reconnect_delay_parse(char *value, struct account_reconnect_delay *p)
{
	memset(p, 0, sizeof(*p));
	/* A whole day seems like a sane "maximum maximum". */
	p->max = 86400;

	/* Format: /[0-9]+([*+][0-9]+(<[0-9+])?)?/ */
	while (*value && g_ascii_isdigit(*value)) {
		p->start = p->start * 10 + *value++ - '0';
	}

	/* Sure, call me evil for implementing my own fscanf here, but it's
	   dead simple and I immediately know where to continue parsing. */

	if (*value == 0) {
		/* If the string ends now, the delay is constant. */
		return 1;
	} else if (*value != '+' && *value != '*') {
		/* Otherwise allow either a + or a * */
		return 0;
	}

	p->op = *value++;

	/* + or * the delay by this number every time. */
	while (*value && g_ascii_isdigit(*value)) {
		p->step = p->step * 10 + *value++ - '0';
	}

	if (*value == 0) {
		/* Use the default maximum (one day). */
		return 1;
	} else if (*value != '<') {
		return 0;
	}

	p->max = 0;
	value++;
	while (*value && g_ascii_isdigit(*value)) {
		p->max = p->max * 10 + *value++ - '0';
	}

	return p->max > 0;
}

char *set_eval_account_reconnect_delay(set_t *set, char *value)
{
	struct account_reconnect_delay p;

	return account_reconnect_delay_parse(value, &p) ? value : SET_INVALID;
}

int account_reconnect_delay(account_t *a)
{
	char *setting = set_getstr(&a->bee->set, "auto_reconnect_delay");
	struct account_reconnect_delay p;

	if (account_reconnect_delay_parse(setting, &p)) {
		if (a->auto_reconnect_delay == 0) {
			a->auto_reconnect_delay = p.start;
		} else if (p.op == '+') {
			a->auto_reconnect_delay += p.step;
		} else if (p.op == '*') {
			a->auto_reconnect_delay *= p.step;
		}

		if (a->auto_reconnect_delay > p.max) {
			a->auto_reconnect_delay = p.max;
		}
	} else {
		a->auto_reconnect_delay = 0;
	}

	return a->auto_reconnect_delay;
}

int protocol_account_islocal(const char* protocol)
{
	const char** p = account_protocols_local;

	do {
		if (strcmp(*p, protocol) == 0) {
			return 1;
		}
	} while (*(++p));
	return 0;
}
