/**
 * Copyright 2010 JogAmp Community. All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without modification, are
 * permitted provided that the following conditions are met:
 *
 *    1. Redistributions of source code must retain the above copyright notice, this list of
 *       conditions and the following disclaimer.
 *
 *    2. Redistributions in binary form must reproduce the above copyright notice, this list
 *       of conditions and the following disclaimer in the documentation and/or other materials
 *       provided with the distribution.
 *
 * THIS SOFTWARE IS PROVIDED BY JogAmp Community ``AS IS'' AND ANY EXPRESS OR IMPLIED
 * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
 * FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL JogAmp Community OR
 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
 * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
 * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
 * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
 * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 *
 * The views and conclusions contained in the software and documentation are those of the
 * authors and should not be interpreted as representing official policies, either expressed
 * or implied, of JogAmp Community.
 */

package com.jogamp.common.util.locks;

import java.io.IOException;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;

import org.junit.Assert;
import org.junit.Test;

import com.jogamp.common.os.Clock;
import com.jogamp.common.os.Platform;
import com.jogamp.common.util.InterruptSource;
import com.jogamp.junit.util.SingletonJunitCase;

import org.junit.FixMethodOrder;
import org.junit.runners.MethodSorters;

@FixMethodOrder(MethodSorters.NAME_ASCENDING)
public class TestRecursiveLock01 extends SingletonJunitCase {

    public enum YieldMode {
        NONE(0), YIELD(1), SLEEP(2);

        public final int id;

        YieldMode(final int id){
            this.id = id;
        }
    }

    static void yield_thread(final YieldMode mode) {
        switch(mode) {
            case YIELD:
                Thread.yield();
                break;
            case SLEEP:
                try {
                    Thread.sleep(10);
                } catch (final InterruptedException ie) {
                    ie.printStackTrace();
                }
                break;
            default:
                break;
        }

    }

    static class LockedObject {
        static final boolean DEBUG = false;

        static class ThreadStat {
            ThreadStat() {
                total = 0;
                counter = 0;
            }
            long total; // ns
            int counter;
        }

        private final RecursiveLock locker; // post
        private int deferredThreadCount = 0; // synced
        private final Map<String, ThreadStat> threadWaitMap = Collections.synchronizedMap(new HashMap<String, ThreadStat>()); // locked

        long avrg; // ns, post
        long max_deviation; // ns, post
        long min_deviation; // ns, post

        public LockedObject(final LockFactory.ImplType implType, final boolean fair) {
            locker = LockFactory.createRecursiveLock(implType, fair);
        }

        private synchronized void incrDeferredThreadCount() {
            deferredThreadCount++;
        }
        private synchronized void decrDeferredThreadCount() {
            deferredThreadCount--;
        }
        public synchronized int getDeferredThreadCount() {
            return deferredThreadCount;
        }

        public final void action1Direct(int l, final YieldMode yieldMode) {
            if(DEBUG) {
                System.err.print("<a1");
            }
            lock();
            try {
                if(DEBUG) {
                    System.err.print("+");
                }
                while(l>0) l--;
                yield_thread(yieldMode);
            } finally {
                if(DEBUG) {
                    System.err.print("-");
                }
                unlock();
                if(DEBUG) {
                    System.err.println(">");
                }
            }
        }

        class Action2 implements Runnable {
            int l;
            YieldMode yieldMode;

            Action2(final int l, final YieldMode yieldMode) {
                this.l=l;
                this.yieldMode=yieldMode;
                incrDeferredThreadCount();
            }

            @Override
            public void run() {
                if(DEBUG) {
                    System.err.print("[a2");
                }
                lock();
                try {
                    if(DEBUG) {
                        System.err.print("+");
                    }
                    while(l>0) l--;
                     yield_thread(yieldMode);
                } finally {
                    if(DEBUG) {
                        System.err.print("-");
                    }
                    unlock();
                    if(DEBUG) {
                        System.err.println("]");
                    }
                }
                decrDeferredThreadCount();
                final int dc = getDeferredThreadCount();
                if(0>dc) {
                    throw new InternalError("deferredThreads: "+dc);
                }
            }
        }

        public final void action2Deferred(final int l, final YieldMode yieldMode) {
            final Action2 action2 = new Action2(l, yieldMode);
            new InterruptSource.Thread(null, action2, Thread.currentThread().getName()+"-deferred").start();
        }

        public final void lock() {
            long td = Clock.currentNanos();
            locker.lock();
            td = Clock.currentNanos() - td;

            final String cur = Thread.currentThread().getName();
            ThreadStat ts = threadWaitMap.get(cur);
            if(null == ts) {
                ts = new ThreadStat();
            }
            ts.total += td;
            ts.counter++;
            threadWaitMap.put(cur, ts);
        }

