/* 
   MultiSync Evolution Plugin - Synchronize Ximian Evolution data
   Copyright (C) 2002-2003 Bo Lincoln <lincoln@lysator.liu.se>

   This program is free software; you can redistribute it and/or modify
   it under the terms of the GNU General Public License version 2 as
   published by the Free Software Foundation;

   THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
   OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
   FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT OF THIRD PARTY RIGHTS.
   IN NO EVENT SHALL THE COPYRIGHT HOLDER(S) AND AUTHOR(S) BE LIABLE FOR ANY
   CLAIM, OR ANY SPECIAL INDIRECT OR CONSEQUENTIAL DAMAGES, OR ANY DAMAGES 
   WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 
   ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 
   OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.

   ALL LIABILITY, INCLUDING LIABILITY FOR INFRINGEMENT OF ANY PATENTS, 
   COPYRIGHTS, TRADEMARKS OR OTHER RIGHTS, RELATING TO USE OF THIS 
   SOFTWARE IS DISCLAIMED.
*/

/*
 *  $Id: evolution_sync.c,v 1.49 2004/02/09 18:53:28 lincoln Exp $
 */

#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <string.h>
#include <glib.h>
#include <gmodule.h>
#include <bonobo/bonobo-main.h>
#include <cal-client/cal-client.h>
#include <cal-client/cal-client-types.h>
#include <cal-util/cal-component.h>
#include <multisync.h>
#include <pthread.h>
#include <dirent.h>
#include "evolution_sync.h"
#include <ical.h>
#include "addr_sync.h"
#include "gui.h"

gboolean versionok = FALSE;

extern gboolean multisync_debug;

void evo_load_state(evolution_connection *conn) {
  char *filename;
  FILE *f;
  char line[256];

  filename = g_strdup_printf("%s/%s%s", sync_get_datapath(conn->sync_pair),
			     (conn->conntype==CONNECTION_TYPE_LOCAL?"local":"remote"), EVOLUTIONFILE);
  if ((f = fopen(filename, "r"))) {
    while (fgets(line, 256, f)) {
      char prop[128], data[256];
      if (sscanf(line, "%128s = %256[^\n]", prop, data) == 2) {
	if (!strcmp(prop, "calendarpath")) {
	  conn->calendarpath = g_strdup(data);
	}
	if (!strcmp(prop, "todopath")) {
	  conn->todopath = g_strdup(data);
	}
	if (!strcmp(prop, "addressbookpath")) {
	  conn->addressbookpath = g_strdup(data);
	}
      }
    }
    fclose(f);
  }
  g_free(filename);
}

void evo_save_state(evolution_connection *conn) {
  FILE *f;
  char *filename;
  
  filename = g_strdup_printf("%s/%s%s", sync_get_datapath(conn->sync_pair), 
			     (conn->conntype==CONNECTION_TYPE_LOCAL?"local":"remote"), EVOLUTIONFILE);
  if ((f = fopen(filename, "w"))) {

    if (conn->calendarpath)
      fprintf(f, "calendarpath = %s\n", conn->calendarpath);
    if (conn->todopath)
      fprintf(f, "todopath = %s\n", conn->todopath);
    if (conn->addressbookpath)
      fprintf(f, "addressbookpath = %s\n", conn->addressbookpath);
    fclose(f);
  }
  g_free(filename);
}

sync_object_type object_type_from_component(CalComponent *comp) {
  sync_object_type type = SYNC_OBJECT_TYPE_UNKNOWN; 
  CalComponentVType objtype;
  
  if (comp) {
    objtype = cal_component_get_vtype(comp);
    switch(objtype) {
    case CAL_COMPONENT_EVENT:
      type = SYNC_OBJECT_TYPE_CALENDAR;
      break;
    case CAL_COMPONENT_TODO:
      type = SYNC_OBJECT_TYPE_TODO;
      break;
    default:
      type = SYNC_OBJECT_TYPE_UNKNOWN;
    }
  }
  return (type);
}

// Like g_list_append, but checks for duplicates
GList *evo_append_change(GList *changelist, changed_object *change) {
  int t;
  for (t = 0; t < g_list_length(changelist); t++) {
    changed_object *c;
    c = g_list_nth_data(changelist, t);
    if (c && c->uid && change->uid && !strcmp(change->uid, c->uid)) {
      sync_free_changed_object(c);
      changelist = g_list_remove(changelist, c);
      t--;
    }
  }
  changelist = g_list_append(changelist, change);
  return(changelist);
}

