package com.brackeen.javagamebook.sound;

import java.io.*;
import javax.sound.sampled.*;
import javax.sound.midi.*;
import com.brackeen.javagamebook.util.ThreadPool;
import com.brackeen.javagamebook.util.LoopingByteInputStream;


/**
    Klasa SoundManager zarządza odtwarzaniem dźwięku. Dziedziczy ona po
    klasie ThreadPool - każdy z wątków odtwarza w danym momencie jeden dźwięk.
    Pozwala to na łatwe ograniczenie jednocześnie odtwarzanych dźwięków.
    <p>Możliwe rozszerzenia tej klasy:<ul>
    <li>dodanie metody setMasterVolume(), wykorzystującej Controls do
        ustawienia głośności we wszystkich obiektach Line;
    <li>blokowanie odtwarzania dźwięku, jeżeli od żądania minęło więcej
        niż na przykład 500 ms.
    </ul>
*/
public class SoundManager extends ThreadPool {

    private AudioFormat playbackFormat;
    private ThreadLocal localLine;
    private ThreadLocal localBuffer;
    private Object pausedLock;
    private boolean paused;

    /**
        Tworzy nowy obiekt SoundManager, pozwalający na jednoczesne
        odtwarzanie podanej liczby dźwięków.
    */
    public SoundManager(AudioFormat playbackFormat) {
        this(playbackFormat,
            getMaxSimultaneousSounds(playbackFormat));
    }

    /**
        Tworzy nowy obiekt SoundManager, pozwalający na jednoczesne
        odtwarzanie podanej liczby dźwięków.
    */
    public SoundManager(AudioFormat playbackFormat,
        int maxSimultaneousSounds)
    {
        super(Math.min(maxSimultaneousSounds,
            getMaxSimultaneousSounds(playbackFormat)));
        this.playbackFormat = playbackFormat;
        localLine = new ThreadLocal();
        localBuffer = new ThreadLocal();
        pausedLock = new Object();
        // powiedomienie wątków w puli
        synchronized (this) {
            notifyAll();
        }
    }

    /**
        Zwraca maksymalną liczbę jednocześnie odtwarzanych dźwięków
        w formacie określonym przez AudioFormat.
    */
    public static int getMaxSimultaneousSounds(
        AudioFormat playbackFormat)
    {
        DataLine.Info lineInfo = new DataLine.Info(
            SourceDataLine.class, playbackFormat);
        Mixer mixer = AudioSystem.getMixer(null);
        return mixer.getMaxLines(lineInfo);
    }


    /**
        Wykonuje czyszczenie przed zamknięciem.
    */
    protected void cleanUp() {
        // Wyłączenie pauzy:
        setPaused(false);

        // Wybór miksera (zatrzymuje wszystkie dźwięki):
        Mixer mixer = AudioSystem.getMixer(null);
        if (mixer.isOpen()) {
            mixer.close();
        }
    }

    public void close() {
        cleanUp();
        super.close();
    }


    public void join() {
        cleanUp();
        super.join();
    }

    /**
        Włącza tryb pauzy. Dźwięki mogą nie zostać przerwane natychmiast.
    */
    public void setPaused(boolean paused) {
        if (this.paused != paused) {
            synchronized (pausedLock) {
                this.paused = paused;
                if (!paused) {
                    // Przywrócenie dźwięków:
                    pausedLock.notifyAll();
                }
            }
        }
    }

    /**
        Zwraca stan pauzy.
    */
    public boolean isPaused() {
        return paused;
    }

    /**
        Ładuje dźwięk z systemu plików. W przypadku błędu zwraca wartość null.
    */
    public Sound getSound(String filename) {
        return getSound(getAudioInputStream(filename));
    }

    /**
        Ładuje dźwięk ze strumienia dźwiękowego.
        W przypadku błędu zwraca wartość null.
    */
    public Sound getSound(InputStream is) {
        return getSound(getAudioInputStream(is));
    }

    /**
        Ładuje dźwięk ze strumienia dźwiękowego.
    */
    public Sound getSound(AudioInputStream audioStream) {
        if (audioStream == null) {
            return null;
        }

        // Liczba bajtów do odczytania:
        int length = (int)(audioStream.getFrameLength() *
            audioStream.getFormat().getFrameSize());

        // Odczyt całego strumienia:
        byte[] samples = new byte[length];
        DataInputStream is = new DataInputStream(audioStream);
        try {
            is.readFully(samples);
            is.close();
        }
        catch (IOException ex) {
            ex.printStackTrace();
        }

        // Zwracanie próbek:
        return new Sound(samples);
    }

    /**
        Tworzy obiekt AudioInputStream z pliku dźwiękowego
        zapisanego w systemie plików.
    */
    public AudioInputStream getAudioInputStream(String filename) {
        try {
            return getAudioInputStream(
                new FileInputStream(filename));
        }
        catch (IOException ex) {
            ex.printStackTrace();
            return null;
        }
    }