        public final void unlock() {
            locker.unlock();
        }

        public final boolean isLocked() {
            return locker.isLocked();
        }

        public void stats(final boolean dump) {
            long timeAllLocks=0;
            int numAllLocks=0;
            for(final Iterator<String> i = threadWaitMap.keySet().iterator(); i.hasNext(); ) {
                final String name = i.next();
                final ThreadStat ts = threadWaitMap.get(name);
                timeAllLocks += ts.total;
                numAllLocks += ts.counter;
            }
            max_deviation = Long.MIN_VALUE;
            min_deviation = Long.MAX_VALUE;
            avrg = timeAllLocks/numAllLocks;
            if(dump) {
                System.err.printf("Average: %6d ms / %6d times = %8d ns",
                        timeAllLocks/1000000, numAllLocks, avrg);
                System.err.println();
            }
            for(final Iterator<String> i = threadWaitMap.keySet().iterator(); i.hasNext(); numAllLocks++) {
                final String name = i.next();
                final ThreadStat ts = threadWaitMap.get(name);
                final long a = ts.total/ts.counter;
                final long d = a - avrg;
                max_deviation = Math.max(max_deviation, d);
                min_deviation = Math.min(min_deviation, d);
                if(dump) {
                    System.err.printf("%-35s %12d ns / %6d times, a %8d ns, d %8d ns",
                            name, ts.total, ts.counter, a, d);
                    System.err.println();
                }
            }
            if(dump) {
                System.err.printf("Deviation (min/max): [%8d ns - %8d ns]", min_deviation, max_deviation);
                System.err.println();
            }
        }

    }

    interface LockedObjectRunner extends Runnable {
        void stop();
        boolean isStopped();
        void waitUntilStopped();
    }

    class LockedObjectRunner1 implements LockedObjectRunner {
        volatile boolean shouldStop;
        volatile boolean stopped;
        LockedObject lo;
        int loops;
        int iloops;
        YieldMode yieldMode;

        public LockedObjectRunner1(final LockedObject lo, final int loops, final int iloops, final YieldMode yieldMode) {
            this.lo = lo;
            this.loops = loops;
            this.iloops = iloops;
            this.shouldStop = false;
            this.stopped = false;
            this.yieldMode = yieldMode;
        }

        @Override
        public final void stop() {
            shouldStop = true;
        }

        @Override
        public final boolean isStopped() {
            return stopped;
        }

