// SPDX-License-Identifier: BSD-3-Clause
//
// Copyright(c) 2018 Intel Corporation. All rights reserved.
//
// Author: Seppo Ingalsuo <seppo.ingalsuo@linux.intel.com>

#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <stdbool.h>
#include <sys/stat.h>
#include <errno.h>
#include <alsa/asoundlib.h>
#include "kernel/abi.h"
#include "kernel/header.h"
#include "ipc/stream.h"
#include "ipc/control.h"

#define BUFFER_TAG_OFFSET	0
#define BUFFER_SIZE_OFFSET	1
#define BUFFER_ABI_OFFSET	2

#define BUFFER_TLV_HEADER_BYTES ((BUFFER_ABI_OFFSET) * sizeof(uint32_t))

/* Definitions for multiple IPCs */
enum sof_ipc_type {
	SOF_IPC_TYPE_3,
	SOF_IPC_TYPE_4,
	SOF_IPC_TYPE_COUNT
};

struct ctl_data {
	/* the input file name */
	char *input_file;

	/* the input file descriptor */
	int in_fd;
	/* the output file descriptor */
	int out_fd;

	/* cached buffer for input/output */
	/* use uint32_t to ensure alignment: Warning on pointer arithmetic. */
	uint32_t *buffer;
	int buffer_size;
	int ctrl_size;

	enum sof_ipc_type ipc_type;

	/* IPC type dependent magic number to use, expect */
	uint32_t magic;

	/* flag for input/output format, binary or CSV */
	bool binary;
	/* flag for input/output format, with or without abi header */
	bool no_abi;
	/* component specific type / param_id, default 0 */
	uint32_t type;
	/* set or get control value */
	bool set;
	/* print ABI header */
	bool print_abi_header;
	int print_abi_size;

	/* name of sound card device */
	char *dev;
	char *cname;

	/* alsa ctl_elem pointers */
	snd_ctl_t *ctl;
	snd_ctl_elem_id_t *id;
	snd_ctl_elem_info_t *info;
	snd_ctl_elem_value_t *value;
};

static void usage(char *name)
{
	fprintf(stdout, "Usage:\t %s [-D <device>] [-c <control name>]", name);
	fprintf(stdout, " [-s <data>]\n");
	fprintf(stdout, "\t %s [-D <device>] [-n <control id>]", name);
	fprintf(stdout, " [-s <data>]\n");
	fprintf(stdout, "\t %s -g <size>\n", name);
	fprintf(stdout, "\t %s -h\n", name);
	fprintf(stdout, "\nWhere:\n");
	fprintf(stdout, " -D device name (default is hw:0)\n");
	fprintf(stdout, " -c control name e.g.");
	fprintf(stdout, " numid=22,name=\\\"EQIIR1.0 EQIIR\\\"\"\n");
	fprintf(stdout, " -n control id e.g. 22\n");
	fprintf(stdout, " -i {3|4} selects the IPC type to use, defaults to 3 (IPC3)\n");
	fprintf(stdout, " -g <size> generates");
	fprintf(stdout, " the current ABI header with given payload size\n");
	fprintf(stdout, " -s set data, default is using ASCII CSV input file\n");
	fprintf(stdout, " -b set/get control in binary mode(e.g. for set, use binary input file, for get, dump out in hex format)\n");
	fprintf(stdout, " -r no abi header for the input file, or not dumping abi header for get.\n");
	fprintf(stdout, " -o specify the output file.\n");
	fprintf(stdout, " -t specify the component specified type (IPC3 specific).\n");
	fprintf(stdout, " -p specify the param_id of the data (IPC4 specific).");
	fprintf(stdout, " Valid range: 0-255\n");
}

static void header_init(struct ctl_data *ctl_data)
{
	struct sof_abi_hdr *hdr =
		(struct sof_abi_hdr *)&ctl_data->buffer[BUFFER_ABI_OFFSET];

	hdr->magic = ctl_data->magic;
	hdr->type = ctl_data->type;
	hdr->abi = SOF_ABI_VERSION;
	ctl_data->buffer[BUFFER_TAG_OFFSET] = SOF_CTRL_CMD_BINARY;
}

