/* 
   MultiSync SyncML plugin - Implementation of SyncML 1.1 and 1.0
   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;

   In addition, as a special exception, Bo Lincoln <lincoln@lysator.liu.se>
   gives permission to link the code of this program with
   the OpenSSL library (or with modified versions of OpenSSL that use the
   same license as OpenSSL), and distribute linked combinations including
   the two.  You must obey the GNU General Public License in all
   respects for all of the code used other than OpenSSL.  If you modify
   this file, you may extend this exception to your version of the
   file, but you are not obligated to do so.  If you do not wish to
   do so, delete this exception statement from your version.

   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: syncml_plugin.c,v 1.40 2004/04/06 09:47:22 lincoln Exp $
 */

#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <multisync.h>
#include "syncml_plugin.h"
#include "syncml_engine.h"
#include "syncml_cmd.h"
#include "gui.h"

extern gboolean multisync_debug;

void syncml_changes_received(syncml_state *state, syncml_connection* conn, 
			     GList *changes, gboolean final, 
			     sync_object_type newdbs) {
  GList *change = changes;
  GList *results = NULL;
  
  while (change) {
    syncobj_modify_result *result;
    syncml_changed_object *obj = change->data;

    if (state->isserver) {
      result = g_malloc0(sizeof(syncobj_modify_result));
      result->result = SYNC_MSG_REQDONE;
      results = g_list_append(results, result);
    }

    // General shape-up
    if (obj->change.comp) {
      char *tmp = 
	sync_vtype_convert(obj->change.comp, 
			   (obj->datatype==SYNCML_DATA_TYPE_VCALENDAR1?VOPTION_CALENDAR1TO2:0)|
			   (obj->datatype==SYNCML_DATA_TYPE_VCARD21?VOPTION_ADDUTF8CHARSET:0)|
			   (conn->removeutc?VOPTION_REMOVEUTC:VOPTION_CONVERTUTC),
			   NULL);
      g_free(obj->change.comp);
      obj->change.comp = tmp;
    }
    
    change = change->next;
  }
  if (state->isserver) {

    syncml_cmd_send_changes_result(state, results);
    conn->changelist = g_list_concat(conn->changelist, changes);
    conn->newdbs = newdbs;
    if (final) {
      if (conn->mode == SYNCML_PLUGIN_MODE_IDLE) {
	// Tell MultiSync we have changes
	conn->mode = SYNCML_PLUGIN_SERVER_MODE_GOTCHANGES;
	sync_object_changed(conn->sync_pair);
	dd(printf("SyncML:  Told sync engine to get changes.\n"));
      }
      if (conn->mode == SYNCML_PLUGIN_SERVER_MODE_SYNCNOTIFIED) {
	// We are still within the get_changes() call
	change_info *info = g_malloc0(sizeof(change_info));
	info->changes = conn->changelist;
	info->newdbs = newdbs;
	conn->changelist = NULL;
	conn->mode = SYNCML_PLUGIN_MODE_IDLE;
	sync_set_requestdata(info, conn->sync_pair);
	dd(printf("SyncML:  Sent changes to sync engine.\n"));
      }
    }
  } else {
    // If we are client, send changes to other plugin directly
    dd(printf("SyncML:  Client: Sending %d modifications to sync engine.\n", 
	   g_list_length(changes)));
    sync_feedthrough_modify(conn->sync_pair, conn->conntype, changes);
    
  }
}

// Syncengine returns our change results
void resp_modify(syncml_connection *conn, int respcode, GList *results) {
  dd(printf("SyncML:  Client: Got modification results.\n"));
  syncml_cmd_send_changes_result(conn->state, results);
  sync_set_requestdone(conn->sync_pair);
}

void syncml_changes_results_received(syncml_state *state,
				     syncml_connection *conn, 
				     GList *results) {
  dd(printf("SyncML:   *** Got change results! *** \n"));
  if (state->isserver) {
    // Send modification results back to sync engine
    sync_set_requestdata(results, conn->sync_pair);
  } else {
    sync_free_modify_results(results);
  }
}

void syncml_sync_done_received(syncml_state *state, syncml_connection* conn) {
  dd(printf("SyncML:   *** Got sync done!*** \n"));
  if (!state->isserver) {
    conn->mode = SYNCML_PLUGIN_MODE_IDLE;
    sync_feedthrough_syncdone(conn->sync_pair, conn->conntype, TRUE);
  }
}

