/*
 *
 *  Multimedia Messaging Service
 *
 *  Copyright (C) 2010-2011  Intel Corporation. All rights reserved.
 *
 *  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.
 *
 *  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 <stdio.h>
#include <sys/stat.h>
#include <sys/mman.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
#include <errno.h>

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

#include "mms.h"

#ifdef TEMP_FAILURE_RETRY
#define TFR TEMP_FAILURE_RETRY
#else
#define TFR
#endif

static const char *digest_to_str(const unsigned char *digest)
{
	static char buf[MMS_SHA1_UUID_LEN * 2 + 1];
	unsigned int i;

	for (i = 0; i < MMS_SHA1_UUID_LEN; i++)
		sprintf(&buf[i * 2], "%02X", digest[i]);

	buf[MMS_SHA1_UUID_LEN * 2] = 0;

	return buf;
}

static int create_dirs(const char *filename, const mode_t mode)
{
	struct stat st;
	char *dir;
	const char *prev, *next;
	int err;

	if (filename[0] != '/')
		return -1;

	err = stat(filename, &st);
	if (!err && S_ISREG(st.st_mode))
		return 0;

	dir = g_try_malloc(strlen(filename) + 1);
	if (dir == NULL)
		return -1;

	strcpy(dir, "/");

	for (prev = filename; (next = strchr(prev + 1, '/')); prev = next) {
		/* Skip consecutive '/' characters */
		if (next - prev == 1)
			continue;

		strncat(dir, prev + 1, next - prev);

		if (mkdir(dir, mode) == -1 && errno != EEXIST) {
			g_free(dir);
			return -1;
		}
	}

	g_free(dir);
	return 0;
}


static const char *generate_uuid_from_pdu(unsigned char *pdu, unsigned int len)
{
	GChecksum *checksum;
	guint8 digest[MMS_SHA1_UUID_LEN];
	gsize digest_size = MMS_SHA1_UUID_LEN;

	DBG("pdu %p len %d", pdu, len);

	checksum = g_checksum_new(G_CHECKSUM_SHA1);
	if (checksum == NULL)
		return NULL;

	g_checksum_update(checksum, pdu, len);

	g_checksum_get_digest(checksum, digest, &digest_size);

	g_checksum_free(checksum);

	return digest_to_str(digest);
}

char *mms_store_get_path(const char *service_id, const char *uuid)
{
	const char *homedir;

	homedir = g_get_home_dir();
	if (homedir == NULL)
		return NULL;

	return g_strdup_printf("%s/.mms/%s/%s", homedir, service_id, uuid);
}

static char *generate_pdu_pathname(const char *service_id, const char *uuid)
{
	char *pathname;

	pathname = mms_store_get_path(service_id, uuid);
	if (pathname == NULL)
		return NULL;

	if (create_dirs(pathname, S_IRUSR | S_IWUSR | S_IXUSR) != 0) {
		mms_error("Failed to create path %s", pathname);

		g_free(pathname);
		return NULL;
	}

	return pathname;
}

/*
 * Write a buffer to a file in a transactionally safe form
 *
 * Given a buffer, write it to a file named after
 * @filename. However, to make sure the file contents are
 * consistent (ie: a crash right after opening or during write()
 * doesn't leave a file half baked), the contents are written to a
 * file with a temporary name and when closed, it is renamed to the
 * specified name (@filename).
 */
static ssize_t write_file(const unsigned char *buffer, size_t len,
							const char *filename)
{
	char *tmp_file;
	ssize_t written;
	int fd;

	tmp_file = g_strdup_printf("%s.XXXXXX.tmp", filename);

	written = -1;

	fd = TFR(g_mkstemp_full(tmp_file, O_WRONLY | O_CREAT | O_TRUNC,
							S_IWUSR | S_IRUSR));
	if (fd < 0)
		goto error_mkstemp_full;

	written = TFR(write(fd, buffer, len));

	TFR(fdatasync(fd));

	TFR(close(fd));

	if (written != (ssize_t) len) {
		written = -1;
		goto error_write;
	}

	/*
	 * Now that the file contents are written, rename to the real
	 * file name; this way we are uniquely sure that the whole
	 * thing is there.
	 */
	unlink(filename);

	/* conserve @written's value from 'write' */
	if (link(tmp_file, filename) == -1)
		written = -1;

error_write:
	unlink(tmp_file);

error_mkstemp_full:
	g_free(tmp_file);

	return written;
}

