/* SPDX-License-Identifier: LGPL-2.1-or-later */
#include "internal.h"

#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/ioctl.h>
#include <errno.h>

#include <linux/firewire-cdev.h>

/**
 * SECTION:fw_node
 * @Title: HinawaFwNode
 * @Short_description: An event listener for FireWire node
 * @include: fw_node.h
 *
 * A #HinawaFwNode is an event listener for a specified node on IEEE 1394 bus.
 * This class is an application of Linux FireWire subsystem.
 * All of operations utilize ioctl(2) with subsystem specific request commands.
 *
 * Since: 1.4.
 */

// 256 comes from an actual implementation in kernel land. Read
// 'drivers/firewire/core-device.c'. This value is calculated by a range for
// configuration ROM in ISO/IEC 13213 (IEEE 1212).
#define MAX_CONFIG_ROM_SIZE	256
#define MAX_CONFIG_ROM_LENGTH	(MAX_CONFIG_ROM_SIZE * 4)

struct _HinawaFwNodePrivate {
	int fd;

	GMutex mutex;
	guint8 config_rom[MAX_CONFIG_ROM_LENGTH];
	gsize config_rom_length;
	struct fw_cdev_event_bus_reset generation;

	GList *transactions;
	GMutex transactions_mutex;
};

G_DEFINE_TYPE_WITH_PRIVATE(HinawaFwNode, hinawa_fw_node, G_TYPE_OBJECT)

/**
 * hinawa_fw_node_error_quark:
 *
 * Return the GQuark for error domain of GError which has code in #HinawaFwNodeError.
 *
 * Returns: A #GQuark.
 */
G_DEFINE_QUARK(hinawa-fw-node-error-quark, hinawa_fw_node_error)

static const char *const err_msgs[] = {
	[HINAWA_FW_NODE_ERROR_DISCONNECTED] = "The associated node is not available for communication",
	[HINAWA_FW_NODE_ERROR_OPENED] = "The instance is already associated to node",
	[HINAWA_FW_NODE_ERROR_NOT_OPENED] = "The instance is not associated to node",
};

#define generate_local_error(exception, code) \
	g_set_error_literal(exception, HINAWA_FW_NODE_ERROR, code, err_msgs[code])

#define generate_file_error(exception, code, format, arg) \
	g_set_error(exception, G_FILE_ERROR, code, format, arg)

#define generate_syscall_error(exception, errno, format, arg)				\
	g_set_error(exception, HINAWA_FW_NODE_ERROR, HINAWA_FW_NODE_ERROR_FAILED,	\
		    format " %d(%s)", arg, errno, strerror(errno))

typedef struct {
	GSource src;
	HinawaFwNode *self;
	gpointer tag;
	size_t len;
	void *buf;
} FwNodeSource;

enum fw_node_prop_type {
	FW_NODE_PROP_TYPE_NODE_ID = 1,
	FW_NODE_PROP_TYPE_LOCAL_NODE_ID,
	FW_NODE_PROP_TYPE_BUS_MANAGER_NODE_ID,
	FW_NODE_PROP_TYPE_IR_MANAGER_NODE_ID,
	FW_NODE_PROP_TYPE_ROOT_NODE_ID,
	FW_NODE_PROP_TYPE_GENERATION,
	FW_NODE_PROP_TYPE_COUNT,
};
static GParamSpec *fw_node_props[FW_NODE_PROP_TYPE_COUNT] = { NULL, };

// This object has two signals.
enum fw_node_sig_type {
	FW_NODE_SIG_TYPE_BUS_UPDATE = 0,
	FW_NODE_SIG_TYPE_DISCONNECTED,
	FW_NODE_SIG_TYPE_COUNT,
};
static guint fw_node_sigs[FW_NODE_SIG_TYPE_COUNT] = { 0 };

static void fw_node_finalize(GObject *obj)
{
	HinawaFwNode *self = HINAWA_FW_NODE(obj);
	HinawaFwNodePrivate *priv = hinawa_fw_node_get_instance_private(self);

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

	g_list_free(priv->transactions);

	G_OBJECT_CLASS(hinawa_fw_node_parent_class)->finalize(obj);
}