        @Override
        public void waitUntilStopped() {
            synchronized(this) {
                while(!stopped) {
                    try {
                        this.wait();
                    } catch (final InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }

        }

        @Override
        public void run() {
            synchronized(this) {
                while(!shouldStop && loops>0) {
                    lo.action1Direct(iloops, yieldMode);
                    lo.action2Deferred(iloops, yieldMode);
                    loops--;
                }
                stopped = true;
                this.notifyAll();
            }
        }
    }

    protected long testLockedObjectImpl(final LockFactory.ImplType implType, final boolean fair,
                                        final int threadNum, final int loops, final int iloops, final YieldMode yieldMode) throws InterruptedException {
        final long t0 = System.currentTimeMillis();
        final LockedObject lo = new LockedObject(implType, fair);
        final LockedObjectRunner[] runners = new LockedObjectRunner[threadNum];
        final InterruptSource.Thread[] threads = new InterruptSource.Thread[threadNum];
        int i;

        for(i=0; i<threadNum; i++) {
            runners[i] = new LockedObjectRunner1(lo, loops, iloops, yieldMode);
            // String name = Thread.currentThread().getName()+"-ActionThread-"+i+"_of_"+threadNum;
            final String name = "ActionThread-"+i+"_of_"+threadNum;
            threads[i] = new InterruptSource.Thread( null, runners[i], name );
            threads[i].start();
        }

        for( i=0; i<threadNum; i++ ) {
            runners[i].waitUntilStopped();
        }
        while( 0 < lo.getDeferredThreadCount() ) {
            Thread.sleep(100);
        }
        Assert.assertEquals(0, lo.locker.getHoldCount());
        Assert.assertEquals(false, lo.locker.isLocked());
        Assert.assertEquals(0, lo.getDeferredThreadCount());

        final long dt = System.currentTimeMillis()-t0;
        lo.stats(false);

        System.err.println();
        final String fair_S = fair ? "fair  " : "unfair" ;
        System.err.printf("---- TestRecursiveLock01.testLockedObjectThreading: i %5s, %s, threads %2d, loops-outter %6d, loops-inner %6d, yield %5s - dt %6d ms, avrg %8d ns, deviation [ %8d .. %8d ] ns",
                implType, fair_S, threadNum, loops, iloops, yieldMode, dt, lo.avrg, lo.min_deviation, lo.max_deviation);
        System.err.println();
        return dt;
    }

    @Test
    public void testLockedObjectThreading5x1000x10000N_Int01_Fair() throws InterruptedException {
        final LockFactory.ImplType t = LockFactory.ImplType.Int01;
        final boolean fair=true;
        int threadNum=5;
        int loops=1000;
        int iloops=10000;
        final YieldMode yieldMode=YieldMode.NONE;

        if( Platform.getCPUFamily() == Platform.CPUFamily.ARM ) {
            threadNum=5; loops=5; iloops=10;
        }

        testLockedObjectImpl(t, fair, threadNum, loops, iloops, yieldMode);
    }

    @Test
    public void testLockedObjectThreading5x1000x10000N_Java5_Fair() throws InterruptedException {
        final LockFactory.ImplType t = LockFactory.ImplType.Java5;
        final boolean fair=true;
        int threadNum=5;
        int loops=1000;
        int iloops=10000;
        final YieldMode yieldMode=YieldMode.NONE;

        if( Platform.getCPUFamily() == Platform.CPUFamily.ARM ) {
            threadNum=5; loops=5; iloops=10;
        }

        testLockedObjectImpl(t, fair, threadNum, loops, iloops, yieldMode);
    }

    @Test
    public void testLockedObjectThreading5x1000x10000N_Int01_Unfair() throws InterruptedException {
        final LockFactory.ImplType t = LockFactory.ImplType.Int01;
        final boolean fair=false;
        int threadNum=5;
        int loops=1000;
        int iloops=10000;
        final YieldMode yieldMode=YieldMode.NONE;

        if( Platform.getCPUFamily() == Platform.CPUFamily.ARM ) {
            threadNum=5; loops=5; iloops=10;
        }

        testLockedObjectImpl(t, fair, threadNum, loops, iloops, yieldMode);
    }

    @Test
    public void testLockedObjectThreading5x1000x10000N_Java5_Unfair() throws InterruptedException {
        final LockFactory.ImplType t = LockFactory.ImplType.Java5;
        final boolean fair=false;
        int threadNum=5;
        int loops=1000;
        int iloops=10000;
        final YieldMode yieldMode=YieldMode.NONE;

        if( Platform.getCPUFamily() == Platform.CPUFamily.ARM ) {
            threadNum=5; loops=5; iloops=10;
        }

        testLockedObjectImpl(t, fair, threadNum, loops, iloops, yieldMode);
    }

    @Test
    public void testLockedObjectThreading25x100x100Y_Int01_Fair() throws InterruptedException {
        final LockFactory.ImplType t = LockFactory.ImplType.Int01;
        final boolean fair=true;
        int threadNum=25;
        int loops=100;
        int iloops=100;
        final YieldMode yieldMode=YieldMode.YIELD;

        if( Platform.getCPUFamily() == Platform.CPUFamily.ARM ) {
            threadNum=5; loops=5; iloops=10;
        }

        testLockedObjectImpl(t, fair, threadNum, loops, iloops, yieldMode);
    }

    @Test
    public void testLockedObjectThreading25x100x100Y_Java5_Fair() throws InterruptedException {
        final LockFactory.ImplType t = LockFactory.ImplType.Java5;
        final boolean fair=true;
        int threadNum=25;
        int loops=100;
        int iloops=100;
        final YieldMode yieldMode=YieldMode.YIELD;

        if( Platform.getCPUFamily() == Platform.CPUFamily.ARM ) {
            threadNum=5; loops=5; iloops=10;
        }

        testLockedObjectImpl(t, fair, threadNum, loops, iloops, yieldMode);
    }

    @Test
    public void testLockedObjectThreading25x100x100Y_Int01_Unair() throws InterruptedException {
        final LockFactory.ImplType t = LockFactory.ImplType.Int01;
        final boolean fair=false;
        int threadNum=25;
        int loops=100;
        int iloops=100;
        final YieldMode yieldMode=YieldMode.YIELD;

        if( Platform.getCPUFamily() == Platform.CPUFamily.ARM ) {
            threadNum=5; loops=5; iloops=10;
        }

        testLockedObjectImpl(t, fair, threadNum, loops, iloops, yieldMode);
    }

    @Test
    public void testLockedObjectThreading25x100x100Y_Java5_Unfair() throws InterruptedException {
        final LockFactory.ImplType t = LockFactory.ImplType.Java5;
        final boolean fair=false;
        int threadNum=25;
        int loops=100;
        int iloops=100;
        final YieldMode yieldMode=YieldMode.YIELD;

        if( Platform.getCPUFamily() == Platform.CPUFamily.ARM ) {
            threadNum=5; loops=5; iloops=10;
        }

        testLockedObjectImpl(t, fair, threadNum, loops, iloops, yieldMode);
    }

    // @Test
    public void testLockedObjectThreading25x100x100S_Int01_Fair() throws InterruptedException {
        final LockFactory.ImplType t = LockFactory.ImplType.Int01;
        final boolean fair=true;
        int threadNum=25;
        int loops=100;
        int iloops=100;
        final YieldMode yieldMode=YieldMode.SLEEP;

        if( Platform.getCPUFamily() == Platform.CPUFamily.ARM ) {
            threadNum=5; loops=5; iloops=10;
        }

        testLockedObjectImpl(t, fair, threadNum, loops, iloops, yieldMode);
    }

    // @Test
    public void testLockedObjectThreading25x100x100S_Java5() throws InterruptedException {
        final LockFactory.ImplType t = LockFactory.ImplType.Java5;
        final boolean fair=true;
        int threadNum=25;
        int loops=100;
        int iloops=100;
        final YieldMode yieldMode=YieldMode.SLEEP;

        if( Platform.getCPUFamily() == Platform.CPUFamily.ARM ) {
            threadNum=5; loops=5; iloops=10;
        }

        testLockedObjectImpl(t, fair, threadNum, loops, iloops, yieldMode);
    }

    @Test
    public void testLockedObjectThreading25x100x100N_Int01_Fair() throws InterruptedException {
        final LockFactory.ImplType t = LockFactory.ImplType.Int01;
        final boolean fair=true;
        int threadNum=25;
        int loops=100;
        int iloops=100;
        final YieldMode yieldMode=YieldMode.NONE;

        if( Platform.getCPUFamily() == Platform.CPUFamily.ARM ) {
            threadNum=5; loops=5; iloops=10;
        }

        testLockedObjectImpl(t, fair, threadNum, loops, iloops, yieldMode);
    }

    @Test
    public void testLockedObjectThreading25x100x100N_Java5_Fair() throws InterruptedException {
        final LockFactory.ImplType t = LockFactory.ImplType.Java5;
        final boolean fair=true;
        int threadNum=25;
        int loops=100;
        int iloops=100;
        final YieldMode yieldMode=YieldMode.NONE;

        if( Platform.getCPUFamily() == Platform.CPUFamily.ARM ) {
            threadNum=5; loops=5; iloops=10;
        }

        testLockedObjectImpl(t, fair, threadNum, loops, iloops, yieldMode);
    }

    @Test
    public void testLockedObjectThreading25x100x100N_Int01_Unfair() throws InterruptedException {
        final LockFactory.ImplType t = LockFactory.ImplType.Int01;
        final boolean fair=false;
        int threadNum=25;
        int loops=100;
        int iloops=100;
        final YieldMode yieldMode=YieldMode.NONE;

        if( Platform.getCPUFamily() == Platform.CPUFamily.ARM ) {
            threadNum=5; loops=5; iloops=10;
        }

        testLockedObjectImpl(t, fair, threadNum, loops, iloops, yieldMode);
    }

    @Test
    public void testLockedObjectThreading25x100x100N_Java5_Unfair() throws InterruptedException {
        final LockFactory.ImplType t = LockFactory.ImplType.Java5;
        final boolean fair=false;
        int threadNum=25;
        int loops=100;
        int iloops=100;
        final YieldMode yieldMode=YieldMode.NONE;

        if( Platform.getCPUFamily() == Platform.CPUFamily.ARM ) {
            threadNum=5; loops=5; iloops=10;
        }

        testLockedObjectImpl(t, fair, threadNum, loops, iloops, yieldMode);
    }

    static int atoi(final String a) {
        int i=0;
        try {
            i = Integer.parseInt(a);
        } catch (final Exception ex) { ex.printStackTrace(); }
        return i;
    }

    public static void main(final String args[]) throws IOException, InterruptedException {
        final String tstname = TestRecursiveLock01.class.getName();
        org.junit.runner.JUnitCore.main(tstname);

        /**
        BufferedReader stdin = new BufferedReader(new InputStreamReader(System.in));
        System.err.println("Press enter to continue");
        System.err.println(stdin.readLine());
        TestRecursiveLock01 t = new TestRecursiveLock01();
        t.testLockedObjectThreading5x1000x10000N_Int01_Unfair();

        t.testLockedObjectThreading5x1000x10000N_Int01_Fair();
        t.testLockedObjectThreading5x1000x10000N_Java5_Fair();
        t.testLockedObjectThreading5x1000x10000N_Int01_Unfair();
        t.testLockedObjectThreading5x1000x10000N_Java5_Unfair();
        */
    }

}
