// SPDX-License-Identifier: BSD-3-Clause
//
// Copyright(c) 2022 Intel Corporation. All rights reserved.
//
// Author: Adrian Bonislawski <adrian.bonislawski@intel.com>
//         Jyri Sarha <jyri.sarha@intel.com> (restructured and moved to this file)

#include <ctype.h>
#include <errno.h>
#include <inttypes.h>
#include <stdbool.h>
#include <stddef.h>
#include <stdio.h>
#include <stdint.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include <unistd.h>

#include "probe_dma_frame.h"

#include "probes_wave.h"

#define APP_NAME "sof-probes"

#define PACKET_MAX_SIZE	4096	/**< Size limit for probe data packet */
#define DATA_READ_LIMIT 4096	/**< Data limit for file read */
#define FILES_LIMIT	32	/**< Maximum num of probe output files */
#define FILE_PATH_LIMIT 128	/**< Path limit for probe output files */
#define IDLE_TIMEOUT_MS 200	/**< Close idle audio files after this many ms */

struct wave_files {
	FILE *fd;
	uint32_t buffer_id;
	uint32_t fmt;
	uint32_t size;
	char path[FILE_PATH_LIMIT];
	struct wave header;
	struct timespec last_write;
};

enum p_state {
	READY = 0,		/**< At this stage app is looking for a SYNC word */
	SYNC,			/**< SYNC received, copying data */
	CHECK			/**< Check crc and save packet if valid */
};

struct dma_frame_parser {
	bool log_to_stdout;
	enum p_state state;
	struct probe_data_packet *packet;
	size_t packet_size;
	uint8_t *w_ptr;				/* Write pointer to copy data to */
	uint32_t total_data_to_copy;		/* Total bytes left to copy */
	int start;				/* Start of unfilled data */
	int len;				/* Data buffer fill level */
	uint8_t data[DATA_READ_LIMIT];
	struct wave_files files[FILES_LIMIT];
};

static uint32_t sample_rate[] = {
	8000, 11025, 12000, 16000, 22050, 24000, 32000, 44100,
	48000, 64000, 88200, 96000, 128000, 176400, 192000
};

int get_buffer_file(struct wave_files *files, uint32_t buffer_id)
{
	int i;

	for (i = 0; i < FILES_LIMIT; i++) {
		if (files[i].fd != NULL && files[i].buffer_id == buffer_id)
			return i;
	}
	return -1;
}

int get_buffer_file_free(struct wave_files *files)
{
	int i;

	for (i = 0; i < FILES_LIMIT; i++) {
		if (files[i].fd == NULL)
			return i;
	}
	return -1;
}

bool is_audio_format(uint32_t format)
{
	return (format & PROBE_MASK_FMT_TYPE) != 0 && (format & PROBE_MASK_AUDIO_FMT) == 0;
}

static void make_buffer_path(char *path, uint32_t buffer_id,
			     uint32_t file_index, bool audio)
{
	const char *ext = audio ? "wav" : "bin";

	if (file_index == 0)
		sprintf(path, "buffer_%#x.%s", buffer_id, ext);
	else
		sprintf(path, "buffer_%#x-%d.%s", buffer_id, file_index, ext);
}

