package phunky;

import java.io.IOException;
import java.io.InputStream;
import java.net.URL;

import org.osgi.framework.BundleContext;

/**
 * SampleStream reads in 3 samples for each of the X, Y and Z axes
 * at startup. It then chooses between them at runtime based on
 * the accelerometer input given to it via its update() method.
 * The accelerometer readings determine which sample will be used
 * for each axis and the chosen three are then mixed and returned
 * as the sample value for the SampleStream itself.
 * 
 * @author finsprings
 *
 */
public class SampleStream extends InputStream {
	private int byteCount = 0;
	
	// streams for the samples we'll mix together based on accelerometer input
	private final InputStream x1;
	private final InputStream x2;
	private final InputStream x3;
	private final InputStream y1;
	private final InputStream y2;
	private final InputStream y3;
	private final InputStream z1;
	private final InputStream z2;
	private final InputStream z3;
	
	private InputStream xIn;
	private InputStream yIn;
	private InputStream zIn;
	
	// most recent accelerometer readings to choose our output from
	private float x = 0.0F;
	private float y = 0.0F;
	private float z = 0.0F;
	
	// thresholds for deciding which sample to choose,
	// calculated with audio module in slot 0 and
	// BUGmotion in slot 1 (and nothing in slots 2 and 3)
	// Values with BUGbase laying on a table were approximately:
	// X=-0.28, Y=-0.14, Z=1.08
	private final float xLowThreshold =  -1.06F; // back raised ~45 deg
	private final float xHighThreshold = 0.23F; // front raised ~45 deg
	
	private final float yLowThreshold = -0.80F; // left raised ~45deg
	private final float yHighThreshold = 0.53F; // right raised ~45deg
	
	private final float zLowThreshold = -0.5F; // Z seems to track x and y ?
	private final float zHighThreshold = 0.8F; // so I just looked at its variation
	
	/// used so we know when to stop
	private boolean tearDownRequested = false;
	
	/**
	 * read in the sample data we'll play with
	 * 
	 * @param bundleContext we need this to get to our sample resources
	 * @throws IOException may throw this trying to read in the samples
	 */
	public SampleStream(final BundleContext bundleContext) throws IOException {
		System.out.println("Loading samples data");

		x1 = loadSample(bundleContext, "claves.raw");
		x2 = loadSample(bundleContext, "cowbell.raw");
		x3 = loadSample(bundleContext, "floortom.raw");
		y1 = loadSample(bundleContext, "highmidtom.raw");
		y2 = loadSample(bundleContext, "vibra.raw");
		y3 = loadSample(bundleContext, "conga.raw");
		z1 = loadSample(bundleContext, "crashcymbal.raw");
		z2 = loadSample(bundleContext, "guiro.raw");
		z3 = loadSample(bundleContext, "snare.raw");
		
		System.out.println("All samples loaded");
	}
	
	/**
	 * read in a sample from our resource bundle
	 * 
	 * @param bundleContext we need this to get to the sample resource
	 * @param sampleFileName the name of the sample to read in
	 * @return
	 * @throws IOException  may throw this trying to read in the samples
	 */
	private Sample loadSample(
			final BundleContext bundleContext,
			final String sampleFileName) throws IOException {
		final URL url = bundleContext.getBundle().getResource(
				"resources/" + sampleFileName);
		return new Sample(url.openStream());
	}
	
	/**
	 * Our canned WAVE header. IAudioPlayer only accepts 44.1 16-bit stereo
	 * little-endian samples so our canned header tells it that's what we will
	 * send. We make the length as large as can be but fortunately IAudioPlayer
	 * doesn't pay any attention to it. If we didn't make it large then it
	 * would stop playing before we wanted it to (we want it to play forever,
	 * as long as this app is running).
	 */
	private static final char [] riffHeader = {
			// RIFF Chunk
			'R', 'I', 'F', 'F',
			0xFF, 0xFF, 0xFF, 0xFF,	// fake file length,
			'W', 'A', 'V', 'E',
			// FORMAT Chunk
			'f', 'm', 't', ' ',
			0x10, 0x00, 0x00, 0x00,	// FORMAT length
			0x01, 0x00,				// always 0x0001
			0x02, 0x00,				// stereo
			0x44, 0xAC, 0x00, 0x00,	// sample rate (44.1kHz) 
			0x10, 0xB1, 0x02, 0x00,	// bytes per second
			0x04, 0x00,				// bytes per sample (16-bit, stereo)
			0x10, 0x00,				// bits per sample
			// DATA Chunk
			'd', 'a', 't', 'a',
			0xFF, 0xFF, 0xFF, 0xFF	// fake sample count
			// actual sample data would be here if we were a real wave file...
	};

	/**
	 * Pick an input stream for a given axis, based on that axis'
	 * current value and its thresholds.
	 * 
	 * @param value the current reading for this axis
	 * @param lowThreshold this axis' low threshold
	 * @param highThreshold this axis' high threshold
	 * @param in1 stream to use if value is less than lowThreshold
	 * @param in2 stream to use if value is between lowThreshold and highThreshold
	 * @param in3 stream to use if value is higher than highThreshold
	 * @return the stream chosen to be used
	 */
	private InputStream chooseInput(
			final float value,
			final float lowThreshold,
			final float highThreshold,
			final InputStream in1,
			final InputStream in2,
			final InputStream in3) {
		if (value < lowThreshold) {
			return in1;
		} else if (value < highThreshold) {
			return in2;
		} else {
			return in3;
		}
	}
	
	/**
	 * Accept accelerometer data to base our sample selection on.
	 * 
	 * @param x X axis reading
	 * @param y Y axis reading
	 * @param z Z axis reading
	 */
	public void update(final float x, final float y, final float z) {
		//System.out.println("X="+x+", Y="+y+", Z="+z);
		this.x = x;
		this.y = y;
		this.z = z;
	}
	
	/**
	 * called when we need to stop sending audio
	 */
	public void stop() {
		this.tearDownRequested  = true;
	}
	
	/**
	 * Return our chosen mix of sample data. Note that
	 * we return the canned WAVE header above then move
	 * onto the sample data after that, which makes us
	 * appear to be a WAVE file to IAudioPlayer.
	 */
	public int read() throws IOException {
		int sample = 0;
		
		if (tearDownRequested) {
			System.out.println("Returning -1 to make audio stream appear done");
			sample = -1;
		}
		else if (byteCount < riffHeader.length) {
			// return the next byte of our canned header
			sample = riffHeader[byteCount];
		}
		else {
			// return the most recently computed sample based on the update() data
			// pick which samples to combine to create out output sample
			// based on the accelerometer values we have been given
			
			if ((byteCount % 4) == 0) {
				// we return sample data 8 bits at a time, but the samples
				// themselves are 16 bits each, and in stereo so there are
				// two samples that go together, so we mustn't switch streams
				// in the middle of a sample or we'll be out of sync (returning
				// half of one sample and half of the next). We can use the same
				// byteCount as we used for the header because the header has an
				// even number of bytes
				xIn = chooseInput(x, xLowThreshold, xHighThreshold, x1, x2, x3);
				yIn = chooseInput(y, yLowThreshold, yHighThreshold, y1, y2, y3);
				zIn = chooseInput(z, zLowThreshold, zHighThreshold, z1, z2, z3);
			}
			
			try {
				sample = xIn.read() + yIn.read() + zIn.read();
			}
			catch (IOException e) {
				// ignore it as it shouldn't happen with our InputStreams anyway
			}
		}
		
		++byteCount;
		return sample;
	}
}