/* Returns the number of bytes written to the control buffer */
static int read_setup(struct ctl_data *ctl_data)
{
	struct sof_abi_hdr *hdr =
		(struct sof_abi_hdr *)&ctl_data->buffer[BUFFER_ABI_OFFSET];
	int n_max = ctl_data->ctrl_size;
	char *mode = ctl_data->binary ? "rb" : "r";
	int abi_size = 0;
	uint32_t x;
	int separator;
	int n = 0;
	FILE *fh;
	int data_start_int_index = 0;
	int data_int_index;

	/* open input file */
	fh = fdopen(ctl_data->in_fd, mode);
	if (!fh) {
		fprintf(stderr, "error: %s\n", strerror(errno));
		return -errno;
	}

	/* create abi header*/
	if (ctl_data->no_abi) {
		header_init(ctl_data);
		abi_size = sizeof(struct sof_abi_hdr);
		data_start_int_index += (abi_size + BUFFER_TLV_HEADER_BYTES) / sizeof(uint32_t);
	}

	if (ctl_data->binary) {
		n = fread(&ctl_data->buffer[data_start_int_index],
			  sizeof(uint8_t), n_max - abi_size, fh);
		goto read_done;
	}

	/* reading for ASCII CSV txt */
	data_int_index = data_start_int_index;
	while (fscanf(fh, "%u", &x) != EOF) {
		if (n < n_max)
			ctl_data->buffer[data_int_index] = x;

		if (n > 0)
			fprintf(stdout, ",");

		fprintf(stdout, "%u", x);
		separator = fgetc(fh);
		while (separator != ',' && separator != EOF)
			separator = fgetc(fh);
		data_int_index++;
		n += sizeof(uint32_t);
	}

	fprintf(stdout, "\n");

read_done:
	if (ctl_data->no_abi) {
		hdr->size = n;
		n += abi_size;
		ctl_data->buffer[BUFFER_SIZE_OFFSET] = n;
		n += BUFFER_TLV_HEADER_BYTES;
	}

	if (n > n_max) {
		fprintf(stderr, "Warning: Read of %d exceeded control size. ",
				n);
		fprintf(stderr, "Please check the data file.\n");
	}

	if (hdr->magic != ctl_data->magic)
		fprintf(stderr,
			"Info: ABI mismatch: expecting: 0x%8.8x, got: 0x%8.8x\n",
			ctl_data->magic, hdr->magic);

	fclose(fh);
	return n;
}

static void header_dump(struct ctl_data *ctl_data)
{
	struct sof_abi_hdr *hdr =
		(struct sof_abi_hdr *)&ctl_data->buffer[BUFFER_ABI_OFFSET];

	fprintf(stdout, "hdr: magic 0x%8.8x\n", hdr->magic);
	if (ctl_data->ipc_type == SOF_IPC_TYPE_3)
		fprintf(stdout, "hdr: type %u\n", hdr->type);
	else
		fprintf(stdout, "hdr: param_id %u\n", hdr->type);
	fprintf(stdout, "hdr: size %d bytes\n", hdr->size);
	fprintf(stdout, "hdr: abi %d:%d:%d\n",
		SOF_ABI_VERSION_MAJOR(hdr->abi),
		SOF_ABI_VERSION_MINOR(hdr->abi),
		SOF_ABI_VERSION_PATCH(hdr->abi));
}

