/**
 * 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 jogamp.common.util.locks;

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.locks.AbstractOwnableSynchronizer;

import com.jogamp.common.util.locks.RecursiveLock;

/**
 * Reentrance locking toolkit, impl a complete fair FIFO scheduler
 *
 * <p>
 * Sync object extends {@link AbstractOwnableSynchronizer}, hence monitoring is possible.</p>
 */
public class RecursiveLockImpl01CompleteFair implements RecursiveLock {

    private static class WaitingThread {
        WaitingThread(final Thread t) {
            thread = t;
            signaledByUnlock = false;
        }
        final Thread thread;
        boolean signaledByUnlock; // if true, it's also removed from queue
    }

    @SuppressWarnings("serial")
    private static class Sync extends AbstractOwnableSynchronizer {
        private Sync() {
            super();
        }
        private final Thread getOwner() {
            return getExclusiveOwnerThread();
        }
        private final void setOwner(final Thread t) {
            setExclusiveOwnerThread(t);
        }
        private final void setLockedStack(final Throwable s) {
            final List<Throwable> ls = LockDebugUtil.getRecursiveLockTrace();
            if(s==null) {
                ls.remove(lockedStack);
            } else {
                ls.add(s);
            }
            lockedStack = s;
        }
        /** lock count by same thread */
        private int holdCount = 0;
        /** waiting thread queue */
        final ArrayList<WaitingThread> queue = new ArrayList<WaitingThread>();
        /** stack trace of the lock, only used if DEBUG */
        private Throwable lockedStack = null;
    }
    private final Sync sync = new Sync();

    public RecursiveLockImpl01CompleteFair() {
    }

    /**
     * Returns the Throwable instance generated when this lock was taken the 1st time
     * and if {@link com.jogamp.common.util.locks.Lock#DEBUG} is turned on, otherwise it returns always <code>null</code>.
     * @see com.jogamp.common.util.locks.Lock#DEBUG
     */
    public final Throwable getLockedStack() {
        synchronized(sync) {
            return sync.lockedStack;
        }
    }

    @Override
    public final Thread getOwner() {
        synchronized(sync) {
            return sync.getOwner();
        }
    }

    @Override
    public final boolean isOwner(final Thread thread) {
        synchronized(sync) {
            return sync.getOwner() == thread ;
        }
    }

    @Override
    public final boolean isLocked() {
        synchronized(sync) {
            return null != sync.getOwner();
        }
    }

    @Override
    public final boolean isLockedByOtherThread() {
        synchronized(sync) {
            final Thread o = sync.getOwner();
            return null != o && Thread.currentThread() != o ;
        }
    }

    @Override
    public final int getHoldCount() {
        synchronized(sync) {
            return sync.holdCount;
        }
    }

    @Override
    public final void validateLocked() throws RuntimeException {
        synchronized(sync) {
            if ( Thread.currentThread() != sync.getOwner() ) {
                if ( null == sync.getOwner() ) {
                    throw new RuntimeException(threadName(Thread.currentThread())+": Not locked: "+toString());
                }
                if(null!=sync.lockedStack) {
                    sync.lockedStack.printStackTrace();
                }
                throw new RuntimeException(Thread.currentThread()+": Not owner: "+toString());
            }
        }
    }

    @Override
    public final void lock() {
        synchronized(sync) {
            try {
                if(!tryLock(TIMEOUT)) {
                    if(null!=sync.lockedStack) {
                        sync.lockedStack.printStackTrace();
                    }
                    throw new RuntimeException("Waited "+TIMEOUT+"ms for: "+toString()+" - "+threadName(Thread.currentThread()));
                }
            } catch (final InterruptedException e) {
                throw new RuntimeException("Interrupted", e);
            }
        }
    }

