package client.audio;

import java.io.IOException;
import java.util.Arrays;

import client.util.FileUtils;
import common.log.Log;

public class MidiDecoder {
	private class Track {
		byte[] buffer;
		int size;
		int pos;
		byte status;
		boolean ending;
		int wait;
		int trknum;

		public int readInt() {
			int value = 0;
			for(int h = 0; h < 4; h++) {
				if(this.pos >= this.size) {
					break;
				}
				value |= ((int)(this.buffer[this.pos++] & 255));
				value <<= h < 3 ? 8 : 0;
			}
			return value;
		}

		public int readInt24() {
			int value = 0;
			for(int h = 0; h < 3; h++) {
				if(this.pos >= this.size) {
					break;
				}
				value |= ((int)(this.buffer[this.pos++] & 255));
				value <<= h < 2 ? 8 : 0;
			}
			return value;
		}

		public short readShort() {
			short value = 0;
			for(int h = 0; h < 2; h++) {
				if(this.pos >= this.size) {
					break;
				}
				value |= ((short)(this.buffer[this.pos++] & 255));
				value <<= h < 1 ? 8 : 0;
			}
			return value;
		}

		public byte readByte() {
			byte value = 0;
			if(this.pos < this.size) {
				value = this.buffer[this.pos++];
			}
			return value;
		}

		public void readData(int len, byte[] chars) {
			Arrays.fill(chars, (byte)0);
			for(int h = 0; h < len; h++) {
				if(this.pos >= this.size) {
					break;
				}
				chars[h] = this.buffer[this.pos++];
			}
		}

		public int readVarLen() {
			int value;
			byte bt;

			if(this.pos >= this.size) {
				return 0;
			}
			if(((value = (this.buffer[this.pos++] & 255)) & 0x80) != 0)
			{
			 	value &= 0x7f;
			 	do {
					if(this.pos >= this.size) {
						break;
					}
					value = (value << 7) + ((bt = this.buffer[this.pos++]) & 0x7f);
				}
				while((bt & 0x80) != 0);
			}
			return value;
		} 
	}
	
	private static final byte midev_noteoff     = (byte)0x80,
		midev_noteon      = (byte)0x90,
		midev_aftertouch  = (byte)0xa0,
		midev_control     = (byte)0xb0,
		midev_progchg     = (byte)0xc0,
		midev_chnpressure = (byte)0xd0,
		midev_pitchbend   = (byte)0xe0,
		midev_sysex       = (byte)0xf0,
		midev_songpos     = (byte)0xf2,
		midev_songsel     = (byte)0xf3,
		midev_tunereq     = (byte)0xf6,
		midev_endsysex    = (byte)0xf7,
		midev_clock       = (byte)0xf8,
		midev_start       = (byte)0xfa,
		midev_continue    = (byte)0xfb,
		midev_stop        = (byte)0xfc,
		midev_actsense    = (byte)0xfe,
		midev_meta        = (byte)0xff;

	private static final byte midmt_seqnum      = 0x00, // nn nn
		midmt_text        = 0x01, // text...
		midmt_copyright   = 0x02, // text...
		midmt_trackname   = 0x03, // text...
		midmt_instrname   = 0x04, // text...
		midmt_lyric       = 0x05, // text...
		midmt_marker      = 0x06, // text...
		midmt_cuepoint    = 0x07, // text...
		midmt_chnprefix   = 0x20, // cc
		midmt_endtrack    = 0x2f, // 
		midmt_tempo       = 0x51, // tt tt tt
		midmt_smpte       = 0x54, // hr mn se fr ff
		midmt_timesig     = 0x58, // nn dd cc bb
		midmt_keysig      = 0x59, // sf mi
		midmt_seqspec     = 0x7f; // data...

	private static final byte[] mid_hdr = "MThd".getBytes();
	private static final byte[] mid_trk = "MTrk".getBytes();

	public final Track[] tracks;
	public final int tpqn;
	public final boolean nowait;
	public final OPLChip chip;
	public final MidiHandle bank;
	
	public int uspb;
	public int ticktime;
	public long time;
	public int tick;
	public boolean playing;
	public boolean debug;

	private static int readInt(byte[] data, int offset) {
		int value = 0;
		for(int h = 0; h < 4; h++) {
			value |= ((int)(data[offset + h] & 255));
			value <<= h < 3 ? 8 : 0;
		}
		return value;
	}

