/* 
   MultiSync Backup Plugin - Backup your PIM 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: backup_plugin.c,v 1.9 2003/08/26 20:05:49 lincoln Exp $
 */
#include <stdlib.h>
#include <glib.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <time.h>
#include <unistd.h>
#include <gmodule.h>
#include <gtk/gtkmain.h>
#include <gtk/gtksignal.h>
#include <multisync.h>
#include "backup_plugin.h"
#include "gui.h"

#define BACKUPFILE "backup"

void backup_load_state(backup_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"), BACKUPFILE);
  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, "backupdir")) {
	  conn->backupdir = g_strdup(data);
	}
	if (!strcmp(prop, "rebackupall")) {
	  if (!strcmp(data, "yes"))
	    conn->rebackupall = TRUE;
	  else
	    conn->rebackupall = FALSE;
	}
	if (!strcmp(prop, "harddelete")) {
	  if (!strcmp(data, "yes"))
	    conn->harddelete = TRUE;
	  else
	    conn->harddelete = FALSE;
	}
      }
    }
    fclose(f);
  }
  g_free(filename);
}

void backup_save_state(backup_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"), BACKUPFILE);
  if ((f = fopen(filename, "w"))) {

    if (conn->backupdir)
      fprintf(f, "backupdir = %s\n", conn->backupdir);
    fprintf(f, "rebackupall = %s\n", conn->rebackupall?"yes":"no");
    fprintf(f, "harddelete = %s\n", conn->harddelete?"yes":"no");
    fclose(f);
  }
  g_free(filename);
}

//Return a line of data from a vCARD, vCALENDAR etc. Free the string 
// using g_free().
char* backup_get_entry_data(char *card, char *key) {
  char *pos = card;
  int l = strlen(key);

  while (pos) {
    if (!strncmp(pos, key, l) && (pos[l] == ':' || pos[l] == ';')) {
      char *start, *end;
      start = strstr(pos+l, ":");
      if (start) {
	start++;
	end = strstr(start, "\n");
	if (!end)
	  end = card+strlen(card);
	if (*(end-1) == '\r')
	  end--;
	return(g_strndup(start, end-start));
      }
    }
    pos = strstr(pos, "\n");
    if (pos)
      pos += 1;
  }
  return(NULL);
}

#define BACKUPENTRYFILE "backup_entries"

void backup_load_entries(backup_connection *conn) {
  char *filename;
  FILE *f;
  char line[512];


  /* If the backupdir hasn't been configured, skip this function */
  if(conn->backupdir == NULL) return;

  filename = g_strdup_printf ("%s/%s", conn->backupdir, 
			      BACKUPENTRYFILE);
  if ((f = fopen(filename, "r"))) {
    while (fgets(line, 512, f)) {
      char uid[256];
      int objtype, changetype;
      if (sscanf(line, "%d %d %256s", &objtype, &changetype, uid) >= 3) {
	backup_object *entry = g_malloc(sizeof(backup_object));
	g_assert(entry);
	entry->uid = g_strdup(uid);
	entry->object_type = objtype;
	entry->type = changetype;
	conn->entries = g_list_append(conn->entries, entry);
      }
    }
    fclose(f);
  }
  g_free(filename);
}

void backup_save_entries(backup_connection *conn) {
  char *filename;
  FILE *f;

  filename = g_strdup_printf ("%s/%s", conn->backupdir, 
			      BACKUPENTRYFILE);
  if ((f = fopen(filename, "w"))) {
    GList *l;
    for (l=conn->entries; l; l = l->next) {
      int changetype;
      int objtype;
      backup_object *entry;
      entry = l->data;
      objtype = entry->object_type;
      changetype = entry->type;
      fprintf(f, "%d %d %s\n", objtype, changetype, entry->uid);
    }
    fclose(f);
  }
  g_free(filename);
}

void backup_free_entries(backup_connection *conn) {
  while (conn->entries) {
    GList *entries;
    backup_object *entry;
    
    entries = g_list_first(conn->entries);
    if (entries->data) {
      entry = entries->data;
      if (entry->uid) 
	g_free(entry->uid);
      g_free(entry);
    }
    conn->entries = g_list_remove_link(conn->entries, entries);
  }
}

