// SPDX-License-Identifier: GPL-2.0-or-later
/*
 *
 *  OBEX IrMC Sync Server
 *
 *  Copyright (C) 2010  Marcel Mol <marcel@mesa.nl>
 *
 */

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

#define _GNU_SOURCE
#include <stdio.h>
#include <string.h>
#include <errno.h>
#include <glib.h>
#include <stdlib.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <inttypes.h>

#include "obexd/src/obexd.h"
#include "obexd/src/plugin.h"
#include "obexd/src/log.h"
#include "obexd/src/obex.h"
#include "obexd/src/service.h"
#include "obexd/src/manager.h"
#include "obexd/src/mimetype.h"
#include "phonebook.h"
#include "filesystem.h"

struct aparam_header {
	uint8_t tag;
	uint8_t len;
	uint8_t val[0];
} __attribute__ ((packed));

#define DID_LEN 18

struct irmc_session {
	struct obex_session *os;
	struct apparam_field *params;
	uint16_t entries;
	GString *buffer;
	char sn[DID_LEN];
	char did[DID_LEN];
	char manu[DID_LEN];
	char model[DID_LEN];
	void *request;
};

#define IRMC_TARGET_SIZE 9

static const guint8 IRMC_TARGET[IRMC_TARGET_SIZE] = {
			0x49, 0x52, 0x4d, 0x43,  0x2d, 0x53, 0x59, 0x4e, 0x43 };

/* FIXME:
 * the IrMC specs state the first vcard should be the owner
 * vcard. As there is no simple way to collect ownerdetails
 * just create an empty vcard (which is allowed according to the
 * specs).
 */
static const char *owner_vcard =
		"BEGIN:VCARD\r\n"
		"VERSION:2.1\r\n"
		"N:\r\n"
		"TEL:\r\n"
		"X-IRMX-LUID:0\r\n"
		"END:VCARD\r\n";

static void phonebook_size_result(const char *buffer, size_t bufsize,
					int vcards, int missed,
					gboolean lastpart, void *user_data)
{
	struct irmc_session *irmc = user_data;

	DBG("vcards %d", vcards);

	irmc->params->maxlistcount = vcards;

	if (irmc->request) {
		phonebook_req_finalize(irmc->request);
		irmc->request = NULL;
	}
}

static void query_result(const char *buffer, size_t bufsize, int vcards,
				int missed, gboolean lastpart, void *user_data)
{
	struct irmc_session *irmc = user_data;
	const char *s, *t;

	DBG("bufsize %zu vcards %d missed %d", bufsize, vcards, missed);

	if (irmc->request) {
		phonebook_req_finalize(irmc->request);
		irmc->request = NULL;
	}

	/* first add a 'owner' vcard */
	if (!irmc->buffer)
		irmc->buffer = g_string_new(owner_vcard);
	else
		irmc->buffer = g_string_append(irmc->buffer, owner_vcard);

	if (buffer == NULL)
		goto done;

	/* loop around buffer and add X-IRMC-LUID attribs */
	s = buffer;
	while ((t = strstr(s, "UID:")) != NULL) {
		/* add up to UID: into buffer */
		irmc->buffer = g_string_append_len(irmc->buffer, s, t-s);
		/*
		 * add UID: line into buffer
		 * Not sure if UID is still needed if X-IRMC-LUID is there
		 */
		s = t;
		t = strstr(s, "\r\n");
		t += 2;
		irmc->buffer = g_string_append_len(irmc->buffer, s, t-s);
		/* add X-IRMC-LUID with same number as UID */
		irmc->buffer = g_string_append_len(irmc->buffer,
							"X-IRMC-LUID:", 12);
		s += 4; /* point to uid number */
		irmc->buffer = g_string_append_len(irmc->buffer, s, t-s);
		s = t;
	}
	/* add remaining bit of buffer */
	irmc->buffer = g_string_append(irmc->buffer, s);

done:
	obex_object_set_io_flags(irmc, G_IO_IN, 0);
}