	private static short readShort(byte[] data, int offset) {
		short value = 0;
		for(int h = 0; h < 2; h++) {
			value |= ((short)(data[offset + h] & 255));
			value <<= h < 1 ? 8 : 0;
		}
		return value;
	}
	
	public MidiDecoder(byte[] data, boolean nowait, int voices, MidiBank bank, boolean keep, boolean useunkn, int velofunc) throws IOException {
		int size = data.length;
		if(size < 14) {
			throw new IOException("Datei zu klein");
		}
		if(!Arrays.equals(data, 0, 4, mid_hdr, 0, 4)) {
			throw new IOException("Fehlerhafter Dateiheader oder falscher Dateityp");
		}
		if(readInt(data, 4) != 6) {
			throw new IOException("Dateiheader mit falscher Länge");
		}
		boolean mtrack;
		short type = readShort(data, 8);
		if(type == 0) {
			mtrack = false;
		}
		else if((type == 1) || (type == 2)) {
			mtrack = true;
		}
		else {
			throw new IOException("Fehlerhaftes Spurformat");
		}
		Track[] tracks = new Track[readShort(data, 10)];
		if(tracks.length == 0) {
			throw new IOException("Keine Spuren definiert");
		}
		else if(!mtrack && tracks.length > 1) {
			throw new IOException("Einzelspur-Format mit mehreren Spuren");
		}
		int tpqn = readShort(data, 12);
		if((tpqn & 0x8000) > 0) {
			throw new IOException("SMPTE-Zeitformat nicht unterstuetzt");
		}
		if(tpqn == 0) {
			throw new IOException("Zeitformat teilt durch Null");
		}
		int pos = 14;
		for(short trk = 0; trk < tracks.length; trk++) {
			if(pos + 8 > size) {
				throw new IOException("Ende der Datei erreicht");
			}
			if(!Arrays.equals(data, pos, pos + 4, mid_trk, 0, 4)) {
				throw new IOException("Fehlerhafter Spurheader");
			}
			int trks = readInt(data, pos + 4);
			pos += 8;
			if((pos + trks) > size) {
				throw new IOException("Spur #" + (trk + 1) + " außerhalb der Datei");
			}
			Track track = tracks[trk] = new Track();
			track.buffer = new byte[trks];
			track.trknum = trk;
			track.size = trks;
			System.arraycopy(data, pos, track.buffer, 0, trks);
			pos += trks;
		}
		this.tracks = tracks;
		this.tpqn = tpqn;
		this.nowait = nowait;
		this.setTempo(500000);
		this.chip = new OPLChip(48000, voices, velofunc, 0, false, false);
		this.bank = new MidiHandle(this.chip, bank.getData(), keep, useunkn);
		this.playing = true;
	}
	
	public MidiDecoder(MidiBank bank, int start, int end, int delay, int note) {
		this.tracks = null;
		this.tpqn = note;
		this.nowait = false;
		this.chip = new OPLChip(48000, 16, 0, 0, false, false);
		this.bank = new MidiHandle(this.chip, bank.getData(), false, true);
		this.ticktime = start;
		this.uspb = end;
		this.time = (long)delay * 1000000L;
		this.debug = true;
	}

	private void setTempo(int tempo) {
		this.uspb = tempo;
		this.ticktime = this.uspb / this.tpqn;
	}
	
	private void log(String fmt, Object ... args) {
		Log.SOUND.info(fmt, args);
	}
	
	private void debug(String fmt, Object ... args) {
		if(this.debug)
			Log.SOUND.debug(fmt, args);
	}

