/*
 * Copyright (C) 2009-2013 Red Hat, Inc. All rights reserved.
 *
 * This file is part of LVM2.
 *
 * This copyrighted material is made available to anyone wishing to use,
 * modify, copy, or redistribute it subject to the terms and conditions
 * of the GNU Lesser General Public License v.2.1.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with this program; if not, write to the Free Software Foundation,
 * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
 */

#include "clvmd-common.h"

#include <pthread.h>

#include "locking.h"
#include "clvm.h"
#include "clvmd-comms.h"
#include "clvmd.h"

#include <sys/un.h>
#include <sys/socket.h>
#include <fcntl.h>

static const char SINGLENODE_CLVMD_SOCKNAME[] = DEFAULT_RUN_DIR "/clvmd_singlenode.sock";
static int listen_fd = -1;

static struct dm_hash_table *_locks;
static int _lockid;

static pthread_mutex_t _lock_mutex = PTHREAD_MUTEX_INITIALIZER;
/* Using one common condition for all locks for simplicity */
static pthread_cond_t _lock_cond = PTHREAD_COND_INITIALIZER;

struct lock {
	struct dm_list list;
	int lockid;
	int mode;
};

static void close_comms(void)
{
	if (listen_fd != -1 && close(listen_fd))
		stack;
	(void)unlink(SINGLENODE_CLVMD_SOCKNAME);
	listen_fd = -1;
}

static int init_comms(void)
{
	mode_t old_mask;
	struct sockaddr_un addr = { .sun_family = AF_UNIX };

	if (!dm_strncpy(addr.sun_path, SINGLENODE_CLVMD_SOCKNAME,
			sizeof(addr.sun_path))) {
		DEBUGLOG("%s: singlenode socket name too long.",
			 SINGLENODE_CLVMD_SOCKNAME);
		return -1;
	}

	close_comms();

	(void) dm_prepare_selinux_context(SINGLENODE_CLVMD_SOCKNAME, S_IFSOCK);
	old_mask = umask(0077);

	listen_fd = socket(PF_UNIX, SOCK_STREAM, 0);
	if (listen_fd < 0) {
		DEBUGLOG("Can't create local socket: %s\n", strerror(errno));
		goto error;
	}
	/* Set Close-on-exec */
	if (fcntl(listen_fd, F_SETFD, 1)) {
		DEBUGLOG("Setting CLOEXEC on client fd failed: %s\n", strerror(errno));
		goto error;
	}

	if (bind(listen_fd, (struct sockaddr *)&addr, sizeof(addr)) < 0) {
		DEBUGLOG("Can't bind local socket: %s\n", strerror(errno));
		goto error;
	}
	if (listen(listen_fd, 10) < 0) {
		DEBUGLOG("Can't listen local socket: %s\n", strerror(errno));
		goto error;
	}

	umask(old_mask);
	(void) dm_prepare_selinux_context(NULL, 0);
	return 0;
error:
	umask(old_mask);
	(void) dm_prepare_selinux_context(NULL, 0);
	close_comms();
	return -1;
}

static int _init_cluster(void)
{
	int r;

	if (!(_locks = dm_hash_create(128))) {
		DEBUGLOG("Failed to allocate single-node hash table.\n");
		return 1;
	}

	r = init_comms();
	if (r) {
		dm_hash_destroy(_locks);
		_locks = NULL;
		return r;
	}

	DEBUGLOG("Single-node cluster initialised.\n");
	return 0;
}

static void _cluster_closedown(void)
{
	close_comms();

	/* If there is any awaited resource, kill it softly */
	pthread_mutex_lock(&_lock_mutex);
	dm_hash_destroy(_locks);
	_locks = NULL;
	_lockid = 0;
	pthread_cond_broadcast(&_lock_cond); /* wakeup waiters */
	pthread_mutex_unlock(&_lock_mutex);
}

static void _get_our_csid(char *csid)
{
	int nodeid = 1;
	memcpy(csid, &nodeid, sizeof(int));
}

static int _csid_from_name(char *csid, const char *name)
{
	return 1;
}