gboolean backup_do_connect(gpointer data) {
  backup_connection *conn = data;
  struct stat statbuf;

  if (!conn->backupdir) {
    backup_show_msg("Backup plugin: Please set the backup directory\nin the backup options first.");
    goto err;
  }
  
  if (stat(conn->backupdir, &statbuf) == -1) {
    if (mkdir(conn->backupdir, 0700))
      goto err;
  }
  backup_load_entries(conn);
  sync_set_requestdone(conn->sync_pair);
  return(FALSE);
  err:
  sync_set_requestfailed(conn->sync_pair);
  return(FALSE);
}  

backup_connection* sync_connect(sync_pair* handle, connection_type type,
				sync_object_type object_types) {
  backup_connection *conn;
  conn = g_malloc0(sizeof(backup_connection));
  conn->sync_pair = handle;
  conn->conntype = type;
  backup_load_state(conn);
  
  g_idle_add(backup_do_connect, conn);
  return(conn);
}

void backup_free_connection(backup_connection *conn) {
  if (conn) {
    backup_free_entries(conn);
    if (conn->backupdir)
      g_free(conn->backupdir);
    g_free(conn);
  }
}

void sync_disconnect(backup_connection *conn) {
  sync_pair *sync_pair = conn->sync_pair;
  backup_free_connection(conn);
  sync_set_requestdone(sync_pair);
}

typedef struct {
  backup_connection *conn;
  sync_object_type newdbs;
} backup_get_changes_arg;


gboolean backup_do_get_changes(gpointer data) {
  backup_get_changes_arg *arg = data;
  backup_connection *conn = arg->conn;
  int t;
  GList *changes = NULL;
  change_info *chinfo;
  sync_object_type newdbs = arg->newdbs;

  g_free(arg);
  if (newdbs) {
    gboolean ask = FALSE;
    for (t = 0; t < g_list_length(conn->entries); t++) {
      backup_object *entry = g_list_nth_data(conn->entries, t);
      if (newdbs & entry->object_type)
	ask = TRUE;
    }
    if (ask) {
      if (!backup_show_question("One or more of the other side databases\nseem to have been reset.\nWould you like to restore the data from backup?"))
	newdbs = 0;
    }
  }
  for (t = 0; t < g_list_length(conn->entries); t++) {
    backup_object *entry = g_list_nth_data(conn->entries, t);
    if (entry) {
      if (entry->type == BACKUP_TYPE_RESTORE || 
	  (newdbs&entry->object_type && entry->type != BACKUP_TYPE_DELETED)) {
	struct stat statbuf;
	char *fname;
	changed_object *change = g_malloc0(sizeof(changed_object));
	fname = g_strdup_printf("%s/%s", conn->backupdir, entry->uid);
	if (!stat(fname, &statbuf)) {
	  FILE *f = NULL;
	  change->comp = g_malloc0(statbuf.st_size+1);
	  if ((f = fopen(fname, "r"))) {
	    fread(change->comp, 1, statbuf.st_size, f);
	    fclose(f);
	  }
	  change->uid = g_strdup(entry->uid);
	  change->change_type = SYNC_OBJ_MODIFIED;
	  change->object_type = entry->object_type;
	  changes = g_list_append(changes, change);
	}
	g_free(fname);
      }
    }
  }
  chinfo = g_malloc0(sizeof(change_info));
  chinfo->changes = changes;
  chinfo->newdbs = 0;
  sync_set_requestdata(chinfo, conn->sync_pair);
  return(FALSE);
}

void get_changes(backup_connection* conn, sync_object_type newdbs) { 
  backup_get_changes_arg *arg;

  if (conn->rebackupall) {
    change_info *chinfo = g_malloc0(sizeof(change_info));
    chinfo->newdbs = SYNC_OBJECT_TYPE_ANY;
    sync_set_requestdata(chinfo, conn->sync_pair);
    backup_free_entries(conn);
    backup_save_state(conn);
    return;
  }
  arg = g_malloc0(sizeof(backup_get_changes_arg));
  arg->conn = conn;
  arg->newdbs = newdbs;
  g_idle_add(backup_do_get_changes, arg);
}