GList *evo_cal_get_changes(GList *changes, evolution_connection *conn) {
  GList *cal_changes, *l;
  if (!conn->cal_client)
    return(changes);
  cal_changes =  cal_client_get_changes(conn->cal_client, 
					CALOBJ_TYPE_EVENT, 
					conn->changedbname);
  for (l=cal_changes; l; l = l->next) {
    CalClientChange *clientchange;
    CalComponent *comp;
    icalcomponent *icalcomp;
    
    clientchange = l->data;
    comp = clientchange->comp;
    if (comp) {
      char *uid = NULL;
      changed_object* change;
      icalproperty *dtend;

      change = g_malloc0(sizeof(changed_object));
      g_assert(change);
      cal_component_get_uid(comp, (const char**) &uid);
      if (uid) 
	change->uid = g_strdup(uid);
      icalcomp = cal_component_get_icalcomponent (comp);
      if (icalcomp) {
	change->comp = g_strdup_printf("BEGIN:VCALENDAR\r\nVERSION:2.0\r\n%s"
				       "END:VCALENDAR\r\n",
				       cal_component_get_as_string(comp));
	if (clientchange->type & CAL_CLIENT_CHANGE_DELETED)
	  change->change_type = SYNC_OBJ_HARDDELETED;
	else if (clientchange->type & CAL_CLIENT_CHANGE_MODIFIED)
	  change->change_type = SYNC_OBJ_MODIFIED;
	else
	  change->change_type = SYNC_OBJ_ADDED;
	dtend = icalcomponent_get_first_property(icalcomp, 
						 ICAL_DTEND_PROPERTY);
	if (dtend)
	  change->removepriority = 
	    g_strdup(icaltime_as_ical_string(icalproperty_get_dtend(dtend)));
	change->object_type = object_type_from_component(comp);
	changes = evo_append_change(changes, change);
      }
    }
  }
  cal_client_change_list_free (cal_changes);
  return(changes);
}  

GList *evo_todo_get_changes(GList *changes, evolution_connection *conn) {
  GList *todo_changes, *l;
  if (!conn->todo_client)
    return(changes);
  todo_changes =  cal_client_get_changes(conn->todo_client, 
					CALOBJ_TYPE_TODO, 
					conn->changedbname);
  for (l=todo_changes; l; l = l->next) {
    CalClientChange *clientchange;
    CalComponent *comp;
    icalcomponent *icalcomp;
    
    clientchange = l->data;
    comp = clientchange->comp;
    if (comp) {
      char *uid = NULL;
      changed_object* change;
      icalproperty *dtend;

      change = g_malloc0(sizeof(changed_object));
      g_assert(change);
      cal_component_get_uid(comp, (const char**) &uid);
      if (uid) 
	change->uid = g_strdup(uid);
      icalcomp = cal_component_get_icalcomponent (comp);
      if (icalcomp) {
	change->comp = g_strdup_printf("BEGIN:VCALENDAR\r\nVERSION:2.0\r\n%s"
				       "END:VCALENDAR\r\n",
				       cal_component_get_as_string(comp));
	if (clientchange->type & CAL_CLIENT_CHANGE_DELETED)
	  change->change_type = SYNC_OBJ_HARDDELETED;
	else if (clientchange->type & CAL_CLIENT_CHANGE_MODIFIED)
	  change->change_type = SYNC_OBJ_MODIFIED;
	else
	  change->change_type = SYNC_OBJ_ADDED;
	dtend = icalcomponent_get_first_property(icalcomp, 
						 ICAL_DTEND_PROPERTY);
	if (dtend)
	  change->removepriority = 
	    g_strdup(icaltime_as_ical_string(icalproperty_get_dtend(dtend)));
	change->object_type = object_type_from_component(comp);
	changes = evo_append_change(changes, change);
      }
    }
  }
  cal_client_change_list_free (todo_changes);
  return(changes);
}  


gboolean evo_get_changes(gpointer data) {
  evolution_connection *conn = data;
  GList *changes = NULL;

  if (conn->cal_client) {
    if (!conn->modifying && (conn->newdbs & SYNC_OBJECT_TYPE_CALENDAR))
      changes = evo_cal_get_all(changes, conn);
    else
      changes = evo_cal_get_changes(changes, conn);
  }
  if (conn->todo_client) {
    if (!conn->modifying && (conn->newdbs & SYNC_OBJECT_TYPE_TODO))
      changes = evo_todo_get_all(changes, conn);
    else
      changes = evo_todo_get_changes(changes, conn);
  }
  if (!conn->modifying && (conn->newdbs & SYNC_OBJECT_TYPE_PHONEBOOK))
    evo_addr_get_all(changes, conn, evo_get_changes_done);
  else
    evo_addr_get_changes(changes, conn, evo_get_changes_done);
  return(FALSE);
}

