/*******************************************************************************
 * Copyright (c) 2014-2015-2016 Oak Ridge National Laboratory.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v1.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/legal/epl-v10.html
 ******************************************************************************/
package org.csstudio.javafx.rtplot.util;

import static org.csstudio.javafx.rtplot.Activator.logger;

import java.util.concurrent.Executors;
import java.util.concurrent.RejectedExecutionException;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.logging.Level;

/** Throttle for updates.
 *
 *  <p>Initial trigger will result in update.
 *  Further triggers are suppressed during a 'dormant' period.
 *  At end, accumulated triggers received while dormant result
 *  in another update.
 *
 *  @author Kay Kasemir
 */
@SuppressWarnings("nls")
public class RTPlotUpdateThrottle
{
    final private ScheduledExecutorService timer =
            Executors.newSingleThreadScheduledExecutor(new NamedThreadFactory("RTPlotUpdateThrottle"));

    /** How long to stay dormant after an update */
    private volatile long dormant_ms;

    /** The update to perform */
    final private Runnable update_then_wake;

    /** Are updates currently suppressed? */
    final private AtomicBoolean dormant = new AtomicBoolean();

    /** Scheduled wakeUp call when no longer dormant */
    private ScheduledFuture<?> scheduled_wakeup;

    /** Any pending triggers while dormant */
    final private AtomicBoolean pending_trigger = new AtomicBoolean();


    /** Initialize
     *  @param dormant_time How long throttle remains dormant after a trigger
     *  @param unit Units for the dormant period
     *  @param update {@link Runnable} to invoke for triggers
     */
    public RTPlotUpdateThrottle(final long dormant_time, final TimeUnit unit, final Runnable update)
    {
        setDormantTime(dormant_time, unit);
        this.update_then_wake = () ->
        {   // Perform the update
            try
            {
                // Wait a little to allow more updates to accumulate
                Thread.sleep(20);
                pending_trigger.set(false);
                update.run();
            }
            catch (InterruptedException ex)
            {
                // Shutdown, don't schedule another wakeup
                return;
            }
            catch (Throwable ex)
            {
                logger.log(Level.WARNING, "Update failed", ex);
            }
            // Schedule wakeup
            scheduled_wakeup = timer.schedule(this::wakeUp, dormant_ms, TimeUnit.MILLISECONDS);
        };
    }

    /** Update the dormant time
     *  @param dormant_time How long throttle remains dormant after a trigger
     *  @param unit Units for the dormant period
     */
    public void setDormantTime(final long dormant_time, final TimeUnit unit)
    {
        dormant_ms = unit.toMillis(dormant_time);
    }

    /** Call to request an update.
     *
     *  <p>First call will cause an update.
     *  Follow-up calls are noted but delayed until end of dormant period,
     *  resulting in one(!) follow-up update.
     */
    public void trigger()
    {
        if (dormant.getAndSet(true))
        {   // In dormant period, note additional triggers but don't act
            pending_trigger.set(true);
        }
        else
        {
            // In idle period, react to trigger, but on timer thread
            try
            {
                timer.execute(update_then_wake);
            }
            catch (RejectedExecutionException ex)
            {
                if (timer.isShutdown())
                    logger.log(Level.FINE, "Update throttle thread already closed", ex);
                else
                    throw ex;
            }
        }
    }

    private void wakeUp()
    {   // End dormant period
        dormant.set(false);
        // React on (one or more) pending triggers
        // received while dormant.
        if (pending_trigger.getAndSet(false))
            trigger();
    }

    /** Call to cancel scheduled updates */
    public void dispose()
    {
        // In case an update is running right now, terminate()
        timer.shutdownNow();
        // Waiting for updates to complete via
        //  timer.shutdown();
        //  timer.awaitTermination(2, TimeUnit.SECONDS)
        // doesn't help because threads may be in BufferUtil.getBufferedImage(),
        // waiting on UI thread, i.e. the current thread, and would need some time to time out.
        // terminate() is faster.

        pending_trigger.set(false);
        if (scheduled_wakeup != null)
            scheduled_wakeup.cancel(false);
    }
}

@@ -1201,29 +1201,130 @@
 e%0A%7B%0A    
-final private
+/** One timer thread is shared by all throttles%0A     *  to further limit CPU load%0A     */%0A    private static final
  Schedul
@@ -1582,30 +1582,24 @@
  */%0A    
-final 
 private 
 Runnable
@@ -1590,16 +1590,22 @@
 private 
+final 
 Runnable
@@ -1669,38 +1669,32 @@
 sed? */%0A    
-final 
 private 
 AtomicBoolea
@@ -1673,32 +1673,38 @@
  */%0A    private 
+final 
 AtomicBoolean do
@@ -1893,22 +1893,16 @@
     
-final 
 private 
 Atom
@@ -1897,16 +1897,22 @@
 private 
+final 
 AtomicBo
@@ -1957,16 +1957,127 @@
 ean();%0A%0A
+    /** Stop ongoing activity because throttle was disposed? */%0A    private volatile boolean disposed = false;%0A
 %0A    /**
@@ -2677,32 +2677,68 @@
 ger.set(false);%0A
+                if (! disposed)%0A    
                 
@@ -3068,32 +3068,64 @@
 Schedule wakeup%0A
+            if (! disposed)%0A    
             sche
@@ -3768,32 +3768,74 @@
 trigger()%0A    %7B%0A
+        if (disposed)%0A            return;%0A
         if (dorm
@@ -4771,436 +4771,24 @@
     
-// In case an update is running right now, terminate()%0A        timer.shutdownNow();%0A        // Waiting for updates to complete via%0A        //  timer.shutdown();%0A        //  timer.awaitTermination(2, TimeUnit.SECONDS)%0A        // doesn't help because threads may be in BufferUtil.getBufferedImage(),%0A        // waiting on UI thread, i.e. the current thread, and would need some time to time out.%0A        // terminate() is faster.%0A
+disposed = true;
 %0A   

Use one timer for all RTPlotUpdateThrottles