package com.danitheskunk.skunkworks.audio;

import com.danitheskunk.skunkworks.audio.nodes.Node;
import org.lwjgl.openal.AL;
import org.lwjgl.openal.ALC;

import javax.sound.sampled.AudioInputStream;
import javax.sound.sampled.AudioSystem;
import javax.sound.sampled.UnsupportedAudioFileException;
import java.io.BufferedInputStream;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.IntBuffer;

import static org.lwjgl.openal.AL10.*;
import static org.lwjgl.openal.ALC10.*;

public class AudioEngine {
	private final int bufferSize;
	private final long context;
	private final long device;
	private Node node;
	private final int sampleRate;
	private final int source;

	public AudioEngine(int sampleRate, int bufferSize, int bufferCount) {
		this.sampleRate = sampleRate;
		this.bufferSize = bufferSize;

		device = alcOpenDevice((ByteBuffer) null);
		if(device == 0) {
			throw new RuntimeException("couldn't init openal device");
		}
		var deviceCaps = ALC.createCapabilities(device);
		context = alcCreateContext(device, (IntBuffer) null);
		if(context == 0) {
			throw new RuntimeException("couldn't create openal context");
		}
		alcMakeContextCurrent(context);
		AL.createCapabilities(deviceCaps);

		source = alGenSources();

		for(int i = 0; i < bufferCount; ++i) {
			addEmptyBuffer();
		}
		alSourcePlay(source);
	}

	private void addEmptyBuffer() {
		var buf = alGenBuffers();
		fillBuffer(buf);
		alSourceQueueBuffers(source, buf);
	}

	public void close() {
		alcDestroyContext(context);
		alcCloseDevice(device);
	}

	private void fillBuffer(int buffer) {
		var shortBuffer = new short[bufferSize * 2];

		if(node != null) {
			var buf = node.getBuffer(0);
			for(int i = 0; i < bufferSize; ++i) {
				shortBuffer[i * 2 + 1] = (short) (buf.getLeft(i) * 32760);
				shortBuffer[i * 2 + 1] = (short) (buf.getRight(i) * 32760);
			}
		}
		alBufferData(buffer, AL_FORMAT_STEREO16, shortBuffer, sampleRate);
	}

	public int getBufferSize() {
		return bufferSize;
	}

	public Node getNode() {
		return node;
	}

	public int getSampleRate() {
		return sampleRate;
	}

	public ISample loadSample(String path) {
		//todo: clean up this code
		FileInputStream file;
		try {
			file = new FileInputStream(path);
		} catch(FileNotFoundException e) {
			throw new RuntimeException(e);
		}
		var bufFile = new BufferedInputStream(file);
		AudioInputStream audio;
		byte[] bytes;
		try {
			audio = AudioSystem.getAudioInputStream(bufFile);
		} catch(UnsupportedAudioFileException | IOException e) {
			throw new RuntimeException(e);
		}
		var format = audio.getFormat();
		int channels = format.getChannels();
		if(channels <= 0 || channels > 2) {
			throw new RuntimeException("can only handle mono or stereo " +
				"samples");
		}
		int bytesPerFrame = format.getFrameSize();
		if(bytesPerFrame / channels != 2) {
			throw new RuntimeException("currently only 16-bit audio samples " +
				"supported");
		}

		try {
			bytes = audio.readAllBytes();
			audio.close();
			file.close();
		} catch(IOException e) {
			throw new RuntimeException(e);
		}
		var sample = new Samplei(bytes.length / 2 / channels, channels == 2);
		for(int i = 0; i < bytes.length / 2 / channels; ++i) {
			var l = (bytes[i * 2 * channels] & 0xFF) +
				(bytes[i * 2 * channels + 1] & 0xFF) * 256;
			if(channels == 2) {
				var r = (bytes[i * 2 * channels + 2] & 0xFF) +
					(bytes[i * 2 * channels + 3] & 0xFF) * 256;
				sample.setSamplei(i, (short) l, (short) r);
			} else {
				sample.setSamplei(i, (short) l);
			}
		}
		return sample;
	}

	public void refill() {
		var bufCount = alGetSourcei(source, AL_BUFFERS_PROCESSED);
		var bufs = new int[bufCount];
		alSourceUnqueueBuffers(source, bufs);
		for(int i = 0; i < bufCount; ++i) {
			fillBuffer(bufs[i]);
			alSourceQueueBuffers(source, bufs[i]);
		}
		if(alGetSourcei(source, AL_SOURCE_STATE) != AL_PLAYING) {
			System.out.println("buffer underrun, addng buffer");
			addEmptyBuffer();
			alSourcePlay(source);
		}
	}

	public void setNode(Node node) {
		this.node = node;
	}
}