// Callback from addressbook get-changes
void evo_get_changes_done(gpointer data, evolution_connection *conn) {
  GList *changes = data;
  change_info *chinfo;

  if (conn->modifying) {
    int n = 0;
    
    while (n < g_list_length(changes)) {
      changed_object *change = 
	g_list_nth_data(changes, n);
      if (evo_check_change(conn, change->uid, change->change_type)) {
	changes = g_list_remove(changes, change);
	sync_free_changed_object(change);
      } else
	n++;
    }
    if (g_list_length(changes) > 0) {
      dd(printf("We found %d unexpected changes\n", g_list_length(changes)));
      add_internal_changes(changes, conn); // Add to internal list
      sync_object_changed(conn->sync_pair); // New, unexpected change
      sync_free_changes(changes);
    }
    sync_set_requestdata(conn->modify_results, conn->sync_pair);
    conn->modifying = FALSE;
    conn->modify_results = NULL;
    conn->modify_objects = NULL;
  } else {
    add_internal_changes(changes, conn); // Add to internal list
    sync_free_changes(changes);
    
    changes = get_internal_changes(conn); // Get all changes
    chinfo = g_malloc0(sizeof(change_info));
    chinfo->changes = changes;
    chinfo->newdbs = 0;
    sync_set_requestdata(chinfo, conn->sync_pair);
  }
}

// Return TRUE fo this change was expected (we did it)
gboolean evo_check_change(evolution_connection *conn, char *id,
			       int change_type) {
  GList *changes = conn->modify_objects, *results = conn->modify_results;
  gboolean same = FALSE;
  while (changes && results && !same) {
    changed_object *change = changes->data;
    syncobj_modify_result *result = results->data;
    if ((id && result->returnuid && !strcmp(result->returnuid, id)) ||
	(id && change->uid && !strcmp(change->uid, id))) {
      if ((change->change_type == SYNC_OBJ_MODIFIED ||
	   change->change_type == SYNC_OBJ_ADDED) &&
	  (change_type == SYNC_OBJ_HARDDELETED ||
	   change_type == SYNC_OBJ_SOFTDELETED))
	same = FALSE;
      else
	same = TRUE;
    }
    changes = changes->next;
    results = results->next;
  }
  return(same);
}

GList *evo_cal_get_all (GList *changes, evolution_connection *conn) {
  GList *cal_changes, *known_changes, *l;
  if (!conn->cal_client)
    return(changes);

  cal_changes =  cal_client_get_uids(conn->cal_client, CALOBJ_TYPE_EVENT);
  for (l=cal_changes; l; l = l->next) {
    CalComponent *comp;
    icalcomponent *icalcomp;
    char *uid;

    if (cal_client_get_object(conn->cal_client, l->data, &comp) ==
	CAL_CLIENT_GET_SUCCESS) {
      changed_object* change;
      icalproperty *dtend;

      change = g_malloc0(sizeof(changed_object));
      g_assert(change);
      cal_component_get_uid(comp, (const char**) &uid);
      if (uid) 
	change->uid = g_strdup(uid);
      icalcomp = cal_component_get_icalcomponent (comp);

      change->comp = g_strdup_printf("BEGIN:VCALENDAR\r\nVERSION:2.0\r\n%s"
				     "END:VCALENDAR\r\n",
				     cal_component_get_as_string(comp));
      change->change_type = SYNC_OBJ_MODIFIED;
      change->object_type = object_type_from_component(comp);
      dtend = icalcomponent_get_first_property(icalcomp, 
					       ICAL_DTEND_PROPERTY);
      if (dtend)
	change->removepriority = 
	  g_strdup(icaltime_as_ical_string(icalproperty_get_dtend(dtend)));
      changes = evo_append_change(changes, change);
    }
  }
  known_changes =  cal_client_get_changes(conn->cal_client, 
					  CALOBJ_TYPE_EVENT,
					  conn->changedbname);
  cal_obj_uid_list_free(known_changes);
  cal_obj_uid_list_free(cal_changes);
  return(changes);
}

GList *evo_todo_get_all (GList *changes, evolution_connection *conn) {
  GList *todo_changes, *known_changes, *l;
  if (!conn->todo_client)
    return(changes);
  
  todo_changes =  cal_client_get_uids(conn->todo_client, CALOBJ_TYPE_TODO);
  
  for (l=todo_changes; l; l = l->next) {
    CalComponent *comp;
    icalcomponent *icalcomp;
    char *uid;

    if (cal_client_get_object(conn->todo_client, l->data, &comp) ==
	CAL_CLIENT_GET_SUCCESS) {
      changed_object* change;

      change = g_malloc0(sizeof(changed_object));
      g_assert(change);
      cal_component_get_uid(comp, (const char**) &uid);
      if (uid) 
	change->uid = g_strdup(uid);
      icalcomp = cal_component_get_icalcomponent (comp);

      change->comp = g_strdup_printf("BEGIN:VCALENDAR\r\nVERSION:2.0\r\n%s"
				     "END:VCALENDAR\r\n",
				     cal_component_get_as_string(comp));
      change->change_type = SYNC_OBJ_MODIFIED;
      change->object_type = object_type_from_component(comp);
      changes = evo_append_change(changes, change);
    }
  }
  known_changes =  cal_client_get_changes(conn->todo_client, 
					  CALOBJ_TYPE_TODO, 
					  conn->changedbname);
  cal_obj_uid_list_free(known_changes);
  cal_obj_uid_list_free(todo_changes);
  return(changes);
}

