/*-
 * Copyright (c) 2007, 2008 Edward Tomasz Napierała <trasz@FreeBSD.org>
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in the
 *    documentation and/or other materials provided with the distribution.
 *
 * ALTHOUGH THIS SOFTWARE IS MADE OF WIN AND SCIENCE, IT IS PROVIDED BY THE
 * AUTHOR AND CONTRIBUTORS ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES,
 * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY
 * AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL
 * THE AUTHOR OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
 * TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA,
 * OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
 * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
 * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 *
 */

/**
 * \file
 *
 * Standard MIDI File writer.
 *
 */

/* Reference: http://www.borg.com/~jglatt/tech/midifile.htm */

#include <stdlib.h>
#include <string.h>
#include <assert.h>
#include <math.h>
#include <errno.h>
#include "smf.h"
#include "smf_private.h"

#define MAX_VLQ_LENGTH 128

/**
 * Extends (reallocates) smf->file_buffer and returns pointer to the newly added space,
 * that is, pointer to the first byte after the previous buffer end.  Returns NULL in case
 * of error.
 */
static void *
smf_extend(smf_t *smf, const int length)
{
	int i, previous_file_buffer_length = smf->file_buffer_length;
	char *previous_file_buffer = smf->file_buffer;

	/* XXX: Not terribly efficient. */
	smf->file_buffer_length += length;
	smf->file_buffer = realloc(smf->file_buffer, smf->file_buffer_length);
	if (smf->file_buffer == NULL) {
		g_critical("realloc(3) failed: %s", strerror(errno));
		smf->file_buffer_length = 0;
		return (NULL);
	}

	/* Fix up pointers.  XXX: omgwtf. */
	for (i = 1; i <= smf->number_of_tracks; i++) {
		smf_track_t *track;
		track = smf_get_track_by_number(smf, i);
		if (track->file_buffer != NULL)
			track->file_buffer = (char *)track->file_buffer + ((char *)smf->file_buffer - previous_file_buffer);
	}

	return ((char *)smf->file_buffer + previous_file_buffer_length);
}

/**
 * Appends "buffer_length" bytes pointed to by "buffer" to the smf, reallocating storage as needed.  Returns 0
 * if everything went ok, different value if there was any problem.
 */
static int
smf_append(smf_t *smf, const void *buffer, const int buffer_length)
{
	void *dest;

	dest = smf_extend(smf, buffer_length);
	if (dest == NULL) {
		g_critical("Cannot extend track buffer.");
		return (-1);
	}

	memcpy(dest, buffer, buffer_length);

	return (0);
}

/**
 * Appends MThd header to the track.  Returns 0 if everything went ok, different value if not.
 */
static int
write_mthd_header(smf_t *smf)
{
	struct mthd_chunk_struct mthd_chunk;

	memcpy(mthd_chunk.mthd_header.id, "MThd", 4);
	mthd_chunk.mthd_header.length = g_htonl(6);
	mthd_chunk.format = g_htons(smf->format);
	mthd_chunk.number_of_tracks = g_htons(smf->number_of_tracks);
	mthd_chunk.division = g_htons(smf->ppqn);

	return (smf_append(smf, &mthd_chunk, sizeof(mthd_chunk)));
}

/**
 * Extends (reallocates) track->file_buffer and returns pointer to the newly added space,
 * that is, pointer to the first byte after the previous buffer end.  Returns NULL in case
 * of error.
 */
static void *
track_extend(smf_track_t *track, const int length)
{
	void *buf;

	assert(track->smf);

	buf = smf_extend(track->smf, length);
	if (buf == NULL)
		return (NULL);

	track->file_buffer_length += length;
	if (track->file_buffer == NULL)
		track->file_buffer = buf;

	return (buf);
}

/**
 * Appends "buffer_length" bytes pointed to by "buffer" to the track, reallocating storage as needed.  Returns 0
 * if everything went ok, different value if there was any problem.
 */
static int
track_append(smf_track_t *track, const void *buffer, const int buffer_length)
{
	void *dest;

	dest = track_extend(track, buffer_length);
	if (dest == NULL) {
		g_critical("Cannot extend track buffer.");
		return (-1);
	}

	memcpy(dest, buffer, buffer_length);

	return (0);
}

