/*
 *
 *  OBEX Server
 *
 *  Copyright (C) 2007-2010  Nokia Corporation
 *  Copyright (C) 2007-2010  Marcel Holtmann <marcel@holtmann.org>
 *
 *
 *  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
 *  along with this program; if not, write to the Free Software
 *  Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
 *
 */

#ifdef HAVE_CONFIG_H
#include <config.h>
#endif

#include <fcntl.h>
#include <stdio.h>
#include <errno.h>
#include <stdlib.h>
#include <string.h>
#include <dirent.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
#include <inttypes.h>

#include <glib.h>
#include <gdbus/gdbus.h>

#include "obexd.h"
#include "plugin.h"
#include "log.h"
#include "obex.h"
#include "mimetype.h"
#include "service.h"
#include "ftp.h"

#define PCSUITE_CHANNEL 24
#define PCSUITE_WHO_SIZE 8

#define PCSUITE_RECORD "<?xml version=\"1.0\" encoding=\"UTF-8\" ?>	\
<record>								\
  <attribute id=\"0x0001\">						\
    <sequence>								\
      <uuid value=\"00005005-0000-1000-8000-0002ee000001\"/>		\
    </sequence>								\
  </attribute>								\
									\
  <attribute id=\"0x0004\">						\
    <sequence>								\
      <sequence>							\
        <uuid value=\"0x0100\"/>					\
      </sequence>							\
      <sequence>							\
        <uuid value=\"0x0003\"/>					\
        <uint8 value=\"%u\" name=\"channel\"/>				\
      </sequence>							\
      <sequence>							\
        <uuid value=\"0x0008\"/>					\
      </sequence>							\
    </sequence>								\
  </attribute>								\
									\
  <attribute id=\"0x0005\">						\
    <sequence>								\
      <uuid value=\"0x1002\"/>						\
    </sequence>								\
  </attribute>								\
									\
  <attribute id=\"0x0009\">						\
    <sequence>								\
      <sequence>							\
        <uuid value=\"00005005-0000-1000-8000-0002ee000001\"/>		\
        <uint16 value=\"0x0100\" name=\"version\"/>			\
      </sequence>							\
    </sequence>								\
  </attribute>								\
									\
  <attribute id=\"0x0100\">						\
    <text value=\"%s\" name=\"name\"/>					\
  </attribute>								\
</record>"

#define BACKUP_BUS_NAME		"com.nokia.backup.plugin"
#define BACKUP_PATH		"/com/nokia/backup"
#define BACKUP_PLUGIN_INTERFACE	"com.nokia.backup.plugin"
#define BACKUP_DBUS_TIMEOUT	(1000 * 60 * 15)

static const uint8_t FTP_TARGET[TARGET_SIZE] = {
			0xF9, 0xEC, 0x7B, 0xC4, 0x95, 0x3C, 0x11, 0xD2,
			0x98, 0x4E, 0x52, 0x54, 0x00, 0xDC, 0x9E, 0x09 };

static const uint8_t PCSUITE_WHO[PCSUITE_WHO_SIZE] = {
			'P', 'C', ' ', 'S', 'u', 'i', 't', 'e' };

struct pcsuite_session {
	struct ftp_session *ftp;
	char *lock_file;
	int fd;
};

static void *pcsuite_connect(struct obex_session *os, int *err)
{
	struct pcsuite_session *pcsuite;
	struct ftp_session *ftp;
	int fd;
	char *filename;

	DBG("");

	ftp = ftp_connect(os, err);
	if (ftp == NULL)
		return NULL;

	filename = g_build_filename(g_get_home_dir(), ".pcsuite", NULL);

	fd = open(filename, O_WRONLY | O_CREAT | O_EXCL, 0644);
	if (fd < 0 && errno != EEXIST) {
		error("open(%s): %s(%d)", filename, strerror(errno), errno);
		goto fail;
	}

	/* Try to remove the file before retrying since it could be
	   that some process left/crash without removing it */
	if (fd < 0) {
		if (remove(filename) < 0) {
			error("remove(%s): %s(%d)", filename, strerror(errno),
									errno);
			goto fail;
		}

		fd = open(filename, O_WRONLY | O_CREAT | O_EXCL, 0644);
		if (fd < 0) {
			error("open(%s): %s(%d)", filename, strerror(errno),
									errno);
			goto fail;
		}
	}

	DBG("%s created", filename);

	pcsuite = g_new0(struct pcsuite_session, 1);
	pcsuite->ftp = ftp;
	pcsuite->lock_file = filename;
	pcsuite->fd = fd;

	DBG("session %p created", pcsuite);

	if (err)
		*err = 0;

	return pcsuite;

fail:
	if (ftp)
		ftp_disconnect(os, ftp);
	if (err)
		*err = -errno;

	g_free(filename);

	return NULL;
}

