package client.audio;

import java.util.Arrays;

import client.audio.OPLChip.OPLChannel;
import client.audio.OPLChip.OPLSlot;

public class MidiHandle {
	public class Voice {
		final OPLChannel opl;
		
		Channel channel;
		int note = -1;
		
		Voice(OPLChannel opl) {
			this.opl = opl;
		}
	}

	public class Key {
		int velocity;
		Voice voice;
		
		Key() {
		}
	}

	public class Channel {
		final Key[] keys = new Key[BANK_MAX];
		final int[] notes = new int[128];
		
		int keyindex;
		int active;
		int pbank;
		int pan;
		int volume = 127;
		int pitch;
		int program;
		Instrument instr;
		
		Channel(boolean drum) {
			Arrays.fill(this.notes, -1);
			this.volume = 127;
			this.pbank = drum ? 128 : 0;
			this.instr = MidiHandle.this.instruments[drum ? 128 : 0];
			for(int z = 0; z < this.keys.length; z++) {
				this.keys[z] = new Key();
			}
		}
	}

	private static final int BANK_MAX = 64;
	private static final double BANK_PBRANGE = 2.0;

	public final Channel[] channels = new Channel[16];
	public final Voice[] voices;
	public final Instrument[] instruments;
	public final boolean keep;
	public final boolean useunkn;
	
	public int index;
	public int used;

	public MidiHandle(OPLChip chip, Instrument[] instr, boolean keep, boolean useunkn) {
		this.keep = keep;
		this.useunkn = useunkn;
		this.instruments = instr;
		this.voices = new Voice[chip.channel.length];
		for(int z = 0; z < this.channels.length; z++) {
			this.channels[z] = new Channel(z == 9);
		}
		for(int z = 0; z < this.voices.length; z++) {
			this.voices[z] = new Voice(chip.channel[z]);
		}
	}

	private void releaseVoice(Channel ch, Voice voice) {
		if(voice.note == -1)
			return;
		voice.opl.key(false);
		voice.note = -1;
		this.used -= 1;
	}

	private Voice releaseKey(Channel ch, int note) {
		if(/* ch == null || */ ch.active == 0) {
			return null;
		}
		if(ch.notes[note] == -1) {
			return null;
		}
		Key key = ch.keys[ch.notes[note]];
		ch.notes[note] = -1;
		ch.active -= 1;
		if(key.voice != null) {
			this.releaseVoice(ch, key.voice);
		}
		key.velocity = 0;
		return key.voice;
	}

	private Voice getVoice(Channel ch, int note, int velocity) {
		Instrument instr = ch.instr;
		if(ch.pbank == 128)
			instr = this.instruments[128 + note];
		else if(ch.pbank != 0 && !this.useunkn)
			return null;
		if(instr.ops.length == 0)
			return null;
		if(this.used == this.voices.length) {
			if(this.keep)
				return null;
			this.releaseKey(this.voices[this.index].channel, this.voices[this.index].note);
		}
		int vi = this.index;
		while(this.voices[this.index].note != -1) {
			this.index += 1;
			if(this.index >= this.voices.length)
				this.index = 0;
			if(vi == this.index) {
				if(this.keep)
					return null;
				this.releaseKey(this.voices[this.index].channel, this.voices[this.index].note);
				break;
			}
		}
		Voice voice = this.voices[this.index];
		this.index += 1;
		if(this.index >= this.voices.length)
			this.index = 0;
		voice.channel = ch;
		voice.note = note;
		voice.opl.setAlgo(instr.algo, instr.feedback1, instr.feedback2);
		voice.opl.setLevel(velocity, ch.volume, ch.pan);
		for(int o = 0; o < instr.ops.length; o++) {
			Operator op = instr.ops[o];
			OPLSlot slot = voice.opl.slots[o];
			slot.setFlags(op.tremolo, op.vibrato, op.sustaining, op.ksr);
			slot.setMultiplier(op.mult);
			slot.setKSL(op.ksl);
			slot.setLevel(op.level);
			slot.setEnvelope(op.attack, op.decay, op.sustain, op.release);
			slot.setWaveform(op.waveform);
			slot.setOffset(op.offset);
			slot.setDetune(op.detune);
			slot.setFixed(instr.fixed ? instr.percnum : -1);
			slot.setNote(note);
			slot.setPitch(ch.pitch);
		}
		voice.opl.key(true);
		this.used += 1;
		return voice;
	}

