/*
 * This file is part of the DXX-Rebirth project <https://www.dxx-rebirth.com/>.
 * It is copyright by its individual contributors, as recorded in the
 * project's Git history.  See COPYING.txt at the top level for license
 * terms and a link to the Git history.
 */
/*
 * This code handles HMP files. It can:
 * - Open/read/close HMP files
 * - Play HMP via Windows MIDI
 * - Convert HMP to MIDI for further use
 * Based on work of Arne de Bruijn and the JFFEE project
 */
#include <stdexcept>
#include <string.h>
#include <stdlib.h>
#include <stdio.h>

#include "hmp.h"
#include "u_mem.h"
#include "console.h"
#include "timer.h"
#include "serial.h"

#include "dxxsconf.h"
#include "dsx-ns.h"
#include "compiler-range_for.h"
#include "d_range.h"
#include "d_underlying_value.h"
#include "d_zip.h"
#include "partial_range.h"

namespace dcx {

#define MIDIINT(x)	(words_bigendian ? (x) : (SWAPINT(x)))
#define MIDISHORT(x)	(words_bigendian ? (x) : (SWAPSHORT(x)))

#ifdef _WIN32
namespace {
static int midi_volume;
static int channel_volume[16];
static void hmp_stop(hmp_file *hmp);
}
#endif

// READ/OPEN/CLOSE HMP

hmp_file::~hmp_file()
{
#ifdef _WIN32
	hmp_stop(this);
#endif
}

hmp_open_result hmp_open(const char *const filename)
{
	int tempo;
	const auto fpe = PHYSFSX_openReadBuffered(filename);

	if (!fpe.first)
		return {nullptr, hmp_open_error::physfs_open, fpe.second};
	auto &fp = fpe.first;

	char buf[8];
	const auto physfs_error = [](hmp_open_error e) {
		return hmp_open_result{nullptr, e, PHYSFS_getLastErrorCode()};
	};
	const auto data_error = [](hmp_open_error e) {
		return hmp_open_result{nullptr, e, PHYSFS_ErrorCode{}};
	};
	if (constexpr std::size_t buffer_size{std::size(buf)}; PHYSFSX_readBytes(fp, std::data(buf), buffer_size) != buffer_size)
		return physfs_error(hmp_open_error::physfs_read_header);
	if (memcmp(buf, "HMIMIDIP", 8))
		return data_error(hmp_open_error::content_header);

	if (!PHYSFS_seek(fp, 0x30))
		return physfs_error(hmp_open_error::physfs_seek0);

	unsigned num_tracks;
	if (PHYSFSX_readBytes(fp, &num_tracks, 4) != 4)
		return physfs_error(hmp_open_error::physfs_read_tracks);

	if ((num_tracks < 1) || (num_tracks > HMP_TRACKS))
		return data_error(hmp_open_error::track_count);
	std::unique_ptr<hmp_file> hmp(new hmp_file{});
	hmp->num_trks = num_tracks;

	if (!PHYSFS_seek(fp, 0x38))
		return physfs_error(hmp_open_error::physfs_seek1);
	if (PHYSFSX_readBytes(fp, &tempo, 4) != 4)
		return physfs_error(hmp_open_error::physfs_read_tempo);
	hmp->tempo = INTEL_INT(tempo);

	if (!PHYSFS_seek(fp, 0x308))
		return physfs_error(hmp_open_error::physfs_seek2);

	range_for (auto &i, partial_range(hmp->trks, num_tracks))
	{
		std::array<int, 3> tdata;
		/* Discard tdata[0] and tdata[2].  They are read to avoid needing a
		 * seek operation to skip them.
		 */
		if (constexpr std::size_t buffer_size{std::size(tdata) * sizeof(tdata[0])}; PHYSFSX_readBytes(fp, std::data(tdata), buffer_size) != buffer_size)
			return physfs_error(hmp_open_error::physfs_read_track_length);

		int data{tdata[1]};
		data -= 12;
		i.len = data;
		i.data = std::make_unique<uint8_t[]>(data);
		/* finally, read track data */
		if (PHYSFSX_readBytes(fp, i.data.get(), data) != data)
			return physfs_error(hmp_open_error::physfs_read_track_data);
		i.loop_set = 0;
	}
	hmp->filesize = PHYSFS_fileLength(fp);
	return {std::move(hmp), hmp_open_error::None, PHYSFS_ErrorCode{}};
}

#ifdef _WIN32
// PLAY HMP AS MIDI
namespace {

void hmp_stop(hmp_file *hmp)
{
	MIDIHDR *mhdr;
	if (!hmp->stop) {
		hmp->stop = 1;
		//PumpMessages();
		midiStreamStop(hmp->hmidi);
		while (hmp->bufs_in_mm)
			timer_delay(1);
	}
	while ((mhdr = hmp->evbuf)) {
		midiOutUnprepareHeader(reinterpret_cast<HMIDIOUT>(hmp->hmidi), mhdr, sizeof(MIDIHDR));
		hmp->evbuf = mhdr->lpNext;
	}

	if (hmp->hmidi) {
		midiStreamClose(hmp->hmidi);
		hmp->hmidi = NULL;
	}
}

/*
 * read a HMI type variable length number
 */
static int get_var_num_hmi(unsigned char *data, int datalen, unsigned long *value) {
	unsigned char *p;
	unsigned long v = 0;
	int shift = 0;

	p = data;
	while ((datalen > 0) && !(*p & 0x80)) {
		v += *(p++) << shift;
		shift += 7;
		datalen --;
	}
	if (!datalen)
		return 0;
	v += (*(p++) & 0x7f) << shift;
	if (value) *value = v;
	return p - data;
}

/*
 * read a MIDI type variable length number
 */
static int get_var_num(unsigned char *data, int datalen,
	unsigned *value) {
	unsigned char *orgdata = data;
	unsigned long v = 0;

	while ((datalen > 0) && (*data & 0x80))
		v = (v << 7) + (*(data++) & 0x7f);
	if (!datalen)
		return 0;
	v = (v << 7) + *(data++);
	if (value) *value = v;
	return data - orgdata;
}

static int get_event(hmp_file *hmp, event *ev) {
	static const std::array<int, 7> cmdlen{{3,3,3,3,2,2,3}};
	unsigned long got;
	unsigned long mindelta, delta;
	int ev_num;
	hmp_track *fndtrk = nullptr;

	mindelta = INT_MAX;
	range_for (auto &rtrk, partial_range(hmp->trks, static_cast<unsigned>(hmp->num_trks)))
	{
		const auto trk = &rtrk;
		if (!trk->left || (hmp->loop_start && hmp->looping && !trk->loop_set))
			continue;
		if (!(got = get_var_num_hmi(trk->cur, trk->left, &delta)))
			return HMP_INVALID_FILE;
		if (trk->left > got + 2 && *(trk->cur + got) == 0xff
			&& *(trk->cur + got + 1) == 0x2f) {/* end of track */
			trk->left = 0;
			continue;
		}

		if (hmp->loop_start && hmp->looping)
			if (trk->cur == trk->loop)
				delta = trk->loop_start - hmp->loop_start;

        delta += trk->cur_time - hmp->cur_time;
		if (delta < mindelta) {
			mindelta = delta;
			fndtrk = trk;
		}
	}
	const auto trk = fndtrk;
	if (!trk)
			return HMP_EOF;

	got = get_var_num_hmi(trk->cur, trk->left, &delta);

	if (hmp->loop_start && hmp->looping)
		if (trk->cur == trk->loop)
			delta = trk->loop_start - hmp->loop_start;

	trk->cur_time += delta;

	if (!hmp->loop_start && *(trk->cur + got) >> 4 == MIDI_CONTROL_CHANGE && *(trk->cur + got + 1) == HMP_LOOP_START)
	{
	    hmp->loop_start = trk->cur_time;
	    if ((hmp->filesize == 86949) && (trk->cur_time == 29)) // special ugly HACK HACK HACK for Descent2-version of descent.hmp where loop at this point causes instruments not being reset properly. No track supporting HMP loop I know of meets the requirements for the condition below and even if so - it only disabled the HMP loop feature.
	    {
	        hmp->loop_start = 0;
	    }

	}

	if (!hmp->loop_end && *(trk->cur + got) >> 4 == MIDI_CONTROL_CHANGE && *(trk->cur + got + 1) == HMP_LOOP_END)
		hmp->loop_end = trk->cur_time;

	if (hmp->loop_start && !trk->loop_set && trk->cur_time >= hmp->loop_start)
	{
		trk->loop_start = trk->cur_time;
		trk->loop = trk->cur;
		trk->len = trk->left;
		trk->loop_set = 1;
	}

	if (hmp->loop_end && trk->cur_time > hmp->loop_end)
		return HMP_EOF;

	ev->delta = trk->cur_time - hmp->cur_time;
	hmp->cur_time = trk->cur_time;

	if ((trk->left -= got) < 3)
		return HMP_INVALID_FILE;
	trk->cur += got;
	/*memset(ev, 0, sizeof(*ev));*/ev->datalen = 0;
	ev->msg[0] = ev_num = *(trk->cur++);
	trk->left--;
	if (ev_num < 0x80)
		return HMP_INVALID_FILE; /* invalid command */
	if (ev_num < 0xf0) {
		ev->msg[1] = *(trk->cur++);
		trk->left--;
		if (cmdlen[((ev_num) >> 4) - 8] == 3) {
			ev->msg[2] = *(trk->cur++);
			trk->left--;

			if (ev->msg[0] >> 4 == MIDI_CONTROL_CHANGE && ev->msg[1] == MIDI_VOLUME)
			{
				channel_volume[ev->msg[0] & 0xf] = ev->msg[2];

				ev->msg[2] = ev->msg[2] * midi_volume / MIDI_VOLUME_SCALE;
			}
		}
	} else if (ev_num == 0xff) {
		ev->msg[1] = *(trk->cur++);
		trk->left--;
		if (!(got = get_var_num(ev->data = trk->cur,
			trk->left, &ev->datalen)))
			return HMP_INVALID_FILE;
		trk->cur += ev->datalen;
		if (trk->left <= ev->datalen)
			return HMP_INVALID_FILE;
		trk->left -= ev->datalen;
	} else /* sysex -> error */
	    return HMP_INVALID_FILE;
	return 0;
}

static int fill_buffer(hmp_file *hmp) {
	MIDIHDR *mhdr = hmp->evbuf;
	unsigned int *p = reinterpret_cast<unsigned int *>(mhdr->lpData + mhdr->dwBytesRecorded);
	unsigned int *pend = reinterpret_cast<unsigned int *>(mhdr->lpData + mhdr->dwBufferLength);
	unsigned int i;
	event ev{};

	while (p + 4 <= pend) {
		if (hmp->pending_size) {
			i = (p - pend) * 4;
			if (i > hmp->pending_size)
				i = hmp->pending_size;
			*(p++) = hmp->pending_event | i;
			*(p++) = 0;
			memcpy(reinterpret_cast<uint8_t *>(p), hmp->pending, i);
			hmp->pending_size -= i;
			p += (i + 3) / 4;
		} else {
			if ((i = get_event(hmp, &ev))) {
                                mhdr->dwBytesRecorded = (reinterpret_cast<uint8_t *>(p)) - (reinterpret_cast<uint8_t *>(mhdr->lpData));
				return i;
			}
			if (ev.datalen) {
				hmp->pending_size = ev.datalen;
				hmp->pending = ev.data;
				hmp->pending_event = ev.msg[0] << 24;
			} else {
				*(p++) = ev.delta;
				*(p++) = 0;
				*(p++) = (static_cast<DWORD>(MEVT_SHORTMSG) << 24) |
					static_cast<DWORD>(ev.msg[0]) |
					(static_cast<DWORD>(ev.msg[1]) << 8) |
					(static_cast<DWORD>(ev.msg[2]) << 16);
			}
		}
	}
        mhdr->dwBytesRecorded = (reinterpret_cast<uint8_t *>(p)) - (reinterpret_cast<uint8_t *>(mhdr->lpData));
	return 0;
}

static void setup_buffers(hmp_file &hmp)
{
	MIDIHDR *lastbuf = nullptr;
	for (auto &&[header, buffer] : zip(hmp.midi_headers, hmp.midi_buffers))
	{
		header = {};
		header.lpData = buffer.data();
		header.dwBufferLength = std::size(buffer);
		header.dwUser = reinterpret_cast<uintptr_t>(std::addressof(hmp));
		header.lpNext = lastbuf;
		lastbuf = &header;
	}
	hmp.evbuf = lastbuf;
}

static void reset_tracks(struct hmp_file *hmp)
{
	if (hmp->num_trks > 0)
	range_for (auto &i, partial_range(hmp->trks, static_cast<unsigned>(hmp->num_trks)))
	{
		if (i.loop_set)
			i.cur = i.loop;
		else
			i.cur = i.data.get();
		i.left = i.len;
		i.cur_time = 0;
	}
	hmp->cur_time = 0;
}

static void _stdcall midi_callback(HMIDISTRM, UINT uMsg, DWORD, DWORD_PTR dw1, DWORD)
{
	hmp_file *hmp;
	int rc;

	if (uMsg != MOM_DONE)
		return;

	const auto mhdr = reinterpret_cast<MIDIHDR *>(dw1);
	mhdr->dwBytesRecorded = 0;
	hmp = reinterpret_cast<hmp_file *>(mhdr->dwUser);
	mhdr->lpNext = hmp->evbuf;
	hmp->evbuf = mhdr;
	hmp->bufs_in_mm--;

	if (!hmp->stop) {
		while (fill_buffer(hmp) == HMP_EOF) {
			if (hmp->bLoop)
				hmp->stop = 0;
			else
				hmp->stop = 1;

			hmp->looping = 1;

			reset_tracks(hmp);
		}
		if ((rc = midiStreamOut(hmp->hmidi, hmp->evbuf,
			sizeof(MIDIHDR))) != MMSYSERR_NOERROR) {
			/* ??? */
		} else {
			hmp->evbuf = hmp->evbuf->lpNext;
			hmp->bufs_in_mm++;
		}
	}
}

static void setup_tempo(hmp_file *hmp, unsigned long tempo) {
	MIDIHDR *mhdr = hmp->evbuf;
	unsigned int *p = reinterpret_cast<unsigned int *>(mhdr->lpData + mhdr->dwBytesRecorded);
	*(p++) = 0;
	*(p++) = 0;
	*(p++) = ((static_cast<DWORD>(MEVT_TEMPO))<<24) | tempo;
	mhdr->dwBytesRecorded += 12;
}

}

void hmp_setvolume(hmp_file *hmp, int volume)
{
	if (hmp)
		range_for (const int channel, xrange(16u))
			midiOutShortMsg(reinterpret_cast<HMIDIOUT>(hmp->hmidi), static_cast<DWORD>(channel | MIDI_CONTROL_CHANGE << 4 | MIDI_VOLUME << 8 | (channel_volume[channel] * volume / MIDI_VOLUME_SCALE) << 16));

	midi_volume = volume;
}

int hmp_play(hmp_file *hmp, int bLoop)
{
	int rc;
	MIDIPROPTIMEDIV mptd;

	hmp->bLoop = bLoop;
	hmp->loop_start = 0;
	hmp->loop_end = 0;
	hmp->looping = 0;
	hmp->devid = MIDI_MAPPER;

	setup_buffers(*hmp);
	if ((midiStreamOpen(&hmp->hmidi, &hmp->devid, 1, reinterpret_cast<DWORD_PTR>(&midi_callback), 0, CALLBACK_FUNCTION)) != MMSYSERR_NOERROR)
	{
		hmp->hmidi = NULL;
		return HMP_MM_ERR;
	}
	mptd.cbStruct  = sizeof(mptd);
	mptd.dwTimeDiv = hmp->tempo;
	if ((midiStreamProperty(hmp->hmidi, reinterpret_cast<BYTE *>(&mptd), MIDIPROP_SET|MIDIPROP_TIMEDIV)) != MMSYSERR_NOERROR)
	{
		return HMP_MM_ERR;
	}

	reset_tracks(hmp);
	setup_tempo(hmp, 0x0f4240);

	hmp->stop = 0;
	while (hmp->evbuf) {
		if ((rc = fill_buffer(hmp))) {
			if (rc == HMP_EOF) {
				reset_tracks(hmp);
				continue;
			} else
				return rc;
		}
 		if ((rc = midiOutPrepareHeader(reinterpret_cast<HMIDIOUT>(hmp->hmidi), hmp->evbuf,
			sizeof(MIDIHDR))) != MMSYSERR_NOERROR) {
			return HMP_MM_ERR;
		}
		if ((rc = midiStreamOut(hmp->hmidi, hmp->evbuf,
			sizeof(MIDIHDR))) != MMSYSERR_NOERROR) {
			return HMP_MM_ERR;
		}
		hmp->evbuf = hmp->evbuf->lpNext;
		hmp->bufs_in_mm++;
	}
	midiStreamRestart(hmp->hmidi);
	return 0;
}

void hmp_pause(hmp_file *hmp)
{
	if (hmp)
		midiStreamPause(hmp->hmidi);
}

void hmp_resume(hmp_file *hmp)
{
	if (hmp)
		midiStreamRestart(hmp->hmidi);
}

void hmp_reset()
{
	HMIDIOUT hmidi;
	MIDIHDR mhdr;
	MMRESULT rval;
	char GS_Reset[] = { 0xF0, 0x41, 0x10, 0x42, 0x12, 0x40, 0x00, 0x7F, 0x00, 0x41, 0xF7 };


	if ((rval = midiOutOpen(&hmidi, MIDI_MAPPER, 0, 0, 0)) != MMSYSERR_NOERROR)
	{
		switch (rval)
		{
			case MIDIERR_NODEVICE:
				con_printf(CON_DEBUG, "midiOutOpen Error: no MIDI port was found.");
				break;
			case MMSYSERR_ALLOCATED:
				con_printf(CON_DEBUG, "midiOutOpen Error: specified resource is already allocated.");
				break;
			case MMSYSERR_BADDEVICEID:
				con_printf(CON_DEBUG, "midiOutOpen Error: specified device identifier is out of range.");
				break;
			case MMSYSERR_INVALPARAM:
				con_printf(CON_DEBUG, "midiOutOpen Error: specified pointer or structure is invalid.");
				break;
			case MMSYSERR_NOMEM:
				con_printf(CON_DEBUG, "midiOutOpen Error: unable to allocate or lock memory.");
				break;
			default:
				con_printf(CON_DEBUG, "midiOutOpen Error code %i",rval);
				break;
		}
		return;
	}

	range_for (const int channel, xrange(16u))
	{
		midiOutShortMsg(hmidi, static_cast<DWORD>(channel | MIDI_CONTROL_CHANGE << 4 | MIDI_ALL_SOUNDS_OFF << 8 | 0 << 16));
		midiOutShortMsg(hmidi, static_cast<DWORD>(channel | MIDI_CONTROL_CHANGE << 4 | MIDI_RESET_ALL_CONTROLLERS << 8 | 0 << 16));
		midiOutShortMsg(hmidi, static_cast<DWORD>(channel | MIDI_CONTROL_CHANGE << 4 | MIDI_ALL_NOTES_OFF << 8 | 0 << 16));

		channel_volume[channel] = 100;
		midiOutShortMsg(hmidi, static_cast<DWORD>(channel | MIDI_CONTROL_CHANGE << 4 | MIDI_PANPOT << 8 | 64 << 16));
		midiOutShortMsg(hmidi, static_cast<DWORD>(channel | MIDI_CONTROL_CHANGE << 4 | MIDI_REVERB << 8 | 40 << 16));
		midiOutShortMsg(hmidi, static_cast<DWORD>(channel | MIDI_CONTROL_CHANGE << 4 | MIDI_CHORUS << 8 | 0 << 16));

		midiOutShortMsg(hmidi, static_cast<DWORD>(channel | MIDI_CONTROL_CHANGE << 4 | MIDI_BANK_SELECT_MSB << 8 | 0 << 16));
		midiOutShortMsg(hmidi, static_cast<DWORD>(channel | MIDI_CONTROL_CHANGE << 4 | MIDI_BANK_SELECT_LSB << 8 | 0 << 16));
		midiOutShortMsg(hmidi, static_cast<DWORD>(channel | MIDI_PROGRAM_CHANGE << 4 | 0 << 8));
	}

	mhdr.lpData = GS_Reset;
	mhdr.dwBufferLength = sizeof(GS_Reset);
	mhdr.dwFlags = 0;
	if ((rval = midiOutPrepareHeader(hmidi, &mhdr, sizeof(MIDIHDR))) == MMSYSERR_NOERROR)
	{
		if ((rval = midiOutLongMsg(hmidi, &mhdr, sizeof(MIDIHDR))) == MMSYSERR_NOERROR)
		{
			fix64 wait_done = timer_query();
			while (!(mhdr.dwFlags & MHDR_DONE))
			{
				auto timer = timer_update();
				if (timer >= wait_done + F1_0)
				{
					con_printf(CON_DEBUG, "hmp_reset: Timeout waiting for MHDR_DONE");
					break;
				}
			}
		}
		else
		{
			switch (rval)
			{
				case MIDIERR_NOTREADY:
					con_printf(CON_DEBUG, "midiOutLongMsg Error: the hardware is busy with other data.");
					break;
				case MIDIERR_UNPREPARED:
					con_printf(CON_DEBUG, "midiOutLongMsg Error: the buffer pointed to by lpMidiOutHdr has not been prepared.");
					break;
				case MMSYSERR_INVALHANDLE:
					con_printf(CON_DEBUG, "midiOutLongMsg Error: the specified device handle is invalid.");
					break;
				case MMSYSERR_INVALPARAM:
					con_printf(CON_DEBUG, "midiOutLongMsg Error: the specified pointer or structure is invalid.");
					break;
				default:
					con_printf(CON_DEBUG, "midiOutLongMsg Error code %i",rval);
					break;
			}
		}
		midiOutUnprepareHeader(hmidi, &mhdr, sizeof(MIDIHDR));

		timer_delay(F1_0/20);
	}
	else
	{
		switch (rval)
		{
			case MMSYSERR_INVALHANDLE:
				con_printf(CON_DEBUG, "midiOutPrepareHeader Error: The specified device handle is invalid.");
				break;
			case MMSYSERR_INVALPARAM:
				con_printf(CON_DEBUG, "midiOutPrepareHeader Error: The specified address is invalid or the given stream buffer is greater than 64K.");
				break;
			case MMSYSERR_NOMEM:
				con_printf(CON_DEBUG, "midiOutPrepareHeader Error: The system is unable to allocate or lock memory.");
				break;
			default:
				con_printf(CON_DEBUG, "midiOutPrepareHeader Error code %i",rval);
				break;
		}
	}

	range_for (const int channel, xrange(16u))
		midiOutShortMsg(hmidi, static_cast<DWORD>(channel | MIDI_CONTROL_CHANGE << 4 | MIDI_VOLUME << 8 | (100 * midi_volume / MIDI_VOLUME_SCALE) << 16));
	midiOutClose(hmidi);
}
#endif

namespace {

// CONVERSION FROM HMP TO MIDI

static void hmptrk2mid(ubyte* data, int size, std::vector<uint8_t> &midbuf)
{
	uint8_t *dptr = data;
	ubyte lc1 = 0,last_com = 0;
	int n1;

	while (data < dptr + size)
	{
		if (data[0] & 0x80) {
			ubyte b = (data[0] & 0x7F);
			midbuf.emplace_back(b);
		}
		else {
			n1 = 1;
			while ((data[n1] & 0x80) == 0) {
				n1++;
				if (n1 == 4)
					throw std::runtime_error("bad HMP");
				}
			for(int n2 = 0; n2 <= n1; n2++) {
				ubyte b = (data[n1 - n2] & 0x7F);

				if (n2 != n1)
					b |= 0x80;
				midbuf.emplace_back(b);
				}
			data += n1;
		}
		data++;
		if (*data == 0xFF) { //meta?
			midbuf.insert(midbuf.end(), data, data + 3 + data[2]);
			if (data[1] == 0x2F)
				break;
		}
		else {
			lc1=data[0] ;
			if ((lc1&0x80) == 0)
				throw std::runtime_error("bad HMP");
			switch (lc1 & 0xF0) {
				case 0x80:
				case 0x90:
				case 0xA0:
				case 0xB0:
				case 0xE0:
					if (lc1 != last_com)
					{
						midbuf.emplace_back(lc1);
					}
					midbuf.insert(midbuf.end(), data + 1, data + 3);
					data += 3;
					break;
				case 0xC0:
				case 0xD0:
					if (lc1 != last_com)
					{
						midbuf.emplace_back(lc1);
					}
					midbuf.emplace_back(data[1]);
					data += 2;
					break;
				default:
					throw std::runtime_error("bad HMP");
				}
			last_com = lc1;
		}
	}
}

const std::array<uint8_t, 10> magic_header{{
	'M', 'T', 'h', 'd',
	0, 0, 0, 6,
	0, 1,
}};
const std::array<uint8_t, 19> tempo{{'M','T','r','k',0,0,0,11,0,0xFF,0x51,0x03,0x18,0x80,0x00,0,0xFF,0x2F,0}};
const std::array<uint8_t, 8> track_header{{'M', 'T', 'r', 'k', 0, 0, 0, 0}};

struct midhdr
{
	int16_t num_trks;
	int16_t time_div;
	midhdr(hmp_file *hmp) :
		num_trks(hmp->num_trks), time_div(hmp->tempo*1.6)
	{
	}
};

DEFINE_SERIAL_CONST_UDT_TO_MESSAGE(midhdr, m, (magic_header, m.num_trks, m.time_div, tempo));

}

hmpmid_result hmp2mid(const char *hmp_name)
{
	auto &&[hmp, hoe, pec] = hmp_open(hmp_name);
	if (!hmp)
	{
		if (underlying_value(hoe) <= static_cast<unsigned>(hmp_open_error::physfs_read_track_data))
			con_printf(CON_CRITICAL, "Failed to read HMP music %s: hmp_open_error=%u; PHYSFS_ErrorCode=%u/%s", hmp_name, underlying_value(hoe), pec, PHYSFS_getErrorByCode(pec));
		else
			con_printf(CON_CRITICAL, "Failed to read HMP music %s: hmp_open_error=%u", hmp_name, underlying_value(hoe));
		return {std::vector<uint8_t>{}, hoe};
	}

	const midhdr mh(hmp.get());
	std::vector<uint8_t> midbuf;
	// write MIDI-header
	midbuf.resize(serial::message_type<decltype(mh)>::maximum_size);
	serial::writer::be_bytebuffer bb{midbuf.data()};
	serial::process_buffer(bb, mh);

	// tracks
	for (int i = 1; i < hmp->num_trks; i++)
	{
		midbuf.insert(midbuf.end(), track_header.begin(), track_header.end());
		auto size_before = midbuf.size();
		auto midtrklenpos = midbuf.size() - 4;
		hmptrk2mid(hmp->trks[i].data.get(), hmp->trks[i].len, midbuf);
		auto size_after = midbuf.size();
		serial::writer::be_bytebuffer bbmi{&midbuf[midtrklenpos]};
		serial::process_buffer(bbmi, static_cast<int32_t>(size_after - size_before));
	}
	return {midbuf, hmp_open_error::None};
}

}