static void *irmc_connect(struct obex_session *os, int *err)
{
	struct irmc_session *irmc;
	struct apparam_field *param;
	int ret;

	DBG("");

	manager_register_session(os);

	irmc = g_new0(struct irmc_session, 1);
	irmc->os = os;

	/* FIXME:
	 * Ideally get capabilities info here and use that to define
	 * IrMC DID and SN etc parameters.
	 * For now lets used hostname and some 'random' value
	 */
	gethostname(irmc->did, DID_LEN);
	strncpy(irmc->sn, "12345", sizeof(irmc->sn) - 1);
	strncpy(irmc->manu, "obex", sizeof(irmc->manu) - 1);
	strncpy(irmc->model, "mymodel", sizeof(irmc->model) - 1);

	/* We need to know the number of contact/cal/nt entries
	 * somewhere so why not do it now.
	 */
	param = g_new0(struct apparam_field, 1);
	param->maxlistcount = 0; /* to count the number of vcards... */
	param->filter = 0x200085; /* UID TEL N VERSION */
	irmc->params = param;
	irmc->request = phonebook_pull(PB_CONTACTS, irmc->params,
					phonebook_size_result, irmc, err);
	ret = phonebook_pull_read(irmc->request);
	if (err)
		*err = ret;

	return irmc;
}

static int irmc_get(struct obex_session *os, void *user_data)
{
	struct irmc_session *irmc = user_data;
	const char *type = obex_get_type(os);
	const char *name = obex_get_name(os);
	char *path;
	int ret;

	DBG("name %s type %s irmc %p", name, type ? type : "NA", irmc);

	path = g_strdup(name);

	ret = obex_get_stream_start(os, path);

	g_free(path);

	return ret;
}

static void irmc_disconnect(struct obex_session *os, void *user_data)
{
	struct irmc_session *irmc = user_data;

	DBG("");

	manager_unregister_session(os);

	if (irmc->params) {
		if (irmc->params->searchval)
			g_free(irmc->params->searchval);
		g_free(irmc->params);
	}

	if (irmc->buffer)
		g_string_free(irmc->buffer, TRUE);

	g_free(irmc);
}

static int irmc_chkput(struct obex_session *os, void *user_data)
{
	DBG("");
	/* Reject all PUTs */
	return -EBADR;
}

static int irmc_open_devinfo(struct irmc_session *irmc)
{
	if (!irmc->buffer)
		irmc->buffer = g_string_new("");

	g_string_append_printf(irmc->buffer,
				"MANU:%s\r\n"
				"MOD:%s\r\n"
				"SN:%s\r\n"
				"IRMC-VERSION:1.1\r\n"
				"PB-TYPE-TX:VCARD2.1\r\n"
				"PB-TYPE-RX:NONE\r\n"
				"CAL-TYPE-TX:NONE\r\n"
				"CAL-TYPE-RX:NONE\r\n"
				"MSG-TYPE-TX:NONE\r\n"
				"MSG-TYPE-RX:NONE\r\n"
				"NOTE-TYPE-TX:NONE\r\n"
				"NOTE-TYPE-RX:NONE\r\n",
				irmc->manu, irmc->model, irmc->sn);

	return 0;
}

static int irmc_open_pb(struct irmc_session *irmc)
{
	int ret;

	/* how can we tell if the vcard count call already finished? */
	irmc->request = phonebook_pull(PB_CONTACTS, irmc->params,
						query_result, irmc, &ret);
	if (ret < 0) {
		DBG("phonebook_pull failed...");
		return ret;
	}

	ret = phonebook_pull_read(irmc->request);
	if (ret < 0) {
		DBG("phonebook_pull_read failed...");
		return ret;
	}

	return 0;
}

static int irmc_open_info(struct irmc_session *irmc)
{
	if (irmc->buffer == NULL)
		irmc->buffer = g_string_new("");

	g_string_printf(irmc->buffer, "Total-Records:%d\r\n"
				"Maximum-Records:%d\r\n"
				"IEL:2\r\n"
				"DID:%s\r\n",
				irmc->params->maxlistcount,
				irmc->params->maxlistcount, irmc->did);

	return 0;
}

static int irmc_open_cc(struct irmc_session *irmc)
{
	if (irmc->buffer == NULL)
		irmc->buffer = g_string_new("");

	g_string_printf(irmc->buffer, "%d\r\n", irmc->params->maxlistcount);

	return 0;
}

static int irmc_open_cal(struct irmc_session *irmc)
{
	/* no support yet. Just return an empty buffer. cal.vcs */
	DBG("unsupported, returning empty buffer");

	if (!irmc->buffer)
		irmc->buffer = g_string_new("");

	return 0;
}