static int
format_vlq(unsigned char *buf, int length, unsigned long value)
{
	int i;
	unsigned long buffer;

	/* Taken from http://www.borg.com/~jglatt/tech/midifile/vari.htm */
	buffer = value & 0x7F;

	while ((value >>= 7)) {
		buffer <<= 8;
		buffer |= ((value & 0x7F) | 0x80);
	}

	for (i = 0;; i++) {
		buf[i] = buffer;

		if (buffer & 0x80)
			buffer >>= 8;
		else
			break;
	}

	assert(i <= length);

	/* + 1, because "i" is an offset, not a count. */
	return (i + 1);
}

smf_event_t *
smf_event_new_textual(int type, const char *text)
{
	int vlq_length, text_length, required_length;
	smf_event_t *event;

	assert(type >= 1 && type <= 9);

	text_length = strlen(text);

	event = smf_event_new();
	if (event == NULL)
		return (NULL);

	/* "2 +" is for leading 0xFF 0xtype. */
	event->midi_buffer_length = 2 + text_length + MAX_VLQ_LENGTH;
	event->midi_buffer = malloc(event->midi_buffer_length);
	if (event->midi_buffer == NULL) {
		g_critical("Cannot allocate MIDI buffer structure: %s", strerror(errno));
		smf_event_delete(event);

		return (NULL); 
	}

	event->midi_buffer[0] = 0xFF;
	event->midi_buffer[1] = type;

	vlq_length = format_vlq(event->midi_buffer + 2, MAX_VLQ_LENGTH - 2, text_length);
	required_length = g_snprintf((char *)event->midi_buffer + vlq_length + 2, event->midi_buffer_length - vlq_length - 2, "%s", text);

	assert(required_length <= event->midi_buffer_length - vlq_length - 2);
	assert(required_length == text_length);

	event->midi_buffer_length = 2 + vlq_length + text_length;

	return event;
}

/**
  * Appends value, expressed as Variable Length Quantity, to event->track.
  */
static int
write_vlq(smf_event_t *event, unsigned long value)
{
	unsigned char buf[MAX_VLQ_LENGTH];
	int vlq_length;

	vlq_length = format_vlq(buf, MAX_VLQ_LENGTH, value);

	return (track_append(event->track, buf, vlq_length));
}

/**
 * Appends event time as Variable Length Quantity.  Returns 0 if everything went ok,
 * different value in case of error.
 */
static int
write_event_time(smf_event_t *event)
{
	assert(event->delta_time_pulses >= 0);

	return (write_vlq(event, event->delta_time_pulses));
}

static int
write_sysex_contents(smf_event_t *event)
{
	int ret;
	unsigned char sysex_status = 0xF0;

	assert(smf_event_is_sysex(event));

	ret = track_append(event->track, &sysex_status, 1);
	if (ret)
		return (ret);

	/* -1, because length does not include status byte. */
	ret = write_vlq(event, event->midi_buffer_length - 1);
	if (ret)
		return (ret);

	ret = track_append(event->track, event->midi_buffer + 1, event->midi_buffer_length - 1);
	if (ret)
		return (ret);

	return (0);
}

/**
  * Appends contents of event->midi_buffer wrapped into 0xF7 MIDI event.
  */
static int
write_escaped_event_contents(smf_event_t *event)
{
	int ret;
	unsigned char escape_status = 0xF7;

	if (smf_event_is_sysex(event))
		return (write_sysex_contents(event));

	ret = track_append(event->track, &escape_status, 1);
	if (ret)
		return (ret);

	ret = write_vlq(event, event->midi_buffer_length);
	if (ret)
		return (ret);

	ret = track_append(event->track, event->midi_buffer, event->midi_buffer_length);
	if (ret)
		return (ret);

	return (0);
}

/**
 * Appends contents of event->midi_buffer.  Returns 0 if everything went 0,
 * different value in case of error.
 */
static int
write_event_contents(smf_event_t *event)
{
	if (smf_event_is_system_realtime(event) || smf_event_is_system_common(event))
		return (write_escaped_event_contents(event));

	return (track_append(event->track, event->midi_buffer, event->midi_buffer_length));
}

