#include "ldap_plugin.h"

/*Debug helper function*/
void ldap_debug(ldap_connection *conn, int level, char *message, ...)
{
	va_list arglist;
	char buffer[4096];
	char *type = (conn->type==CONNECTION_TYPE_LOCAL?"local":"remote");
	int debug_level = conn->debug_level;

	if (level > debug_level) { return; }
	va_start(arglist, message);
	vsprintf(buffer, message, arglist);

	switch (level) {
		case 0:
			//Error
			printf("[%s] ERROR: %s\n", type, buffer);
			if (conn->handle) {
				async_add_pairlist_log(conn->handle, buffer, SYNC_LOG_ERROR);
			}
			break;
		case 1:
			//Warning
			printf("[%s] WARNING: %s\n", type, buffer);
			break;
		case 2:
			//Information
			printf("[%s] INFORMATION: %s\n", type, buffer);
			break;
		case 3:
			//debug
			printf("[%s] DEBUG: %s\n", type, buffer);
			break;
		case 4:
			//debug
			printf("[%s] FULL DEBUG: %s\n", type, buffer);
			break;
	}
	va_end(arglist);
}

/***********************************************************************
 *
 * Function:    sync_connect
 *
 * Summary:   gets called by multisync when sync starts
 *
 ***********************************************************************/
ldap_connection *sync_connect(sync_pair *handle, connection_type type, sync_object_type object_types)
{
	ldap_connection *conn;

	conn = g_malloc0(sizeof(ldap_connection));
	g_assert(conn);
	conn->handle = handle;
	conn->commondata.object_types = object_types;
	conn->type = type;
	conn->evolution_support = 0;
	ldap_debug(conn, 3, "start: sync_connect");

	sprintf(conn->statefile, "%s/%sldap", sync_get_datapath(handle), (type==CONNECTION_TYPE_LOCAL?"local":"remote"));
	sprintf(conn->dbfile, "%s/%sstate", sync_get_datapath(handle), (type==CONNECTION_TYPE_LOCAL?"local":"remote"));
	ldap_debug(conn, 3, "Statefile: %s", conn->statefile);

	if (load_ldap_state(conn)) {
		sync_set_requestfailed(handle);
		return NULL;
	}

	//connect
	if (ldap_start(conn)) {
		sync_set_requestfailed(handle);
		return NULL;
	}

	//Set ldap v3
	ldap_set_version(conn);

	//set encryption
	if (conn->encryption) {
		if (ldap_encrypt(conn)) {
			if (conn->encryption == 2) {
				//encryption required
				ldap_debug(conn, 0, "Unable to start required encryption");
				sync_set_requestfailed(handle);
				return NULL;
			}
		}
	}

	//Check Authentication
	if (ldap_makebind(conn)) {
		sync_set_requestfailed(handle);
		return NULL;
	}

	//Check evolution support
	if (!ldap_check_evolution(conn)) {
		conn->evolution_support = 1;
	}

	srand(time(NULL));
	ldap_debug(conn, 3, "end: sync_connect");
	sync_set_requestdone(handle);
	return(conn);
}

/*convert data_entry into changed_info*/
changed_object *add_changed(ldap_connection *conn, struct data_entry *data, int change_type)
{
	GString *vcard = NULL;
	changed_object *change = NULL;

	change = g_malloc0(sizeof(changed_object));
	change->uid = data->uid;
	change->change_type = change_type;
	change->object_type = SYNC_OBJECT_TYPE_PHONEBOOK;
	change->comp = NULL;
	change->removepriority = NULL;

	if (change_type != SYNC_OBJ_HARDDELETED && change_type != SYNC_OBJ_SOFTDELETED) {
		//Convert attributes to VCARD
		vcard = ldap2vcard(conn, data->ldapdata);
		ldap_debug(conn, 3, "From ldap generated VCARD:\n%s", vcard->str);
		change->comp = vcard->str;
	}
	free(data);
	return change;
}