char* syncml_error_to_str(syncml_error_type err) {
  switch (err) {
  case SYNCML_ERROR_TIMEOUT:
    return("SyncML timeout.");
  case SYNCML_ERROR_CONNECTIONFAILED:
    return("Connection to server failed.");
  case SYNCML_ERROR_NOPORT:
    return("Could not open port.");
    break;
  case SYNCML_ERROR_OTHERAUTHFAILED:
  case SYNCML_ERROR_MYAUTHFAILED:
    return("Authentication failed.");
  default:
    return("Unknown error.");
  }
}

// Called by the SyncML engine when something went wrong 
void syncml_error(syncml_state *state, syncml_connection *conn,
		  syncml_error_type err) {
  switch (conn->mode) {
  case SYNCML_PLUGIN_SERVER_MODE_GOTCHANGES:
    sync_free_changes(conn->changelist);
    conn->changelist = NULL;
    break;
  case SYNCML_PLUGIN_MODE_GET_DEVINFO:
    syncml_gui_devinfo_received(NULL, syncml_error_to_str(err));
    break;
  case SYNCML_PLUGIN_SERVER_MODE_SENTMODIFY:
  case SYNCML_PLUGIN_SERVER_MODE_SYNCNOTIFIED:
    switch (err) {
    case SYNCML_ERROR_TIMEOUT:
    case SYNCML_ERROR_CONNECTIONFAILED:
      sync_set_requestmsg(SYNC_MSG_CONNECTIONERROR,conn->sync_pair);
      break;
    case SYNCML_ERROR_NOPORT:
      sync_set_requestfailederror("Could not open port.", conn->sync_pair);
      break;
    case SYNCML_ERROR_OTHERAUTHFAILED:
    case SYNCML_ERROR_MYAUTHFAILED:
      sync_set_requestmsgerror(SYNC_MSG_NORIGHTSERROR,
			       "Authentication failed.", 
			       conn->sync_pair);
    default:
      sync_set_requestfailed(conn->sync_pair);
    }
    break;
  case SYNCML_PLUGIN_CLIENT_MODE_SYNC:
    sync_feedthrough_syncdone(conn->sync_pair, conn->conntype, FALSE);
    sync_log(conn->sync_pair, "Could not connect to SyncML server.", SYNC_LOG_ERROR);
    break;
  default:
    // We are idle, but should report some events anyway
    switch (err) {
    case SYNCML_ERROR_NOPORT:
      sync_log(conn->sync_pair, "Could not open server port.", SYNC_LOG_ERROR);
      break;
    case SYNCML_ERROR_OTHERAUTHFAILED:
    case SYNCML_ERROR_MYAUTHFAILED:
      sync_log(conn->sync_pair, "Authentication failed.", SYNC_LOG_ERROR);
    default:
      break;
    }
    break;
  }
  conn->mode = SYNCML_PLUGIN_MODE_IDLE;
  dd(printf("SyncML:  SyncML server: Got error %d.\n", err));
}

void syncml_info(syncml_state *state, syncml_connection *conn, char *txt) {
  sync_log(conn->sync_pair, txt, SYNC_LOG_SUCCESS);
}

// The server has sent us a "please synchronize" command
void syncml_sync_serverinit_received(syncml_state *state, 
				     syncml_connection *conn) {
  if (!conn->isserver) {
    // FIXME: Newdbs?
    dd(printf("SyncML:  Got change notification from server. Getting changes.\n"));
    conn->mode = SYNCML_PLUGIN_CLIENT_MODE_SYNC;
    sync_feedthrough_get_changes(conn->sync_pair, conn->conntype, 0);
  }
}


// Called by sync engine when a change has been detected
void resp_objchanged(syncml_connection *conn) {
  if (!conn->isserver && conn->mode == SYNCML_PLUGIN_MODE_IDLE) {
    dd(printf("SyncML:  Got change notification. Getting changes.\n"));
    conn->mode = SYNCML_PLUGIN_CLIENT_MODE_SYNC;
    sync_feedthrough_get_changes(conn->sync_pair, conn->conntype, 0);
  } else {
    dd(printf("SyncML:  Did not get changes, as mode = %d\n", conn->mode));
  }
  sync_set_requestdone(conn->sync_pair);
}