    @Override
    public final boolean tryLock(long timeout) throws InterruptedException {
        synchronized(sync) {
            final Thread cur = Thread.currentThread();
            if(TRACE_LOCK) {
                System.err.println("+++ LOCK 0 "+toString()+", cur "+threadName(cur));
            }
            if (sync.getOwner() == cur) {
                ++sync.holdCount;
                if(TRACE_LOCK) {
                    System.err.println("+++ LOCK XR "+toString()+", cur "+threadName(cur));
                }
                return true;
            }

            if ( sync.getOwner() != null || ( 0<timeout && 0<sync.queue.size() ) ) {

                if ( 0 >= timeout ) {
                    // locked by other thread and no waiting requested
                    if(TRACE_LOCK) {
                        System.err.println("+++ LOCK XY "+toString()+", cur "+threadName(cur)+", left "+timeout+" ms");
                    }
                    return false;
                }

                // enqueue at the start
                final WaitingThread wCur = new WaitingThread(cur);
                sync.queue.add(0, wCur);
                do {
                    final long t0 = System.currentTimeMillis();
                    try {
                        sync.wait(timeout);
                        timeout -= System.currentTimeMillis() - t0;
                    } catch (final InterruptedException e) {
                        if( !wCur.signaledByUnlock ) {
                            sync.queue.remove(wCur); // O(n)
                            throw e; // propagate interruption not send by unlock
                        } else if( cur != sync.getOwner() ) {
                            // Issued by unlock, but still locked by other thread
                            //
                            timeout -= System.currentTimeMillis() - t0;

                            if(TRACE_LOCK) {
                                System.err.println("+++ LOCK 1 "+toString()+", cur "+threadName(cur)+", left "+timeout+" ms, signaled: "+wCur.signaledByUnlock);
                            }

                            if(0 < timeout) {
                                // not timed out, re-enque - lock was 'stolen'
                                wCur.signaledByUnlock = false;
                                sync.queue.add(sync.queue.size(), wCur);
                            }
                        } // else: Issued by unlock, owning lock .. expected!
                    }
                } while ( cur != sync.getOwner() && 0 < timeout ) ;

                if( 0 >= timeout && cur != sync.getOwner() ) {
                    // timed out
                    if(!wCur.signaledByUnlock) {
                        sync.queue.remove(wCur); // O(n)
                    }
                    if(TRACE_LOCK) {
                        System.err.println("+++ LOCK XX "+toString()+", cur "+threadName(cur)+", left "+timeout+" ms");
                    }
                    return false;
                }

                ++sync.holdCount;
                if(TRACE_LOCK) {
                    System.err.println("+++ LOCK X1 "+toString()+", cur "+threadName(cur)+", left "+timeout+" ms");
                }
            } else {
                ++sync.holdCount;
                if(TRACE_LOCK) {
                    System.err.println("+++ LOCK X0 "+toString()+", cur "+threadName(cur));
                }
            }

            sync.setOwner(cur);
            if(DEBUG) {
                sync.setLockedStack(new Throwable("Previously locked by "+toString()));
            }
            return true;
        }
    }


    @Override
    public final void unlock() {
        synchronized(sync) {
            unlock(null);
        }
    }

    @Override
    public final void unlock(final Runnable taskAfterUnlockBeforeNotify) {
        synchronized(sync) {
            validateLocked();
            final Thread cur = Thread.currentThread();

            --sync.holdCount;

            if (sync.holdCount > 0) {
                if(TRACE_LOCK) {
                    System.err.println("--- LOCK XR "+toString()+", cur "+threadName(cur));
                }
                return;
            }

            if(DEBUG) {
                sync.setLockedStack(null);
            }
            if(null!=taskAfterUnlockBeforeNotify) {
                taskAfterUnlockBeforeNotify.run();
            }

            if(sync.queue.size() > 0) {
                // fair, wakeup the oldest one ..
                // final WaitingThread oldest = queue.removeLast();
                final WaitingThread oldest = sync.queue.remove(sync.queue.size()-1);
                sync.setOwner(oldest.thread);

                if(TRACE_LOCK) {
                    System.err.println("--- LOCK X1 "+toString()+", cur "+threadName(cur)+", signal: "+threadName(oldest.thread));
                }

                oldest.signaledByUnlock = true;
                oldest.thread.interrupt(); // Propagate SecurityException if it happens
            } else {
                sync.setOwner(null);
                if(TRACE_LOCK) {
                    System.err.println("--- LOCK X0 "+toString()+", cur "+threadName(cur)+", signal any");
                }
                sync.notify();
            }
        }
    }

    @Override
    public final int getQueueLength() {
        synchronized(sync) {
            return sync.queue.size();
        }
    }

    @Override
    public String toString() {
        return syncName()+"[count "+sync.holdCount+
                           ", qsz "+sync.queue.size()+", owner "+threadName(sync.getOwner())+"]";
    }

    private final String syncName() {
        return "<"+Integer.toHexString(this.hashCode())+", "+Integer.toHexString(sync.hashCode())+">";
    }
    private final String threadName(final Thread t) { return null!=t ? "<"+t.getName()+">" : "<NULL>" ; }
}