package client.audio;

import java.util.Arrays;
import java.util.List;

import javax.sound.sampled.AudioFormat;
import javax.sound.sampled.AudioFormat.Encoding;
import javax.sound.sampled.AudioSystem;
import javax.sound.sampled.DataLine.Info;

import common.collect.Lists;
import common.log.Log;
import javax.sound.sampled.SourceDataLine;

public class AudioInterface implements Runnable {
	private static class Channel {
		short[] buffer;
		int pos;
		int level;
		int type;
		boolean loop;
		boolean run;
		boolean load;
	}

	private static class Command {
		Object buffer;
		Cmd command;
		short address;
		int param;
	}

	private static enum Cmd {
		STOP,
		QUERY,
		VOLUME,
		PLAY,
		BREAK,
		LEVEL,
		MIDI
	};
	
	private final int blocksize;
	private final int devbufsize;
	private final byte[] buffer;

	private final Thread thread;
	private final List<Command> cmds = Lists.newArrayList();
	private final short[] volumes = new short[Volume.values().length];
	private final Channel[] channels = new Channel[32];
	private final short[] data = new short[2];

	private SourceDataLine line;
	private int smppos;
	private long peak = 32767L;
	private boolean opened;
	private boolean waiting;
	
	private MidiDecoder mid;

	private void push() {
		for(int z = 0; z < 2 * 2; z++) {
			this.buffer[this.smppos * 2 * 2 + z] = (byte)((this.data[z >> 1] >> ((z & 1) * 8)) & 0xff);
		}
		this.smppos++;
		if(this.smppos == this.blocksize) {
			this.write(this.buffer, this.blocksize);
			this.smppos = 0;
		}
	}
	
	private void write(byte[] data, int samples) {
		if(this.line == null)
			return;
		int rc;
		int avail;
		int channels = 2;
		int offset = 0;
		samples *= 2 * 2;
		int block = this.blocksize * 2 * 2;
	  	while(samples > 0) {
			block = samples < block ? samples : block;
			avail = this.line.available();
			rc = this.line.write(data, offset, avail < block ? avail : block);
		  	// this.line.drain();
			if(rc > 0) {
				offset += rc;
				samples -= rc;
			}
		}
	}
	
	private long process() {
		if(this.mid != null) {
			long elapsed = this.mid.process();
			if(elapsed < 0L)
				this.mid = null;
			else
				return elapsed;
		}
		return 1000L;
	}
	
	private void generate() {
		long mixLeft = 0;
		long mixRight = 0;
		if(this.mid != null) {
			int[] data = this.mid.chip.generate();
			mixLeft = (long)data[0] * (long)this.volumes[Volume.MUSIC.ordinal()] * (long)this.volumes[0];
			mixRight = (long)data[1] * (long)this.volumes[Volume.MUSIC.ordinal()] * (long)this.volumes[0];
		}
		for(int z = 0; z < this.channels.length; z++) {
			Channel ch = this.channels[z];
			if(ch.run) {
				long frame = (long)ch.buffer[ch.pos] * (long)ch.level * (((long)this.volumes[ch.type] * (long)this.volumes[0]) / 32767);
				mixLeft += frame;
				mixRight += frame;
				if(++ch.pos >= ch.buffer.length) {
					ch.pos = 0;
					if(!ch.loop)
						ch.run = ch.load = false;
				}
			}
		}
		mixLeft /= 32767L * 32767L;
		mixRight /= 32767L * 32767L;
		long amp = Math.max(Math.abs(mixLeft), Math.abs(mixRight));
		if(amp > 32767L)
			this.peak = Math.max(this.peak, amp);
		else if(this.peak > 32767L)
			this.peak = Math.max(this.peak - 3L, 32767L);
		this.data[0] = (short)((mixLeft * 32767L) / this.peak);
		this.data[1] = (short)((mixRight * 32767L) / this.peak);
	}
	
	private void dispatch(Object buf, Cmd op, short addr, int param) {
		switch(op) {
			case VOLUME:
				this.volumes[addr] = (short)param;
				break;
			case PLAY:
				if(buf != null) {
					this.channels[addr].buffer = (short[])buf;
					this.channels[addr].pos = 0;
					this.channels[addr].type = param & 0xff;
					this.channels[addr].run = this.channels[addr].load = true;
					this.channels[addr].loop = (param & 0x100) != 0;
				}
				break;
			case BREAK:
				this.channels[addr].run = this.channels[addr].load = false;
				break;
			case LEVEL:
				this.channels[addr].level = param;
				break;
			case QUERY:
				this.waiting = false;
				break;
			case MIDI:
				this.mid = (MidiDecoder)buf;
				break;
		}
	}
		