// Get changes from client again
void syncml_reget_changes(syncml_state *state, syncml_connection *conn,
			  sync_object_type newdbs) {
  dd(printf("SyncML:  Getting changes again.\n"));
  conn->mode = SYNCML_PLUGIN_CLIENT_MODE_SYNC;
  sync_feedthrough_get_changes(conn->sync_pair, conn->conntype, newdbs);
}

GList *syncml_convert_copy_change_list(syncml_connection *conn,
				       GList *orig) {
  GList *change_copy = NULL;
  while (orig) {
    char *tmp;
    sync_voption vopts = 0;
    syncml_data_type type = SYNCML_DATA_TYPE_UNKNOWN;
    changed_object *chobj = sync_copy_changed_object(orig->data);
    syncml_changed_object *obj = g_malloc0(sizeof(syncml_changed_object));
    memcpy(obj, chobj, sizeof(changed_object));
    g_free(chobj);
    
    switch (obj->change.object_type) {
    case SYNC_OBJECT_TYPE_CALENDAR:
    case SYNC_OBJECT_TYPE_TODO:
      type = syncml_get_db_datatype(conn, obj->change.object_type, 
				    FALSE, SYNCML_DATA_TYPE_VCALENDAR2);
      if (type == SYNCML_DATA_TYPE_VCALENDAR1 && obj->change.comp) {
	// We have to convert to vCALENDAR 1.0
	vopts |= VOPTION_CALENDAR2TO1 | VOPTION_CONVERTALLDAYEVENT;
	
      }
      vopts |= VOPTION_ADDUTF8CHARSET;
      break;
    case SYNC_OBJECT_TYPE_PHONEBOOK:
      type = syncml_get_db_datatype(conn, obj->change.object_type, 
				    FALSE, SYNCML_DATA_TYPE_VCARD21);
      vopts |= VOPTION_ADDUTF8CHARSET;
      break;
    default:
      break;
    }
    if (vopts) {
      char *tmp = sync_vtype_convert(obj->change.comp, vopts, NULL);
      g_free(obj->change.comp);
      obj->change.comp = tmp;
    }
    obj->datatype = type;
    change_copy = g_list_append(change_copy, obj);
    orig = orig->next;
  }
  return(change_copy);
}


// Called by syncengine when the change list is returned
void resp_get_changes(syncml_connection *conn, int respcode,
		      change_info *changes) {
  change_info *info_copy = g_malloc0(sizeof(change_info));
  GList *change_list = changes->changes;
  if (respcode >=0) {
    // First copy the change list as it will be freed by the sync engine
    info_copy->changes = syncml_convert_copy_change_list(conn, change_list);
    info_copy->newdbs = changes->newdbs;
    syncml_cmd_send_changes(conn->state, info_copy);
  }
  sync_set_requestdone(conn->sync_pair);
}

// If a server, return the changes from the client which we
// already obtained from syncml_changes_received()
void get_changes(syncml_connection* conn, sync_object_type newdbs) {
  change_info *info;
  if (!conn->isserver) {
    sync_set_requestfailed(conn->sync_pair);
    return;
  } 
  dd(printf("SyncML:  Get changes, mode %d\n", conn->mode));
  if (conn->mode == SYNCML_PLUGIN_SERVER_MODE_GOTCHANGES) {
    info = g_malloc0(sizeof(change_info));
    info->changes = conn->changelist;
    info->newdbs = conn->newdbs;
    conn->changelist = NULL;
    conn->mode = SYNCML_PLUGIN_SERVER_MODE_WAITING_FOR_MODIFY;
    sync_set_requestdata(info, conn->sync_pair);
    dd(printf("SyncML:  Returned changes.\n"));
  } else if (conn->mode == SYNCML_PLUGIN_MODE_IDLE) {
    // Send "please sync" to client, but only if MultiSync 
    // (doesn't work otherwise anyway)
    if (syncml_is_partner_multisync(conn->state)) {
      conn->mode = SYNCML_PLUGIN_SERVER_MODE_SYNCNOTIFIED;
      syncml_cmd_send_sync_serverinit(conn->state, newdbs);
    } else
      sync_set_requestfailed(conn->sync_pair);
  } else
    sync_set_requestfailed(conn->sync_pair);
}

// Called when synchronization is done
void sync_done(syncml_connection *conn, gboolean success) {
  dd(printf("SyncML:  SyncML: Got sync done from syncengine.\n"));
  if (conn->isserver) {
    conn->mode = SYNCML_PLUGIN_MODE_IDLE;
    syncml_cmd_send_sync_done(conn->state);
  }
  sync_set_requestdone(conn->sync_pair);
}