static int _name_from_csid(const char *csid, char *name)
{
	strcpy(name, "SINGLENODE");
	return 0;
}

static int _get_num_nodes(void)
{
	return 1;
}

/* Node is now known to be running a clvmd */
static void _add_up_node(const char *csid)
{
}

/* Call a callback for each node, so the caller knows whether it's up or down */
static int _cluster_do_node_callback(struct local_client *master_client,
				     void (*callback)(struct local_client *,
				     const char *csid, int node_up))
{
	return 0;
}

int _lock_file(const char *file, uint32_t flags);

static const char *_get_mode(int mode)
{
	switch (mode) {
	case LCK_NULL: return "NULL";
	case LCK_READ: return "READ";
	case LCK_PREAD: return "PREAD";
	case LCK_WRITE: return "WRITE";
	case LCK_EXCL: return "EXCLUSIVE";
	case LCK_UNLOCK: return "UNLOCK";
	default: return "????";
	}
}

/* Real locking */
static int _lock_resource(const char *resource, int mode, int flags, int *lockid)
{
	/* DLM table of allowed transition states */
	static const int _dlm_table[6][6] = {
	/* Mode	   NL	CR	CW	PR	PW	EX */
	/* NL */ { 1,	 1,	 1,	 1,	 1,	 1},
	/* CR */ { 1,	 1,	 1,	 1,	 1,	 0},
	/* CW */ { 1,	 1,	 1,	 0,	 0,	 0},
	/* PR */ { 1,	 1,	 0,	 1,	 0,	 0},
	/* PW */ { 1,	 1,	 0,	 0,	 0,	 0},
	/* EX */ { 1,	 0,	 0,	 0,	 0,	 0}
	};

	struct lock *lck = NULL, *lckt;
	struct dm_list *head;

	DEBUGLOG("Locking resource %s, flags=0x%02x (%s%s%s), mode=%s (%d)\n",
		 resource, flags,
		 (flags & LCKF_NOQUEUE) ? "NOQUEUE" : "",
		 ((flags & (LCKF_NOQUEUE | LCKF_CONVERT)) ==
		  (LCKF_NOQUEUE | LCKF_CONVERT)) ? "|" : "",
		 (flags & LCKF_CONVERT) ? "CONVERT" : "",
		 _get_mode(mode), mode);

	mode &= LCK_TYPE_MASK;
	pthread_mutex_lock(&_lock_mutex);

retry:
	if (!(head = dm_hash_lookup(_locks, resource))) {
		if (flags & LCKF_CONVERT) {
			/* In real DLM, lock is identified only by lockid, resource is not used */
			DEBUGLOG("Unlocked resource %s cannot be converted\n", resource);
			goto_bad;
		}
		/* Add new locked resource */
		if (!(head = dm_malloc(sizeof(struct dm_list))) ||
		    !dm_hash_insert(_locks, resource, head)) {
			dm_free(head);
			goto_bad;
		}

		dm_list_init(head);
	} else	/* Update/convert locked resource */
		dm_list_iterate_items(lck, head) {
			/* Check is all locks are compatible with requested lock */
			if (flags & LCKF_CONVERT) {
				if (lck->lockid != *lockid)
					continue;

				DEBUGLOG("Converting resource %s lockid=%d mode:%s -> %s...\n",
					 resource, lck->lockid, _get_mode(lck->mode), _get_mode(mode));
				dm_list_iterate_items(lckt, head) {
					if ((lckt->lockid != *lockid) &&
					    !_dlm_table[mode][lckt->mode]) {
						if (!(flags & LCKF_NOQUEUE) &&
						    /* TODO: Real dlm uses here conversion queues */
						    !pthread_cond_wait(&_lock_cond, &_lock_mutex) &&
						    _locks) /* End of the game? */
							goto retry;
						goto bad;
					}
				}
				lck->mode = mode; /* Lock is now converted */
				goto out;
			} else if (!_dlm_table[mode][lck->mode]) {
				DEBUGLOG("Resource %s already locked lockid=%d, mode:%s\n",
					 resource, lck->lockid, _get_mode(lck->mode));
				if (!(flags & LCKF_NOQUEUE) &&
				    !pthread_cond_wait(&_lock_cond, &_lock_mutex) &&
				    _locks) { /* End of the game? */
					DEBUGLOG("Resource %s retrying lock in mode:%s...\n",
						 resource, _get_mode(mode));
					goto retry;
				}
				goto bad;
			}
		}

	if (!(flags & LCKF_CONVERT)) {
		if (!(lck = dm_malloc(sizeof(struct lock))))
			goto_bad;

		*lockid = lck->lockid = ++_lockid;
		lck->mode = mode;
		dm_list_add(head, &lck->list);
	}
out:
	pthread_cond_broadcast(&_lock_cond); /* to wakeup waiters */
	pthread_mutex_unlock(&_lock_mutex);
	DEBUGLOG("Locked resource %s, lockid=%d, mode=%s\n",
		 resource, lck->lockid, _get_mode(lck->mode));

	return 0;
bad:
	pthread_cond_broadcast(&_lock_cond); /* to wakeup waiters */
	pthread_mutex_unlock(&_lock_mutex);
	DEBUGLOG("Failed to lock resource %s\n", resource);

	return 1; /* fail */
}

