package ei.engine.sound;

import java.nio.FloatBuffer;
import java.nio.IntBuffer;
import java.util.ArrayList;

import org.lwjgl.BufferUtils;
import org.lwjgl.openal.AL;
import org.lwjgl.openal.AL10;
import org.lwjgl.openal.OpenALException;

import ei.engine.util.MultiPrintStream;

/**
 * Responsible for holding and playing the sounds used in the game.
 * 
 * @author Ziver Koc
 */
public class SoundManager {
	/** The number of sound sources enabled - default 8 */
	public static final int MAX_SOURCES = 128;
	/** The single instance of this class */
	private static SoundManager instance = new SoundManager();
	
	/** True if sound effects are turned on */
	private boolean enabled;
	/** The OpenGL AL sound sources */
	private ArrayList<SourceNode> sources = new ArrayList<SourceNode>();

	class SourceNode{
		int source;
		int soundId;
		
		public SourceNode(int source, int sound){
			this.source = source;
			this.soundId = sound;
		}
	}
	
	/**
	 * Create a new sound store
	 */
	private SoundManager() {
		init();
	}
	
	/**
	 * Indicate whether sound effects should be played 
	 * 
	 * @param sounds True if sound effects should be played
	 */
	public void setEnabled(boolean enabled) {
		this.enabled = enabled;
	}
	
	/**
	 * Check if sound effects are currently enabled
	 * 
	 * @return True if sound effects are currently enabled
	 */
	public boolean isEnabled() {
		return enabled;
	}
	
	/**
	 * Initialise the sound effects stored. This must be called
	 * before anything else will work
	 */
	private void init() {
		try {
			AL.create();
			enabled = true;
		} catch (Exception e) {
			e.printStackTrace();
			MultiPrintStream.out.println(e);
			enabled = false;
		}
	}

	/**
	 * Play the specified buffer as a sound effect with the specified
	 * pitch and gain.
	 * 
	 * @param buffer The ID of the buffer to play
	 * @param pitch The pitch to play at
	 * @param gain The gain to play at
	 * @return The id of the assigned source
	 */
	public int playSound(Sound sound, boolean loop) {
		if (enabled && sound.getBuffer() >= 0) {
			int sourceId = getAvailableSource();
			if(sourceId >= 0){
				stopSound(sourceId);
				MultiPrintStream.out.println("Playing sound: source="+sourceId+" buffer="+sound.getBuffer());
				
				AL10.alSourcei(sources.get(sourceId).source, AL10.AL_BUFFER, sound.getBuffer());
				AL10.alSourcef(sources.get(sourceId).source, AL10.AL_PITCH, 1.0f);
				AL10.alSourcef(sources.get(sourceId).source, AL10.AL_GAIN, 1.0f);
				AL10.alSourcei(sources.get(sourceId).source, AL10.AL_LOOPING,  (loop ? AL10.AL_TRUE : AL10.AL_FALSE)  );
			    
				
				AL10.alSource3f(sources.get(sourceId).source, AL10.AL_POSITION, 
						sound.getLocation().getX(),
						sound.getLocation().getY(),
						sound.getLocation().getZ());
				AL10.alSource3f(sources.get(sourceId).source, AL10.AL_VELOCITY, 
						sound.getVelocity().getX(),
						sound.getVelocity().getY(),
						sound.getVelocity().getZ());
				
				AL10.alSourcePlay(sources.get(sourceId).source);
				sources.get(sourceId).soundId = sound.getSoundId();
				return sourceId;
			}
		}
		return -1;
	}
	
	/**
	 * Looks for a availablee source. Creates a source if 
	 * necessary
	 * 
	 * @return The source id
	 */
	private int getAvailableSource(){	      
	      int source = -1;
	      MultiPrintStream.out.println("Sound sources: quantity="+sources.size());
	      
	      //looking for a source that is not playing
	      for(int i=0; i<sources.size() ;i++){
	    	  int state = AL10.alGetSourcei(sources.get(i).source, AL10.AL_SOURCE_STATE);
	    	  if(state != AL10.AL_PLAYING){
	    		  source = i;
	    	  }
	      }
	      
	      //creating a new source
	      if(source < 0 && sources.size() <= MAX_SOURCES){
	    	  IntBuffer sourceBuffer = BufferUtils.createIntBuffer(1);
	    	  int result;
		      // Generate a source.
		      AL10.alGenSources(sourceBuffer);
	
		      if ((result = AL10.alGetError()) != AL10.AL_NO_ERROR){
		          throw new OpenALException(getALErrorString(result));
		      }
		      
		      // Save the source id.
		      sources.add(new SourceNode(sourceBuffer.get(0),-1));
		      source = sources.size()-1;
	      }

	      // Return the source id.
	      return source;
	}
	
