package vermilion;

import java.util.ArrayList;
import javax.sound.sampled.AudioFormat;
import javax.sound.sampled.AudioSystem;
import javax.sound.sampled.DataLine;
import javax.sound.sampled.Mixer;
import javax.sound.sampled.SourceDataLine;

//TODO: Make the selection of a soundcard easier, both as a default setting during
//the first run and as a user preference that can later be modified. Consider
//using a Java library that uses ASIO or some other high performance multi
//channel audio API. (Specialised)


/**
 * Encapsulates an audio output and the double-buffering thread that feeds it.
 * This class uses the user's preferences to select the correct sound card,
 * sample rate and iBuffer size. Various constants to do with audio output are
 * also defined here.
 */
public class AudioThread implements Runnable
{
    // Hard coded constants that we need elsewhere.

    /**
     * The number of bytes per sample (per channel). Currently set to 2, for
     * 16-bit audio.
     */
    public final int BYTES_PER_SAMPLE_PER_CHANNEL = 2;

    /**
     * The number of audio channels, currently 2, for stereo. Note we don't
     * often use this, so there are many places where two channels is assumed.
     */
    public final int NUM_CHANNELS = 2;

    /**
     *  The output iBuffer size as chosen by the user preferences. A multiple of
     * this (bufferSize*BUFFER_SCALE) is the internal iBuffer size in samples,
     * and a further BUFFER_FACTOR gives the size of the iBuffer we will request
     * from the audio API.
     */
    private int bufferSize = 16;

    /**
     * The output sample rate as chosen by the user preferences. This is
     * exposed so that other parts of the system can determine the output
     * sample rate for calculations on time and/or frequency.
     */
    public int sampleRate = 48000;

    /**
     * Buffer scale is how large a iBuffer chunk is.
     */
    private final int BUFFER_SCALE = 1024;

    /**
     * The iBuffer factor is how much larger the external iBuffer is
     * compared to the internal one. So 2 means double buffering. This
     * can be adjusted with BUFFER_SCALE to provide different buffering
     * scenarios if need be. TODO: Make BUFFER_FACTOR user accessible?
     */
    private final float BUFFER_FACTOR = 2.0f;

    /**
     * This is how much of the calculated remaining time until a iBuffer refill
     * we will sleep for before checking again. More practically it is
     * a measure of how many milliseconds are left before we stop trusting
     * the OS to return from Sleep() in time and start spin-waiting instead.
     * This should be set at around 20 for XP/Vista/7 if we really wanted to
     * be on time every single time, however increasing the value very quickly
     * adds wasted CPU cycles and is a bit greedy on a single core computer.
     */
    private final int SLEEP_FACTOR = 3;

    // So the internal iBuffer size and external iBuffer size are defined.
    // We use the size in samples (NOT per channel) but also keep the
    // size in bytes handy.

    /**
     * The size of the mixing iBuffer in samples (per channel).
     * The internal iBuffer size is the most samples we will ever ask
     * for at once, so it is exposed publicly so that it can be used
     * in various places to define iBuffer sizes.
     */
    public int internalBufferSize = 0;

    /**
     * The size of the mixing iBuffer in bytes (NOT per channel), which we keep
     * for use during mixing.
     */
    private int byteBufferSize =  0;

    /**
     * The size of the soundcard iBuffer in bytes (NOT per channel).
     */
    private int externalBufferSize = 0;

    // This is the iBuffer we use and the audio line we feed it to.
    private String targetMixerName;
    private SourceDataLine audioLine = null;
    private float[][] iBuffer = null;
    private byte[] oBuffer = null;

    // These will point to the class registered to provide audio
    // etc. for the application.
    private AudioProvider audioProviderClass = null;
    private EventProvider eventProviderClass = null;

    // This is used to control the thread that feeds the audio line.
    private boolean isRunning = false;

    /**
     * Allows a plugin to register itself as the audio mixer for
     * the program. Thus the application and preferences/document
     * handling become a framework on which closely coupled plugins
     * can build the entire user interface and audio engine.
     * @param provider A class which (amongst other things) returns
     * floating point audio data to be played through the soundcard.
     */
    public void registerAudioProviderClass(AudioProvider provider)
    {
        // TODO: If we ever accept multiple connections, we must
        // check the provider is not simply reregistering after
        // loading a new document.
        provider.setBufferRef(iBuffer, internalBufferSize);
        audioProviderClass = provider;
    }