static int pcsuite_get(struct obex_session *os, void *user_data)
{
	struct pcsuite_session *pcsuite = user_data;

	DBG("%p", pcsuite);

	return ftp_get(os, pcsuite->ftp);
}

static int pcsuite_chkput(struct obex_session *os, void *user_data)
{
	struct pcsuite_session *pcsuite = user_data;

	DBG("%p", pcsuite);

	return ftp_chkput(os, pcsuite->ftp);
}

static int pcsuite_put(struct obex_session *os, void *user_data)
{
	struct pcsuite_session *pcsuite = user_data;

	DBG("%p", pcsuite);

	return ftp_put(os, pcsuite->ftp);
}

static int pcsuite_setpath(struct obex_session *os, void *user_data)
{
	struct pcsuite_session *pcsuite = user_data;

	DBG("%p", pcsuite);

	return ftp_setpath(os, pcsuite->ftp);
}

static int pcsuite_action(struct obex_session *os, void *user_data)
{
	struct pcsuite_session *pcsuite = user_data;

	DBG("%p", pcsuite);

	return ftp_action(os, pcsuite->ftp);
}

static void pcsuite_disconnect(struct obex_session *os, void *user_data)
{
	struct pcsuite_session *pcsuite = user_data;

	DBG("%p", pcsuite);

	if (pcsuite->fd >= 0)
		close(pcsuite->fd);

	if (pcsuite->lock_file) {
		remove(pcsuite->lock_file);
		g_free(pcsuite->lock_file);
	}

	if (pcsuite->ftp)
		ftp_disconnect(os, pcsuite->ftp);

	g_free(pcsuite);
}

static struct obex_service_driver pcsuite = {
	.name = "Nokia OBEX PC Suite Services",
	.service = OBEX_PCSUITE,
	.channel = PCSUITE_CHANNEL,
	.secure = TRUE,
	.record = PCSUITE_RECORD,
	.target = FTP_TARGET,
	.target_size = TARGET_SIZE,
	.who = PCSUITE_WHO,
	.who_size = PCSUITE_WHO_SIZE,
	.connect = pcsuite_connect,
	.get = pcsuite_get,
	.put = pcsuite_put,
	.chkput = pcsuite_chkput,
	.setpath = pcsuite_setpath,
	.action = pcsuite_action,
	.disconnect = pcsuite_disconnect
};

struct backup_object {
	char *cmd;
	int fd;
	int oflag;
	int error_code;
	mode_t mode;
	DBusPendingCall *pending_call;
	DBusConnection *conn;
};

static void on_backup_dbus_notify(DBusPendingCall *pending_call,
					void *user_data)
{
	struct backup_object *obj = user_data;
	DBusMessage *reply;
	const char *filename;
	int error_code;

	DBG("Notification received for pending call - %s", obj->cmd);

	reply = dbus_pending_call_steal_reply(pending_call);

	if (reply && dbus_message_get_args(reply, NULL, DBUS_TYPE_INT32,
					&error_code, DBUS_TYPE_STRING,
					&filename, DBUS_TYPE_INVALID)) {

		obj->error_code = error_code;

		if (filename) {
			DBG("Notification - file path = %s, error_code = %d",
					filename, error_code);
			if (error_code == 0)
				obj->fd = open(filename,obj->oflag,obj->mode);
		}

	} else
		DBG("Notification timed out or connection got closed");

	if (reply)
		dbus_message_unref(reply);

	dbus_pending_call_unref(pending_call);
	obj->pending_call = NULL;
	dbus_connection_unref(obj->conn);
	obj->conn = NULL;

	if (obj->fd >= 0) {
		DBG("File opened, setting io flags, cmd = %s",
				obj->cmd);
		if (obj->oflag == O_RDONLY)
			obex_object_set_io_flags(user_data, G_IO_IN, 0);
		else
			obex_object_set_io_flags(user_data, G_IO_OUT, 0);
	} else {
		DBG("File open error, setting io error, cmd = %s",
				obj->cmd);
		obex_object_set_io_flags(user_data, G_IO_ERR, -EPERM);
	}
}