	private void processMeta(Track trk) {
		byte meta = trk.readByte();
		int size = trk.readVarLen();
		switch(meta) {
			case midmt_seqnum:
				if(size == 0) {
					this.debug("MIDI Spur #%d Sequenz-Nummer = %d (eigene)", trk.trknum, trk.trknum);
					return;
				}
				else if(size != 2) {
					trk.pos += size;
					this.log("MIDI Spur #%d Meta-Event 0x%02x - erwartet %d, hat %d", trk.trknum, meta, 2, size);
					return;
				}
				this.debug("MIDI Spur #%d Sequenz-Nummer = %d", trk.trknum, trk.readShort());
				return;
			case midmt_text:
			case midmt_copyright:
			case midmt_trackname:
			case midmt_instrname:
			case midmt_lyric:
			case midmt_marker:
			case midmt_cuepoint: {
				byte[] dt = new byte[size+1];
				dt[size] = 0;
				trk.readData(size, dt);
				this.debug("MIDI Text (%d): %s", meta, new String(dt, FileUtils.UTF_8));
				return;
			}
			case midmt_chnprefix:
				if(size != 1) {
					trk.pos += size;
					this.log("MIDI Spur #%d Meta-Event 0x%02x - erwartet %d, hat %d", trk.trknum, meta, 1, size);
					return;
				}
				this.debug("MIDI Spur #%d Kanal-Präfix = %d", trk.trknum, trk.readByte());
				return;
			case midmt_endtrack:
				trk.pos += size;
				trk.ending = true;
				this.debug("MIDI Spur #%d Ende erreicht", trk.trknum);
				return;
			case midmt_tempo:
				if(size != 3) {
					trk.pos += size;
					this.log("MIDI Spur #%d Meta-Event 0x%02x - erwartet %d, hat %d", trk.trknum, meta, 3, size);
					return;
				}
				this.setTempo(trk.readInt24());
				this.debug("MIDI Tempo = %d", 60000000 / this.uspb);
				return;
			case midmt_smpte:
				if(size != 5) {
					trk.pos += size;
					this.log("MIDI Spur #%d Meta-Event 0x%02x - erwartet %d, hat %d", trk.trknum, meta, 5, size);
					return;
				}
				trk.pos += 5;
				this.debug("MIDI SMPTE-Event (nicht unterstützt)");
				return;
			case midmt_timesig: {
				if(size != 4) {
					trk.pos += size;
					this.log("MIDI Spur #%d Meta-Event 0x%02x - erwartet %d, hat %d", trk.trknum, meta, 4, size);
					return;
				}
				byte n = trk.readByte();
				int d = 1 << (int)trk.readByte();
				byte c = trk.readByte();
				byte b = trk.readByte();
				this.debug("MIDI Takt-Signatur = %d/%d - %d CPC - %d 32PQ", n, d, c, b);
				return;
			}
			case midmt_keysig: {
				if(size != 2) {
					trk.pos += size;
					this.log("MIDI Spur #%d Meta-Event 0x%02x - erwartet %d, hat %d", trk.trknum, meta, 2, size);
					return;
				}
				byte s = trk.readByte();
				byte m = trk.readByte();
				this.debug("MIDI Noten-Signatur = %d - %s", s, (m == 0) ? "MAJOR" : ((m == 1) ? "MINOR" : "- ? -"));
				return;
			}
			case midmt_seqspec: {
				trk.pos += size;
				this.debug("MIDI Spur #%d Sequenzer-Daten empfangen - %d Bytes", trk.trknum, size);
				return;
			}
			default:
				trk.pos += size;
				this.debug("MIDI Spur #%d Meta-Event 0x%02x (%d)", trk.trknum, meta, size);
				return;
		}
	}

