import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.InputStreamReader;
import java.io.PrintStream;

import net.beadsproject.beads.core.*;
import net.beadsproject.beads.data.Sample;
import net.beadsproject.beads.data.buffers.*;
import net.beadsproject.beads.ugens.*;

/**
 * SoundEffect: A parameterised sound effect object, for use with Jsfxr.
 * 
 * build() actually builds the sound, and must be called before playing it
 * When asked to play, the soundeffect attaches its chain to an AudioContext, thus playing.
 * The standard play() function will also remove the chain from the ac once it has finished playing.
 * Calling reset() will reset the soundeffect so it can be played again.
 */
public class SoundEffect extends Bead{

	UGen effect, wrappedEffect;	
	float lengthOfEffect;
	AudioContext ac;
	boolean built;
	
	/**
	 * The sound effect is created by specifying ALL the parameters that define it.
	 **/
	public SoundEffect(
			int GeneratorType,
			float Volume,

			float AttackTime,
			float SustainTime,
			float SustainPunch,
			float DecayTime,

			float Frequency,
			float MinFrequency,
			float Slide,
			float Curvature,
			float VibratoDepth,
			float VibratoSpeed,

			float ChangeAmount,
			float ChangeSpeed,

			float RepeatSpeed,

			float PhaserOffset,
			float PhaserSweep)
	{
		wrappedEffect = null;
		effect = null;
		built = false;
		
		this.GeneratorType = GeneratorType;
		this.Volume = Volume;

		this.AttackTime = AttackTime;
		this.SustainTime = SustainTime;
		this.SustainPunch = SustainPunch;
		this.DecayTime = DecayTime;

		this.Frequency = Frequency;
		this.MinFrequency = MinFrequency;
		this.Slide = Slide;
		this.Curvature = Curvature;
		this.VibratoDepth = VibratoDepth;
		this.VibratoSpeed = VibratoSpeed;

		this.ChangeAmount = ChangeAmount;
		this.ChangeSpeed = ChangeSpeed;

		this.RepeatSpeed = RepeatSpeed;

		this.PhaserOffset = PhaserOffset;
		this.PhaserSweep = PhaserSweep;
	}
	
	public SoundEffect(SoundEffect s)
	{
		wrappedEffect = null;
		effect = null;
		built = false;
		
		this.GeneratorType = s.GeneratorType;
		this.Volume = s.Volume;

		this.AttackTime = s.AttackTime;
		this.SustainTime = s.SustainTime;
		this.SustainPunch = s.SustainPunch;
		this.DecayTime = s.DecayTime;

		this.Frequency = s.Frequency;
		this.MinFrequency = s.MinFrequency;
		this.Slide = s.Slide;
		this.Curvature = s.Curvature;
		this.VibratoDepth = s.VibratoDepth;
		this.VibratoSpeed = s.VibratoSpeed;

		this.ChangeAmount = s.ChangeAmount;
		this.ChangeSpeed = s.ChangeSpeed;

		this.RepeatSpeed = s.RepeatSpeed;

		this.PhaserOffset = s.PhaserOffset;
		this.PhaserSweep = s.PhaserSweep;
	}
	