static gboolean send_backup_dbus_message(const char *oper,
					struct backup_object *obj,
					size_t *size)
{
	DBusConnection *conn;
	DBusMessage *msg;
	DBusPendingCall *pending_call;
	gboolean ret = FALSE;
	dbus_uint32_t file_size;

	file_size = size ? *size : 0;

	conn = g_dbus_setup_bus(DBUS_BUS_SESSION, NULL, NULL);

	if (conn == NULL)
		return FALSE;

	msg = dbus_message_new_method_call(BACKUP_BUS_NAME, BACKUP_PATH,
						BACKUP_PLUGIN_INTERFACE,
						"request");
	if (msg == NULL) {
		dbus_connection_unref(conn);
		return FALSE;
	}

	dbus_message_append_args(msg, DBUS_TYPE_STRING, &oper,
					DBUS_TYPE_STRING, &obj->cmd,
					DBUS_TYPE_INT32, &file_size,
					DBUS_TYPE_INVALID);

	if (strcmp(oper, "open") == 0) {
		ret = g_dbus_send_message_with_reply(conn, msg, &pending_call,
							BACKUP_DBUS_TIMEOUT);
		dbus_message_unref(msg);
		if (ret) {
			obj->conn = conn;
			obj->pending_call = pending_call;
			ret = dbus_pending_call_set_notify(pending_call,
							on_backup_dbus_notify,
							obj, NULL);
		} else
			dbus_connection_unref(conn);
	} else {
		g_dbus_send_message(conn, msg);
		dbus_connection_unref(conn);
	}

	return ret;
}

static void *backup_open(const char *name, int oflag, mode_t mode,
				void *context, size_t *size, int *err)
{
	struct backup_object *obj = g_new0(struct backup_object, 1);

	DBG("cmd = %s", name);

	obj->cmd = g_path_get_basename(name);
	obj->oflag = oflag;
	obj->mode = mode;
	obj->fd = -1;
	obj->pending_call = NULL;
	obj->conn = NULL;
	obj->error_code = 0;

	if (send_backup_dbus_message("open", obj, size) == FALSE) {
		g_free(obj);
		obj = NULL;
	}

	if (err)
		*err = 0;

	return obj;
}

static int backup_close(void *object)
{
	struct backup_object *obj = object;
	size_t size = 0;

	DBG("cmd = %s", obj->cmd);

	if (obj->fd != -1)
		close(obj->fd);

	if (obj->pending_call) {
		dbus_pending_call_cancel(obj->pending_call);
		dbus_pending_call_unref(obj->pending_call);
		dbus_connection_unref(obj->conn);
	}

	send_backup_dbus_message("close", obj, &size);

	g_free(obj->cmd);
	g_free(obj);

	return 0;
}

static ssize_t backup_read(void *object, void *buf, size_t count)
{
	struct backup_object *obj = object;
	ssize_t ret = 0;

	if (obj->pending_call) {
		DBG("cmd = %s, IN WAITING STAGE", obj->cmd);
		return -EAGAIN;
	}

	if (obj->fd != -1) {
		DBG("cmd = %s, READING DATA", obj->cmd);
		ret = read(obj->fd, buf, count);
		if (ret < 0)
			ret = -errno;
	} else {
		DBG("cmd = %s, PERMANENT FAILURE", obj->cmd);
		ret = obj->error_code ? -obj->error_code : -ENOENT;
	}

	return ret;
}

static ssize_t backup_write(void *object, const void *buf, size_t count)
{
	struct backup_object *obj = object;
	ssize_t ret = 0;

	if (obj->pending_call) {
		DBG("cmd = %s, IN WAITING STAGE", obj->cmd);
		return -EAGAIN;
	}

	if (obj->fd != -1) {
		ret = write(obj->fd, buf, count);

		DBG("cmd = %s, WRITTING", obj->cmd);

		if (ret < 0) {
			error("backup: cmd = %s", obj->cmd);
			ret = -errno;
		}
	} else {
		error("backup: cmd = %s", obj->cmd);
		ret = obj->error_code ? -obj->error_code : -ENOENT;
	}

	return ret;
}

static int backup_flush(void *object)
{
	DBG("%p", object);

	return 0;
}

static struct obex_mime_type_driver backup = {
	.target = FTP_TARGET,
	.target_size = TARGET_SIZE,
	.mimetype = "application/vnd.nokia-backup",
	.open = backup_open,
	.close = backup_close,
	.read = backup_read,
	.write = backup_write,
	.flush = backup_flush,
};

static int pcsuite_init(void)
{
	int err;

	err = obex_service_driver_register(&pcsuite);
	if (err < 0)
		return err;

	err = obex_mime_type_driver_register(&backup);
	if (err < 0)
		obex_service_driver_unregister(&pcsuite);

	return err;
}

static void pcsuite_exit(void)
{
	obex_mime_type_driver_unregister(&backup);
	obex_service_driver_unregister(&pcsuite);
}

OBEX_PLUGIN_DEFINE(pcsuite, pcsuite_init, pcsuite_exit)
