/*
 * Schism Tracker - a cross-platform Impulse Tracker clone
 * copyright (c) 2003-2005 Storlek <storlek@rigelseven.com>
 * copyright (c) 2005-2008 Mrs. Brisby <mrs.brisby@nimh.org>
 * copyright (c) 2009 Storlek & Mrs. Brisby
 * copyright (c) 2010-2012 Storlek
 * URL: http://schismtracker.org/
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 */

#define NEED_BYTESWAP
#include "headers.h"
#include "slurp.h"
#include "fmt.h"
#include "log.h"

#include "sndfile.h"


#pragma pack(push,1)
struct mus_header {
	char id[4]; // MUS\x1a
	uint16_t scorelen;
	uint16_t scorestart;
	uint16_t channels;
	uint16_t sec_channels;
	uint16_t instrcnt;
	uint16_t dummy;
};
#pragma pack(pop)

/* --------------------------------------------------------------------- */

int fmt_mus_read_info(dmoz_file_t *file, const uint8_t *data, size_t length)
{
	struct mus_header *hdr = (struct mus_header *) data;

	/* cast necessary for big-endian systems */
	if (!(length > sizeof(*hdr) && memcmp(hdr->id, "MUS\x1a", 4) == 0
	      && (size_t) (bswapLE16(hdr->scorestart) + bswapLE16(hdr->scorelen)) <= length))
		return 0;

	file->description = "Doom Music File";
	file->title = strdup("");
	file->type = TYPE_MODULE_MOD;
	return 1;
}

/* --------------------------------------------------------------------------------------------------------- */
/* I really don't know what I'm doing here -- I don't know much about either midi or adlib at all, and I've
never even *played* Doom. Frankly, I'm surprised that this produces something that's actually listenable.

Some things yet to tackle:
- Pitch wheel support is nonexistent. Shouldn't be TOO difficult; keep track of the target pitch value and how
  much of a slide has already been done, insert EFx/FFx effects, adjust notes when inserting them if the pitch
  wheel is more than a semitone off, and keep the speed at 1 if there's more sliding to do.
- Percussion channel isn't handled. Get a few adlib patches from some adlib S3Ms?
- Volumes for a couple of files are pretty screwy -- don't know whether I'm doing something wrong here, or if
  adlib's doing something funny with the volume, or maybe it's with the patches I'm using...
- awesomus/d_doom.mus has some very strange timing issues: I'm getting note events with thousands of ticks.
- Probably ought to clean up the warnings so messages only show once... */


#define MUS_ROWS_PER_PATTERN 200
#define MUS_SPEED_CHANNEL 15 // where the speed adjustments go (counted from 0 -- 15 is the drum channel)
#define MUS_BREAK_CHANNEL (MUS_SPEED_CHANNEL + 1)
#define MUS_TICKADJ_CHANNEL (MUS_BREAK_CHANNEL + 1) // S6x tick adjustments go here *and subsequent channels!!*

// Tick calculations are done in fixed point for better accuracy
#define FRACBITS 12
#define FRACMASK ((1 << FRACBITS) - 1)