// Add a list of changes to the internal list of changes (for 
// interrupted synchronizations etc). This is only a workaround 
// for evolution's stupid change-db handling.
void add_internal_changes(GList *changes, evolution_connection *conn) {
  GList *l;

  for (l=changes; l; l = l->next) {
    changed_object *change = l->data;
    changed_object *tmp = sync_copy_changed_object(change);
    // Copy the changed object, and expand the struct
    internal_changed_object *obj = g_malloc0(sizeof(internal_changed_object));
    memcpy(obj, tmp, sizeof(changed_object));
    obj->reported = FALSE;
    g_free(tmp);
    conn->internal_changes = evo_append_change(conn->internal_changes, 
					       (changed_object*) obj);
  }
  save_internal_changes(conn);
}

// Returns a list of "change_object"s for get_changes
GList *get_internal_changes(evolution_connection *conn) {
  GList *l;
  GList *sync_changes = NULL;

  for (l=conn->internal_changes; l; l = l->next) {
    changed_object *change;
    internal_changed_object *intchg;
    change = sync_copy_changed_object(l->data);
    sync_changes = evo_append_change(sync_changes, change);
    intchg = l->data;
    intchg->reported = TRUE;
  }    
  return(sync_changes);
}

void remove_internal_changes(evolution_connection *conn) {
  int n = 0;
  GList *sync_changes = NULL;
  
  while (n < g_list_length(conn->internal_changes)) {
    internal_changed_object *intchg = 
      g_list_nth_data(conn->internal_changes, n);
    if (intchg && intchg->reported) {
      conn->internal_changes = g_list_remove(conn->internal_changes,
					     intchg);
      sync_free_changed_object((changed_object*) intchg);
    } else
      n++;
  }
  save_internal_changes(conn);
}

void load_internal_changes(evolution_connection *conn) {
  char *filename;
  FILE *f;
  char *line = NULL;

  line = g_malloc(65536);
  filename = g_strdup_printf ("%s/%s%s", sync_get_datapath(conn->sync_pair),
			      (conn->conntype==CONNECTION_TYPE_LOCAL?"local":
			       "remote"), INTERNAL_CHANGE_FILE);
  if ((f = fopen(filename, "r"))) {
    while (fgets(line, 65536, f)) {
      char objtype, changetype;
      if (sscanf(line, "2 %c %c", &objtype, &changetype) >= 2) {
	char *pos = line+6;
	internal_changed_object *change = 
	  g_malloc0(sizeof(internal_changed_object));
	change->obj.object_type = (objtype=='C'?SYNC_OBJECT_TYPE_CALENDAR:
			       (objtype=='T'?SYNC_OBJECT_TYPE_TODO:
				SYNC_OBJECT_TYPE_PHONEBOOK));
	change->obj.change_type = (changetype=='D'?SYNC_OBJ_SOFTDELETED:
			       (changetype=='H'?SYNC_OBJ_HARDDELETED:
				(changetype=='A'?SYNC_OBJ_ADDED:
				 SYNC_OBJ_MODIFIED)));
	change->obj.comp = evo_decode_line_to_string(pos);
	pos = strstr(pos, " ");
	if (pos)
	  pos++;
	change->obj.uid = evo_decode_line_to_string(pos);
	pos = strstr(pos, " ");
	if (pos)
	  pos++;
	change->obj.removepriority = evo_decode_line_to_string(pos);
	change->reported = FALSE;
	conn->internal_changes = evo_append_change(conn->internal_changes, 
						   (changed_object*) change);
      }
    }
    fclose(f);
  }
  g_free(filename);
  g_free(line);
}

// Encode string to one line without spaces.
// Returned string is gmalloced
char *evo_encode_string_to_line(char* instr) {
  GString *str;
  int len,t;
  char *ret = NULL;

  if (!instr)
    return(g_strdup(""));
  str = g_string_new("");
  len = strlen(instr);
  for (t = 0; t < len; t++) {
    switch(instr[t]) {
    case '\\':
      g_string_append(str, "\\\\");
      break;
    case '\n':
      g_string_append(str, "\\n");
      break;
    case '\r':
      g_string_append(str, "\\r");
      break;
    case ' ':
      g_string_append(str, "\\_");
      break;
    case '\t':
      g_string_append(str, "\\t");
      break;
    default: {
      char tmp[2] = " ";
      tmp[0] = instr[t];
      g_string_append(str, tmp);
    }
    }
  }
  ret = str->str;
  g_string_free(str, FALSE);
  return(ret);
}

