/*
 * Copyright (C) 1997-2001 Id Software, Inc.
 * 
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License
 * as published by the Free Software Foundation; either version 2
 * of the License, or (at your option) any later version.
 * 
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
 * 
 * See the GNU General Public License for more details.
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
 */

package lwjake2.sound.lwjgl;

import lwjake2.Defines;
import lwjake2.Globals;
import lwjake2.client.CL_ents;
import lwjake2.game.entity_state_t;
import lwjake2.qcommon.Com;
import lwjake2.sound.Sound;
import lwjake2.sound.sfx_t;
import lwjake2.sound.sfxcache_t;
import lwjake2.util.Lib;
import lwjake2.util.Math3D;

import java.nio.ByteBuffer;
import java.nio.FloatBuffer;
import java.nio.IntBuffer;
import java.util.Hashtable;
import java.util.Iterator;
import java.util.Map;

import org.lwjgl.openal.AL10;
import org.lwjgl.openal.AL11;
import org.lwjgl.openal.EFX10;

/**
 * Channel
 * 
 * @author dsanders/cwei
 */
public class Channel {

	final static int LISTENER = 0;
	final static int FIXED = 1;
	final static int DYNAMIC = 2;
	final static int MAX_CHANNELS = 128;
	
	private static Channel[] channels = new Channel[MAX_CHANNELS];
	private static IntBuffer sources = Lib.newIntBuffer(MAX_CHANNELS);
	// a reference of LWJGLSoundImpl.buffers 
	private static IntBuffer buffers;
	private static Map<Integer, Channel> looptable = new Hashtable<Integer, Channel>(MAX_CHANNELS);

	private static int numChannels;
    
    // stream handling
    private static boolean streamingEnabled = false;
    private static int streamQueue = 0;
		
	// sound attributes
	private int type;
	private int entnum;
	private int entchannel;
	private int bufferId;
	private int sourceId;
	private float volume;
	private float rolloff;
	private float[] origin = {0, 0, 0};

	// update flags
	private boolean autosound;
	private boolean active;
	private boolean modified;
	private boolean bufferChanged;
	private boolean volumeChanged;

	private Channel(int sourceId) {
		this.sourceId = sourceId;
		clear();
		volumeChanged = false;
		volume = 1.0f;
	}

	private void clear() {
		entnum = entchannel = bufferId = -1;
		bufferChanged = false;
		rolloff = 0;
		autosound = false;
		active = false;
		modified = false;
	}
	
	private static IntBuffer tmp = Lib.newIntBuffer(1);

	static int init(IntBuffer buffers) {
		Channel.buffers = buffers;
	    // create channels
		int sourceId;
		for (int i = 0; i < MAX_CHANNELS; i++) {
			
			AL10.alGenSources(tmp);
			sourceId = tmp.get(0);
			
            // can't generate more sources 
			if (sourceId <= 0) break;
			
			sources.put(i, sourceId);

			channels[i] = new Channel(sourceId);
			numChannels++;
			
			// set default values for AL sources
			AL10.alSourcef (sourceId, AL10.AL_GAIN, 1.0f);
			AL10.alSourcef (sourceId, AL10.AL_PITCH, 1.0f);
			AL10.alSourcei (sourceId, AL10.AL_SOURCE_ABSOLUTE,  AL10.AL_TRUE);
			AL10.alSource3f(sourceId, AL10.AL_VELOCITY, 0, 0, 0);
			AL10.alSourcei (sourceId, AL10.AL_LOOPING, AL10.AL_FALSE);
			AL10.alSourcef (sourceId, AL10.AL_REFERENCE_DISTANCE, 200.0f);
			AL10.alSourcef (sourceId, AL10.AL_MIN_GAIN, 0.0005f);
			AL10.alSourcef (sourceId, AL10.AL_MAX_GAIN, 1.0f);
		}
		return numChannels;
	}
	
	static void reset() {
		for (int i = 0; i < numChannels; i++) {
			AL10.alSourceStop(sources.get(i));
			AL10.alSourcei(sources.get(i), AL10.AL_BUFFER, 0);
			channels[i].clear();
		}
	}
	
	static void shutdown() {
		AL10.alDeleteSources(sources);
		numChannels = 0;
	}
	
    static void enableStreaming() {
        if (streamingEnabled) return;
        
        // use the last source
        numChannels--;
        streamingEnabled = true;
        streamQueue = 0;

        int source = channels[numChannels].sourceId;
        AL10.alSourcei (source, AL10.AL_SOURCE_RELATIVE,  AL10.AL_TRUE);
        AL10.alSourcef(source, AL10.AL_GAIN, 1.0f);
        channels[numChannels].volumeChanged = true;

        Com.DPrintf("streaming enabled\n");
    }

