/*
 * Copyright 2009 Phil Burk, Mobileer Inc
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.jsyn.research;

import org.junit.jupiter.api.Test;

import java.util.ArrayList;

import static org.junit.jupiter.api.Assertions.assertEquals;

// TODO: Use thread pools, or maybe JMH?
public class BenchMultiThreading {

    private static final int FRAMES_PER_BLOCK = 64;
    int numThreads = 4;
    int numLoops = 100000;
    private ArrayList<CustomThread> threadList;

    static class CustomThread extends Thread {
        long frameCount = 0;
        long desiredFrame = 0;
        Object semaphore = new Object();
        Object goSemaphore = new Object();
        volatile boolean go = true;
        long startNano;
        long stopNano;
        long maxElapsed;

        @Override
        public void run() {
            try {
                startNano = System.nanoTime();
                while (go) {
                    // Watch for long delays.
                    stopNano = System.nanoTime();
                    long elapsed = stopNano - startNano;
                    startNano = System.nanoTime();
                    if (elapsed > maxElapsed) {
                        maxElapsed = elapsed;
                    }

                    synchronized (semaphore) {
                        // Audio synthesis would occur here.
                        frameCount += 1;
                        // LOGGER.debug( this + " generating frame  " +
                        // frameCount );
                        semaphore.notify();
                    }
                    synchronized (goSemaphore) {
                        while (desiredFrame <= frameCount) {
                            goSemaphore.wait();
                        }
                    }
                    long stopNano = System.nanoTime();
                }
            } catch (InterruptedException e) {
                System.out.println("CustomThread interrupted. ");
            }
            System.out.println("Finishing " + this);
        }

        public void abort() {
            go = false;
            interrupt();
        }

        public void waitForFrame(long targetFrame) throws InterruptedException {
            synchronized (semaphore) {
                while (frameCount < targetFrame) {
                    semaphore.wait();
                }
            }
        }

        public void generateFrame(long desiredFrame) {
            synchronized (goSemaphore) {
                this.desiredFrame = desiredFrame;
                goSemaphore.notify();
            }
        }

    }

    @Test
    public void testMultiThreads() {
        threadList = new ArrayList<>();
        for (int i = 0; i < numThreads; i++) {
            CustomThread thread = new CustomThread();
            threadList.add(thread);
            thread.start();
        }

        long frameCount = 0;
        long startTime = System.currentTimeMillis();
        try {
            for (int i = 0; i < numLoops; i++) {
                frameCount += 1;
                waitForThreads(frameCount);
                // LOGGER.debug("got frame " + frameCount );
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        long stopTime = System.currentTimeMillis();
        long elapsedTime = stopTime - startTime;
        double elapsedSeconds = 0.001 * elapsedTime;
        double blocksPerSecond = numLoops / elapsedSeconds;
        System.out.format("blocksPerSecond = %10.3f\n", blocksPerSecond);
        double framesPerSecond = blocksPerSecond * FRAMES_PER_BLOCK;
        System.out.format("audio framesPerSecond = %10.3f at %d frames per block\n",
                framesPerSecond, FRAMES_PER_BLOCK);

        for (CustomThread thread : threadList) {
            System.out.format("max elapsed time is %d nanos or %f msec\n", thread.maxElapsed,
                    (thread.maxElapsed / 1000000.0));
        }
        for (CustomThread thread : threadList) {
            assertEquals(frameCount, thread.frameCount, "BlockCount must match ");
            thread.abort();
        }

    }

    private void waitForThreads(long frameCount) throws InterruptedException {
        for (CustomThread thread : threadList) {
            // Ask threads to wake up and generate up to this frame.
            thread.generateFrame(frameCount);
        }
        for (CustomThread thread : threadList) {
            // Wait for all the threads to catch up.
            thread.waitForFrame(frameCount);
        }
    }
}