    /**
        Tworzy obiekt AudioInputStream z pliku dźwiękowego
        przesłanego za pomocą strumienia.
    */
    public AudioInputStream getAudioInputStream(InputStream is) {

        try {
            if (!is.markSupported()) {
                is = new BufferedInputStream(is);
            }
            // Otwarcie strumienia źródłowego:
            AudioInputStream source =
                AudioSystem.getAudioInputStream(is);

            // Konwersja na format odtwarzania:
            return AudioSystem.getAudioInputStream(
                playbackFormat, source);
        }
        catch (UnsupportedAudioFileException ex) {
            ex.printStackTrace();
        }
        catch (IOException ex) {
            ex.printStackTrace();
        }
        catch (IllegalArgumentException ex) {
            ex.printStackTrace();
        }

        return null;
    }

    /**
        Odtwarzanie dźwięku. Metoda natychmiast kończy działanie.
    */
    public InputStream play(Sound sound) {
        return play(sound, null, false);
    }

    /**
        Odtwarzanie dźwięku z opcjonalnym filtrem SoundFilter oraz
        opcjonalnym zapętleniem. Metoda natychmiast kończy działanie.
    */
    public InputStream play(Sound sound, SoundFilter filter,
        boolean loop)
    {
        InputStream is;
        if (sound != null) {
            if (loop) {
                is = new LoopingByteInputStream(
                    sound.getSamples());
            }
            else {
                is = new ByteArrayInputStream(sound.getSamples());
            }

            return play(is, filter);
        }
        return null;
    }

    /**
        Odtwarzanie dźwięku ze strumienia InputStream. 
        Metoda natychmiast kończy działanie.
    */
    public InputStream play(InputStream is) {
        return play(is, null);
    }

    /**
        Odtwarzanie dźwięku ze strumienia InputStream z 
        opcjonalnym filtrem SoundFilter.
        Metoda natychmiast kończy działanie.
    */
    public InputStream play(InputStream is, SoundFilter filter) {
        if (is != null) {
            if (filter != null) {
                is = new FilteredSoundStream(is, filter);
            }
            runTask(new SoundPlayer(is));
        }
        return is;
    }

    /**
        Sygnalizuje moment uruchomienia PooledThread. 
        Tworzy obiekt Line oraz bufor.
    */
    protected void threadStarted() {
        // Oczekiwanie na zakończenie konstruktora SoundManager:
        synchronized (this) {
            try {
                wait();
            }
            catch (InterruptedException ex) { }
        }

        // Używamy małego bufora - 100ms (1/10 s) -
        // dla filtrów zmieniających się w czasie działania.
        int bufferSize = playbackFormat.getFrameSize() *
            Math.round(playbackFormat.getSampleRate() / 10);

        // Utworzenie, otwarcie i uruchomienie obiektu Line:
        SourceDataLine line;
        DataLine.Info lineInfo = new DataLine.Info(
            SourceDataLine.class, playbackFormat);
        try {
            line = (SourceDataLine)AudioSystem.getLine(lineInfo);
            line.open(playbackFormat, bufferSize);
        }
        catch (LineUnavailableException ex) {
            // Obiekt Line jest niedostępny - sygnalizacja zakończenia wątku:
            Thread.currentThread().interrupt();
            return;
        }

        line.start();

        // Tworzenie bufora:
        byte[] buffer = new byte[bufferSize];

        // Ustawienie zmiennych lokalnych wątku:
        localLine.set(line);
        localBuffer.set(buffer);
    }

    /**
        Sygnalizacja zatrzymania PooledThread. Opróżnia i zatrzymuje
        obiekt Line dla wątku.
    */
    protected void threadStopped() {
        SourceDataLine line = (SourceDataLine)localLine.get();
        if (line != null) {
            line.drain();
            line.close();
        }
    }

    /**
        Klasa SoundPlayer jest zadaniem wykonywanym przez wątki PooledThreads.
        Otrzymuje obiekt Line z wątku oraz bufor ze zmiennych
        ThreadLocal i odtwarza dźwięk z InputStream.
        <p>Klasa ta działa tylko wtedy, gdy jest wywołana z PooledThread.
    */
    protected class SoundPlayer implements Runnable {

        private InputStream source;

        public SoundPlayer(InputStream source) {
            this.source = source;
        }

        public void run() {
            // Pobranie Line i bufora z ThreadLocals:
            SourceDataLine line = (SourceDataLine)localLine.get();
            byte[] buffer = (byte[])localBuffer.get();
            if (line == null || buffer == null) {
                // Obiekt Line niedostępny.
                return;
            }

            // Kopiowanie danych do Line:
            try {
                int numBytesRead = 0;
                while (numBytesRead != -1) {
                    // Jeżeli włączona jest pauza, wyłącz ją.
                    synchronized (pausedLock) {
                        if (paused) {
                            try {
                                pausedLock.wait();
                            }
                            catch (InterruptedException ex) {
                                return;
                            }
                        }
                    }
                    // Kopiowanie danych:
                    numBytesRead =
                        source.read(buffer, 0, buffer.length);
                    if (numBytesRead != -1) {
                        line.write(buffer, 0, numBytesRead);
                    }
                }
            }
            catch (IOException ex) {
                ex.printStackTrace();
            }
        }
    }
}