int init_wave(struct dma_frame_parser *p, uint32_t buffer_id, uint32_t format)
{
	bool audio = is_audio_format(format);
	uint32_t file_index;
	int i;

	i = get_buffer_file_free(p->files);
	if (i == -1) {
		fprintf(stderr, "error: too many buffers\n");
		exit(0);
	}

	/* Find a filename that does not collide with existing files */
	for (file_index = 0; ; file_index++) {
		make_buffer_path(p->files[i].path, buffer_id, file_index, audio);
		if (access(p->files[i].path, F_OK) != 0)
			break;
	}

	fprintf(stderr, "%s:\t Creating file %s\n", APP_NAME, p->files[i].path);

	if (!audio && p->log_to_stdout) {
		p->files[i].fd = stdout;
	} else {
		p->files[i].fd = fopen(p->files[i].path, "wb");
		if (!p->files[i].fd) {
			fprintf(stderr, "error: unable to create file %s, error %d\n",
				p->files[i].path, errno);
			exit(0);
		}
	}

	p->files[i].buffer_id = buffer_id;
	p->files[i].fmt = format;
	clock_gettime(CLOCK_MONOTONIC, &p->files[i].last_write);

	if (!audio)
		return i;

	p->files[i].header.riff.chunk_id = HEADER_RIFF;
	p->files[i].header.riff.format = HEADER_WAVE;
	p->files[i].header.fmt.subchunk_id = HEADER_FMT;
	p->files[i].header.fmt.subchunk_size = 16;
	p->files[i].header.fmt.audio_format = 1;
	p->files[i].header.fmt.num_channels = ((format & PROBE_MASK_NB_CHANNELS) >> PROBE_SHIFT_NB_CHANNELS) + 1;
	p->files[i].header.fmt.sample_rate = sample_rate[(format & PROBE_MASK_SAMPLE_RATE) >> PROBE_SHIFT_SAMPLE_RATE];
	p->files[i].header.fmt.bits_per_sample = (((format & PROBE_MASK_CONTAINER_SIZE) >> PROBE_SHIFT_CONTAINER_SIZE) + 1) * 8;
	p->files[i].header.fmt.byte_rate = p->files[i].header.fmt.sample_rate *
					p->files[i].header.fmt.num_channels *
					p->files[i].header.fmt.bits_per_sample / 8;
	p->files[i].header.fmt.block_align = p->files[i].header.fmt.num_channels *
					  p->files[i].header.fmt.bits_per_sample / 8;
	p->files[i].header.data.subchunk_id = HEADER_DATA;

	fwrite(&p->files[i].header, sizeof(struct wave), 1, p->files[i].fd);

	return i;
}

/**
 * Update WAV header sizes and close a single audio file.
 * Safe to call on non-audio, NULL-fd, or stdout entries (no-ops).
 */
static void close_wave_file(struct wave_files *f, bool log)
{
	uint32_t chunk_size;

	if (!f->fd || f->fd == stdout) {
		f->fd = NULL;
		return;
	}

	if (log)
		fprintf(stderr, "%s:\t Closing idle file %s\n",
			APP_NAME, f->path);

	if (is_audio_format(f->fmt)) {
		chunk_size = f->size + sizeof(struct wave) -
			     offsetof(struct riff_chunk, format);

		fseek(f->fd, sizeof(uint32_t), SEEK_SET);
		fwrite(&chunk_size, sizeof(uint32_t), 1, f->fd);
		fseek(f->fd, sizeof(struct wave) -
		      offsetof(struct data_subchunk, subchunk_size),
		      SEEK_SET);
		fwrite(&f->size, sizeof(uint32_t), 1, f->fd);
	}

	fclose(f->fd);
	f->fd = NULL;
}

void finalize_wave_files(struct dma_frame_parser *p)
{
	uint32_t i;

	for (i = 0; i < FILES_LIMIT; i++) {
		if (!p->files[i].fd)
			continue;

		close_wave_file(&p->files[i], false);
	}
}

void parser_close_idle_files(struct dma_frame_parser *p)
{
	struct timespec now;
	long long elapsed_ms;
	uint32_t i;

	clock_gettime(CLOCK_MONOTONIC, &now);

	for (i = 0; i < FILES_LIMIT; i++) {
		if (!p->files[i].fd)
			continue;

		/* Only idle-close audio files */
		if (!is_audio_format(p->files[i].fmt))
			continue;

		elapsed_ms =
			(now.tv_sec - p->files[i].last_write.tv_sec) * 1000LL +
			(now.tv_nsec - p->files[i].last_write.tv_nsec) / 1000000LL;

		if (elapsed_ms >= IDLE_TIMEOUT_MS)
			close_wave_file(&p->files[i], true);
	}
}

int validate_data_packet(struct probe_data_packet *packet)
{
	uint64_t *checksump;
	uint64_t sum;

	sum = (uint32_t) (packet->sync_word +
			  packet->buffer_id  +
			  packet->format +
			  packet->timestamp_high +
			  packet->timestamp_low +
			  packet->data_size_bytes);

	checksump = (uint64_t *) (packet->data + packet->data_size_bytes);

	if (sum != *checksump) {
		fprintf(stderr, "Checksum error 0x%016" PRIx64 " != 0x%016" PRIx64 "\n",
			sum, *checksump);
		return -EINVAL;
	}

	return 0;
}