    static void disableStreaming() {
        if (!streamingEnabled) return;
        unqueueStreams();
        int source = channels[numChannels].sourceId;
        AL10.alSourcei (source, AL10.AL_SOURCE_ABSOLUTE,  AL10.AL_TRUE);

        // free the last source
        //numChannels++;
        streamingEnabled = false;
        Com.DPrintf("streaming disabled\n");
    }
    
    static void unqueueStreams() {
        if (!streamingEnabled) return;
        int source = channels[numChannels].sourceId;

        // stop streaming
        AL10.alSourceStop(source);
        int count = AL10.alGetSourcei(source, AL10.AL_BUFFERS_QUEUED);
        Com.DPrintf("unqueue " + count + " buffers\n");
        while (count-- > 0) {
            AL10.alSourceUnqueueBuffers(source, tmp);
        }
        streamQueue = 0;
    }

    static void updateStream(ByteBuffer samples, int count, int format, int rate) {
        enableStreaming();
        int source = channels[numChannels].sourceId;
        int processed = AL10.alGetSourcei(source, AL10.AL_BUFFERS_PROCESSED);
        
        boolean playing = (AL10.alGetSourcei(source, AL10.AL_SOURCE_STATE) == AL10.AL_PLAYING);
        boolean interupted = !playing && streamQueue > 2;
        
        IntBuffer buffer = tmp;
        if (interupted) {
            unqueueStreams();
            buffer.put(0, buffers.get(Sound.MAX_SFX + streamQueue++));
            Com.DPrintf("queue " + (streamQueue - 1) + '\n');
        } else if (processed < 2) {
            // check queue overrun
            if (streamQueue >= Sound.STREAM_QUEUE) return;
            buffer.put(0, buffers.get(Sound.MAX_SFX + streamQueue++));
            Com.DPrintf("queue " + (streamQueue - 1) + '\n');
        } else {
            // reuse the buffer
            AL10.alSourceUnqueueBuffers(source, buffer);
        }

        samples.position(0);
        samples.limit(count);
        AL10.alBufferData(buffer.get(0), format, samples, rate);
        AL10.alSourceQueueBuffers(source, buffer);
        
        if (streamQueue > 1 && !playing) {
            Com.DPrintf("start sound\n");
            AL10.alSourcePlay(source);
        }
    }
    
	static void addPlaySounds() {
		while (Channel.assign(PlaySound.nextPlayableSound()));
	}
	
	private static boolean assign(PlaySound ps) {
	    if (ps == null) return false;
		Channel ch = null;
		int i;
		for (i = 0; i < numChannels; i++) {
			ch = channels[i];

			if (ps.entchannel != 0 && ch.entnum == ps.entnum && ch.entchannel == ps.entchannel) {
				// always override sound from same entity
				if (ch.bufferId != ps.bufferId) {
				    AL10.alSourceStop(ch.sourceId);
				}
				break;
			}

			// don't let monster sounds override player sounds
			if ((ch.entnum == Globals.cl.playernum+1) && (ps.entnum != Globals.cl.playernum+1) && ch.bufferId != -1)
				continue;

			// looking for a free AL source
			if (!ch.active) {
				break;
			}
		}
		
		if (i == numChannels)
		    return false;
		
		ch.type = ps.type;
		if (ps.type == Channel.FIXED)
		    Math3D.VectorCopy(ps.origin, ch.origin);
		ch.entnum = ps.entnum;
		ch.entchannel = ps.entchannel;
		ch.bufferChanged = (ch.bufferId != ps.bufferId);			
		ch.bufferId = ps.bufferId;
		ch.rolloff = ps.attenuation * 2;
		ch.volumeChanged = (ch.volume != ps.volume);
		ch.volume = ps.volume;
		ch.active = true;
		ch.modified = true;
		return true;
	}
	
	private static Channel pickForLoop(int bufferId, float attenuation) {
        Channel ch;
        for (int i = 0; i < numChannels; i++) {
            ch = channels[i];
            // looking for a free AL source
            if (!ch.active) {
                ch.entnum = 0;
                ch.entchannel = 0;
        		ch.bufferChanged = (ch.bufferId != bufferId);			
                ch.bufferId = bufferId;
           		ch.volumeChanged = (ch.volume != 1.0f);
           		ch.volume = 1.0f;
           	    ch.rolloff = attenuation * 2;
                ch.active = true;
                ch.modified = true;
                return ch;
            }
        }
        return null;
    }
	
	private static FloatBuffer sourceOriginBuffer = Lib.newFloatBuffer(3);

	//stack variable
	private static float[] entityOrigin = {0, 0, 0};
 