static void fw_node_get_property(GObject *obj, guint id,
				 GValue *val, GParamSpec *spec)
{
	HinawaFwNode *self = HINAWA_FW_NODE(obj);
	HinawaFwNodePrivate *priv = hinawa_fw_node_get_instance_private(self);

	g_mutex_lock(&priv->mutex);

	switch (id) {
	case FW_NODE_PROP_TYPE_NODE_ID:
		g_value_set_uint(val, priv->generation.node_id);
		break;
	case FW_NODE_PROP_TYPE_LOCAL_NODE_ID:
		g_value_set_uint(val, priv->generation.local_node_id);
		break;
	case FW_NODE_PROP_TYPE_BUS_MANAGER_NODE_ID:
		g_value_set_uint(val, priv->generation.bm_node_id);
		break;
	case FW_NODE_PROP_TYPE_IR_MANAGER_NODE_ID:
		g_value_set_uint(val, priv->generation.irm_node_id);
		break;
	case FW_NODE_PROP_TYPE_ROOT_NODE_ID:
		g_value_set_uint(val, priv->generation.root_node_id);
		break;
	case FW_NODE_PROP_TYPE_GENERATION:
		g_value_set_uint(val, priv->generation.generation);
		break;
	default:
		G_OBJECT_WARN_INVALID_PROPERTY_ID(obj, id, spec);
		break;
	}

	g_mutex_unlock(&priv->mutex);
}

static void hinawa_fw_node_class_init(HinawaFwNodeClass *klass)
{
	GObjectClass *gobject_class = G_OBJECT_CLASS(klass);

	gobject_class->finalize = fw_node_finalize;
	gobject_class->get_property = fw_node_get_property;

	fw_node_props[FW_NODE_PROP_TYPE_NODE_ID] =
		g_param_spec_uint("node-id", "node-id",
				  "Node-ID of this node at this generation.",
				  0, G_MAXUINT32, 0,
				  G_PARAM_READABLE);
	fw_node_props[FW_NODE_PROP_TYPE_LOCAL_NODE_ID] =
		g_param_spec_uint("local-node-id", "local-node-id",
				  "Node-ID for a node which this node use to "
				  "communicate to the other nodes on the bus "
				  "at this generation.",
				  0, G_MAXUINT32, 0,
				  G_PARAM_READABLE);
	fw_node_props[FW_NODE_PROP_TYPE_BUS_MANAGER_NODE_ID] =
		g_param_spec_uint("bus-manager-node-id", "bus-manager-node-id",
				  "Node-ID for bus manager on the bus at this "
				  "generation.",
				  0, G_MAXUINT32, 0,
				  G_PARAM_READABLE);
	fw_node_props[FW_NODE_PROP_TYPE_IR_MANAGER_NODE_ID] =
		g_param_spec_uint("ir-manager-node-id", "ir-manager-node-id",
				  "Node-ID for isochronous resource manager "
				  "on the bus at this generation",
				  0, G_MAXUINT32, 0,
				  G_PARAM_READABLE);
	fw_node_props[FW_NODE_PROP_TYPE_ROOT_NODE_ID] =
		g_param_spec_uint("root-node-id", "root-node-id",
				  "Node-ID for root of bus topology at this "
				  "generation.",
				  0, G_MAXUINT32, 0,
				  G_PARAM_READABLE);
	fw_node_props[FW_NODE_PROP_TYPE_GENERATION] =
		g_param_spec_uint("generation", "generation",
				  "current level of generation on this bus.",
				  0, G_MAXUINT32, 0,
				  G_PARAM_READABLE);

	g_object_class_install_properties(gobject_class,
					  FW_NODE_PROP_TYPE_COUNT,
					  fw_node_props);

	/**
	 * HinawaFwNode::bus-update:
	 * @self: A #HinawaFwNode.
	 *
	 * When IEEE 1394 bus is updated, the #HinawaFwNode::bus-update signal is generated.
	 * Handlers can read current generation in the bus via 'generation'
	 * property.
	 *
	 * Since: 1.4.
	 */
	fw_node_sigs[FW_NODE_SIG_TYPE_BUS_UPDATE] =
		g_signal_new("bus-update",
			     G_OBJECT_CLASS_TYPE(klass),
			     G_SIGNAL_RUN_LAST,
			     G_STRUCT_OFFSET(HinawaFwNodeClass, bus_update),
			     NULL, NULL,
			     g_cclosure_marshal_VOID__VOID,
			     G_TYPE_NONE, 0, G_TYPE_NONE);

	/**
	 * HinawaFwNode::disconnected:
	 * @self: A #HinawaFwNode.
	 *
	 * When phicical FireWire devices are disconnected from IEEE 1394 bus,
	 * the #HinawaFwNode::disconnected signal is generated.
	 *
	 * Since: 1.4.
	 */
	fw_node_sigs[FW_NODE_SIG_TYPE_DISCONNECTED] =
		g_signal_new("disconnected",
			     G_OBJECT_CLASS_TYPE(klass),
			     G_SIGNAL_RUN_LAST,
			     G_STRUCT_OFFSET(HinawaFwNodeClass, disconnected),
			     NULL, NULL,
			     g_cclosure_marshal_VOID__VOID,
			     G_TYPE_NONE, 0, G_TYPE_NONE);
}