/**
 * Writes out an event.
 */
static int
write_event(smf_event_t *event)
{
	int ret;

	ret = write_event_time(event);
	if (ret)
		return (ret);

	ret = write_event_contents(event);
	if (ret)
		return (ret);

	return (0);
}

/**
 * Writes out MTrk header, except of MTrk chunk length, which is written by write_mtrk_length().
 */
static int
write_mtrk_header(smf_track_t *track)
{
	struct chunk_header_struct mtrk_header;

	memcpy(mtrk_header.id, "MTrk", 4);

	return (track_append(track, &mtrk_header, sizeof(mtrk_header)));
}

/**
 * Updates MTrk chunk length of a given track.
 */
static int
write_mtrk_length(smf_track_t *track)
{
	struct chunk_header_struct *mtrk_header;

	assert(track->file_buffer != NULL);
	assert(track->file_buffer_length >= 6);

	mtrk_header = (struct chunk_header_struct *)track->file_buffer;
	mtrk_header->length = g_htonl(track->file_buffer_length - sizeof(struct chunk_header_struct));

	return (0);
}

/**
 * Writes out the track.
 */
static int
write_track(smf_track_t *track)
{
	int ret;
	smf_event_t *event;

	ret = write_mtrk_header(track);
	if (ret)
		return (ret);

	while ((event = smf_track_get_next_event(track)) != NULL) {
		ret = write_event(event);
		if (ret)
			return (ret);
	}

	ret = write_mtrk_length(track);
	if (ret)
		return (ret);

	return (0);
}

/**
 * Takes smf->file_buffer and saves it to the file.
 */
static int
write_file(smf_t *smf, const char *file_name)
{
	FILE *stream;

	stream = fopen(file_name, "wb+");
	if (stream == NULL) {
		g_critical("Cannot open input file: %s", strerror(errno));

		return (-1);
	}

	if (fwrite(smf->file_buffer, 1, smf->file_buffer_length, stream) != smf->file_buffer_length) {
		g_critical("fwrite(3) failed: %s", strerror(errno));

		return (-2);
	}

	if (fclose(stream)) {
		g_critical("fclose(3) failed: %s", strerror(errno));

		return (-3);
	}

	return (0);
}

static void
free_buffer(smf_t *smf)
{
	int i;
	smf_track_t *track;

	/* Clear the pointers. */
	memset(smf->file_buffer, 0, smf->file_buffer_length);
	free(smf->file_buffer);
	smf->file_buffer = NULL;
	smf->file_buffer_length = 0;

	for (i = 1; i <= smf->number_of_tracks; i++) {
		track = smf_get_track_by_number(smf, i);
		assert(track);
		track->file_buffer = NULL;
		track->file_buffer_length = 0;
	}
}

#ifndef NDEBUG

/**
 * \return Nonzero, if all pointers supposed to be NULL are NULL.  Triggers assertion if not.
 */
static int
pointers_are_clear(smf_t *smf)
{
	int i;

	smf_track_t *track;
	assert(smf->file_buffer == NULL);
	assert(smf->file_buffer_length == 0);

	for (i = 1; i <= smf->number_of_tracks; i++) {
		track = smf_get_track_by_number(smf, i);

		assert(track != NULL);
		assert(track->file_buffer == NULL);
		assert(track->file_buffer_length == 0);
	}

	return (1);
}

#endif /* !NDEBUG */

/**
 * \return Nonzero, if event is End Of Track metaevent.
 */
int
smf_event_is_eot(const smf_event_t *event)
{
	if (event->midi_buffer_length != 3)
		return (0);

	if (event->midi_buffer[0] != 0xFF || event->midi_buffer[1] != 0x2F || event->midi_buffer[2] != 0x00)
		return (0);

	return (1);
}

/**
 * Check if SMF is valid and add missing EOT events.
 *
 * \return 0, if SMF is valid.
 */