    /**
     * Allows a plugin to register itself as the event provider for
     * the program.
     * @param provider A class which can play back preprogrammed events
     * to control the machines in the audio chain.
     */
    public void registerEventProviderClass(EventProvider provider)
    {
        // TODO: If we ever accept multiple cueing systems, we must
        // check the provider is not simply reregistering after
        // loading a new document.
        eventProviderClass = provider;
    }

    /**
     *  The constructor does nothing, so we can reuse this class.
     */
    public AudioThread()
    {
        // Try to read the sample rate and iBuffer size preferences.
        if (Main.settings.getValue("Audio/SampleRate") != null
         && Main.settings.getValue("Audio/BufferSize") != null)
        {
            // If we find the preferences adjust our iBuffer size settings.
            sampleRate = (int) ((UsefulFileObjectSimple) Main.settings.getValue("Audio/SampleRate")).longValue;
            bufferSize = (int) ((UsefulFileObjectSimple) Main.settings.getValue("Audio/BufferSize")).longValue;
        }
        else
        {
            sampleRate = 22050;
            bufferSize = 8;
        }

        internalBufferSize = BUFFER_SCALE * bufferSize;
        byteBufferSize =  internalBufferSize * BYTES_PER_SAMPLE_PER_CHANNEL * NUM_CHANNELS;
        externalBufferSize = (int) (byteBufferSize * BUFFER_FACTOR);
        externalBufferSize = (externalBufferSize/4)*4; // Hack size correctness.

        // Get the name of the preferred mixer.
        UsefulFileObjectSimple obj = (UsefulFileObjectSimple) Main.settings.getValue("Audio/SoundDevice");
        targetMixerName = "";
        if (obj != null) targetMixerName = obj.strValue;
    }


    /**
     * Creates an audio output and starts a thread to feed it data. This is where
     * we read the preferences and calculate running iBuffer sizes. If we are
     * able to open an audio line we then start a thread in run().
     */
    public void start()
    {
        // Get the list of available mixers and choose the first one.
        Mixer.Info[] mixers = AudioSystem.getMixerInfo();
        Mixer.Info targetMixerInfo = mixers[0];

        // If we can find the preferred mixer (by name) choose that instead.
        for (int x = 0; x < mixers.length; x++)
        {
            if (mixers[x].toString().equals(targetMixerName)) targetMixerInfo = mixers[x];
        }

        // Get the actual mixer object out of the audio API.
        Mixer targetMixer = AudioSystem.getMixer(targetMixerInfo);

        // Create format descriptions for the output line with our
        // desired sample rate.
        AudioFormat format = new AudioFormat(sampleRate, 16, 2, true, false);
        DataLine.Info info = new DataLine.Info(SourceDataLine.class, format);

        // Try to open a line on the desired mixer with the desired sample format
        // and iBuffer size.
        try
        {
            audioLine = (SourceDataLine) targetMixer.getLine(info);

            audioLine.open(format, externalBufferSize);

            // TODO: Figure latency, including if if BUFFER_FACTOR != 2.0f
            System.out.println("Internal buffer time is " + (int) (internalBufferSize * 1000 / sampleRate / NUM_CHANNELS) + "ms");

            // Note this doesn't actually start audio output.
            audioLine.start();

            isRunning = true;
        }
        catch (Exception e)
        {
            System.out.println("Unable to open audio line: " + e.getMessage());
        }

        // If we manage to open the line, create our internal iBuffer, start
        // the audio line and create a thread to feed it.
        if (isRunning)
        {
            iBuffer = new float[NUM_CHANNELS][internalBufferSize];
            oBuffer = new byte[byteBufferSize];

            for (int x = 0; x < byteBufferSize; x++)
            {
                oBuffer[x] = 0;
            }

            // Start the audio thread which loops until we call stop.
            (new Thread(this)).start();

            // Start the audio line by sending the first (silent) data through.
            audioLine.write(oBuffer, 0, internalBufferSize);
        }
    }