static void hinawa_fw_node_init(HinawaFwNode *self)
{
	HinawaFwNodePrivate *priv = hinawa_fw_node_get_instance_private(self);

	priv->fd = -1;
	g_mutex_init(&priv->mutex);
	g_mutex_init(&priv->transactions_mutex);
}

/**
 * hinawa_fw_node_new:
 *
 * Instantiate #HinawaFwNode object and return the instance.
 *
 * Returns: an instance of #HinawaFwNode.
 * Since: 1.4.
 */
HinawaFwNode *hinawa_fw_node_new(void)
{
	return g_object_new(HINAWA_TYPE_FW_NODE, NULL);
}

static int update_info(HinawaFwNode *self)
{
	HinawaFwNodePrivate *priv = hinawa_fw_node_get_instance_private(self);
	struct fw_cdev_get_info info = {0};
	guint32 *rom;
	unsigned int quads;
	int i;

	// Duplicate generation parameters in userspace.
	info.version = 4;
	info.rom = (__u64)priv->config_rom;
	info.rom_length = MAX_CONFIG_ROM_LENGTH;
	info.bus_reset = (__u64)&priv->generation;
	info.bus_reset_closure = (__u64)self;
	if (ioctl(priv->fd, FW_CDEV_IOC_GET_INFO, &info) < 0)
		return errno;

	// Align buffer for configuration ROM according to host endianness,
	// because Linux firewire subsystem copies raw data to it.
	rom = (guint32 *)priv->config_rom;
	quads = (info.rom_length + 3) / 4;
	for (i = 0; i < quads; ++i)
		rom[i] = GUINT32_FROM_BE(rom[i]);
	priv->config_rom_length = info.rom_length;

	return 0;
}

/**
 * hinawa_fw_node_open:
 * @self: A #HinawaFwNode
 * @path: A path to Linux FireWire character device
 * @exception: A #GError. Error can be generated with two domains; #g_file_error_quark(), and
 *	       #hinawa_fw_node_error_quark().
 *
 * Open Linux FireWire character device to operate node on IEEE 1394 bus.
 *
 * Since: 1.4.
 */
void hinawa_fw_node_open(HinawaFwNode *self, const gchar *path,
			 GError **exception)
{
	HinawaFwNodePrivate *priv;
	int err;

	g_return_if_fail(HINAWA_IS_FW_NODE(self));
	g_return_if_fail(path != NULL && strlen(path) > 0);
	g_return_if_fail(exception == NULL || *exception == NULL);

	priv = hinawa_fw_node_get_instance_private(self);
	if (priv->fd >= 0) {
		generate_local_error(exception, HINAWA_FW_NODE_ERROR_OPENED);
		return;
	}

	priv->fd = open(path, O_RDONLY);
	if (priv->fd < 0) {
		if (errno == ENODEV) {
			generate_local_error(exception, HINAWA_FW_NODE_ERROR_DISCONNECTED);
		} else {
			GFileError code = g_file_error_from_errno(errno);
			if (code != G_FILE_ERROR_FAILED)
				generate_file_error(exception, code, "open(%s)", path);
			else
				generate_syscall_error(exception, errno, "open(%s)", path);
		}
		return;
	}

	g_mutex_lock(&priv->mutex);
	err = update_info(self);
	g_mutex_unlock(&priv->mutex);

	if (err > 0) {
		if (err == ENODEV)
			generate_local_error(exception, HINAWA_FW_NODE_ERROR_DISCONNECTED);
		else
			generate_syscall_error(exception, errno, "ioctl(%s)", "FW_CDEV_IOC_GET_INFO");
		close(priv->fd);
		priv->fd = -1;
	}
}