	/**
	 * Stops playing a sound
	 * 
	 * @param sourceId The id of the source
	 */
	public void stopSound(Sound sound){
		if(sound.getSourceId() >= 0 && sources.get(sound.getSourceId()).soundId == sound.getSoundId()){
			MultiPrintStream.out.println("Stoping sound: source="+sound.getSourceId());
			AL10.alSourceStop(sources.get(sound.getSourceId()).source);
		}
	}
	
	/**
	 * Stops playing a sound
	 * 
	 * @param sourceId The id of the source
	 */
	private void stopSound(int sourceId){
		if(sourceId >= 0){
			MultiPrintStream.out.println("Stoping sound: source="+sourceId);
			AL10.alSourceStop(sources.get(sourceId).source);
		}
	}
	
	/**
	 * Specify the location of the sound
	 * 
	 * @param sourceId The id of the source
	 * @param x The x axes
	 * @param y The y axes
	 * @param z The z axes
	 */
	public void setSoundLocation(Sound sound, float x, float y, float z){
		//specify where the sound is comming from by tree axes x,y and z
		if(sound.getSourceId() >= 0 && sources.get(sound.getSourceId()).soundId == sound.getSoundId()){
			AL10.alSource3f(sources.get(sound.getSourceId()).source, AL10.AL_POSITION, x, y, z);   
		}
	}

	/**
	 * Set the location of the listener
	 * 
	 * @param x The x coordinate
	 * @param y The y coordinate
	 * @param z The z coordinate
	 */
	public void setListenerLocation(float x, float y, float z){
	    AL10.alListener3f(AL10.AL_POSITION, x, y, z);
	}
	
	/**
	 * Set the velocity of the listener
	 * 
	 * @param x The x coordinate
	 * @param y The y coordinate
	 * @param z The z coordinate
	 */
	public void setListenerVelocity(float x, float y, float z){
	    AL10.alListener3f(AL10.AL_VELOCITY, x, y, z);
	}
	
	/**
	 * Set the orientation of the listener
	 * 
	 * @param rx The x rotation
	 * @param ry The y rotation
	 * @param rz The z rotation
	 * @param x The x coordinate
	 * @param y The y coordinate
	 * @param z The z coordinate
	 */
	public void setListenerOrientation(float rx, float ry, float rz,float x, float y, float z){
		float[] data = new float[] { rx, ry, rz , x, y, z };
		FloatBuffer listenerOrientation = BufferUtils.createFloatBuffer(data.length).put(data);
		listenerOrientation.flip();

		AL10.alListener(AL10.AL_ORIENTATION,listenerOrientation);
	}
	
	/**
	 * 1) Identify the error code.
	 * 2) Return the error as a string.
	 */
	public static String getALErrorString(int err) {
		switch (err) {
		case AL10.AL_NO_ERROR:
			return "AL_NO_ERROR";
		case AL10.AL_INVALID_NAME:
			return "AL_INVALID_NAME";
		case AL10.AL_INVALID_ENUM:
			return "AL_INVALID_ENUM";
		case AL10.AL_INVALID_VALUE:
			return "AL_INVALID_VALUE";
		case AL10.AL_INVALID_OPERATION:
			return "AL_INVALID_OPERATION";
		case AL10.AL_OUT_OF_MEMORY:
			return "AL_OUT_OF_MEMORY";
		default:
			return "No such error code";
		}
	}
	
	/**
	 * Get the single instance of this class
	 * 
	 * @return The single instnace of this class
	 */
	public static SoundManager getInstnace() {
		if(instance == null){
			instance = new SoundManager();
		}
		return instance;
	}
}