char* evo_decode_line_to_string(char *line) {
  GString *str;
  int len,t;
  char *ret = NULL;
  int mode = 0;

  if (!line)
    return(NULL);
  str = g_string_new("");
  len = strlen(line);
  for (t = 0; t < len && line[t] != ' '; t++) {
    if (mode) {
      switch(line[t]) {
      case '\\':
	g_string_append(str, "\\");
	break;
      case 'n':
	g_string_append(str, "\n");
	break;
      case 'r':
	g_string_append(str, "\r");
	break;
      case '_':
	g_string_append(str, " ");
	break;
      case 't':
	g_string_append(str, "\t");
	break;
      }
      mode = 0;
    } else {
      if (line[t] == '\\')
	mode = 1;
      else  {
	char tmp[2] = " ";
	tmp[0] = line[t];
	g_string_append(str, tmp);
      }
    }
  }
  ret = str->str;
  g_string_free(str, FALSE);
  if (strlen(ret) == 0) {
    g_free(ret);
    ret = NULL;
  }
  return(ret);
}

void save_internal_changes(evolution_connection *conn) {
  char *filename;
  FILE *f;
  
  filename = g_strdup_printf ("%s/%s%s", sync_get_datapath(conn->sync_pair),
			      (conn->conntype==CONNECTION_TYPE_LOCAL?"local":
			       "remote"), INTERNAL_CHANGE_FILE);
  if ((f = fopen(filename, "w"))) {
    GList *l;
    for (l=conn->internal_changes; l; l = l->next) {
      char changetype, objtype;
      changed_object *change;
      char *comp, *uid, *removepriority;
      change = l->data;
      objtype = (change->object_type==SYNC_OBJECT_TYPE_CALENDAR?'C':
		 (change->object_type==SYNC_OBJECT_TYPE_TODO?'T':'P'));
      changetype = (change->change_type==SYNC_OBJ_SOFTDELETED?'S':
		    (change->change_type==SYNC_OBJ_HARDDELETED?'D':
		     (change->change_type==SYNC_OBJ_ADDED?'A':'M')));
      comp = evo_encode_string_to_line(change->comp);
      uid = evo_encode_string_to_line(change->uid);
      removepriority = evo_encode_string_to_line(change->removepriority);
      // 2 for version 2.
      fprintf(f, "2 %c %c %s %s %s\n", objtype, changetype, comp, uid, 
	      removepriority);
      g_free(comp);
      g_free(uid);
      g_free(removepriority);
    }
    fclose(f);
  }
  g_free(filename);
}

/* Callback used when a calendar is opened */
static void
cal_opened_cb (CalClient *client, CalClientOpenStatus status, gpointer data)
{
  evolution_connection *conn = data;
  
  if (status == CAL_CLIENT_OPEN_SUCCESS) {
    if (conn->callback)
      (conn->callback)(NULL, conn); // We have loaded the DB
  }
  else {
    sync_set_requestfailed(conn->sync_pair);
    g_object_unref (G_OBJECT (client));
  }
}

/* Callback used when an object is updated */
static void obj_updated_cb (CalClient *client, const char *uid, gpointer data)
{
  evolution_connection *conn = data;

  if (!conn->modifying)
    sync_object_changed(conn->sync_pair);
}

/* Callback used when an object is removed */
static void obj_removed_cb (CalClient *client, const char *uid, gpointer data)
{
  evolution_connection *conn = data;
  
  if (!conn->modifying)
    sync_object_changed(conn->sync_pair);
}

/* Creates a calendar client and tries to load the specified URI into it */
CalClient* create_client (evolution_connection *conn, const char *uri, 
			  gboolean only_if_exists) {
  gboolean result;
  CalClient *client = NULL;
  
  client = cal_client_new ();
  if (!client) {
    evo_display_error("Evolution plugin: Could not connect to Evolution!");
    return(NULL);
  }

  g_signal_connect (client, "obj_removed",
		    G_CALLBACK (obj_removed_cb), conn);
  g_signal_connect (client, "cal_opened",
		      G_CALLBACK (cal_opened_cb), conn);
  g_signal_connect (client, "obj_updated",
		      G_CALLBACK (obj_updated_cb), conn);

  dd(printf ("Calendar loading `%s'...\n", uri));

  result = cal_client_open_calendar (client, uri, only_if_exists);

  if (!result) {
    char *err = g_strdup_printf("Evolution plugin: Could not open \"%s\"!",
				uri);
    evo_display_error(err);
    g_free(err);
    return(NULL);
  }
  return(client);
}

void get_changes(evolution_connection* conn, sync_object_type newdbs) {
  conn->newdbs = newdbs;
  g_idle_add(evo_get_changes, conn);
}