/***********************************************************************
 *
 * Function:    get_changes
 *
 * Summary:  Get the list of changes from the ldap server by comparing it against
 *			a local xml database
 *
 ***********************************************************************/
void get_changes(ldap_connection *conn, sync_object_type newdbs)
{
	GList *changes = NULL;
	change_info *chinfo = NULL;
	GList *xmllist = NULL, *ldaplist = NULL;
	struct data_entry *xmlentry, *ldapentry;
	GList *xmlfirst, *ldapfirst;
	int i = 0, n = 0;

	//Get all entries from ldap server and xml file
	ldaplist = load_ldap_entries(conn);
	xmllist = load_xml_entries(conn);
	ldap_debug(conn, 3, "Got %i from ldap, %i from xml", g_list_length(ldaplist), g_list_length(xmllist));

	ldap_debug(conn, 2, "Searching for changes");
	// Now create the list of changes
	for (i = 0; g_list_nth(ldaplist, i);) {
		ldapentry = ((struct data_entry *)(g_list_nth_data(ldaplist, i)));
		ldap_debug(conn, 4, "New ldapentry: on list %i, %i", g_list_length(ldaplist), i);
		n = 0;
		for (n = 0; g_list_nth(xmllist, n);) {
			ldap_debug(conn, 4, "New xmlentry: on list %i, %i", g_list_length(g_list_first(xmllist)), n);
			xmlentry = ((struct data_entry *)(g_list_nth_data(xmllist, n)));
			ldap_debug(conn, 3, "Comparing %s with %s", ldapentry->uid, xmlentry->uid);
			if (strcmp(xmlentry->uid, ldapentry->uid) == 0) {
				ldap_debug(conn, 3, "Entries equal: Tsldap %s, Tsxml %s", ldapentry->modifyTimestamp, xmlentry->modifyTimestamp);
				if (strcmp(xmlentry->modifyTimestamp, ldapentry->modifyTimestamp) != 0) {
					//Entry has been modified, Update on ldaplist
					ldap_debug(conn, 2, "Modified entry found: %s", ldapentry->uid);
					get_ldap_data(conn, ldapentry);
					if (newdbs) {
						g_list_nth(ldaplist, i)->data = add_changed(conn, ldapentry, SYNC_OBJ_ADDED);
					} else {
						g_list_nth(ldaplist, i)->data = add_changed(conn, ldapentry, SYNC_OBJ_MODIFIED);
					}
					//remove from xmllist
					xmllist = g_list_remove(xmllist, xmlentry);
					//Move ldaplist on
					i++;
				} else {
					//Entry has not been modified
					ldap_debug(conn, 2, "Unmodified entry found: %s", ldapentry->uid);
					//Remove it from ldaplist and xmllist
					if (newdbs) {
						get_ldap_data(conn, ldapentry);
						g_list_nth(ldaplist, i)->data = add_changed(conn, ldapentry, SYNC_OBJ_ADDED);
						//Move ldaplist on
						i++;
					} else {
						ldaplist = g_list_remove(ldaplist, ldapentry);
					}
					xmllist = g_list_remove(xmllist, xmlentry);
				}
				goto nextldap;
			}
			n++;
		}
		//Could not find it on the xmllist, so must be new
		//Update it on the ldaplist
		ldap_debug(conn, 2, "New entry found: %s", ldapentry->uid);
		get_ldap_data(conn, ldapentry);
		g_list_nth(ldaplist, i)->data = add_changed(conn, ldapentry, SYNC_OBJ_ADDED);
		//Move ldaplist on
		i++;
		nextldap:
		;
	}
	done:
	ldap_debug(conn, 3, "Got %i on ldap, %i on xml", g_list_length(ldaplist), g_list_length(xmllist));

	ldap_debug(conn, 2, "Looking for deleted items");
	// Now get the deleted entries
	n = 0;
	for (n = 0; g_list_nth(xmllist, n); n++) {
		if (!newdbs) {
			ldap_debug(conn, 2, "Deleted entry found: %s", ((struct data_entry*)(g_list_nth_data(xmllist, n)))->uid);
			g_list_nth(xmllist, n)->data = add_changed(conn, ((struct data_entry*)(g_list_nth_data(xmllist, n))), SYNC_OBJ_HARDDELETED);
		}
	}
	ldap_debug(conn, 2, "Done searching for changes");

	//Now merge ldaplist and xmllist
	if (xmllist && !newdbs) {
		xmllist = g_list_concat(ldaplist, xmllist);
	} else {
		xmllist = ldaplist;
	}

	chinfo = g_malloc0(sizeof(change_info));
	chinfo->newdbs = 0;
	chinfo->changes = xmllist;

	sync_set_requestdata(chinfo, conn->handle);
	ldap_debug(conn, 2, "Found %i changes", g_list_length(xmllist));
}