// Called by sync engine to make modifications
void syncobj_modify_list(syncml_connection *conn, GList *mods) {
  GList *change_copy = NULL;
  GList *change_list = mods;
  change_info *info = g_malloc0(sizeof(change_info));
  dd(printf("SyncML:  Got modifications (%d of them).\n", g_list_length(mods)));
  // Send modification list
  if (!conn->isserver) {
    sync_set_requestfailed(conn->sync_pair);  
    return;
  }
  // First copy the change list as it will be freed by the sync engine
  change_copy = syncml_convert_copy_change_list(conn, change_list);
  
  conn->mode = SYNCML_PLUGIN_SERVER_MODE_SENTMODIFY;
  dd(printf("SyncML:  Sending modify.\n"));
  info->changes = change_copy;
  info->newdbs = 0;
  syncml_cmd_send_changes(conn->state, info);
}

void syncml_get_devinfo(syncml_connection *conn){
  conn->mode = SYNCML_PLUGIN_MODE_GET_DEVINFO;
  syncml_cmd_get_devinfo(conn->state);
  
}


void syncml_devinfo_received(syncml_state *state,
			     syncml_connection *conn, 
			     syncml_devinfo *devinfo) {
  dd(printf("SyncML:  Received device info.\n"));
  if (conn->devinfo)
    syncml_free_devinfo(conn->devinfo);
  conn->devinfo = devinfo;
  if (conn->mode == SYNCML_PLUGIN_MODE_GET_DEVINFO) {
    syncml_gui_devinfo_received(devinfo, NULL);
    conn->mode = SYNCML_PLUGIN_MODE_IDLE;
  }
}



// Return a datatype which the remote db supports. If it supports pref,
// then retunr pref, otherwise whatever is in "Rx-Pref".
syncml_data_type syncml_get_db_datatype(syncml_connection *conn,
					sync_object_type objtype,
					gboolean tx, 
					syncml_data_type pref) {
  if (conn->devinfo) {
    GList *stores = conn->devinfo->datastores;
    while (stores) {
      syncml_datastore *store = stores->data;
      GList *types;
      if (tx)
	types = store->tx;
      else
	types = store->rx;
      while (types) {
	syncml_data_type type = (syncml_data_type) types->data;
	if (syncml_data_type_to_objtype(type) & objtype) {
	  if (type == pref)
	    return(pref);
	}
	types = types->next;
      }
      if (tx && (syncml_data_type_to_objtype(store->txpref) & objtype))
	return(store->txpref);
      if (!tx && (syncml_data_type_to_objtype(store->rxpref) & objtype))
	return(store->rxpref);
      stores = stores->next;
    }
  }
  return(pref);
}


// Just a wrapper
char *syncml_get_datapath(syncml_connection *conn) {
  return(sync_get_datapath(conn->sync_pair));
}


#define SYNCMLFILE "syncml"
#define SYNCMLENGINEFILE "syncmlengine"

void syncml_load_state(syncml_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"), SYNCMLFILE);
  conn->isserver = TRUE;
  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, "isserver")) {
	  if (!strcmp(data, "yes"))
	    conn->isserver = TRUE;
	  else
	    conn->isserver = FALSE;
	}
	if (!strcmp(prop, "serverURI"))
	  conn->serverURI = g_strdup(data);
	if (!strcmp(prop, "login"))
	  conn->login = g_strdup(data);
	if (!strcmp(prop, "passwd"))
	  conn->passwd = g_strdup(data);
	if (!strcmp(prop, "othercalendardb"))
	  conn->othercalendardb = g_strdup(data);
	if (!strcmp(prop, "otherphonebookdb"))
	  conn->otherphonebookdb = g_strdup(data);
	if (!strcmp(prop, "removeutc")) {
	  if (!strcmp(data, "yes"))
	    conn->removeutc = TRUE;
	  else
	    conn->removeutc = FALSE;
	}
      }
    }
    fclose(f);
  }
  g_free(filename);
}



