/* Copyright (c) 2013 The Chromium OS Authors. All rights reserved.
 * Use of this source code is governed by a BSD-style license that can be
 * found in the LICENSE file.
 */

#include <errno.h>
#include <stddef.h>
#include <stdlib.h>
#include <string.h>

#include "cras_alert.h"
#include "utlist.h"

/* A list of callbacks for an alert */
struct cras_alert_cb_list {
	cras_alert_cb callback;
	void *arg;
	struct cras_alert_cb_list *prev, *next;
};

/* A list of data args to callbacks. Variable-length structure. */
struct cras_alert_data {
	struct cras_alert_data *prev, *next;
	/* This field must be the last in this structure. */
	char buf[];
};

struct cras_alert {
	int pending;
	unsigned int flags;
	cras_alert_prepare prepare;
	struct cras_alert_cb_list *callbacks;
	struct cras_alert_data *data;
	struct cras_alert *prev, *next;
};

/* A list of all alerts in the system */
static struct cras_alert *all_alerts;
/* If there is any alert pending. */
static int has_alert_pending;

struct cras_alert *cras_alert_create(cras_alert_prepare prepare,
				     unsigned int flags)
{
	struct cras_alert *alert;
	alert = calloc(1, sizeof(*alert));
	if (!alert)
		return NULL;
	alert->prepare = prepare;
	alert->flags = flags;
	DL_APPEND(all_alerts, alert);
	return alert;
}

int cras_alert_add_callback(struct cras_alert *alert, cras_alert_cb cb,
			    void *arg)
{
	struct cras_alert_cb_list *alert_cb;

	if (cb == NULL)
		return -EINVAL;

	DL_FOREACH(alert->callbacks, alert_cb)
		if (alert_cb->callback == cb && alert_cb->arg == arg)
			return -EEXIST;

	alert_cb = calloc(1, sizeof(*alert_cb));
	if (alert_cb == NULL)
		return -ENOMEM;
	alert_cb->callback = cb;
	alert_cb->arg = arg;
	DL_APPEND(alert->callbacks, alert_cb);
	return 0;
}

int cras_alert_rm_callback(struct cras_alert *alert, cras_alert_cb cb,
			   void *arg)
{
	struct cras_alert_cb_list *alert_cb;

	DL_FOREACH(alert->callbacks, alert_cb)
		if (alert_cb->callback == cb && alert_cb->arg == arg) {
			DL_DELETE(alert->callbacks, alert_cb);
			free(alert_cb);
			return 0;
		}
	return -ENOENT;
}

/* Checks if the alert is pending, and invoke the prepare function and callbacks
 * if so. */
static void cras_alert_process(struct cras_alert *alert)
{
	struct cras_alert_cb_list *cb;
	struct cras_alert_data *data;

	if (!alert->pending)
		return;

	alert->pending = 0;
	if (alert->prepare)
		alert->prepare(alert);

	if (!alert->data) {
		DL_FOREACH(alert->callbacks, cb)
			cb->callback(cb->arg, NULL);
	}

	/* Have data arguments, pass each to the callbacks. */
	DL_FOREACH(alert->data, data) {
		DL_FOREACH(alert->callbacks, cb)
			cb->callback(cb->arg, (void *)data->buf);
		DL_DELETE(alert->data, data);
		free(data);
	}
}

void cras_alert_pending(struct cras_alert *alert)
{
	alert->pending = 1;
	has_alert_pending = 1;
}

void cras_alert_pending_data(struct cras_alert *alert,
			     void *data, size_t data_size)
{
	struct cras_alert_data *d;

	alert->pending = 1;
	has_alert_pending = 1;
	d = calloc(1, offsetof(struct cras_alert_data, buf) + data_size);
	memcpy(d->buf, data, data_size);

	if (!(alert->flags & CRAS_ALERT_FLAG_KEEP_ALL_DATA) && alert->data) {
		/* There will never be more than one item in the list. */
		free(alert->data);
		alert->data = NULL;
	}

	/* Even when there is only one item, it is important to use DL_APPEND
	 * here so that d's next and prev pointers are setup correctly. */
	DL_APPEND(alert->data, d);
}

void cras_alert_process_all_pending_alerts()
{
	struct cras_alert *alert;

	while (has_alert_pending) {
		has_alert_pending = 0;
		DL_FOREACH(all_alerts, alert)
			cras_alert_process(alert);
	}
}

void cras_alert_destroy(struct cras_alert *alert)
{
	struct cras_alert_cb_list *cb;
	struct cras_alert_data *data;

	if (!alert)
		return;

	DL_FOREACH(alert->callbacks, cb) {
		DL_DELETE(alert->callbacks, cb);
		free(cb);
	}

	DL_FOREACH(alert->data, data) {
		DL_DELETE(alert->data, data);
		free(data);
	}

	alert->callbacks = NULL;
	DL_DELETE(all_alerts, alert);
	free(alert);
}

void cras_alert_destroy_all()
{
	struct cras_alert *alert;
	DL_FOREACH(all_alerts, alert)
		cras_alert_destroy(alert);
}