/***********************************************************************
 *
 * Function:    syncobj_modify
 *
 * Summary:   Modify or add a ldap entry
 *
 ***********************************************************************/
void syncobj_modify(ldap_connection *conn, char *comp, char *uid, sync_object_type objtype, char *uidret, int *uidretlen)
{
	LDAPMod **ldapdata = NULL;

	if (!conn->write) {
		//Write support disabled
		sync_set_requestdone(conn->handle);
		return;
	}

	ldap_debug(conn, 2, "start: syncobj_modify");
	ldap_debug(conn, 3, "COMP: %s\n", comp);

	//Convert the vcard to our LDAP data
	ldapdata = vcard2ldap(conn, comp);

	if (uid) {
		//Modify a entry
		ldap_debug(conn, 2, "Modifying: %s", uid);
		//Delete it
		uid = quoted_decode(uid);
		if (ldap_delete_entry(conn, uid) != 0) {
			ldap_debug(conn, 1, "Could not delete entry! Possible duplicates");
		}
		//free(uid);
		//We need to send a requestfailed, so that multisync allows us to change the uid of the object. Plain stupid.
		sync_set_requestfailed(conn->handle);
	} else {
		//Add a new entry
		if (ldap_add_entry(conn, ldapdata, uidret, 0) != 0) {
			sync_set_requestfailed(conn->handle);
		} else {
			sync_set_requestdone(conn->handle);
		}
	}
	//free(ldapdata);
	*uidretlen = strlen(uidret);
}

/*Delete a entry given by the encoded cn*/
void syncobj_delete(ldap_connection *conn, char *uid, sync_object_type objtype, int softdelete)
{
	if (!conn->write) {
		//Write support disabled
		sync_set_requestdone(conn->handle);
		return;
	}

	//Decode the uid
	uid = quoted_decode(uid);

	if (ldap_delete_entry(conn, uid) != 0) {
		sync_set_requestfailed(conn->handle);
	} else {
		sync_set_requestdone(conn->handle);
	}
	free(uid);
}

void sync_done(ldap_connection *conn, gboolean success)
{
	if (success) {
		save_xml_entries(conn);
	}
	sync_set_requestdone(conn->handle);
	ldap_debug(conn, 2, "Done syncing");
}

void sync_disconnect(ldap_connection *conn)
{
	//Check if we have an Ldap connection
	if(conn->ld) {
		if(ldap_unbind(conn->ld) != LDAP_SUCCESS) {
			sync_log(conn->handle, "Couldn't unbind LDAP server", SYNC_LOG_ERROR);
		} else {
			sync_log(conn->handle, "Disconnected from LDAP server", SYNC_LOG_SUCCESS);
		}
	}
	//ldap_memfree(conn->ld);
	conn->ld = NULL;

	sync_set_requestdone(conn->handle);
}

char* short_name()
{
	return("ldap-sync");
}

char *long_name()
{
	return("LDAP");
}

sync_object_type object_types()
{
	return(SYNC_OBJECT_TYPE_PHONEBOOK);
}

void plugin_init(void)
{
}

char *plugin_info()
{
	return("Allows synchronization of a addressbook against a specific DN in a LDAP server.");
}

gboolean always_connected()
{
	return(FALSE);
}

int plugin_API_version(void)
{
	return(3);
}