gboolean cal_connect(gpointer data) {
  evolution_connection *conn = data;
  char *dir;

  conn->callback = evo_sync_loaddbs_cb; // To be called when loaded
  if (conn->commondata.object_types & SYNC_OBJECT_TYPE_CALENDAR) {
    conn->nodbs++;
    if (conn->calendarpath) {
      conn->cal_client = create_client (conn, conn->calendarpath, FALSE);
    } else {
      char *dir = g_strdup_printf ("%s/evolution/local/Calendar/calendar.ics", 
				   g_get_home_dir());
      conn->cal_client = create_client (conn, dir, TRUE);
      g_free (dir);
    }
  } 
  if (conn->commondata.object_types & SYNC_OBJECT_TYPE_TODO) {
    conn->nodbs++;
    if (conn->todopath) {
      conn->todo_client = create_client (conn, conn->todopath, FALSE);
    } else {
      dir = g_strdup_printf ("%s/evolution/local/Tasks/tasks.ics", 
			     g_get_home_dir());
      conn->todo_client = create_client (conn, dir, TRUE);
      g_free (dir);
    }
  }
  if (conn->commondata.object_types & SYNC_OBJECT_TYPE_PHONEBOOK) {
    evo_addr_connect(conn);
  }
  if (conn->nodbs == 0)
    sync_set_requestdone(conn->sync_pair);
  return(FALSE);
}

void evo_sync_loaddbs_cb(gpointer data, evolution_connection *conn) {
  conn->nodbsloaded++;
  if (conn->nodbsloaded >= conn->nodbs)
    sync_set_requestdone(conn->sync_pair); // All DBs are loaded!
}

evolution_connection* sync_connect(sync_pair* handle, connection_type type,
				   sync_object_type object_types) {
  evolution_connection *conn;
  int i;
  char *slash;
  char *datapath;

  if (!versionok) {
    sync_set_requestfailed(handle);
    return(NULL);
  }
  conn = g_malloc0(sizeof(evolution_connection));
  g_assert(conn);
  conn->sync_pair = handle;
  conn->conntype = type;
  conn->commondata.object_types = object_types;
  evo_load_state(conn);
  datapath = sync_get_datapath(handle);
  i = strlen(datapath)-1;
  if (datapath[i] == '/') i--;
  while(i > 0 && datapath[i] != '/')
    i--;
  i++;
  conn->changedbname = g_strdup_printf("%s%s", 
				       (type==CONNECTION_TYPE_LOCAL?"local":
					"remote"), datapath+i);
  if ((slash = strstr(conn->changedbname, "/")))
    slash[0] = 0;
  load_internal_changes(conn);
  g_idle_add(cal_connect, conn);
  return(conn);
}


gboolean evo_do_disconnect(gpointer data) {
  evolution_connection *conn = data;
  sync_free_changes(conn->internal_changes);
  if (conn->cal_client)
    g_object_unref (G_OBJECT (conn->cal_client));
  conn->cal_client = NULL;
  if (conn->todo_client)
    g_object_unref (G_OBJECT (conn->todo_client));
  conn->todo_client = NULL;
  evo_addr_disconnect(conn);
  sync_set_requestdone(conn->sync_pair);
  g_free(conn);
  return(FALSE);
}

void sync_disconnect(evolution_connection *conn) {
  g_idle_add(evo_do_disconnect, conn);
}

// Free the returned string using g_free()
char *evo_replace(char *str, char *from, char* to) {
  GString *outstr;
  char *ret=NULL;
  char *pos = str, *last = str;
  outstr = g_string_new("");
  while ((pos = strstr(pos, from))) {
    char *tmp = g_strndup(last, pos-last);
    g_string_append(outstr, tmp);
    g_free(tmp);
    g_string_append(outstr, to);
    pos+=strlen(from);
    last = pos;
  }
  g_string_append(outstr, last);
  ret = outstr->str;
  g_string_free(outstr, FALSE);
  return(ret);
}