	private Voice pressKey(Channel ch, int note, int velocity) {
		if(ch.notes[note] != -1) {
			this.releaseKey(ch, note);
		}
		if(ch.active == BANK_MAX) {
			return null;
		}
		int ki = ch.keyindex;
		while(ch.keys[ch.keyindex].velocity != 0) {
			ch.keyindex += 1;
			if(ch.keyindex == BANK_MAX) {
				ch.keyindex = 0;
			}
			if(ki == ch.keyindex) {
				return null;
			}
		}
		Voice voice = this.getVoice(ch, note, velocity);
		ch.notes[note] = ch.keyindex;
		ch.active += 1;
		Key key = ch.keys[ch.keyindex];
		key.velocity = velocity;
		key.voice = voice;
		return voice;
	}

	private void notesoff(Channel ch) {
		for(int h = 0; h < ch.notes.length && ch.active > 0; h++) {
			this.releaseKey(ch, h);
		}
	}

	private void progupdate(Channel ch) {
		this.notesoff(ch);
		int id = ch.program;
		if(ch.pbank == 128) {
			id = 128;
		}
		else if(ch.pbank != 0 && !this.useunkn) {
			id = 0;
		}
		ch.instr = this.instruments[id];
	}

	private void levelupdate(Channel ch) {
		int done = 0;
		for(int h = 0; (h < BANK_MAX) && (done < ch.active); h++) {
			Key key = ch.keys[h];
			if((key.velocity == 0) || (key.voice == null)) {
				continue;
			}
			key.voice.opl.setLevel(key.velocity, ch.volume, ch.pan);
			done++;
		}
	}

	private void frequpdate(Channel ch) {
		int done = 0;
		for(int h = 0; (h < BANK_MAX) && (done < ch.active); h++) {
			Key key = ch.keys[h];
			if((key.velocity == 0) || (key.voice == null)) {
				continue;
			}
			for(OPLSlot slot : key.voice.opl.slots) {
				slot.setPitch(ch.pitch);
			}
			done++;
		}
	}

	public void noteon(int channel, int key, int velocity) {
		Channel ch = this.channels[channel];
		this.pressKey(ch, key, velocity);
	}

	public void noteoff(int channel, int key, int velocity) {
		Channel ch = this.channels[channel];
		this.releaseKey(ch, key);
	}

	public void progchange(int channel, int program) {
		Channel ch = this.channels[channel];
		ch.program = program;
		this.progupdate(ch);
	}

	public void pitchbend(int channel, int pitch) {
		Channel ch = this.channels[channel];
		ch.pitch = (int)((((double)pitch) / 8191.0 * BANK_PBRANGE) * 10000.0);
		this.frequpdate(ch);
	}

	public void control(int channel, int control, int value) {
		Channel ch = this.channels[channel];
		switch(control) {
			case 0x00: // bank MSB
				// if((channel == 9) && (value == 0)) {
				// 	ch.pbank = 128;
				// }
				// else {
				if(channel != 9) {
					ch.pbank &= 0x007f;
					ch.pbank |= (value << 7) & 0x3f80;
				}
				this.progupdate(ch);
				break;
			case 0x20: // bank LSB
				// if((channel == 9) && (value == 0)) {
				// 	ch.pbank = 128;
				// }
				// else {
				if(channel != 9) {
					ch.pbank &= 0x3f80;
					ch.pbank |= value & 0x007f;
				}
				this.progupdate(ch);
				break;
			case 0x07: // volume MSB
				ch.volume = value;
				this.levelupdate(ch);
				break;
			case 0x0a: // pan MSB
				ch.pan = value - 64;
				this.levelupdate(ch);
				break;
			case 0x79: // reset
				ch.pbank = (channel == 9) ? 128 : 0;
				ch.volume = 127;
				ch.pan = 0;
				this.progupdate(ch);
				this.levelupdate(ch);
				break;
			case 0x7b: // all off
				this.notesoff(ch);
				break;
		}
	}

	public void alloff() {
		for(int h = 0; h < 16; h++) {
			this.notesoff(this.channels[h]);
		}
	}
}