void sync_done(backup_connection *conn, gboolean success) {
  if (success) {
    int t;
    for (t = 0; t < g_list_length(conn->entries); t++) {
      backup_object *entry = g_list_nth_data(conn->entries, t);
      if (entry && (entry->type == BACKUP_TYPE_RESTORE ||
		    entry->type == BACKUP_TYPE_REBACKUP))
	entry->type = BACKUP_TYPE_SAVED;
    }
    if (conn->rebackupall) {
      conn->rebackupall = FALSE;
      backup_save_state(conn);
    }
    backup_save_entries(conn);
  }
  sync_set_requestdone(conn->sync_pair);
}
 

void backup_hard_delete(backup_connection *conn, backup_object *entry) {
  char *fname;
  if (entry) {
    conn->entries = g_list_remove(conn->entries, entry);
    fname = g_strdup_printf("%s/%s", conn->backupdir, entry->uid);
    unlink(fname);
    g_free(fname);
    if (entry->uid)
      g_free(entry->uid);
    g_free(entry);
  }
}

void backup_modify_or_delete(backup_connection *conn, 
			     char *comp, 
			     char *uid, sync_object_type objtype, 
			     char *uidret, int *uidretlen,
			     gboolean softdelete) {
  char *luid = NULL;
  backup_object *entry = NULL;
  int t;

  if (!uid && !comp) {
    sync_set_requestfailed(conn->sync_pair);
    return;
  }
  if (uid)
    luid = g_strdup(uid);
  if (!luid) {
    // Generate a UID
    int count = 0;
    while (!luid) {
      char *fname;
      struct stat statbuf;
      luid = g_strdup_printf("multisync%d-%d", (int) time(NULL), count);
      fname = g_strdup_printf("%s/%s", conn->backupdir, luid);
      if (!stat(fname, &statbuf)) {
	count++;
	g_free(luid);
	luid = NULL;
      }
      g_free(fname);
    }
  }
  for (t = 0; t < g_list_length(conn->entries); t++) {
    backup_object *e;
    e = g_list_nth_data(conn->entries, t);
    if (e->uid && !strcmp(e->uid, luid)) {
      entry = e;
    }
  }
  if (!entry && uid) {
    // Non-existing UID given
    sync_set_requestfailed(conn->sync_pair);
    return;
  }
  if (!entry) {
    // If no entry is found, add it
    entry = g_malloc0(sizeof(backup_object));
    entry->uid = g_strdup(luid);
    conn->entries = g_list_append(conn->entries, entry);
  }
  entry->object_type = objtype;
  
  if (comp)
    entry->type = BACKUP_TYPE_SAVED;
  else {
    entry->type = BACKUP_TYPE_DELETED;
  }
  if (conn->harddelete && !comp)
    backup_hard_delete(conn, entry);
  backup_save_entries(conn);

  if (comp) {
    // Save the data
    char *fname;
    FILE *f;
    fname = g_strdup_printf("%s/%s", conn->backupdir, luid);
    if ((f = fopen(fname, "w"))) {
      fputs(comp, f);
      fclose(f);
    }
    g_free(fname);
  }
  if (!uid && uidret) {
    // Return our generated LUID
    strncpy(uidret, luid, *uidretlen);
    *uidretlen = strlen(luid);
  }
  g_free(luid);
  sync_set_requestdone(conn->sync_pair);
}

void syncobj_modify(backup_connection *conn, 
		    char *comp, 
		    char *uid, sync_object_type objtype, 
		    char *uidret, int *uidretlen) {
  backup_modify_or_delete(conn, comp, uid, objtype, uidret, uidretlen, FALSE);
}

void syncobj_delete(backup_connection *conn, char *uid, 
		    sync_object_type objtype, int softdelete) {
  backup_modify_or_delete(conn, NULL, uid, objtype, NULL, NULL, softdelete);
}

// 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("backup-plugin");
}

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

// Return the types of objects that this client handle
sync_object_type object_types() {
  return(SYNC_OBJECT_TYPE_ANY);
}



void plugin_init(void) {
}

char* plugin_info(void) {
  return("This plugin keeps a backup of your data. You can manage the stored entries is the plugin options (above).");
}

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