package client.audio;

import java.io.BufferedInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;

import client.Client;
import client.util.FileUtils;
import common.collect.BiMap;
import common.collect.HashBiMap;
import common.collect.Lists;
import common.collect.Maps;
import common.entity.npc.EntityNPC;
import common.init.SoundEvent;
import common.log.Log;
import common.rng.Random;
import common.sound.MovingSound;
import common.sound.Sound;
import common.util.ExtMath;

public class SoundManager {
	private class Source {
		private final boolean attModel;
		private final float distOrRoll;
		private final int source;

		private float positionX;
		private float positionY;
		private float positionZ;
		private float volume;

		public Source(short[] buffer, Volume type, boolean toLoop, String sourcename, float x, float y, float z, boolean attModel,
				float distOrRoll, float volume) {
			this.positionX = x;
			this.positionY = y;
			this.positionZ = z;
			this.attModel = attModel;
			this.distOrRoll = distOrRoll;
			this.volume = volume;
			this.source = SoundManager.this.getNextChannel(sourcename);
			updateGain();
			SoundManager.this.gm.getAudioInterface().alSourcePlay(this.source, buffer, type, toLoop);
		}

		public void updateGain() {
			float gain;
			if(attModel) {
				double dX = positionX - SoundManager.this.listenerX;
				double dY = positionY - SoundManager.this.listenerY;
				double dZ = positionZ - SoundManager.this.listenerZ;
				float dist = (float)Math.sqrt(dX * dX + dY * dY + dZ * dZ);
				if(dist <= 0) {
					gain = 1.0f;
				}
				else if(dist >= distOrRoll) {
					gain = 0.0f;
				}
				else {
					gain = 1.0f - (dist / distOrRoll);
				}
				if(gain > 1.0f)
					gain = 1.0f;
				if(gain < 0.0f)
					gain = 0.0f;
			}
			else {
				gain = 1.0f;
			}
			SoundManager.this.gm.getAudioInterface().alGain(source, (gain * volume));
		}
	}
	
	private static final Random RANDOM = new Random();
	private static final Map<SoundEvent, short[][]> BUFFERS = Maps.newEnumMap(SoundEvent.class);
	
    private final Map<String, Sound> playingSounds = HashBiMap.<String, Sound>create();
    private final Map<Sound, String> invPlayingSounds = ((BiMap)this.playingSounds).inverse();
    private final List<MovingSound> tickableSounds = Lists.<MovingSound>newArrayList();
    private final Map<String, Integer> playingSoundsStopTime = Maps.<String, Integer>newHashMap();
	private final Map<String, Source> sources = new HashMap<String, Source>();
	private final String[] channels = new String[28];
	private final Client gm;
	
	private float listenerX;
	private float listenerY;
	private float listenerZ;
	private int nextChannel = 0;
    private int playTime = 0;
	private int eventId = 0;
	
	public SoundManager(Client gm) {
		this.gm = gm;
	}
	
	public static void loadSounds() {
		for(SoundEvent entry : SoundEvent.values()) {
			short[][] buffers = new short[entry.getSounds().length][];
			for(int z = 0; z < entry.getSounds().length; z++) {
				String sound = entry.getSounds()[z];
				Log.SOUND.trace("Lade Sound %s", sound);
				buffers[z] = readOgg("sounds/" + sound + ".ogg");
			}
			BUFFERS.put(entry, buffers);
		}
	}
	
	private static short[] readOgg(String filename) {
		InputStream in = null;
		try {
			in = new BufferedInputStream(FileUtils.getResource(filename));
			return CodecJOrbis.readAll(in);
		}
		catch(FileNotFoundException e) {
			Log.IO.error("Fehler beim Lesen von OPUS-Datei '%s': Datei nicht gefunden", filename);
			return null;
		}
		catch(Exception e) {
			Log.IO.error("Fehler beim Lesen von OPUS-Datei '%s': %s", filename, e.getMessage());
			return null;
		}
		finally {
			try {
				if(in != null)
					in.close();
			}
			catch(IOException e) {
			}
		}
	}
	
	private static short[] getBuffer(SoundEvent event) {
		return RANDOM.pick(BUFFERS.get(event));
	}
    
    public void unload()
    {
        this.stopSounds();
		for(int x = 0; x < this.channels.length; x++) {
			channels[x] = null;
		}
		this.sources.clear();
		this.nextChannel = 0;
    	this.eventId = 0;
    }
    