	public void run() {
		AudioFormat format = new AudioFormat(Encoding.PCM_SIGNED, 48000, 16, 2, 2 * 2, 48000, false);
		Info info = new Info(SourceDataLine.class, format, this.devbufsize == 0 ? AudioSystem.NOT_SPECIFIED : this.devbufsize);
		try {
			this.line = (SourceDataLine)AudioSystem.getLine(info);
			this.line.open(format, this.devbufsize == 0 ? AudioSystem.NOT_SPECIFIED : this.devbufsize);
		}
		catch(Exception e) {
			this.line = null;
			Log.SOUND.error(e, "Konnte Audiogerät nicht öffnen");
		}
		if(this.line != null)
			this.line.start();
		this.opened = this.line != null;
		long sampletime = 1000000000L / 48000L; // * (long)this.blocksize;
		boolean running = true;
		long elapsed = 0;
		while(running) {
			synchronized(this.cmds) {
				while(!this.cmds.isEmpty()) {
					Command cmd = this.cmds.remove(0);
					if(cmd.command == Cmd.STOP)
						running = false;
					else
						this.dispatch(cmd.buffer, cmd.command, cmd.address, cmd.param);
				}
			}
			elapsed += this.process();
			while((elapsed >= sampletime)) {
				this.generate();
				this.push();
				elapsed -= sampletime;
			}
		}
		if(this.smppos != 0)
			this.write(this.buffer, this.smppos);
		if(this.line != null) {
			this.line.drain();
			this.line.stop();
			this.line.close();
		}
		this.line = null;
		this.waiting = false;
	}
	
	private void instruct(Object buf, Cmd op, int addr, int param) {
		if(this.thread == null)
			return;
		Command cmd = new Command();
		cmd.command = op;
		cmd.address = (short)addr;
		cmd.param = param;
		cmd.buffer = buf;
		synchronized(this.cmds) {
			this.cmds.add(cmd);
		}
	}
	
	public AudioInterface(int frames, int bufsize, boolean disabled) {
		this.blocksize = frames;
		this.devbufsize = bufsize;
		this.buffer = new byte[this.blocksize * 2 * 2];
		this.thread = disabled ? null : new Thread(this, "Audio Thread");
		for(int z = 0; z < this.channels.length; z++) {
			this.channels[z] = new Channel();
		}
		Arrays.fill(this.volumes, (short)32767);
	}
	
	public boolean start() {
		if(this.thread == null)
			return false;
		this.thread.start();
		this.waiting = true;
		this.instruct(null, Cmd.QUERY, 0, 0);
		while(this.waiting) {
			try {
				Thread.sleep(1L);
			}
			catch(InterruptedException e) {
			}
		}
		return this.opened;
	}
	
	public boolean end() {
		if(this.thread == null)
			return false;
		this.waiting = true;
		this.instruct(null, Cmd.STOP, 0, 0);
		while(this.waiting) {
			try {
				Thread.sleep(1L);
			}
			catch(InterruptedException e) {
			}
		}
		return this.opened;
	}
		
	public void alSetVolume(Volume channel, short volume) {
		this.instruct(null, Cmd.VOLUME, channel.ordinal(), volume);
	}

	public void alSourcePlay(int source, short[] buffer, Volume channel, boolean loop) {
		this.channels[source].load = true;
		this.instruct(buffer, Cmd.PLAY, source, channel.ordinal() | (loop ? 0x100 : 0));
	}
	
	public void alSourceStop(int source) {
		this.channels[source].load = false;
		this.instruct(null, Cmd.BREAK, source, 0);
	}
	
	public boolean alPlaying(int source) {
		return this.channels[source].load; 
	}
	
	public void alGain(int source, float value) {
		this.instruct(null, Cmd.LEVEL, source, (int)(value * 32767.0f));
	}
	
	public void alMidi(MidiDecoder midi) {
		this.instruct(midi, Cmd.MIDI, 0, 0);
	}
	
	public MidiDecoder alGetMidi() {
		return this.mid;
	}
}