static int irmc_open_nt(struct irmc_session *irmc)
{
	/* no support yet. Just return an empty buffer. nt.vnt */
	DBG("unsupported, returning empty buffer");

	if (!irmc->buffer)
		irmc->buffer = g_string_new("");

	return 0;
}

static int irmc_open_luid(struct irmc_session *irmc)
{
	if (irmc->buffer == NULL)
		irmc->buffer = g_string_new("");

	DBG("changelog request, force whole book");
	g_string_printf(irmc->buffer, "SN:%s\r\n"
					"DID:%s\r\n"
					"Total-Records:%d\r\n"
					"Maximum-Records:%d\r\n"
					"*\r\n",
					irmc->sn, irmc->did,
					irmc->params->maxlistcount,
					irmc->params->maxlistcount);

	return 0;
}

static void *irmc_open(const char *name, int oflag, mode_t mode, void *context,
							size_t *size, int *err)
{
	struct irmc_session *irmc = context;
	int ret = 0;
	char *path;

	DBG("name %s context %p", name, context);

	if (oflag != O_RDONLY) {
		ret = -EPERM;
		goto fail;
	}

	if (name == NULL) {
		ret = -EBADR;
		goto fail;
	}

	/* Always contains the absolute path */
	if (g_path_is_absolute(name))
		path = g_strdup(name);
	else
		path = g_build_filename("/", name, NULL);

	if (g_str_equal(path, PB_DEVINFO))
		ret = irmc_open_devinfo(irmc);
	else if (g_str_equal(path, PB_CONTACTS))
		ret = irmc_open_pb(irmc);
	else if (g_str_equal(path, PB_INFO_LOG))
		ret = irmc_open_info(irmc);
	else if (g_str_equal(path, PB_CC_LOG))
		ret = irmc_open_cc(irmc);
	else if (g_str_has_prefix(path, PB_CALENDAR_FOLDER))
		ret = irmc_open_cal(irmc);
	else if (g_str_has_prefix(path, PB_NOTES_FOLDER))
		ret = irmc_open_nt(irmc);
	else if (g_str_has_prefix(path, PB_LUID_FOLDER))
		ret = irmc_open_luid(irmc);
	else
		ret = -EBADR;

	g_free(path);

	if (ret == 0)
		return irmc;

fail:
	if (err)
		*err = ret;

	return NULL;
}

static int irmc_close(void *object)
{
	struct irmc_session *irmc = object;

	DBG("");

	if (irmc->buffer) {
		g_string_free(irmc->buffer, TRUE);
		irmc->buffer = NULL;
	}

	if (irmc->request) {
		phonebook_req_finalize(irmc->request);
		irmc->request = NULL;
	}

	return 0;
}

static ssize_t irmc_read(void *object, void *buf, size_t count)
{
	struct irmc_session *irmc = object;
	int len;

	DBG("buffer %p count %zu", irmc->buffer, count);
	if (!irmc->buffer)
                return -EAGAIN;

	len = string_read(irmc->buffer, buf, count);
	DBG("returning %d bytes", len);
	return len;
}

static const struct obex_mime_type_driver irmc_driver = {
	.target = IRMC_TARGET,
	.target_size = IRMC_TARGET_SIZE,
	.open = irmc_open,
	.close = irmc_close,
	.read = irmc_read,
};

static const struct obex_service_driver irmc = {
	.name = "IRMC Sync server",
	.service = OBEX_IRMC,
	.target = IRMC_TARGET,
	.target_size = IRMC_TARGET_SIZE,
	.connect = irmc_connect,
	.get = irmc_get,
	.disconnect = irmc_disconnect,
	.chkput = irmc_chkput
};

static int irmc_init(void)
{
	int err;

	DBG("");
	err = phonebook_init();
	if (err < 0)
		return err;

	err = obex_mime_type_driver_register(&irmc_driver);
	if (err < 0)
		goto fail_mime_irmc;

	err = obex_service_driver_register(&irmc);
	if (err < 0)
		goto fail_irmc_reg;

	return 0;

fail_irmc_reg:
	obex_mime_type_driver_unregister(&irmc_driver);
fail_mime_irmc:
	phonebook_exit();

	return err;
}

static void irmc_exit(void)
{
	DBG("");
	obex_service_driver_unregister(&irmc);
	obex_mime_type_driver_unregister(&irmc_driver);
	phonebook_exit();
}

OBEX_PLUGIN_DEFINE(irmc, irmc_init, irmc_exit)