int fmt_mus_load_song(song_t *song, slurp_t *fp, UNUSED unsigned int lflags)
{
	struct mus_header hdr;
	int n;
	song_note_t *note;
	int pat, row;
	int finished = 0;
	size_t reallen;
	int tickfrac = 0; // fixed point
	struct {
		uint8_t note; // the last note played in this channel
		uint8_t instrument; // 1 -> 128
		uint8_t volume; // 0 -> 64
	} chanstate[16] = {};
	uint8_t prevspeed = 1;
	uint8_t patch_samples[128] = {0};
	uint8_t patch_percussion[128] = {0};
	uint8_t nsmp = 1; // Next free sample

	slurp_read(fp, &hdr, sizeof(hdr));
	hdr.scorelen = bswapLE16(hdr.scorelen);
	hdr.scorestart = bswapLE16(hdr.scorestart);

	if (memcmp(hdr.id, "MUS\x1a", 4) != 0)
		return LOAD_UNSUPPORTED;
	else if (hdr.scorestart + hdr.scorelen > fp->length)
		return LOAD_FORMAT_ERROR;


	for (n = 16; n < 64; n++)
		song->channels[n].flags |= CHN_MUTE;

	slurp_seek(fp, hdr.scorestart, SEEK_SET);

	// Narrow the data buffer to simplify reading
	reallen = fp->length;
	fp->length = MIN(fp->length, hdr.scorestart + hdr.scorelen);

	/* start the first pattern */
	pat = 0;
	row = 0;
	song->pattern_size[pat] = song->pattern_alloc_size[pat] = MUS_ROWS_PER_PATTERN;
	song->patterns[pat] = csf_allocate_pattern(MUS_ROWS_PER_PATTERN);
	note = song->patterns[pat];
	song->orderlist[pat] = pat;

	while (!finished && !slurp_eof(fp)) {
		uint8_t event, b1, b2, type, ch;

		event = slurp_getc(fp);
		type = (event >> 4) & 7;
		ch = event & 15;

		switch (type) {
		case 0: // Note off - figure out what channel the note was playing in and stick a === there.
			b1 = slurp_getc(fp) & 127; // & 127 => note number
			b1 = MIN((b1 & 127) + 1, NOTE_LAST);
			if (chanstate[ch].note == b1) {
				// Ok, we're actually playing that note
				if (!NOTE_IS_NOTE(note[ch].note))
					note[ch].note = NOTE_OFF;
			}
			break;
		case 1: // Play note
			b1 = slurp_getc(fp); // & 128 => volume follows, & 127 => note number
			if (b1 & 128) {
				chanstate[ch].volume = ((slurp_getc(fp) & 127) + 1) >> 1;
				b1 &= 127;
			}
			chanstate[ch].note = MIN(b1 + 1, NOTE_LAST);
			if (ch == 15) {
				// Percussion
				b1 = CLAMP(b1, 24, 84); // ?
				if (!patch_percussion[b1]) {
					if (nsmp < MAX_SAMPLES) {
						// New sample!
						patch_percussion[b1] = nsmp;
						strncpy(song->samples[nsmp].name,
							midi_percussion_names[b1 - 24], 25);
						song->samples[nsmp].name[25] = '\0';
						nsmp++;
					} else {
						// Phooey.
						log_appendf(4, " Warning: Too many samples");
						note[ch].note = NOTE_OFF;
					}
				}
#if 0
				note[ch].note = NOTE_MIDC;
				note[ch].instrument = patch_percussion[b1];
#else
				/* adlib is broken currently: it kind of "folds" every 9th channel, but only
				for SOME events ... what this amounts to is attempting to play notes from
				both of any two "folded" channels will cause everything to go haywire.
				for the moment, ignore the drums. even if we could load them, the playback
				would be completely awful.
				also reset the channel state, so that random note-off events don't stick ===
				into the channel, that's even enough to screw it up */
				chanstate[ch].note = NOTE_NONE;
#endif
			} else {
				if (chanstate[ch].instrument) {
					note[ch].note = chanstate[ch].note;
					note[ch].instrument = chanstate[ch].instrument;
				}
			}
			note[ch].voleffect = VOLFX_VOLUME;
			note[ch].volparam = chanstate[ch].volume;
			break;
		case 2: // Pitch wheel (TODO)
			b1 = slurp_getc(fp);
			break;
		case 3: // System event
			b1 = slurp_getc(fp) & 127;
			switch (b1) {
			case 10: // All sounds off
				for (n = 0; n < 16; n++) {
					note[ch].note = chanstate[ch].note = NOTE_CUT;
					note[ch].instrument = 0;
				}
				break;
			case 11: // All notes off
				for (n = 0; n < 16; n++) {
					note[ch].note = chanstate[ch].note = NOTE_OFF;
					note[ch].instrument = 0;
				}
				break;
			case 14: // Reset all controllers
				// ?
				memset(chanstate, 0, sizeof(chanstate));
				break;
			case 12: // Mono
			case 13: // Poly
				break;
			}
			break;
		case 4: // Change controller
			b1 = slurp_getc(fp) & 127; // controller
			b2 = slurp_getc(fp) & 127; // new value
			switch (b1) {
			case 0: // Instrument number
				if (ch == 15) {
					// don't fall for this nasty trick, this is the percussion channel
					break;
				}
				if (!patch_samples[b2]) {
					if (nsmp < MAX_SAMPLES) {
						// New sample!
						patch_samples[b2] = nsmp;
						adlib_patch_apply(song->samples + nsmp, b2);
						nsmp++;
					} else {
						// Don't have a sample number for this patch, and never will.
						log_appendf(4, " Warning: Too many samples");
						note[ch].note = NOTE_OFF;
					}
				}
				chanstate[ch].instrument = patch_samples[b2];
				break;
			case 3: // Volume
				b2 = (b2 + 1) >> 1;
				chanstate[ch].volume = b2;
				note[ch].voleffect = VOLFX_VOLUME;
				note[ch].volparam = chanstate[ch].volume;
				break;
			case 1: // Bank select
			case 2: // Modulation pot
			case 4: // Pan
			case 5: // Expression pot
			case 6: // Reverb depth
			case 7: // Chorus depth
			case 8: // Sustain pedal (hold)
			case 9: // Soft pedal
				// I have no idea
				break;
			}
			break;
		case 6: // Score end
			finished = 1;
			break;
		default: // Unknown (5 or 7)
			// Hope it doesn't take any parameters, otherwise things are going to end up broken
			log_appendf(4, " Warning: Unknown event type %d", type);
			break;
		}

		if (finished) {
			int leftover = (tickfrac + (1 << FRACBITS)) >> FRACBITS;
			note[MUS_BREAK_CHANNEL].effect = FX_PATTERNBREAK;
			note[MUS_BREAK_CHANNEL].param = 0;
			if (leftover && leftover != prevspeed) {
				note[MUS_SPEED_CHANNEL].effect = FX_SPEED;
				note[MUS_SPEED_CHANNEL].param = leftover;
			}
		} else if (event & 0x80) {
			// Read timing information and advance the row
			int ticks = 0;

			do {
				b1 = slurp_getc(fp);
				ticks = 128 * ticks + (b1 & 127);
				if (ticks > 0xffff)
					ticks = 0xffff;
			} while (b1 & 128);
			ticks = MIN(ticks, (0x7fffffff / 255) >> 12); // protect against overflow

			ticks <<= FRACBITS; // convert to fixed point
			ticks = ticks * 255 / 350; // 140 ticks/sec * 125/50hz => tempo of 350 (scaled)
			ticks += tickfrac; // plus whatever was leftover from the last row
			tickfrac = ticks & FRACMASK; // save the fractional part
			ticks >>= FRACBITS; // and back to a normal integer

			if (ticks < 1) {
#if 0
				// There's only part of a tick - compensate by skipping one tick later
				tickfrac -= 1 << FRACBITS;
				ticks = 1;
#else
				/* Don't advance the row: if there's another note right after one of the ones
				inserted already, the existing note will be rendered more or less irrelevant
				anyway, so just allow any following events to overwrite the data.
				Also, there's no need to write the speed, because it'd just be trampled over
				later anyway.
				The only thing that would necessitate advancing the row is if there's a pitch
				adjustment that's at least 15/16 of a semitone; in that case, "steal" a tick
				(see above). */
				continue;
#endif
			} else if (ticks > 255) {
				/* Too many ticks for a single row with Axx.
				We can increment multiple rows easily, but that only allows for exact multiples
				of some number of ticks, so adding in some "padding" is necessary. Since there
				is no guarantee that rows after the current one even exist, any adjusting has
				to happen on *this* row. */

				int adjust = ticks % 255;
				int s6xch = MUS_TICKADJ_CHANNEL;
				while (adjust) {
					int s6x = MIN(adjust, 0xf);
					note[s6xch].effect = FX_SPECIAL;
					note[s6xch].param = 0x60 | s6x;
					adjust -= s6x;
					s6xch++;
				}
			}
			if (prevspeed != MIN(ticks, 255)) {
				prevspeed = MIN(ticks, 255);
				note[MUS_SPEED_CHANNEL].effect = FX_SPEED;
				note[MUS_SPEED_CHANNEL].param = prevspeed;
			}
			ticks = ticks / 255 + 1;
			row += ticks;
			note += 64 * ticks;
		}

		while (row >= MUS_ROWS_PER_PATTERN) {
			/* Make a new pattern. */
			pat++;
			row -= MUS_ROWS_PER_PATTERN;
			if (pat >= MAX_PATTERNS) {
				log_appendf(4, " Warning: Too much note data");
				finished = 1;
				break;
			}
			song->pattern_size[pat] = song->pattern_alloc_size[pat] = MUS_ROWS_PER_PATTERN;
			song->patterns[pat] = csf_allocate_pattern(MUS_ROWS_PER_PATTERN);
			note = song->patterns[pat];
			song->orderlist[pat] = pat;

			note[MUS_SPEED_CHANNEL].effect = FX_SPEED;
			note[MUS_SPEED_CHANNEL].param = prevspeed;

			note += 64 * row;
		}
	}

	// Widen the buffer again.
	fp->length = reallen;

	song->flags |= SONG_NOSTEREO;
	song->initial_speed = 1;
	song->initial_tempo = 255;

	strcpy(song->tracker_id, "Doom Music File"); // ?

	return LOAD_SUCCESS;
}