/**
 * hinawa_fw_node_get_config_rom:
 * @self: A #HinawaFwNode
 * @image: (array length=length)(out)(transfer none): The content of
 *	   configuration ROM.
 * @length: (out): The number of bytes consists of the configuration rom.
 * @exception: A #GError.
 *
 * Get cached content of configuration ROM.
 *
 * Since: 1.4.
 */
void hinawa_fw_node_get_config_rom(HinawaFwNode *self, const guint8 **image,
				   gsize *length, GError **exception)
{
	HinawaFwNodePrivate *priv;

	g_return_if_fail(HINAWA_IS_FW_NODE(self));
	g_return_if_fail(image != NULL);
	g_return_if_fail(length != NULL);
	g_return_if_fail(exception == NULL || *exception == NULL);


	priv = hinawa_fw_node_get_instance_private(self);
	if (priv->fd < 0) {
		generate_local_error(exception, HINAWA_FW_NODE_ERROR_NOT_OPENED);
		return;
	}

	g_mutex_lock(&priv->mutex);

	*image = priv->config_rom;
	*length = priv->config_rom_length;

	g_mutex_unlock(&priv->mutex);
}

static void handle_update(HinawaFwNode *self)
{
	HinawaFwNodePrivate *priv;

	g_return_if_fail(HINAWA_IS_FW_NODE(self));
	priv = hinawa_fw_node_get_instance_private(self);

	g_mutex_lock(&priv->mutex);
	update_info(self);
	g_mutex_unlock(&priv->mutex);

	g_signal_emit(self, fw_node_sigs[FW_NODE_SIG_TYPE_BUS_UPDATE], 0, NULL);
}

static gboolean check_src(GSource *gsrc)
{
	FwNodeSource *src = (FwNodeSource *)gsrc;
	GIOCondition condition;

	// Don't go to dispatch if nothing available. As an exception, return
	// TRUE for POLLERR to call .dispatch for internal destruction.
	condition = g_source_query_unix_fd(gsrc, src->tag);
	return !!(condition & (G_IO_IN | G_IO_ERR));
}

static gboolean dispatch_src(GSource *gsrc, GSourceFunc cb, gpointer user_data)
{
	FwNodeSource *src = (FwNodeSource *)gsrc;
	HinawaFwNodePrivate *priv;
	GIOCondition condition;
	struct fw_cdev_event_common *common;
	ssize_t len;

	priv = hinawa_fw_node_get_instance_private(src->self);
	if (priv->fd < 0)
		return G_SOURCE_REMOVE;

	condition = g_source_query_unix_fd(gsrc, src->tag);
	if (condition & G_IO_ERR) {
		g_signal_emit(src->self,
			fw_node_sigs[FW_NODE_SIG_TYPE_DISCONNECTED], 0);
		return G_SOURCE_REMOVE;
	}

	len = read(priv->fd, src->buf, src->len);
	if (len < 0) {
		if (errno == EAGAIN)
			return G_SOURCE_CONTINUE;

		return G_SOURCE_REMOVE;
	}

	common = (struct fw_cdev_event_common *)src->buf;

	if (HINAWA_IS_FW_NODE(common->closure) && common->type == FW_CDEV_EVENT_BUS_RESET) {
		handle_update(src->self);
	} else if (HINAWA_IS_FW_RESP(common->closure) && common->type == FW_CDEV_EVENT_REQUEST2) {
		hinawa_fw_resp_handle_request(HINAWA_FW_RESP(common->closure),
				(struct fw_cdev_event_request2 *)common);
	} else if (HINAWA_IS_FW_REQ(common->closure) && common->type == FW_CDEV_EVENT_RESPONSE) {
		struct fw_cdev_event_response *ev = (struct fw_cdev_event_response *)common;
		HinawaFwReq *req = HINAWA_FW_REQ(ev->closure);
		GList *entry;

		// Don't process request invalidated in advance.
		g_mutex_lock(&priv->transactions_mutex);
		entry = g_list_find(priv->transactions, req);
		if (entry) {
			priv->transactions = g_list_delete_link(priv->transactions, entry);
			hinawa_fw_req_handle_response(req, ev);
		}
		g_mutex_unlock(&priv->transactions_mutex);
	}

	// Just be sure to continue to process this source.
	return G_SOURCE_CONTINUE;
}