/* dump binary data out with 16bit hex format */
static void hex_data_dump(struct ctl_data *ctl_data)
{
	unsigned int int_offset;
	uint16_t *config;
	int n;
	int i;

	/* calculate the dumping units */
	n = (ctl_data->buffer[BUFFER_SIZE_OFFSET] + BUFFER_TLV_HEADER_BYTES) / sizeof(uint16_t);

	/* exclude the type and size header */
	int_offset = 0;

	/* exclude abi header if '-r' specified */
	if (ctl_data->no_abi) {
		int_offset += (sizeof(struct sof_abi_hdr) + BUFFER_TLV_HEADER_BYTES) /
			      sizeof(uint32_t);
		n -= (sizeof(struct sof_abi_hdr) + BUFFER_TLV_HEADER_BYTES) /
		     sizeof(uint16_t);
	}

	/* get the dumping start address */
	config = (uint16_t *)&ctl_data->buffer[int_offset];

	/* Print out in 16bit hex format */
	for (i = 0; i < n; i++) {
		if (!(i % 8))
			fprintf(stdout, "%08zx ",
				i * sizeof(uint16_t));
		fprintf(stdout, "%04x ", config[i]);
		if ((i % 8) == 7)
			fprintf(stdout, "\n");
	}
	fprintf(stdout, "\n");
}

/* dump binary data out with CSV txt format */
static void csv_data_dump(struct ctl_data *ctl_data, FILE *fh)
{
	uint32_t *config;
	int n;
	int i;
	int s = 0;

	config = &ctl_data->buffer[0];
	n = (ctl_data->buffer[BUFFER_SIZE_OFFSET] + BUFFER_TLV_HEADER_BYTES) / sizeof(uint32_t);

	if (ctl_data->no_abi)
		s = (sizeof(struct sof_abi_hdr) + BUFFER_TLV_HEADER_BYTES) / sizeof(uint32_t);

	/* Print out in CSV txt formal */
	for (i = s; i < n; i++) {
		if (i == n - 1)
			fprintf(fh, "%u\n", config[i]);
		else
			fprintf(fh, "%u,", config[i]);
	}
}

/*
 * Print the read kcontrol configuration data with either
 * 16bit Hex binary format or ASCII CSV format.
 */
static void data_dump(struct ctl_data *ctl_data)
{
	if (ctl_data->binary)
		hex_data_dump(ctl_data);
	else
		csv_data_dump(ctl_data, stdout);
}

static int get_file_size(int fd)
{
	struct stat st;
	int ret;

	if (fstat(fd, &st) == -1)
		ret = -EINVAL;
	else
		ret = st.st_size;

	return ret;
}

static int buffer_alloc(struct ctl_data *ctl_data)
{
	int buffer_size;

	/*
	 * Allocate buffer for tlv write/read. The buffer needs a two
	 * words header with tag (SOF_CTRL_CMD_BINARY) and size in bytes.
	 */
	buffer_size = ctl_data->ctrl_size + 2 * sizeof(uint32_t);
	ctl_data->buffer = calloc(1, buffer_size);
	if (!ctl_data->buffer) {
		fprintf(stderr,
			"Error: Failed to allocate buffer for user data.\n");
		return -EINVAL;
	}

	ctl_data->buffer_size = buffer_size;
	return 0;
}

static void buffer_free(struct ctl_data *ctl_data)
{
	free(ctl_data->buffer);
	ctl_data->buffer_size = 0;
}

