/*
 *  @author Ranides Atterwim <ranides@gmail.com>
 *  @copyright Ranides Atterwim
 *  @license WTFPL
 *  @url http://ranides.net/projects/
 */
package net.ranides.assira.events;

import java.util.Date;
import net.ranides.assira.time.TimeUtils;
import net.ranides.assira.trace.LoggerUtils;
import static org.junit.Assert.*;
import org.junit.Ignore;
import org.junit.Test;


/**
 *
 * @author ranides
 */
@SuppressWarnings("PMD.AvoidDuplicateLiterals")
public class EventLockTest {

    /**
     * MUSIMY tu mieć duże opóźnienia, jeśli jednocześnie chcemy sprawdzić
     * naprawdę EventLock oraz uniknąć race-condition. Moglibyśmy wprowadzić
     * jakąś synchronizację... ale wtedy z definicji byśmy testowali tę 
     * smart-synchronizację, a nie nasz EventLock.
     */
    @Test
    public void testEventLockRace() throws InterruptedException {
        final EventReactor main = EventReactor.newInstance("name", 32, 1000);

        new Thread(new Runnable(){

            @Override
            public void run() {
                // sleep unikają race condition (event wysłany przed rozpoczęciem waitForEvent)
                
                TimeUtils.sleep(200); // 1 pierwszy powinien długo czekać               
                main.signalEvent(new WriteEvent(77));
                TimeUtils.sleep(200); // 2
                main.signalEvent(new ReadEvent(55));
                TimeUtils.sleep(200); // 3
                main.signalEvent(new ReadEvent(57));
                TimeUtils.sleep(200); // 4
                main.signalEvent(new ReadEvent(59));
                TimeUtils.sleep(200); // 5
                main.signalEvent(new Event(){});
                TimeUtils.sleep(200); // 6
                main.signalEvent(new ReadEvent(44));
                TimeUtils.sleep(200); // 7
                main.signalEvent(new WriteEvent(12));
                TimeUtils.sleep(200); // 8
                main.signalEvent(new ReadEvent(77));
                
                main.stop();
            }
            
        }).start();
        
        
        IOEvent event;
        
        // taka konstrukcja normalnie jest podatna na race condition
        event = EventLock.singleLock(WriteEvent.class, main).waitForEvent(WriteEvent.class);
        assertEquals(77, event.handle());
        
        event = EventLock.singleLock(ReadEvent.class, main).waitForEvent(ReadEvent.class);
        assertEquals(55, event.handle());
        
        event = EventLock.singleLock(WriteEvent.class, main).waitForEvent(WriteEvent.class);
        assertEquals(12, event.handle());

        EventLock.singleLock(CoreEvent.Shutdown.class, main).waitForEvent();
        
        assertTrue(true);

    }
    
    
    @Test
    public void testEventLock() throws InterruptedException {
        final EventReactor main = EventReactor.newInstance("name", 32, 1000);

        final EventLock lock = EventLock.lock(Event.class, main);
        
        new Thread(new Runnable(){

            @Override
            public void run() {
                // w tym teście race-condition nie ma prawa się wydarzyć :) Lock niczego nie przeoczy :)
                main.signalEvent(new WriteEvent(77));
                main.signalEvent(new ReadEvent(55));
                main.signalEvent(new ReadEvent(57));
                main.signalEvent(new ReadEvent(59));
                main.signalEvent(new Event(){});
                main.signalEvent(new ReadEvent(44));
                main.signalEvent(new WriteEvent(12));
                main.signalEvent(new ReadEvent(77));
                main.stop();
            }
            
        }).start();
        
        
        IOEvent event;
        
        event = lock.waitForEvent(WriteEvent.class);
        assertEquals(77, event.handle());
        
        event = lock.waitForEvent(ReadEvent.class);
        assertEquals(55, event.handle());
        
        event = lock.waitForEvent(ReadEvent.class);
        assertEquals(57, event.handle());
        
        event = lock.waitForEvent(WriteEvent.class);
        assertEquals(12, event.handle());

        lock.waitForEvent(CoreEvent.Shutdown.class);
        
        assertTrue(true);

    }
    
    public void runPerf(final int counter, final int delay) throws InterruptedException {

        final EventReactor main = EventReactor.newInstance("name", 32, 1000);

        final Thread producer = new Thread(new Runnable(){
            @Override
            public void run() {
                for(int i=0; i<counter; i++) {
                    main.signalEvent(new TimeEvent());
                    if(delay > 0) { TimeUtils.sleep(delay); }
                }
                TimeUtils.sleep(500);
                main.stop();
            }
        });
        
        final EventLock timer = EventLock.lock(TimeEvent.class, main);
       
        producer.start();
        
        long sum = 0;
        for(int i=0; i<counter; i++) {
            TimeEvent event = timer.waitForEvent(TimeEvent.class);
            long delta = (new Date().getTime() - event.time().getTime() );
            sum += delta;
        }
        
        LoggerUtils.dump("sum overhead = %d ms\n", sum);
        LoggerUtils.dump("avg overhead = %f ms\n", (double)sum / counter );
        
        EventLock.singleLock(CoreEvent.Shutdown.class, main).waitForEvent();
        
        assertTrue(true);

    }
    
    @Ignore("manual slow")
    @Test
    public void testOverhead() throws InterruptedException {
        // tak naprawdę to mierzy poślizg w głównym wątku, który się dławi
        runPerf(10000, 0);
        assertTrue(true);
    }
    
    @Ignore("manual slow")
    @Test
    public void testOverheadSleep() throws InterruptedException {
        runPerf(500, 20);
        assertTrue(true);
    }
    
    static class TimeEvent implements Event {
        private static int amt = 0;
        
        private final Date time;
        private final int counter;
        
        public TimeEvent() {
            counter = ++amt;
            time = new Date();
        }

        public Date time() {
            return time;
        }

        public int counter() {
            return counter;
        }
        
    }
    
    class IOEvent implements Event { 
        private final int handle;

        public IOEvent(int handle) {
            this.handle = handle;
        }

        public int handle() {
            return handle;
        }
        
    }

    @SuppressWarnings("PMD")
    class ReadEvent extends IOEvent {

        public ReadEvent(int handle) {
            super(handle);
        }
 }

    @SuppressWarnings("PMD")
    class WriteEvent extends IOEvent {

        public WriteEvent(int handle) {
            super(handle);
        }
 }
}