void syncml_save_state(syncml_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"), SYNCMLFILE);
  if ((f = fopen(filename, "w"))) {
    fprintf(f, "isserver = %s\n", conn->isserver?"yes":"no");
    if (conn->serverURI)
      fprintf(f, "serverURI = %s\n", conn->serverURI);
    if (conn->login)
      fprintf(f, "login = %s\n", conn->login);
    if (conn->passwd)
      fprintf(f, "passwd = %s\n", conn->passwd);
    if (conn->othercalendardb)
      fprintf(f, "othercalendardb = %s\n", conn->othercalendardb);
    if (conn->otherphonebookdb)
      fprintf(f, "otherphonebookdb = %s\n", conn->otherphonebookdb);
    fprintf(f, "removeutc = %s\n", conn->removeutc?"yes":"no");
    fclose(f);
  }
  g_free(filename);
}

gboolean syncml_start_syncml_engine(syncml_connection *conn) {
  char *filename;
  filename = g_strdup_printf("%s/%s%s", sync_get_datapath(conn->sync_pair),
			     (conn->conntype==CONNECTION_TYPE_LOCAL?"local":"remote"), SYNCMLENGINEFILE);
  
  conn->state = syncml_create(conn->isserver, conn->serverURI, filename, conn);
  g_free(filename);
  if (!conn->state)
    return FALSE;
  if (conn->isserver)
    conn->commondata.is_feedthrough = FALSE;
  else
    conn->commondata.is_feedthrough = TRUE;
  syncml_set_login(conn->state, conn->login, conn->passwd);
  if (conn->commondata.object_types & SYNC_OBJECT_TYPE_PHONEBOOK) {
    syncml_add_db(conn->state, "addressbook", SYNC_OBJECT_TYPE_PHONEBOOK);
    if (!conn->isserver)
      syncml_add_remote_db(conn->state, "addressbook", conn->otherphonebookdb);
  }
  if (conn->commondata.object_types & 
      (SYNC_OBJECT_TYPE_CALENDAR|SYNC_OBJECT_TYPE_TODO)) {
    syncml_add_db(conn->state, "calendar", 
		  SYNC_OBJECT_TYPE_CALENDAR|SYNC_OBJECT_TYPE_TODO);  
    if (!conn->isserver)
      syncml_add_remote_db(conn->state, "calendar", conn->othercalendardb);
  }

  return(TRUE);
}

void syncml_stop_syncml_engine(syncml_connection *conn) {
  if (conn->state) {
    syncml_save_engine_state(conn->state);
    syncml_cmd_quit(conn->state);
    conn->state = NULL;
  }
}

syncml_connection* sync_connect(sync_pair* handle, connection_type type,
				sync_object_type object_types) {
  syncml_connection *conn;
  conn = g_malloc0(sizeof(syncml_connection));
  conn->sync_pair = handle;
  conn->conntype = type;
  conn->commondata.object_types = object_types;

  syncml_load_state(conn);
  if (!conn->othercalendardb)
    conn->othercalendardb = g_strdup("calendar");
  if (!conn->otherphonebookdb)
    conn->otherphonebookdb = g_strdup("addressbook");
  if (!syncml_start_syncml_engine(conn))
    goto err;

  sync_set_requestdone(conn->sync_pair);
  return(conn);
  
 err:
  g_free(conn);
  sync_set_requestfailed(conn->sync_pair);
  return(NULL);
}

void syncml_free_connection(syncml_connection *conn) {
  SYNCML_FREE_STRING(conn->serverURI);
  SYNCML_FREE_STRING(conn->login);
  SYNCML_FREE_STRING(conn->passwd);
  SYNCML_FREE_STRING(conn->othercalendardb);
  SYNCML_FREE_STRING(conn->otherphonebookdb);

  if (conn->changelist)
    sync_free_changes(conn->changelist);
  if (conn->devinfo)
    syncml_free_devinfo(conn->devinfo);

  g_free(conn);
}

void sync_disconnect(syncml_connection *conn) {
  syncml_engine_cmd cmd;
  sync_pair *pair = conn->sync_pair;

  syncml_stop_syncml_engine(conn);
  syncml_free_connection(conn);
  
  sync_set_requestdone(pair);
}

gboolean always_connected() {
  return(TRUE);
}

char* short_name() {
  return("syncml-plugin");
}

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

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

void plugin_init(void) {
  syncml_init();
}


char* plugin_info(void) {
  return("The SyncML plugin works as either a server or a client. It "
	 "can connect to "
	 "any SyncML-compliant device, or be used to connect two MultiSync's "
	 "over the internet.");
}

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