static int ctl_setup(struct ctl_data *ctl_data)
{
	int mode = SND_CTL_NONBLOCK;
	int ctrl_size;
	int read;
	int write;
	int type;
	int ret;

	/* Open the device, mixer control and get read/write/type properties.
	 */
	ret = snd_ctl_open(&ctl_data->ctl, ctl_data->dev, mode);
	if (ret) {
		fprintf(stderr, "Error: Could not open device %s.\n",
			ctl_data->dev);
		return ret;
	}

	/* Allocate buffers for pointers info, id, and value. */
	ret = snd_ctl_elem_info_malloc(&ctl_data->info);
	if (ret) {
		fprintf(stderr, "Error: Could not malloc elem_info!\n");
		goto ctl_close;
	}
	ret = snd_ctl_elem_id_malloc(&ctl_data->id);
	if (ret) {
		fprintf(stderr, "Error: Could not malloc elem_id!\n");
		goto info_free;
	}
	ret = snd_ctl_elem_value_malloc(&ctl_data->value);
	if (ret) {
		fprintf(stderr, "Error: Could not malloc elem_value!\n");
		goto id_free;
	}

	/* Get handle id for the ascii control name. */
	ret = snd_ctl_ascii_elem_id_parse(ctl_data->id, ctl_data->cname);
	if (ret) {
		fprintf(stderr, "Error: Can't find %s.\n", ctl_data->cname);
		goto value_free;
	}

	/* Get handle info from id. */
	snd_ctl_elem_info_set_id(ctl_data->info, ctl_data->id);
	ret = snd_ctl_elem_info(ctl_data->ctl, ctl_data->info);
	if (ret) {
		fprintf(stderr, "Error: Could not get elem info.\n");
		goto value_free;
	}

	if (ctl_data->binary && ctl_data->set) {
		/* set ctrl_size to file size */
		ctrl_size = get_file_size(ctl_data->in_fd);
		if (ctrl_size <= 0) {
			fprintf(stderr, "Error: Input file unavailable.\n");
			goto value_free;
		}

		/* need more space for raw data file(no header in the file) */
		if (ctl_data->no_abi)
			ctrl_size += sizeof(struct sof_abi_hdr);
	} else {
		/* Get control attributes from info. */
		ctrl_size = snd_ctl_elem_info_get_count(ctl_data->info);
	}

	fprintf(stderr, "Control size is %d.\n", ctrl_size);
	read = snd_ctl_elem_info_is_tlv_readable(ctl_data->info);
	write = snd_ctl_elem_info_is_tlv_writable(ctl_data->info);
	type = snd_ctl_elem_info_get_type(ctl_data->info);

	if (!read && !write) {
		fprintf(stderr, "Error: Not a read/write control\n");
		goto value_free;
	}
	if (type != SND_CTL_ELEM_TYPE_BYTES) {
		fprintf(stderr, "Error: control type has no bytes support.\n");
		goto value_free;
	}

	ctl_data->ctrl_size = ctrl_size;

	/* allocate buffer for tlv data */
	ret = buffer_alloc(ctl_data);
	if (ret < 0) {
		fprintf(stderr, "Error: Could not allocate buffer, ret:%d\n",
			ret);
		goto value_free;
	}

	return ret;

value_free:
	snd_ctl_elem_value_free(ctl_data->value);
id_free:
	snd_ctl_elem_id_free(ctl_data->id);
info_free:
	snd_ctl_elem_info_free(ctl_data->info);
ctl_close:
	ret |= snd_ctl_close(ctl_data->ctl);
	return ret;
}

static int ctl_free(struct ctl_data *ctl_data)
{
	int ret;

	buffer_free(ctl_data);

	snd_ctl_elem_value_free(ctl_data->value);
	snd_ctl_elem_id_free(ctl_data->id);
	snd_ctl_elem_info_free(ctl_data->info);

	ret = snd_ctl_close(ctl_data->ctl);

	return ret;
}

static void ctl_dump(struct ctl_data *ctl_data, size_t dump_size)
{
	FILE *fh;
	int offset = 0;
	size_t n = 0;/* in bytes */

	if (ctl_data->out_fd > 0) {
		if (ctl_data->binary) {
			/* output ctl_data(exclude the header)to file */
			/* open input file */
			fh = fdopen(ctl_data->out_fd, "wb");
			if (!fh) {
				fprintf(stderr, "error: %s\n", strerror(errno));
				return;
			}

			if (dump_size)
				n  = dump_size;
			else
				n = ctl_data->buffer[BUFFER_SIZE_OFFSET] + BUFFER_TLV_HEADER_BYTES;

			if (ctl_data->no_abi) {
				offset = sizeof(struct sof_abi_hdr) / sizeof(int) +
					BUFFER_ABI_OFFSET;
				n -= sizeof(struct sof_abi_hdr);
				n -= BUFFER_ABI_OFFSET * sizeof(uint32_t);
			}
			n = fwrite(&ctl_data->buffer[offset],
				   1, n, fh);
		} else {
			fh = fdopen(ctl_data->out_fd, "w");
			if (!fh) {
				fprintf(stderr, "error: %s\n", strerror(errno));
				return;
			}
			csv_data_dump(ctl_data, fh);
		}

		fprintf(stdout, "%zd bytes written to file.\n", n);
		fclose(fh);
	} else {
		/* dump to stdout */
		header_dump(ctl_data);
		data_dump(ctl_data);
	}
}