static void finalize_src(GSource *gsrc)
{
	FwNodeSource *src = (FwNodeSource *)gsrc;

	g_free(src->buf);
}

/**
 * hinawa_fw_node_create_source:
 * @self: A #HinawaFwNode.
 * @gsrc: (out): A #GSource.
 * @exception: A #GError. Error can be generated with domain of #hinawa_fw_node_error_quark()
 *	       and code of #HinawaFwNodeError.
 *
 * Create Gsource for GMainContext to dispatch events for the node on IEEE 1394
 * bus.
 *
 * Since: 1.4.
 */
void hinawa_fw_node_create_source(HinawaFwNode *self, GSource **gsrc,
				  GError **exception)
{
        static GSourceFuncs funcs = {
                .check          = check_src,
                .dispatch       = dispatch_src,
                .finalize       = finalize_src,
        };
	HinawaFwNodePrivate *priv;
	FwNodeSource *src;

	g_return_if_fail(HINAWA_IS_FW_NODE(self));
	g_return_if_fail(gsrc != NULL);
	g_return_if_fail(exception == NULL || *exception == NULL);

	priv = hinawa_fw_node_get_instance_private(self);
	if (priv->fd < 0) {
		generate_local_error(exception, HINAWA_FW_NODE_ERROR_NOT_OPENED);
		return;
	}

        *gsrc = g_source_new(&funcs, sizeof(FwNodeSource));
	src = (FwNodeSource *)(*gsrc);

        g_source_set_name(*gsrc, "HinawaFwNode");
        g_source_set_priority(*gsrc, G_PRIORITY_HIGH_IDLE);
        g_source_set_can_recurse(*gsrc, TRUE);

        // MEMO: allocate one page because we cannot assume the size of
        // transaction frame.
        src->len = sysconf(_SC_PAGESIZE);
        src->buf = g_malloc0(src->len);

	src->self = self;
	src->tag = g_source_add_unix_fd(*gsrc, priv->fd, G_IO_IN);
}

// Internal use only.
int hinawa_fw_node_ioctl(HinawaFwNode *self, unsigned long req, void *args, GError **exception)
{
	HinawaFwNodePrivate *priv;

	g_return_val_if_fail(HINAWA_IS_FW_NODE(self), ENXIO);
	g_return_val_if_fail(exception != NULL, EINVAL);

	priv = hinawa_fw_node_get_instance_private(self);
	if (priv->fd < 0) {
		generate_local_error(exception, HINAWA_FW_NODE_ERROR_NOT_OPENED);
		return ENXIO;
	}

	// To invalidate the transaction in a case of timeout.
	if (req == FW_CDEV_IOC_SEND_REQUEST) {
		struct fw_cdev_send_request *data = args;
		HinawaFwReq *req = (HinawaFwReq *)data->closure;
		priv->transactions = g_list_prepend(priv->transactions, req);
	}

	if (ioctl(priv->fd, req, args) < 0) {
		if (errno == ENODEV)
			generate_local_error(exception, HINAWA_FW_NODE_ERROR_DISCONNECTED);
		return errno;
	}

	return 0;
}

void hinawa_fw_node_invalidate_transaction(HinawaFwNode *self, HinawaFwReq *req)
{
	HinawaFwNodePrivate *priv;
	GList *entry;

	g_return_if_fail(HINAWA_IS_FW_NODE(self));
	priv = hinawa_fw_node_get_instance_private(self);

	g_mutex_lock(&priv->transactions_mutex);
	entry = g_list_find(priv->transactions, req);
	if (entry) {
		priv->transactions =
				g_list_delete_link(priv->transactions, entry);
	}
	g_mutex_unlock(&priv->transactions_mutex);
}
