/**
 * @test
 * @bug 7088367
 * @summary SourceDataLine.write and TargetDataLine.read don't throw ArrayIndexOutOfBoundsException
 * @author Alex Menkov
 */

import javax.sound.sampled.AudioSystem;
import javax.sound.sampled.DataLine;
import javax.sound.sampled.Line;
import javax.sound.sampled.LineUnavailableException;
import javax.sound.sampled.Mixer;
import javax.sound.sampled.SourceDataLine;
import javax.sound.sampled.TargetDataLine;

public class DataLine_ArrayIndexOutOfBounds {

    static int total = 0;
    static int failed = 0;

    // shared buffer for all tests
    static final byte[] buffer = new byte[5000000];

    // the class describes different test scenarios (buffer properties)
    static abstract class Scenario {
        abstract int getBufferOffset(DataLine line);
        abstract int getBufferLength(DataLine line);
    }

    // scenarios to tests
    static Scenario[] scenarios = new Scenario[]{
        new Scenario() {
            public String toString() {
                return "offset is near Integer.MAX_VALUE";
            }
            public int getBufferOffset(DataLine line) {
                return Integer.MAX_VALUE - 4096;
            }
            public int getBufferLength(DataLine line) {
                return 65536;
            }
        },
        new Scenario() {
            public String toString() {
                return "offset is less than buffer.length, length is large";
            }
            int getBufferOffset(DataLine line) {
                return buffer.length / 10;
            }
            int getBufferLength(DataLine line) {
                return Integer.MAX_VALUE - getBufferOffset(line) + 4096;
            }
        }
    };

    public static void main(String[] args) throws Exception {
        Mixer.Info[] infos = AudioSystem.getMixerInfo();
        log("" + infos.length + " mixers detected");
        for (int i=0; i<infos.length; i++) {
            Mixer mixer = AudioSystem.getMixer(infos[i]);
            log("Mixer " + (i+1) + ": " + infos[i]);
            try {
                mixer.open();
                for (Scenario scenario: scenarios) {
                    testSDL(mixer, scenario);
                    testTDL(mixer, scenario);
                }
                mixer.close();
            } catch (LineUnavailableException ex) {
                log("LineUnavailableException: " + ex);
            }
        }
        if (failed == 0) {
            log("PASSED (" + total + " tests)");
        } else {
            log("FAILED (" + failed + " of " + total + " tests)");
            throw new Exception("Test FAILED");
        }
    }

    final static int STOPPER_DELAY = 5000;  // 1 sec

    static class AsyncLineStopper implements Runnable {
        private final DataLine line;
        private final long delayMS;  // delay before stop the line
        private final Thread thread;
        private final Object readyEvent = new Object();
        private final Object startEvent = new Object();

        public AsyncLineStopper(DataLine line, long delayMS) {
            this.line = line;
            this.delayMS = delayMS;
            thread = new Thread(this);
            thread.setDaemon(true);
            // starts the thread and waits until it becomes ready
            synchronized (readyEvent) {
                thread.start();
                try {
                    readyEvent.wait();
                } catch (InterruptedException ex) { }
            }
        }

        // makes the delay and then stops the line
        public void schedule() {
            synchronized(startEvent) {
                startEvent.notifyAll();
            }
        }

        // force stop/close the line
        public void force() {
            thread.interrupt();
            try {
                thread.join();
            } catch (InterruptedException ex) {
                log("join exception: " + ex);
            }
        }

        // Runnable implementation
        public void run() {
            try {
                synchronized(readyEvent) {
                    readyEvent.notifyAll();
                }
                synchronized(startEvent) {
                    startEvent.wait();
                }
                // delay
                Thread.sleep(delayMS);
            } catch (InterruptedException ex) {
                log("    AsyncLineStopper has been interrupted: " + ex);
            }
            // and flush
            log("    stop...");
            line.stop();
            log("    close...");
            line.close();
        }
    }

    static void testSDL(Mixer mixer, Scenario scenario) {
        log("  Testing SDL (scenario: " + scenario + ")...");
        Line.Info linfo = new Line.Info(SourceDataLine.class);
        SourceDataLine line = null;
        try {
            line = (SourceDataLine)mixer.getLine(linfo);
            log("    got line: " + line);
            log("    open...");
            line.open();
        } catch (IllegalArgumentException ex) {
            log("    unsupported (IllegalArgumentException)");
            return;
        } catch (LineUnavailableException ex) {
            log("    unavailable: " + ex);
            return;
        }

        total++;

        log("    start...");
        line.start();

        AsyncLineStopper lineStopper = new AsyncLineStopper(line, STOPPER_DELAY);
        int offset = scenario.getBufferOffset(line);
        int len = scenario.getBufferLength(line);
        // ensure len represents integral number of frames
        len -= len % line.getFormat().getFrameSize();

        log("    write...");
        lineStopper.schedule();
        try {
            line.write(buffer, offset, len);
            log("    ERROR: didn't get ArrayIndexOutOfBoundsException");
            failed++;
        } catch (ArrayIndexOutOfBoundsException  ex) {
            log("    OK: got ArrayIndexOutOfBoundsException: " + ex);
        }
        lineStopper.force();
    }

    static void testTDL(Mixer mixer, Scenario scenario) {
        log("  Testing TDL (scenario: " + scenario + ")...");
        Line.Info linfo = new Line.Info(TargetDataLine.class);
        TargetDataLine line = null;
        try {
            line = (TargetDataLine)mixer.getLine(linfo);
            log("    got line: " + line);
            log("    open...");
            line.open();
        } catch (IllegalArgumentException ex) {
            log("    unsupported (IllegalArgumentException)");
            return;
        } catch (LineUnavailableException ex) {
            log("    unavailable: " + ex);
            return;
        }

        total++;

        log("    start...");
        line.start();

        AsyncLineStopper lineStopper = new AsyncLineStopper(line, STOPPER_DELAY);
        int offset = scenario.getBufferOffset(line);
        int len = scenario.getBufferLength(line);
        // ensure len represents integral number of frames
        len -= len % line.getFormat().getFrameSize();

        log("    read...");
        try {
            line.read(buffer, offset, len);
            log("    ERROR: didn't get ArrayIndexOutOfBoundsException");
            failed++;
        } catch (ArrayIndexOutOfBoundsException  ex) {
            log("    OK: got ArrayIndexOutOfBoundsException: " + ex);
        }
        lineStopper.force();
    }

    static void log(String s) {
        System.out.println(s);
        System.out.flush();
    }
}