static int ctl_set_get(struct ctl_data *ctl_data)
{
	int ret;
	size_t read_size, ref_size;

	if (!ctl_data->buffer) {
		fprintf(stderr, "Error: No buffer for set/get!\n");
		return -EINVAL;
	}

	if (ctl_data->set) {
		fprintf(stdout, "Applying configuration \"%s\" ",
			ctl_data->input_file);
		fprintf(stdout, "into device %s control %s.\n",
			ctl_data->dev, ctl_data->cname);
		read_size = read_setup(ctl_data);
		if (read_size < 1) {
			fprintf(stderr, "Error: failed data read from %s.\n",
				ctl_data->input_file);
			return -EINVAL;
		}

		ref_size = ctl_data->buffer[BUFFER_SIZE_OFFSET] + BUFFER_TLV_HEADER_BYTES;
		if (read_size != ref_size) {
			fprintf(stderr,
				"Error: Blob TLV header size %u (plus %lu) does not match with read bytes count %zu.\n",
				ctl_data->buffer[BUFFER_SIZE_OFFSET], BUFFER_TLV_HEADER_BYTES,
				read_size);
			return -EINVAL;
		}

		ret = snd_ctl_elem_tlv_write(ctl_data->ctl, ctl_data->id,
					     ctl_data->buffer);
		if (ret < 0) {
			fprintf(stderr, "Error: failed TLV write (%d)\n", ret);
			return ret;
		}
		fprintf(stdout, "Success.\n");

	} else {
		fprintf(stdout, "Retrieving configuration for ");
		fprintf(stdout, "device %s control %s.\n",
			ctl_data->dev, ctl_data->cname);
		/* set the ABI header to pass the param ID */
		header_init(ctl_data);
		ctl_data->buffer[BUFFER_SIZE_OFFSET] = ctl_data->ctrl_size;
		ret = snd_ctl_elem_tlv_read(ctl_data->ctl, ctl_data->id,
					    ctl_data->buffer,
					    ctl_data->buffer_size);
		if (ret < 0) {
			fprintf(stderr, "Error: failed TLV read.\n");
			return ret;
		}
		fprintf(stdout, "Success.\n");
	}

	return 0;
}