    /**
     * Runs the audio thread, an infinite loop that feeds the output when
     * needed. This should not be called externally as it runs on its own
     * thread and constantly polls the audio iBuffer. Two main operations are
     * performed before the internal audio iBuffer (our iBuffer) is downmixed to
     * the external audio iBuffer (in the Java API). First the event provider
     * is asked to process events. It returns the number of samples that remain
     * until the next event. Second, the audio provider is asked to mix either
     * the number of samples until the next event or the number of samples in
     * the internal iBuffer, whichever is greatest.
     */
    @Override
    public void run()
    {
        //long lastTime = new Date().getTime();
        //double avgTime = 0.0;
        while (isRunning)
        {
            try
            {
                int available = audioLine.available();
                if (available >= byteBufferSize)
                {
                    doMixing();
                }
                else
                {
                    doSleeping(available);
                }
            }
            catch (Exception e) { System.out.println("Audio thread crashed: " + e.toString()); e.printStackTrace(); }
        }
    }

    private void doSleeping(int available) throws InterruptedException
    {
        int samplesRemaining = (byteBufferSize - available) / (BYTES_PER_SAMPLE_PER_CHANNEL * NUM_CHANNELS);
        int millisRemaining = samplesRemaining * 1000 / sampleRate;
        if (millisRemaining < 0) {
            millisRemaining = 0;
        }
        Thread.sleep(millisRemaining / SLEEP_FACTOR);
    }

    private void doMixing()
    {
        int samplesToProcess = internalBufferSize;
        if (audioProviderClass != null) {
            EventCollection events = null;
            if (eventProviderClass != null) {
                events = eventProviderClass.processEvents();
            }
            if (events != null) {
                samplesToProcess = events.samplesUntilNextEvent;
                audioProviderClass.processEvents(events.events);
            }
            if (samplesToProcess > internalBufferSize) {
                samplesToProcess = internalBufferSize;
            }
            try {
                audioProviderClass.fillBuffer(samplesToProcess);
            } catch (Exception ex) {
                System.out.println("Exception processing audio: " + ex.toString());
            }
        } else {
            testFillBuffer(samplesToProcess);
        }
        int baseAddr = 0;
        for (int x = 0; x < samplesToProcess; x++) 
        {
            // NOTE: Hard coded for 2 channels.
            int bVal1 = (int) (iBuffer[0][x] * 32767.0f);
            int bVal2 = (int) (iBuffer[1][x] * 32767.0f);
            oBuffer[baseAddr++] = (byte) (bVal1 & 0xFF);
            oBuffer[baseAddr++] = (byte) ((bVal1 >>> 8) & 0xFF);
            oBuffer[baseAddr++] = (byte) (bVal2 & 0xFF);
            oBuffer[baseAddr++] = (byte) ((bVal2 >>> 8) & 0xFF);
        }
        audioLine.write(oBuffer, 0, baseAddr);
    }

    /**
     * Stops the audio thread and then the output itself. This class must
     * be reinitialised once this has been called.
     */
    public void stop()
    {
        isRunning = false;
        if (audioLine != null)
        {
            audioLine.stop();
            audioLine.close();
        }
    }

    /**
     * Returns a list of audio devices for the user to choose from on
     * the preferences tab.
     * @return An array of available audio device description strings.
     */
    public static String[] getDeviceNames()
    {
        Mixer.Info[] mixers = AudioSystem.getMixerInfo();
        ArrayList<String> list = new ArrayList<String>();

        for (int x = 0; x < mixers.length; x++)
        {
            Mixer m = AudioSystem.getMixer(mixers[x]);
            if (m.getSourceLineInfo().length > 0)
            {
                list.add(mixers[x].toString());
            }
        }

        String[] names = new String[list.size()];
        for (int x = 0; x < list.size(); x++)
        {
            names[x] = list.get(x);
        }

        return names;
    }

    /**
     * A counter for use in test sigal generation in testFillBuffer.
     */
    private int _count = 0;

    /**
     * This method produces sine waves which will be heard if the program
     * is running without any mixer to provide audio (or if something
     * goes wrong while constructing said mixer).
     * @param samplesToProcess Should always be internalBufferSize.
     */
    private void testFillBuffer(int samplesToProcess)
    {
        for (int x = 0; x < samplesToProcess; x++)
        {
            _count++;
            double cycleTime = (100 + (_count * 0.3)) / sampleRate;
            cycleTime *= Math.PI;
            float val = (float) (Math.sin(_count * cycleTime)) * 0.2f;
            cycleTime /= 10.0;
            float val2 = (float) (Math.sin(_count * cycleTime)) * 0.2f;

            iBuffer[0][x] = val;
            iBuffer[1][x] = val2;
        }
    }
}