	public void build(AudioContext ac) 
	{
		built = true;
		this.ac = ac;
		float ScaledFrequency = Frequency;
		switch (GeneratorType)
		{
		case GT_SQUARE: 
			effect = new WavePlayer(ac,ScaledFrequency,new SquareBuffer().getDefault()); 
			break; 
		case GT_SAW: 
			effect = new WavePlayer(ac,ScaledFrequency,new SawBuffer().getDefault()); 
			break;
		case GT_SINE: 
			effect = new WavePlayer(ac,ScaledFrequency,new SineBuffer().getDefault()); 
			break;
		case GT_NOISE: 
			ScaledFrequency = Frequency/NoiseMaxFreq;
			effect = new WavePlayer(ac,ScaledFrequency,new NoiseBuffer().generateBuffer((int)ac.msToSamples(1000.f)));
			break;
		};

		float totalSoundLength = AttackTime+0.01f+SustainTime+DecayTime;
		// int totalSoundLengthSamples = (int)ac.msToSamples(totalSoundLength);
		int maxSoundLengthSamples = (int)ac.msToSamples(MaxSoundTime);
		lengthOfEffect = totalSoundLength;

		// Frequency modulation
		UGen freq;
		Envelope freqEnv = new Envelope(ac,ScaledFrequency);
		float SlideSc = Slide*Slide;

		float MaxFreq = 22000.f;
		float MinFreq = MinFrequency;

		if (GeneratorType==GT_NOISE)
		{
			MaxFreq/=NoiseMaxFreq;      
			MinFreq/=NoiseMaxFreq;
		}

		// Frequency Sweep
		if (Slide > 0)
			freqEnv.addSegment(MaxFreq,1000.f/SlideSc,Curvature,null);
		else if (Slide < 0)
			freqEnv.addSegment(MinFreq,1000.f/SlideSc,Curvature,null);

		// Vibrato
		freq = new Mult(ac,freqEnv,
				new Add(ac,
						new Mult(ac,
								new WavePlayer(ac,VibratoSpeed*VibratoSpeed*50,new SineBuffer().getDefault()),
								//new WavePlayer(ac,1,new NoiseBuffer().getDefault()),
								//new Static(ac,1.0)
								new Static(ac,VibratoDepth)
						),
						new Static(ac,1.f))
		);

		// Add pitch change multiplier
		Envelope pitchChange = new Envelope(ac,1.f);
		float pitchChangeAmount = 1+ChangeAmount;
		float pitchChangeTime = (1-ChangeSpeed)*1000.f;    
		pitchChange.addSegment(1.f,pitchChangeTime);
		pitchChange.addSegment(pitchChangeAmount,0.01f);   
		UGen pc = new Mult(ac,pitchChange,freq);

		// Add repeater
		int repeatSamples = 1+(int)(Math.pow(1-RepeatSpeed,2)*maxSoundLengthSamples);
		UGen rep = repeater(ac,pc,repeatSamples);
		((WavePlayer)effect).setFrequencyEnvelope(rep);

		// Apply sweeping phaser
		float PhaserOffsetScaled = (float)ac.samplesToMs(PhaserOffset*1024*16);
		Envelope phaserEnvelope = new Envelope(ac,PhaserOffsetScaled);
		float phaserTarget = (float)ac.samplesToMs(PhaserSweep*10*MaxSoundTime + PhaserOffsetScaled);   
		phaserTarget = Math.max(0.f,phaserTarget);
		phaserEnvelope.addSegment(phaserTarget,MaxSoundTime);
		TapIn ti = new TapIn(ac,10000.f);  
		ti.addInput(effect);
		TapOut to = new TapOut(ac,ti,phaserEnvelope);  
		ScalingMixer sm = new ScalingMixer(ac);
		sm.addInput(effect);
		sm.addInput(to);

		// Apply amplitude envelope
		// and limit to (-1,1) range
		Envelope volEnv = new Envelope(ac,0.f);  
		volEnv.addSegment(Volume/1000.f,AttackTime);
		volEnv.addSegment(SustainPunch*Volume/1000.f,0.01f);
		volEnv.addSegment(Volume/1000.f,SustainTime,.8f,null);
		volEnv.addSegment(0.f,DecayTime);
		Gain volume = new Gain(ac,1,volEnv);
		volume.addInput(effect);
		effect = volume;		
	}


	/**
	 * Plays the effect at a given bit depth and sample rate
	 * 
	 * @param BitDepth either BITS8 or BITS18
	 * @param SampleRate either FORTYFOUR or TWENTYTWO
	 */
	public void play(AudioContext ac, int BitDepth, int SampleRate)
	{
		if (!built) build(ac);
		wrappedEffect = ugen(BitDepth,SampleRate);	

		// begin playing
		ac.out.addInput(wrappedEffect);
		// remove after it has finished
		new DelayTrigger(ac,lengthOfEffect,this);		
	}

	public int lengthInSamples()
	{
		return (int) ac.msToSamples(lengthOfEffect);
	}

	public float lengthInMs()
	{
		return lengthOfEffect;
	}

	public UGen ugen()
	{
		RangeLimiter rl = new RangeLimiter(ac,1);
		rl.addInput(effect);
		return rl;
	}

	public UGen ugen(int BitDepth, int SampleRate)
	{	
		RangeLimiter rl = new RangeLimiter(ac,1);
		rl.addInput(effect);

		// Convert to 8 or 16-bit
		int numbits = BitDepth==BITS8?8:16;
		NBitsConverter ebp = new NBitsConverter(ac,numbits);
		ebp.addInput(rl);
		UGen u = ebp;

		// Downsample if necessary
		if (SampleRate==TWENTYTWO)
		{
			// a coarse downsampler
			UGen to22 = new UGen(ac,1,1){
				public void calculateBuffer()
				{
					for(int i=0;i<bufferSize;i+=2)
					{
						bufOut[0][i] = bufIn[0][i];
						bufOut[0][i+1] = bufIn[0][i];
					}
				}      
			};

			to22.addInput(u);
			u = to22;
		}

		return u;
	}
	