int main(int argc, char *argv[])
{
	char *input_file = NULL;
	char *output_file = NULL;
	struct ctl_data *ctl_data;
	int ipc_type = 3;
	char nname[256];
	int ret = 0;
	int opt;
	struct sof_abi_hdr *hdr;

	ctl_data = calloc(1, sizeof(struct ctl_data));
	if (!ctl_data) {
		fprintf(stderr,
			"Error: Failed to allocate buffer for ctl_data\n");
		return -ENOMEM;
	}

	ctl_data->dev = "hw:0";

	while ((opt = getopt(argc, argv, "hD:c:s:n:i:o:t:p:g:br")) != -1) {
		switch (opt) {
		case 'D':
			ctl_data->dev = optarg;
			break;
		case 'c':
			ctl_data->cname = optarg;
			break;
		case 'n':
			sprintf(nname, "numid=%d", atoi(optarg));
			ctl_data->cname = nname;
			break;
		case 'i':
			ipc_type = atoi(optarg);
			break;
		case 's':
			input_file = optarg;
			ctl_data->input_file = input_file;
			ctl_data->set = true;
			break;
		case 'o':
			output_file = optarg;
			break;
		case 'b':
			ctl_data->binary = true;
			break;
		case 'r':
			ctl_data->no_abi = true;
			break;
		case 't':
		case 'p':
			ctl_data->type = atoi(optarg);
			break;
		case 'g':
			ctl_data->print_abi_header = true;
			ctl_data->print_abi_size = atoi(optarg);
			break;
		case 'h':
		/* pass through */
		default:
			usage(argv[0]);
			goto struct_free;
		}
	}

	/* All arguments are switches, error if something still remains in command line */
	if (optind < argc) {
		fprintf(stderr, "Error: Non-supported argument %s.\n",
			argv[optind]);
		return -EINVAL;
	}

	switch (ipc_type) {
	case 3:
		ctl_data->ipc_type = SOF_IPC_TYPE_3;
		ctl_data->magic = SOF_ABI_MAGIC;
		break;
	case 4:
		if (ctl_data->type > 255) {
			fprintf(stderr, "error: The param_id %u is out of range\n\n",
				ctl_data->type);
			usage(argv[0]);
			goto struct_free;
		}

		ctl_data->ipc_type = SOF_IPC_TYPE_4;
		ctl_data->magic = SOF_IPC4_ABI_MAGIC;
		break;
	default:
		fprintf(stderr, "error: Unsupported IPC type: %u\n\n", ipc_type);
			usage(argv[0]);
		goto struct_free;
	}

	/* open output file */
	if (output_file) {
		ctl_data->out_fd = open(output_file, O_CREAT | O_TRUNC | O_RDWR,
					0600);
		if (ctl_data->out_fd <= 0) {
			fprintf(stderr, "error: %s\n", strerror(errno));
			goto struct_free;
		}
	}

	/* Just print the ABI header if requested */
	if (ctl_data->print_abi_header) {
		ctl_data->ctrl_size = sizeof(struct sof_abi_hdr);
		buffer_alloc(ctl_data);
		header_init(ctl_data);
		hdr = (struct sof_abi_hdr *)
			&ctl_data->buffer[BUFFER_ABI_OFFSET];
		hdr->size = ctl_data->print_abi_size;
		ctl_data->buffer[BUFFER_SIZE_OFFSET] = ctl_data->ctrl_size + hdr->size;
		ctl_dump(ctl_data, sizeof(struct sof_abi_hdr) + BUFFER_TLV_HEADER_BYTES);
		buffer_free(ctl_data);
		goto out_fd_close;
	}

	/* The control need to be defined. */
	if (!ctl_data->cname) {
		fprintf(stderr, "Error: No control was requested.\n");
		usage(argv[0]);
		goto out_fd_close;

	}

	/* open input file */
	if (input_file) {
		ctl_data->in_fd = open(input_file, O_RDONLY);
		if (ctl_data->in_fd <= 0) {
			fprintf(stderr, "error: %s\n", strerror(errno));
			goto out_fd_close;
		}
	}

	/* set up ctl_elem, allocate buffers */
	ret = ctl_setup(ctl_data);
	if (ret < 0) {
		fprintf(stderr, "Error: ctl_data setup failed, ret:%d", ret);
		goto in_fd_close;
	}

	/* set/get the tlv bytes kcontrol */
	ret = ctl_set_get(ctl_data);
	if (ret < 0) {
		fprintf(stderr, "Error: Could not %s control, ret:%d\n",
			ctl_data->set ? "set" : "get", ret);
		goto data_free;
	}

	/* dump the tlv buffer to a file or stdout */
	ctl_dump(ctl_data, 0);

data_free:
	ret = ctl_free(ctl_data);

in_fd_close:
	if (ctl_data->out_fd)
		close(ctl_data->out_fd);

out_fd_close:
	if (ctl_data->in_fd)
		close(ctl_data->in_fd);

struct_free:
	free(ctl_data);

	return ret;
}