	private void process(Track trk, MidiHandle bank) {
		if(trk.pos >= trk.size) {
			return;
		}
		byte status = trk.buffer[trk.pos++];
		if((status & 0x80) == 0) {
			status = trk.status;
			trk.pos -= 1;
		}
		else {
			trk.status = status;
		}
		if((status & 0xf0) != 0xf0) {
			byte channel = (byte)(status & 0x0f);
			status &= 0xf0;
			switch(status) {
				case midev_noteoff: {
					byte o_key = trk.readByte();
					byte o_velocity = trk.readByte();
					bank.noteoff(channel, o_key, o_velocity);
					this.debug("MIDI Note-Aus - C%02d N%03d V%03d", channel+1, o_key, o_velocity);
					break;
				}
				case midev_noteon: {
					byte key = trk.readByte();
					byte velocity = trk.readByte();
					if(velocity == 0)
						bank.noteoff(channel, key, velocity);
					else
						bank.noteon(channel, key, velocity);
					this.debug("MIDI Note-An - C%02d N%03d V%03d", channel+1, key, velocity);
					break;
				}
				case midev_aftertouch: {
					byte pressure = trk.readByte();
					this.debug("MIDI Aftertouch - C%02d P%03d", channel+1, pressure);
					break;
				}
				case midev_control: {
					byte control = trk.readByte();
					byte value = trk.readByte();
					bank.control(channel, control, value);
					this.debug("MIDI Controller - C%02d N%03d V%03d", channel+1, control, value);
					break;
				}
				case midev_progchg: {
					byte program = trk.readByte();
					bank.progchange(channel, program);
					this.debug("MIDI Programm - C%02d P%03d", channel+1, program);
					break;
				}
				case midev_chnpressure: {
					byte cpressure = trk.readByte();
					this.debug("MIDI Kanal-Druck - C%02d P%03d", channel+1, cpressure);
					break;
				}
				case midev_pitchbend: {
					short pb = (short)(((short)trk.readByte()) | (((short)trk.readByte()) << 7));
					short pitch = (short)(pb - 0x2000);
					bank.pitchbend(channel, pitch);
					this.debug("MIDI Pitch-Bend - C%02d P%03d", channel+1, pitch);
					break;
				}
			}
		}
		else {
			switch(status) {
				case midev_sysex: {
					int slen = trk.readVarLen();
					trk.pos += slen;
					this.debug("MIDI Sysex (Normal) mit Länge = %d", slen);
					break;
				}
				case midev_songpos:
					this.debug("MIDI Song-Position = %d", ((short)trk.readByte()) | (((short)trk.readByte()) << 7));
					break;
				case midev_songsel:
					this.debug("MIDI Song-Auswahl = %d", trk.readByte());
					break;
				case midev_tunereq:
					this.debug("MIDI Stimmung angefordert, nichts zu tun?!");
					break;
				case midev_endsysex: {
					int elen = trk.readVarLen();
					trk.pos += elen;
					this.debug("MIDI Sysex (Escape) mit Länge = %d", elen);
					break;
				}
				case midev_clock:
				case midev_start:
				case midev_continue:
				case midev_stop:
				case midev_actsense:
					this.debug("MIDI Status %d", status);
					break;
				case midev_meta:
					this.processMeta(trk);
					break;
				default:
					this.log("MIDI Status unbekannt: 0x%02x", status);
					break; 
			}
		}
	}

	private boolean tick(MidiHandle bank) {
		boolean end = true;
		for(int trk = 0; trk < this.tracks.length; trk++) {
			Track track = this.tracks[trk];
			if(track.ending)
				continue;
			if(track.wait > 0) {
				track.wait -= 1;
				if(track.wait > 0) {
					end = false;
					continue;
				}
			}
			while(true) {
				if(track.pos > 0) {
					this.process(track, bank);
					if(!track.ending && track.pos >= track.size) {
						this.log("MIDI Spur #%d endete zu früh", track.trknum);
						track.ending = true;
					}
					if(track.ending)
						break;
				}
				track.wait = track.readVarLen();
				if(track.wait > 0)
					break;
			}
			end &= track.ending;
		}
		if(!end) {
			this.tick += 1;
			this.time += (long)this.ticktime;
		}
		return !end;
	}
	
	public long process() {
		if(this.tracks == null) {
			int pos = this.ticktime + (this.tick % (this.uspb + 1 - this.ticktime));
			this.bank.progchange(0, pos < 128 ? pos : 0);
			this.bank.progchange(9, 0);
			Log.SOUND.user("MIDI-Test %s%d", pos < 128 ? "#" : "P", pos < 128 ? pos : pos - 128);
			this.bank.noteon(pos < 128 ? 0 : 9, pos < 128 ? this.tpqn : pos - 128, 127);
			pos++;
			if(pos == 128)
				pos = 128 + 35;
			else if(pos == 128 + 82)
				pos = 0;
			return this.time;
		}
		
		if(!this.playing && (this.nowait || !this.chip.isPlaying())) {
			return -1L;
		}
		else if(this.playing && this.tick(this.bank)) {
			return ((long)this.ticktime) * 1000L;
		}
		else if(this.playing) {
			this.playing = false;
			if(this.bank != null)
				this.bank.alloff();
		}
		return 1000L;
	}
	
	public void setDebug(boolean debug) {
		if(this.tracks != null)
			this.debug = debug;
	}
}