const char *mms_store(const char *service_id, unsigned char *pdu,
							unsigned int len)
{
	char *pathname;
	const char *uuid;

	uuid = generate_uuid_from_pdu(pdu, len);
	if (uuid == NULL)
		return NULL;

	pathname = generate_pdu_pathname(service_id, uuid);
	if (pathname == NULL)
		return NULL;

	if (write_file(pdu, len, pathname) < 0) {
		mms_error("Failed to write to %s", pathname);

		uuid = NULL;
	}

	DBG("pathname %s", pathname);

	g_free(pathname);

	return uuid;
}

const char *mms_store_file(const char *service_id, const char *path)
{
	char *pathname;
	const char *uuid;
	int fd;
	struct stat st;
	unsigned char *pdu;

	fd = open(path, O_RDONLY);
	if (fd < 0) {
		mms_error("Failed to open %s\n", path);
		return NULL;
	}

	if (fstat(fd, &st) < 0) {
		mms_error("Failed to fstat %s\n", path);
		close(fd);
		return NULL;
	}

	pdu = mmap(NULL, st.st_size, PROT_READ, MAP_SHARED, fd, 0);
	if (pdu == NULL || pdu == MAP_FAILED) {
		mms_error("Failed to mmap %s\n", path);
		close(fd);
		return NULL;
	}

	uuid = generate_uuid_from_pdu(pdu, st.st_size);

	munmap(pdu, st.st_size);

	close(fd);

	if (uuid == NULL)
		return NULL;

	pathname = generate_pdu_pathname(service_id, uuid);
	if (pathname == NULL)
		return NULL;

	if (g_rename(path, pathname) < 0) {
		mms_error("Failed to rename %s to %s\n", path, pathname);

		uuid = NULL;
	}

	DBG("pathname %s", pathname);

	g_free(pathname);

	return uuid;
}

void mms_store_remove(const char *service_id, const char *uuid)
{
	char *pdu_path;
	char *meta_path;

	pdu_path = mms_store_get_path(service_id, uuid);
	if (pdu_path == NULL)
		return;

	unlink(pdu_path);

	meta_path = g_strdup_printf("%s%s", pdu_path, MMS_META_UUID_SUFFIX);

	g_free(pdu_path);

	unlink(meta_path);

	g_free(meta_path);
}

GKeyFile *mms_store_meta_open(const char *service_id, const char *uuid)
{
	GKeyFile *keyfile;
	char *pdu_path;
	char *meta_path;

	pdu_path = generate_pdu_pathname(service_id, uuid);
	if (pdu_path == NULL)
		return NULL;

	meta_path = g_strdup_printf("%s%s", pdu_path, MMS_META_UUID_SUFFIX);

	g_free(pdu_path);

	keyfile = g_key_file_new();

	g_key_file_load_from_file(keyfile, meta_path, 0, NULL);

	g_free(meta_path);

	return keyfile;
}

static void meta_store_sync(const char *service_id, const char *uuid,
							GKeyFile *keyfile)
{
	char *data;
	gsize length = 0;
	char *pdu_path;
	char *meta_path;

	pdu_path = mms_store_get_path(service_id, uuid);
	if (pdu_path == NULL)
		return;

	meta_path = g_strdup_printf("%s%s", pdu_path, MMS_META_UUID_SUFFIX);

	g_free(pdu_path);

	data = g_key_file_to_data(keyfile, &length, NULL);

	g_file_set_contents(meta_path, data, length, NULL);

	g_free(data);

	g_free(meta_path);
}

void mms_store_meta_close(const char *service_id, const char *uuid,
					GKeyFile *keyfile, gboolean save)
{
	if (save == TRUE)
		meta_store_sync(service_id, uuid, keyfile);

	g_key_file_free(keyfile);
}

GKeyFile *mms_settings_open(const char *service_id, const char *store)
{
	GKeyFile *keyfile;
	char *path;

	if (store == NULL)
		return NULL;

	path = mms_store_get_path(service_id, store);
	if (path == NULL)
		return NULL;

	if (create_dirs(path, S_IRUSR | S_IWUSR | S_IXUSR) != 0) {
		mms_error("Failed to create path %s", path);

		g_free(path);
		return NULL;
	}

	keyfile = g_key_file_new();

	g_key_file_load_from_file(keyfile, path, 0, NULL);

	g_free(path);

	return keyfile;
}

static void settings_sync(const char *service_id, const char *store,
							GKeyFile *keyfile)
{
	char *path;
	char *data;
	gsize length = 0;

	path = mms_store_get_path(service_id, store);
	if (path == NULL)
		return;

	data = g_key_file_to_data(keyfile, &length, NULL);

	g_file_set_contents(path, data, length, NULL);

	g_free(data);
	g_free(path);
}

void mms_settings_close(const char *service_id, const char *store,
					GKeyFile *keyfile, gboolean save)
{
	if (save == TRUE)
		settings_sync(service_id, store, keyfile);

	g_key_file_free(keyfile);
}