gboolean evo_cal_modify_one(evolution_connection *conn,
			    changed_object *obj,
			    char **uidret) {
  // UID exists, this is modify
  CalComponent *calcomp = NULL;
  CalClientResult res = 0;
  int modified = 0;

  if (obj->comp) {
    icalcomponent *icalcomp;
    char *tmp = evo_replace(obj->comp, "\r\n", "\n");
    char *start, *end;
    start = strstr(tmp, "BEGIN:VEVENT");
    end = strstr(tmp, "END:VEVENT");
    if (end) {
      end+=strlen("END:VEVENT"+1);
      end[0] = 0;
    }
    if (!start || !end) {
      start = strstr(tmp, "BEGIN:VTODO");
      end = strstr(tmp, "END:VTODO");
      if (end) {
	end+=strlen("END:VTODO"+1);
	end[0] = 0;
      }
    }
    if (!start)
      start = tmp;
    icalcomp = icalcomponent_new_from_string(start);
    g_free(tmp);
    calcomp = cal_component_new ();
    g_assert(calcomp);
    cal_component_set_icalcomponent (calcomp,icalcomp);
    if (obj->uid) {
      cal_component_set_uid (calcomp, obj->uid);
    } else {
      char* uid = cal_component_gen_uid();
      cal_component_set_uid (calcomp, uid);
      if (uidret)
	*uidret = g_strdup(uid);
    }
    if (obj->object_type == SYNC_OBJECT_TYPE_CALENDAR) 
      res = cal_client_update_object (conn->cal_client, calcomp);
    if (obj->object_type == SYNC_OBJECT_TYPE_TODO) 
      res = cal_client_update_object (conn->todo_client, calcomp);
    if (res != CAL_CLIENT_RESULT_SUCCESS && obj->uid) {
      // Modify failed, try adding
      char* uid = cal_component_gen_uid();
      cal_component_set_uid (calcomp, uid);
      if (uidret)
	*uidret = g_strdup(uid);
      if (obj->object_type == SYNC_OBJECT_TYPE_CALENDAR) 
	res = cal_client_update_object (conn->cal_client, calcomp);
      if (obj->object_type == SYNC_OBJECT_TYPE_TODO) 
	res = cal_client_update_object (conn->todo_client, calcomp);
    }
    modified = (res == CAL_CLIENT_RESULT_SUCCESS);
    icalcomponent_free(icalcomp);
  } else {
    if (obj->uid) {
      if (obj->object_type == SYNC_OBJECT_TYPE_CALENDAR) {
	if (cal_client_remove_object(conn->cal_client, obj->uid) ==
	    CAL_CLIENT_RESULT_SUCCESS)
	  modified = 1;
      }
      if (obj->object_type == SYNC_OBJECT_TYPE_TODO) {
	if (cal_client_remove_object(conn->todo_client, obj->uid) ==
	    CAL_CLIENT_RESULT_SUCCESS)
	  modified = 1;
      }
    }
  }
  return(modified);
}

gboolean evo_cal_modify(gpointer data) {
  evolution_connection *conn = data;
  GList *objects = conn->modify_objects, *results = conn->modify_results;
  while (objects && results) {
    changed_object *object = objects->data;
    syncobj_modify_result *result = results->data;
    if (object->object_type == SYNC_OBJECT_TYPE_CALENDAR ||
	object->object_type == SYNC_OBJECT_TYPE_TODO) {
      if (evo_cal_modify_one(conn, object, &(result->returnuid)))
	result->result = SYNC_MSG_REQDONE;
    }
    objects = objects->next;
    results = results->next;
  }
  if (conn->callback)
    (conn->callback)(NULL, conn); // We have done the modifications
  return(FALSE);
}



void evo_addr_modify_done_cb(gpointer data, evolution_connection *conn) {
  g_idle_add(evo_get_changes, conn); // Remove known changes
 }

void evo_cal_modify_done_cb(gpointer data, evolution_connection *conn) {
  conn->callback = evo_addr_modify_done_cb; // Continue and modify addr
  g_idle_add(evo_addr_modify, conn);
}


void syncobj_modify_list(evolution_connection *conn, GList *changes) {
  GList *node = changes;
  conn->modify_results = NULL;
  while (node) { // Create an empty response list
    changed_object *obj = node->data;
    syncobj_modify_result *result = g_malloc0(sizeof(syncobj_modify_result));
    result->result = SYNC_MSG_REQFAILED;
    conn->modify_results = g_list_append(conn->modify_results, result);
    node = node->next;
  }
  
  conn->modify_objects = changes;
  conn->callback = evo_cal_modify_done_cb;
  conn->modifying = TRUE;
  g_idle_add(evo_cal_modify, conn);
}


typedef struct {
  evolution_connection *conn;
  changed_object *obj;
  time_t origstart;
  GList *recurs;
} cal_get_recurring_args;