static int
smf_validate(smf_t *smf)
{
	int trackno, eventno, eot_found;
	smf_track_t *track;
	smf_event_t *event;

	if (smf->format < 0 || smf->format > 2) {
		g_critical("SMF error: smf->format is less than zero of greater than two.");
		return (-1);
	}

	if (smf->number_of_tracks < 1) {
		g_critical("SMF error: number of tracks is less than one.");
		return (-2);
	}

	if (smf->format == 0 && smf->number_of_tracks > 1) {
		g_critical("SMF error: format is 0, but number of tracks is more than one.");
		return (-3);
	}

	if (smf->ppqn <= 0) {
		g_critical("SMF error: PPQN has to be > 0.");
		return (-4);
	}

	for (trackno = 1; trackno <= smf->number_of_tracks; trackno++) {
		track = smf_get_track_by_number(smf, trackno);
		assert(track);

		eot_found = 0;

		for (eventno = 1; eventno <= track->number_of_events; eventno++) {
			event = smf_track_get_event_by_number(track, eventno);
			assert(event);

			if (!smf_event_is_valid(event)) {
				g_critical("Event #%d on track #%d is invalid.", eventno, trackno);
				return (-5);
			}

			if (smf_event_is_eot(event)) {
				if (eot_found) {
					g_critical("Duplicate End Of Track event on track #%d.", trackno);
					return (-6);
				}

				eot_found = 1;
			}
		}

		if (!eot_found) {
			if (smf_track_add_eot_delta_pulses(track, 0)) {
				g_critical("smf_track_add_eot_delta_pulses failed.");
				return (-6);
			}
		}
				
	}

	return (0);
}

#ifndef NDEBUG

static void
assert_smf_event_is_identical(const smf_event_t *a, const smf_event_t *b)
{
	assert(a->event_number == b->event_number);
	assert(a->delta_time_pulses == b->delta_time_pulses);
	assert(abs(a->time_pulses - b->time_pulses) <= 2);
	assert(fabs(a->time_seconds - b->time_seconds) <= 0.01);
	assert(a->track_number == b->track_number);
	assert(a->midi_buffer_length == b->midi_buffer_length);
	assert(memcmp(a->midi_buffer, b->midi_buffer, a->midi_buffer_length) == 0);
}

static void
assert_smf_track_is_identical(const smf_track_t *a, const smf_track_t *b)
{
	int i;

	assert(a->track_number == b->track_number);
	assert(a->number_of_events == b->number_of_events);

	for (i = 1; i <= a->number_of_events; i++)
		assert_smf_event_is_identical(smf_track_get_event_by_number(a, i), smf_track_get_event_by_number(b, i));
}

static void
assert_smf_is_identical(const smf_t *a, const smf_t *b)
{
	int i;

	assert(a->format == b->format);
	assert(a->ppqn == b->ppqn);
	assert(a->frames_per_second == b->frames_per_second);
	assert(a->resolution == b->resolution);
	assert(a->number_of_tracks == b->number_of_tracks);

	for (i = 1; i <= a->number_of_tracks; i++)
		assert_smf_track_is_identical(smf_get_track_by_number(a, i), smf_get_track_by_number(b, i));

	/* We do not need to compare tempos explicitly, as tempo is always computed from track contents. */
}

static void
assert_smf_saved_correctly(const smf_t *smf, const char *file_name)
{
	smf_t *saved;

	saved = smf_load(file_name);
	assert(saved != NULL);

	assert_smf_is_identical(smf, saved);

	smf_delete(saved);
}

#endif /* !NDEBUG */

/**
  * Writes the contents of SMF to the file given.
  * \param smf SMF.
  * \param file_name Path to the file.
  * \return 0, if saving was successfull.
  */
int
smf_save(smf_t *smf, const char *file_name)
{
	int i, error;
	smf_track_t *track;

	smf_rewind(smf);

	assert(pointers_are_clear(smf));

	if (smf_validate(smf))
		return (-1);

	if (write_mthd_header(smf))
		return (-2);

	for (i = 1; i <= smf->number_of_tracks; i++) {
		track = smf_get_track_by_number(smf, i);

		assert(track != NULL);

		error = write_track(track);
		if (error) {
			free_buffer(smf);
			return (error);
		}
	}

	error = write_file(smf, file_name);

	free_buffer(smf);

	if (error)
		return (error);

#ifndef NDEBUG
	assert_smf_saved_correctly(smf, file_name);
#endif

	return (0);
}