static int _unlock_resource(const char *resource, int lockid)
{
	struct lock *lck;
	struct dm_list *head;
	int r = 1;

	if (lockid < 0) {
		DEBUGLOG("Not tracking unlock of lockid -1: %s, lockid=%d\n",
			 resource, lockid);
		return 1;
	}

	DEBUGLOG("Unlocking resource %s, lockid=%d\n", resource, lockid);
	pthread_mutex_lock(&_lock_mutex);
	pthread_cond_broadcast(&_lock_cond); /* wakeup waiters */

	if (!(head = dm_hash_lookup(_locks, resource))) {
		pthread_mutex_unlock(&_lock_mutex);
		DEBUGLOG("Resource %s is not locked.\n", resource);
		return 1;
	}

	dm_list_iterate_items(lck, head)
		if (lck->lockid == lockid) {
			dm_list_del(&lck->list);
			dm_free(lck);
			r = 0;
			goto out;
		}

	DEBUGLOG("Resource %s has wrong lockid %d.\n", resource, lockid);
out:
	if (dm_list_empty(head)) {
		//DEBUGLOG("Resource %s is no longer hashed (lockid=%d).\n", resource, lockid);
		dm_hash_remove(_locks, resource);
		dm_free(head);
	}

	pthread_mutex_unlock(&_lock_mutex);

	return r;
}

static int _is_quorate(void)
{
	return 1;
}

static int _get_main_cluster_fd(void)
{
	return listen_fd;
}

static int _cluster_fd_callback(struct local_client *fd, char *buf, int len,
				const char *csid,
				struct local_client **new_client)
{
	return 1;
}

static int _cluster_send_message(const void *buf, int msglen,
				 const char *csid,
				 const char *errtext)
{
	return 0;
}

static int _get_cluster_name(char *buf, int buflen)
{
	return dm_strncpy(buf, "localcluster", buflen) ? 0 : 1;
}

static struct cluster_ops _cluster_singlenode_ops = {
	.name                     = "singlenode",
	.cluster_init_completed   = NULL,
	.cluster_send_message     = _cluster_send_message,
	.name_from_csid           = _name_from_csid,
	.csid_from_name           = _csid_from_name,
	.get_num_nodes            = _get_num_nodes,
	.cluster_fd_callback      = _cluster_fd_callback,
	.get_main_cluster_fd      = _get_main_cluster_fd,
	.cluster_do_node_callback = _cluster_do_node_callback,
	.is_quorate               = _is_quorate,
	.get_our_csid             = _get_our_csid,
	.add_up_node              = _add_up_node,
	.reread_config            = NULL,
	.cluster_closedown        = _cluster_closedown,
	.get_cluster_name         = _get_cluster_name,
	.sync_lock                = _lock_resource,
	.sync_unlock              = _unlock_resource,
};

struct cluster_ops *init_singlenode_cluster(void)
{
	if (!_init_cluster())
		return &_cluster_singlenode_ops;

	return NULL;
}