// Check if the recur instance is of our UID, then add to the
// recur list.
gboolean cal_recur_instance (CalComponent *comp, time_t instance_start, 
	  time_t instance_end, gpointer data) {
  cal_get_recurring_args *arg = data;
  struct icaltimetype dtstart;
  struct icaltimetype dtend;
  const char *summary;
  char *newsummary;
  char *uid;
  icalcomponent* icalcomp;
  changed_object *recurobj;
  icalcomponent* changedcomp;

  cal_component_get_uid(comp, (const char**) &uid);
  if (!strcmp(uid, arg->obj->uid) && g_list_length(arg->recurs) < 50) {
    if (arg->origstart != instance_start) {
      changed_object *recurobj;
      icalcomp = cal_component_get_icalcomponent (comp);
      changedcomp = icalcomponent_new_clone(icalcomp);
      
      recurobj = g_malloc0(sizeof(changed_object));
      g_assert(recurobj);

      dtstart = icaltime_from_timet(instance_start, 0);
      dtend = icaltime_from_timet(instance_end, 0);
      icalcomponent_set_dtstart(changedcomp, dtstart);
      icalcomponent_set_dtend(changedcomp, dtend);
      recurobj->uid = g_strdup(uid);
      recurobj->change_type = SYNC_OBJ_RECUR;
      recurobj->object_type = object_type_from_component(comp);
      recurobj->removepriority = g_strdup(icaltime_as_ical_string(dtend));
      summary = icalcomponent_get_summary(changedcomp);
      newsummary = g_strdup_printf("%s [Recur]", summary);
      icalcomponent_set_summary(changedcomp, newsummary);
      g_free(newsummary);
      recurobj->comp = g_strdup_printf("BEGIN:VCALENDAR\r\nVERSION:2.0\r\n%s"
				       "END:VCALENDAR\r\n",
				       icalcomponent_as_ical_string(changedcomp));
      arg->recurs = g_list_append(arg->recurs, recurobj);
      icalcomponent_free(changedcomp);
    }
  }
  return(TRUE);
}


gboolean do_cal_get_recurring(gpointer data) {
  cal_get_recurring_args *arg = data;
  icalcomponent *comp;
  time_t recurstart;
  time_t recurend;

  comp = icalcomponent_new_from_string(arg->obj->comp);
  recurstart = icaltime_as_timet(icalcomponent_get_dtstart(comp));
  recurend = recurstart + 3600*24*365*10; // Ten years ahead
  arg->origstart=recurstart;
  
  if (arg->obj->object_type == SYNC_OBJECT_TYPE_CALENDAR) {
    cal_client_generate_instances (arg->conn->cal_client, 
				   CALOBJ_TYPE_EVENT|CALOBJ_TYPE_TODO,
				   recurstart, recurend,
				   cal_recur_instance, data);
  } 
  sync_set_requestdata(arg->recurs,arg->conn->sync_pair);
  icalcomponent_free(comp);
  g_free(arg);
  return(FALSE);
}

// Return the recurances object in obj from dtstart to dtstart+one year
void syncobj_get_recurring(evolution_connection *conn, 
			   changed_object *obj) {
  cal_get_recurring_args *arg;
  
  if (obj->comp &&
      (obj->object_type == SYNC_OBJECT_TYPE_CALENDAR ||
      obj->object_type == SYNC_OBJECT_TYPE_TODO)) {
    char *rrule = sync_get_key_data(obj->comp, "RRULE");
    if (rrule) {
      g_free(rrule);
      arg = g_malloc0(sizeof(cal_get_recurring_args));
      g_assert(arg);
      arg->conn = conn;
      arg->obj = obj;
      g_idle_add(do_cal_get_recurring, arg);
    } else {
      sync_set_requestdata(NULL,conn->sync_pair);
    }
  } else
    sync_set_requestfailed(conn->sync_pair);
  return;
}

// Called by the syncengine after synchronization 
// (if success, we can discard our internal change list)
void sync_done(evolution_connection *conn, gboolean success) {
  if (success) {
    // No need to do it asyncronously
    remove_internal_changes(conn);
  }
  sync_set_requestdone(conn->sync_pair);
}

// Return TRUE if this client does not have to be polled 
// (i.e. can be constantly connected)
gboolean always_connected() {
  return(TRUE);
}

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

char* long_name() {
  return("Ximian Evolution");
}

// Return the types of objects that this client handle
sync_object_type object_types() {
  return(SYNC_OBJECT_TYPE_CALENDAR | SYNC_OBJECT_TYPE_TODO |
	 SYNC_OBJECT_TYPE_PHONEBOOK);
}



void plugin_init(void) {
  int fd[2];
  char version[256] = "";

  g_type_init();
  pipe(fd);
  if (!fork()) {
    dup2(fd[1],1);
    execlp("evolution", "evolution", "--version", NULL);
    close(fd[1]);
    exit(0);
  }
  if (read(fd[0],version,256) > 0) {
    int majver, minver = 0, micver = 0;
    if (sscanf(version,"Gnome evolution %d.%d.%d",&majver, &minver, &micver) 
	>= 2) {
      dd(printf("Detected evolution %d.%d.%d.\n", majver, minver, micver));
      if (majver > 1 || (majver==1 && minver >= 4))
	versionok = TRUE;
      else
	evo_async_display_error("Evolution plugin: This plugin requires Evolution 1.4 or greater.");
    }
  }
}

char* plugin_info(void) {
  return("Plugin for Ximian Evolution.");
}

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