	public Sample toSample(AudioContext ac)
	{
		if (!built) build(ac);
		
		Sample sample = new Sample(ac.getAudioFormat(), lengthInSamples());
		Recorder rec = new Recorder(ac,sample);
		// start recording
		rec.addInput(ugen());
		ac.out.addDependent(rec);
		ac.runForNSecondsNonRealTime(lengthInMs()/1000.f);
		ac.out.removeDependent(rec);
		return sample;		
	}

	/**
	 * Write the parameters of this soundeffect to an output stream.
	 * @param f
	 */
	public void write(PrintStream f)
	{
		f.print(GeneratorType); f.print(' ');
		f.print(Volume); f.print(' ');

		f.print(AttackTime); f.print(' ');
		f.print(SustainTime); f.print(' ');
		f.print(SustainPunch); f.print(' ');
		f.print(DecayTime); f.print(' ');

		f.print(Frequency); f.print(' ');
		f.print(MinFrequency); f.print(' ');
		f.print(Slide); f.print(' ');
		f.print(Curvature); f.print(' ');
		f.print(VibratoDepth); f.print(' ');
		f.print(VibratoSpeed); f.print(' ');

		f.print(ChangeAmount); f.print(' ');
		f.print(ChangeSpeed); f.print(' ');

		f.print(RepeatSpeed); f.print(' ');

		f.print(PhaserOffset); f.print(' ');
		f.print(PhaserSweep); f.print(' ');
	}
	
	static public SoundEffect read(String str) throws Exception
	{
		String[] s = str.split(" ");
		if (s.length!=17) throw new Exception("Can't read .jsfxr file.");
		int gt = Integer.parseInt(s[0]);
		float params[] = new float[s.length-1];
		for(int i=1;i<s.length;i++)
		{
			params[i-1] = Float.parseFloat(s[i]);
		}
		return new SoundEffect(gt,
				params[0],params[1],params[2],params[3],params[4],
				params[5],params[6],params[7],params[8],params[9],
				params[10], params[11], params[12], params[13], params[14], params[15]);
	}

	static public SoundEffect read(File file) throws Exception
	{
		BufferedReader br = new BufferedReader(
                new InputStreamReader(
                new FileInputStream(file)));
		return read(br.readLine());
	}

	public void remove()
	{
		ac.out.removeAllConnections(wrappedEffect);
	}

	public void messageReceived(Bead message)
	{
		remove();		
	}

	/// Generator types
	static public final int GT_SQUARE = 0;
	static public final int GT_SAW = 1;
	static public final int GT_SINE = 2;
	static public final int GT_NOISE = 3;

	// bit-depths
	static public final int BITS8 = 0;
	static public final int BITS16 = 1;

	// sample rates
	static public final int FORTYFOUR = 0;
	static public final int TWENTYTWO = 1;

	/// misc constants
	static final float NoiseMaxFreq = 6000.f;
	static final float MaxSoundTime = 9000.f;

	// parameters of this sound effect
	int GeneratorType;
	float Volume;

	float AttackTime;
	float SustainTime;
	float SustainPunch;
	float DecayTime;

	float Frequency;
	float MinFrequency;
	float Slide;
	float Curvature;
	float VibratoDepth;
	float VibratoSpeed;

	float ChangeAmount;
	float ChangeSpeed;

	float RepeatSpeed;

	float PhaserOffset;
	float PhaserSweep;

	/// Helper functions
	// repeater: returns a UGen that
	// records numSamples from source and loops it infinitely
	// used for the pewpew lasers
	public UGen repeater(AudioContext ac, UGen source, int numSamples)
	{
		Sample s = new Sample(ac.getAudioFormat(),numSamples);
		Recorder rec = new Recorder(ac,s);
		rec.start();
		rec.addInput(source);  
		SamplePlayer sp = new SamplePlayer(ac,s);
		sp.addDependent(rec);
		sp.setLoopType(SamplePlayer.LoopType.LOOP_FORWARDS);
		return sp;
	}

}