int process_sync(struct dma_frame_parser *p)
{
	struct probe_data_packet *temp_packet;

	/* request to copy data_size from probe packet and 64-bit checksum */
	p->total_data_to_copy = p->packet->data_size_bytes + sizeof(uint64_t);

	if (sizeof(struct probe_data_packet) + p->total_data_to_copy >
	    p->packet_size) {
		p->packet_size = sizeof(struct probe_data_packet) +
			p->total_data_to_copy;

		temp_packet = realloc(p->packet, p->packet_size);

		if (!temp_packet)
			return -ENOMEM;

		p->packet = temp_packet;
	}

	p->w_ptr = (uint8_t *)p->packet->data;

	return 0;
}

struct dma_frame_parser *parser_init(void)
{
	struct dma_frame_parser *p = malloc(sizeof(*p));
	if (!p) {
		fprintf(stderr, "error: allocation failed, err %d\n",
			errno);
		return NULL;
	}
	memset(p, 0, sizeof(*p));
	p->packet = malloc(PACKET_MAX_SIZE);
	if (!p) {
		fprintf(stderr, "error: allocation failed, err %d\n",
			errno);
		free(p);
		return NULL;
	}
	memset(p->packet, 0, PACKET_MAX_SIZE);
	p->packet_size = PACKET_MAX_SIZE;
	return p;
}

void parser_free(struct dma_frame_parser *p)
{
	free(p->packet);
	free(p);
}

void parser_log_to_stdout(struct dma_frame_parser *p)
{
	p->log_to_stdout = true;
}

void parser_fetch_free_buffer(struct dma_frame_parser *p, uint8_t **d, size_t *len)
{
	*d = &p->data[p->start];
	*len = sizeof(p->data) - p->start;
}

int parser_parse_data(struct dma_frame_parser *p, size_t d_len)
{
	uint i = 0;

	p->len = p->start + d_len;
	/* processing all loaded bytes */
	while (i < p->len) {
		if (p->total_data_to_copy == 0) {
			switch (p->state) {
			case READY:
				/* check for SYNC */
				if (p->len - i < sizeof(p->packet->sync_word)) {
					p->start = p->len - i;
					memmove(&p->data[0], &p->data[i], p->start);
					i += p->start;
				} else if (*((uint32_t *)&p->data[i]) ==
					   PROBE_EXTRACT_SYNC_WORD) {
					memset(p->packet, 0, p->packet_size);
					/* request to copy full data packet */
					p->total_data_to_copy =
						sizeof(struct probe_data_packet);
					p->w_ptr = (uint8_t *)p->packet;
					p->state = SYNC;
					p->start = 0;
				} else {
					i++;
				}
				break;
			case SYNC:
				/* SYNC -> CHECK */
				if (process_sync(p) < 0) {
					fprintf(stderr, "OOM, quitting\n");
					return -ENOMEM;
				}
				p->state = CHECK;
				break;
			case CHECK:
				/* CHECK -> READY */
				/* find corresponding file and save data if valid */
				if (validate_data_packet(p->packet) == 0) {
					int file = get_buffer_file(p->files,
								   p->packet->buffer_id);

					if (file < 0)
						file = init_wave(p, p->packet->buffer_id,
								 p->packet->format);

					if (file < 0) {
						fprintf(stderr,
							"unable to open file for %#x\n",
							p->packet->buffer_id);
						return -EIO;
					}

					fwrite(p->packet->data, 1,
					       p->packet->data_size_bytes,
					       p->files[file].fd);
					p->files[file].size += p->packet->data_size_bytes;
					clock_gettime(CLOCK_MONOTONIC,
						      &p->files[file].last_write);
				}
				p->state = READY;
				break;
			}
		}
		/* data copying section */
		if (p->total_data_to_copy > 0) {
			uint data_to_copy;

			/* check if there is enough bytes loaded */
			/* or copy partially if not */
			if (i + p->total_data_to_copy > p->len) {
				data_to_copy = p->len - i;
				p->total_data_to_copy -= data_to_copy;
			} else {
				data_to_copy = p->total_data_to_copy;
				p->total_data_to_copy = 0;
			}
			memcpy(p->w_ptr, &p->data[i], data_to_copy);
			p->w_ptr += data_to_copy;
			i += data_to_copy;
		}
	}
	return 0;
}