    public void stopSounds()
    {
        for (String s : this.playingSounds.keySet())
        {
    		Source src = sources.get(s);
    		if(src != null)
    			this.gm.getAudioInterface().alSourceStop(src.source);
    		sources.remove(s);
        }
        this.playingSounds.clear();
        this.tickableSounds.clear();
        this.playingSoundsStopTime.clear();
    }

    public void update()
    {
        ++this.playTime;

        for (MovingSound itickablesound : this.tickableSounds)
        {
            itickablesound.update();

            if (itickablesound.isDonePlaying())
            {
                String s = (String)this.invPlayingSounds.get(itickablesound);

                if (s != null)
                {
            		Source src = sources.get(s);
            		if(src != null)
            			this.gm.getAudioInterface().alSourceStop(src.source);
            		sources.remove(s);
                }
            }
            else
            {
                String s = (String)this.invPlayingSounds.get(itickablesound);
        		Source source = sources.get(s);
        		if(source != null) {
        			source.volume = ExtMath.clampf(itickablesound.getVolume(), 0.0f, 1.0f);
        			source.positionX = itickablesound.getXPosF();
        			source.positionY = itickablesound.getYPosF();
        			source.positionZ = itickablesound.getZPosF();
        			source.updateGain();
        		}
            }
        }

        Iterator<Entry<String, Sound>> iterator = this.playingSounds.entrySet().iterator();

        while (iterator.hasNext())
        {
            Entry<String, Sound> entry = (Entry)iterator.next();
            String s1 = (String)entry.getKey();
            Sound isound = (Sound)entry.getValue();

    		Source source = this.sources.get(s1);
            if (source == null || !this.gm.getAudioInterface().alPlaying(source.source))
            {
                int i = ((Integer)this.playingSoundsStopTime.get(s1)).intValue();

                if (i <= this.playTime)
                {
                    iterator.remove();
                    this.sources.remove(s1);
                    this.playingSoundsStopTime.remove(s1);

                    if (isound instanceof MovingSound)
                    {
                        this.tickableSounds.remove(isound);
                    }
                }
            }
        }
    }

    public void playSound(Sound sound)
    {
        float volume = sound.getVolume();
        float attn = 16.0F;
        if (volume > 1.0F)
            attn *= volume;
        volume = ExtMath.clampf(volume, 0.0f, 1.0f);
        if (volume > 0.0F)
        {
            String s = String.format("snd-%016x", this.eventId++);
            this.sources.put(s, new Source(getBuffer(sound.getSoundLocation()), Volume.getByType(sound.getChannelType()), sound.canRepeat(), s, sound.getXPosF(), sound.getYPosF(), sound.getZPosF(), sound.getAttenuationType(), attn, volume));
            this.playingSoundsStopTime.put(s, this.playTime + 20);
            this.playingSounds.put(s, sound);
            if (sound instanceof MovingSound moving)
                this.tickableSounds.add(moving);
        }
    }
    
    public void setListener(EntityNPC player, float partial)
    {
        double x = player.prevX + (player.posX - player.prevX) * (double)partial;
        double y = player.prevY + (player.posY - player.prevY) * (double)partial + (double)player.getEyeHeight();
        double z = player.prevZ + (player.posZ - player.prevZ) * (double)partial;
		this.listenerX = (float)x;
		this.listenerY = (float)y;
		this.listenerZ = (float)z;
		for(Source source : this.sources.values()) {
			source.updateGain();
		}
    }

	private int getNextChannel(String sourcename) {
		int x;
		int next;
		String name;
		next = nextChannel;

		int n = next;
		Source src;
		for(x = 0; x < channels.length; x++) {
			name = this.channels[n];
			src = name == null ? null : sources.get(name);

			if(src == null || !this.gm.getAudioInterface().alPlaying(src.source)) {
				nextChannel = n + 1;
				if(nextChannel >= channels.length)
					nextChannel = 0;
				this.channels[n] = sourcename;
				if(src != null)
					this.sources.remove(name);
				return n;
			}
			n++;
			if(n >= channels.length)
				n = 0;
		}

		n = next;
		name = this.channels[n];
		src = name == null ? null : sources.get(name);
		
		nextChannel = n + 1;
		if(nextChannel >= channels.length)
			nextChannel = 0;
		this.channels[n] = sourcename;
		if(src != null)
			this.sources.remove(name);
		return n;
	}
}