	static void playAllSounds(FloatBuffer listenerOrigin, int currentEffectIndex, int currentFilterIndex) {
		FloatBuffer sourceOrigin = sourceOriginBuffer;
		Channel ch;
		int sourceId;
		int state;

		for (int i = 0; i < numChannels; i++) {
			ch = channels[i];
			if (ch.active) {
				sourceId = ch.sourceId;
				switch (ch.type) {
					case Channel.LISTENER:
					    sourceOrigin.put(0, listenerOrigin.get(0));
						sourceOrigin.put(1, listenerOrigin.get(1));
						sourceOrigin.put(2, listenerOrigin.get(2));
						break;
					case Channel.DYNAMIC:
						CL_ents.GetEntitySoundOrigin(ch.entnum, entityOrigin);
						convertVector(entityOrigin, sourceOrigin);
						break;
					case Channel.FIXED:
						convertVector(ch.origin, sourceOrigin);
						break;
				}

				if (ch.modified) {
					if (ch.bufferChanged) {
						AL10.alSourcei(sourceId, AL10.AL_BUFFER, ch.bufferId);
					}
					if (ch.volumeChanged) {
						AL10.alSourcef (sourceId, AL10.AL_GAIN, ch.volume);
					}
					AL10.alSourcef (sourceId, AL10.AL_ROLLOFF_FACTOR, ch.rolloff);
					AL10.alSource3f(sourceId, AL10.AL_POSITION, sourceOrigin.get(), sourceOrigin.get(), sourceOrigin.get());
					AL11.alSource3i(sourceId, EFX10.AL_AUXILIARY_SEND_FILTER, currentEffectIndex, 0, EFX10.AL_FILTER_NULL);
					AL10.alSourcei(sourceId, EFX10.AL_DIRECT_FILTER, currentFilterIndex);
					AL10.alSourcePlay(sourceId);
					sourceOrigin.rewind();
					ch.modified = false;
				} else {
					state = AL10.alGetSourcei(sourceId, AL10.AL_SOURCE_STATE);
					if (state == AL10.AL_PLAYING) {
						AL10.alSource3f(sourceId, AL10.AL_POSITION, sourceOrigin.get(), sourceOrigin.get(), sourceOrigin.get());
						sourceOrigin.rewind();
					} else {
						ch.clear();
					}
				}
				ch.autosound = false;
			}
		}
	}
	
	/*
	 * 	adddLoopSounds
	 * 	Entities with a ->sound field will generated looped sounds
	 * 	that are automatically started, stopped, and merged together
	 * 	as the entities are sent to the client
	 */
	static void addLoopSounds() {
		
		if ((Globals.cl_paused.value != 0.0f) || (Globals.cls.state != Globals.ca_active) || !Globals.cl.sound_prepped) {
			removeUnusedLoopSounds();
			return;
		}
		
		Channel ch;
		sfx_t	sfx;
		sfxcache_t sc;
		int num;
		entity_state_t ent;
		int key;
		int sound = 0;

		for (int i=0 ; i<Globals.cl.frame.num_entities ; i++) {
			num = (Globals.cl.frame.parse_entities + i)&(Defines.MAX_PARSE_ENTITIES-1);
			ent = Globals.cl_parse_entities[num];
			sound = ent.sound;

			if (sound == 0) continue;

			key = ent.number;
			ch = looptable.get(key);

			if (ch != null) {
				// keep on looping
				ch.autosound = true;
				Math3D.VectorCopy(ent.origin, ch.origin);
				continue;
			}

			sfx = Globals.cl.sound_precache[sound];
			if (sfx == null)
				continue;		// bad sound effect

			sc = sfx.cache;
			if (sc == null)
				continue;

			// allocate a channel
			ch = Channel.pickForLoop(buffers.get(sfx.bufferId), 6);
			if (ch == null)
				break;
				
			ch.type = FIXED;
			Math3D.VectorCopy(ent.origin, ch.origin);
			ch.autosound = true;
			
			looptable.put(key, ch);
			AL10.alSourcei(ch.sourceId, AL10.AL_LOOPING, AL10.AL_TRUE);
		}

		removeUnusedLoopSounds();

	}
	
	private static void removeUnusedLoopSounds() {
		Channel ch;
		// stop unused loopsounds
		for (Iterator<Channel> iter = looptable.values().iterator(); iter.hasNext();) {
			ch = iter.next();
			if (!ch.autosound) {
				AL10.alSourceStop(ch.sourceId);
				AL10.alSourcei(ch.sourceId, AL10.AL_LOOPING, AL10.AL_FALSE);
				iter.remove();
				ch.clear();
			}
		}
	}

	static void convertVector(float[] from, FloatBuffer to) {
		to.put(0, from[0]);
		to.put(1, from[2]);
		to.put(2, -from[1]);
	}

	static void convertOrientation(float[] forward, float[] up, FloatBuffer orientation) {
		orientation.put(0, forward[0]);
		orientation.put(1, forward[2]);
		orientation.put(2, -forward[1]);
		orientation.put(3, up[0]);
		orientation.put(4, up[2]);
		orientation.put(5, -up[1]);
	}

}
