# PSTimer.java

    /*
    ** Module   : PSTimer.java
    ** Abstract : An implementation of the Timer FWD extension to 4GL.
    **
    ** Copyright (c) 2017-2023, Golden Code Development Corporation.
    **
    ** -#- -I- --Date-- --------------------------------Description-----------------------------------
    ** 001 OM  20170530 First commit.
    ** 002 OM  20180425 Keep a reference to procedure that created the PSTimer to locate the callback.
    ** 003 CA  20180517 Fixed comhandle FWD-specific resource management.
    ** 004 OM  20190225 Pushed creator field up to super class.
    **     CA  20190315 Fixed resource cleanup on session end.
    **                  Resources are added to the unnamed pools, if exist (TODO: add named pool
    **                  support).
    **     CA  20190322 Wait for the fired async event to finish before continuing.
    ** 005 HC  20190810 Fixed initial timer values.
    ** 006 CA  20200930 Avoid string calculation for log messages, if logging is disabled.
    ** 007 VVT 20220329 Typos fixed in Javadoc.
    ** 008 GBB 20230512 Logging methods replaced by CentralLogger/ConversionStatus.
    */

    /*
    ** This program is free software: you can redistribute it and/or modify
    ** it under the terms of the GNU Affero General Public License as
    ** published by the Free Software Foundation, either version 3 of the
    ** License, or (at your option) any later version.
    **
    ** This program is distributed in the hope that it will be useful,
    ** but WITHOUT ANY WARRANTY; without even the implied warranty of
    ** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    ** GNU Affero General Public License for more details.
    **
    ** You may find a copy of the GNU Affero GPL version 3 at the following
    ** location: https://www.gnu.org/licenses/agpl-3.0.en.html
    **
    ** Additional terms under GNU Affero GPL version 3 section 7:
    **
    **   Under Section 7 of the GNU Affero GPL version 3, the following additional
    **   terms apply to the works covered under the License.  These additional terms
    **   are non-permissive additional terms allowed under Section 7 of the GNU
    **   Affero GPL version 3 and may not be removed by you.
    **
    **   0. Attribution Requirement.
    **
    **     You must preserve all legal notices or author attributions in the covered
    **     work or Appropriate Legal Notices displayed by works containing the covered
    **     work.  You may not remove from the covered work any author or developer
    **     credit already included within the covered work.
    **
    **   1. No License To Use Trademarks.
    **
    **     This license does not grant any license or rights to use the trademarks
    **     Golden Code, FWD, any Golden Code or FWD logo, or any other trademarks
    **     of Golden Code Development Corporation. You are not authorized to use the
    **     name Golden Code, FWD, or the names of any author or contributor, for
    **     publicity purposes without written authorization.
    **
    **   2. No Misrepresentation of Affiliation.
    **
    **     You may not represent yourself as Golden Code Development Corporation or FWD.
    **
    **     You may not represent yourself for publicity purposes as associated with
    **     Golden Code Development Corporation, FWD, or any author or contributor to
    **     the covered work, without written authorization.
    **
    **   3. No Misrepresentation of Source or Origin.
    **
    **     You may not represent the covered work as solely your work.  All modified
    **     versions of the covered work must be marked in a reasonable way to make it
    **     clear that the modified work is not originating from Golden Code Development
    **     Corporation or FWD.  All modified versions must contain the notices of
    **     attribution required in this license.
    */

    package com.goldencode.p2j.extension;

    import com.goldencode.p2j.comauto.*;
    import com.goldencode.p2j.net.*;
    import com.goldencode.p2j.security.*;
    import com.goldencode.p2j.ui.*;
    import com.goldencode.p2j.ui.client.event.*;
    import com.goldencode.p2j.util.*;
    import com.goldencode.p2j.util.logging.*;

    import java.util.concurrent.*;
    import java.util.function.*;
    import java.util.logging.*;

    /**
     * Implementation of the FWD TIMER Extension. An {@code PSTimer} object can be scheduled to run
     * repetitive tasks at fixed intervals. This is not fully compatible with ABL standard language,
     * it is an extension to 4GL specific to FWD.
     *
     * <p>
     * To create a timer use the following syntax:
     * <pre>
     *    CREATE TIMER handle [ASSIGN
     *       [INTERVAL = n-millis]
     *       [CALLBACK = procedure]
     *       [ENABLED = auto-start]].
     * </pre>
     * Where:
     * <ul>
     *    <li><strong>handle</strong> is the handle that will hold the new timer object;</li>
     *    <li><strong>n-millis</strong> is the interval between two consecutive ticks, in
     *       milliseconds. Note that the actual time where the callback procedure is executed may
     *       be delayed, depending on how the event messages get processed by FWD. It is illegal to
     *       set a negative value. Default is 0, not active.</li>
     *    <li><strong>procedure</strong> is the callback procedure legacy name that will be called
     *       when the timer elapses. The procedure must not have any parameters;</li>
     *    <li><strong>auto-start</strong>. If set to {@code yes} the timer is started automatically
     *       at the moment it is created, otherwise a call to {@link #start()} or setting the
     *       {@code enabled} property to {@code true} is needed to arm it.</li>
     * </ul>
     * Setting any of the properties to same value has no effect. Setting a new value for
     * {@code interval} when the timer is armed will force the timer to restart, he next event will
     * be fired after the timeout of the newly set interval. There are a few ways to stop the timer,
     * with small differences among them:
     * <ul>
     *    <li>set the {@code interval} to 0. It remain enabled, setting the {@code interval} to a new
     *       value will automatically arm it;</li>
     *    <li>set the {@code enabled} to {@code false}. The timer will stop until it is restarted by
     *       setting the value back to {@code yes} or calling {@code start};</li>
     *    <li>call {@code cease}, has the same effect like above;</li>
     *    <li>setting the {@code callback} to unknown will make the timer unable to call an event
     *       procedure, but it will continue to tick.</li>
     * </ul>
     * At the end of lifetime of the object, it must be deleted, as usual with
     * <pre>DELETE OBJECT th.</pre>
     *
     * <p>
     * The {@code PSTimer} object extends the {@code ComObject} class. This means that the programmer
     * can use a {@link comhandle} interface to access any properties and/or methods annotated as such
     * of the object, even they are known at compile time. The COM mode of operation is somewhat
     * compatible with legacy ActiveX objects from ABL.
     */
    public class PSTimer
    extends ComObject
    implements Runnable,
               FWDTimer,
               SessionListener
    {
       /**
        * The logger used for tracing internal events.
        */
       private static final CentralLogger LOG = CentralLogger.get(PSTimer.class.getName());

       /** Flag indicating that the logging is enabled or not. */
       private static final boolean LOG_ENABLED = LOG.isLoggable(Level.FINE);

       /**
        * The background thread that does the power-horse work. It is {@code null} if this timer has
        * not been started yet, or if the object was released/deleted.
        */
       private AssociatedThread timer = null;

       /**
        * The ticking interval, in milliseconds. Maps holds {@code INTERVAL} property/attribute.
        * If it is zero, the timer is not enabled (does not fire). It is illegal to set it to a
        * negative value.
        */
       private volatile int interval = 1000;

       /**
        * Flags if this timer is enabled. If not enabled, the timer will not fire the {@code tick}
        * event.
        */
       private volatile boolean enabled = false;

       /**
        * Flags indicating whether this timer was reset while armed ({@code interval} changed to
        * another valid value while timer was sleeping). In this case it must be re-initiated with
        * the new interval.
        */
       private volatile boolean timerReset = false;

       /**
        * The callback procedure in non-COM mode (legacy name). If not set timer is useless. One may
        * set it to {@code unknown} / {@code null} if you want some ticks to be skipped. The
        * difference from setting the enabled to {@code NO} is that in this case the timer is not
        * restarted.
        */
       private String callback = null;

       /**
        * Flags the operation mode. In the COM-mode the object is compatible with ABL {@code PSTimer}
        * ActiveX object. There are two differences:
        * <ul>
        *    <li><strong>enabled</strong> attribute/property is by default {@code yes} in COM mode
        *       and {@code false} in extension mode. This means that in COM mode, setting the
        *       {@code INTERVAL} will automatically arm the timer, while in extension mode, one
        *       need to explicitly set {@code enabled} to {@code yes} or invoke the {@code start}
        *       method;</li>
        *    <li>the <strong>callback</strong> is only used in extension mode, if set. In COM/ActiveX
        *       mode the {@code callback} property is ignored and ABL compatible algorithm is used
        *       to identify the proper event procedure to call. The legacy name of the procedure is
        *       computed using {@code <ControlFrame-name>.<ActiveX-name>.<Event-name>}.</li>
        * </ul>
        */
       private boolean isCom = true;

       /**
        * Flags if this object has been yet destroyed and is no longer valid. A {@code PSTimer} object
        * is valid throughout it lifetime until it is disposed using {@link #delete()}.
        */
       private boolean invalid = false;

       /**
        * A t<sub>0</sub> timestamp used to relate all events in LOG.
        */
       private long t0 = System.currentTimeMillis();

       /**
        * The constructor. It makes sure that the object is fully and safely initialized.
        * The super c'tor {@code ComObject} will be calling {@link comhandle#resourceId} and
        * configure the {@code parentProcedure}.
        */
       public PSTimer()
       {
          comhandle.resourceId(this);
       }

       /**
        * Static factory method for initialization of a fresh new TIMER as an extension object.
        *
        * @param   h
        *          The {@link handle} to store the newly created object.
        */
       public static void create(handle h)
       {
          PSTimer newInstance = new PSTimer();
          newInstance.isCom = false;
          newInstance.enabled = false;

          h.assign(newInstance);

          // TODO: add named widget pool support
          WidgetPool.addResource(newInstance, null);
       }

       /**
        * Obtain the name of this ActiveX object.
        *
        * @return  the name of this ActiveX object.
        */
       public String getActivexName()
       {
          return "PSTimer";
       }

       /**
        * The emulation of the ActiveX {@code AboutBox} legacy method. In ABL it will open a modal
        * dialog with the name of the ActiveX and its version. The emulated method is a NO-OP, except
        * for the optional log of the call.
        */
       @ComMethod(name = "AboutBox")
       public void AboutBox()
       {
          log(() -> "Executing PSTimer:AboutBox() COM method...");
       }

       /**
        * Check whether this timer is armed. However, it will not fire unless the {@code interval} is
        * also set to a strictly positive value.
        *
        * @return  The value of the {@code Enabled} property/attribute.
        */
       @ComProperty(name = "Enabled")
       public logical isEnabled()
       {
          return new logical(enabled);
       }

       /**
        * Sets the value of the {@code Enabled} property/attribute. If {@code interval} property is
        * already set, the timer gets armed.
        *
        * @param   setOn
        *          new value of the {@code Enabled} property/attribute.
        */
       // @ComProperty(name = "Enabled", setter = true)
       public void setEnabled(boolean setOn)
       {
          if (setOn == enabled)
          {
             return;
          }

          log(() -> "Enabled = " + setOn);

          enabled = setOn;
          if (setOn)
          {
             startWorker();
          }
          else
          {
             if (timer != null)
             {
                timer.interrupt();
             }
          }
          // otherwise this is a NO-OP
       }

       /**
        * Sets the value of the {@code Enabled} property/attribute. If {@code interval} property is
        * already set, the timer gets armed.
        *
        * @param   setOn
        *          new value of the {@code Enabled} property/attribute.
        */
       @ComProperty(name = "Enabled", setter = true)
       public void setEnabled(logical setOn)
       {
          if (setOn != null && !setOn.isUnknown())
          {
             setEnabled(setOn.booleanValue());
          }
       }

       /**
        * Obtain the current {@code interval} between two consecutive events.
        *
        * @return  the current value of {@code interval} property.
        */
       @ComProperty(name = "Interval")
       public integer getInterval()
       {
          return new integer(interval);
       }

       /**
        * Sets the current {@code interval} between two consecutive events. If the timer is already
        * {@code enabled}, it will be automatically armed and will fire after {@code interval} millis
        * elapses. Setting it to 0, will stop the timer. Setting to a negative value will lead to
        * an error message to be displayed.
        *
        * @param   interval
        *          The new interval between two timer events, in milliseconds.
        */
       // @ComProperty(name = "Interval", setter = true)
       public void setInterval(long interval)
       throws PropertyAccessException
       {
          if (interval < 0)
          {
             String msg = "The Interval property must not be a negative number";
             throw new PropertyAccessException(msg, "0x80020009");
          }
          if (interval == this.interval)
          {
             return; // no-op ?
          }

          log(() -> "Interval = " + interval);
          this.interval = (int) interval;
          startWorker();
       }

       /**
        * Sets the current {@code interval} between two consecutive events. If the timer is already
        * {@code enabled}, it will be automatically armed and will fire after {@code interval} millis
        * elapses. Setting it to 0, will stop the timer. Setting to a negative value will lead to
        * an error message to be displayed.
        *
        * @param   interval
        *          The new interval between two timer events, in milliseconds.
        */
       @ComProperty(name = "Interval", setter = true)
       public void setInterval(NumberType interval)
       throws PropertyAccessException
       {
          if (interval == null || interval.isUnknown())
          {
             throw new PropertyAccessException("Type mismatch", "0x80020005");
          }

          setInterval(interval.longValue());
       }

       /**
        * Gets the legacy name of the callback procedure to be called at fixed interval when running
        * in FWD extension mode (not as a COM).
        *
        * @return  the legacy name of the callback procedure.
        */
       @Override
       public character getCallback()
       {
          return new character(callback);
       }

       /**
        * Sets the legacy name of the callback procedure to be called at fixed interval when running
        * in FWD extension mode (ignored when in COM mode). If set to {@code unknown} the timer will
        * continue 'ticking' but since there is no known peer to call, nothing will happen. This
        * property can be set while the timer is active, without resetting it.
        *
        * @param   callback
        *          the new legacy name of the callback procedure.
        */
       @Override
       public void setCallback(character callback)
       {
          if (callback != null && !callback.isUnknown())
          {
             setCallback(callback.toStringMessage());
          }
       }

       /**
        * Sets the legacy name of the callback procedure to be called at fixed interval when running
        * in FWD extension mode (ignored when in COM mode).  If set to {@code unknown} the timer will
        * continue 'ticking' but since there is no known peer to call, nothing will happen. This
        * property can be set while the timer is active, without resetting it.
        *
        * @param   callback
        *          the new legacy name of the callback procedure.
        */
       @Override
       public void setCallback(String callback)
       {
          this.callback = callback;
       }

       /**
        * Force the timer to cease firing events. It has the exact effect like setting the
        * {@code enabled} property to {@code false}.
        * <p>
        * The name of the method was selected intentionally for the FWD extension since {@code STOP}
        * would create some syntactic incompatibility with standard ABL attributes.
        */
       @ComMethod(name = "cease")
       @Override
       public void cease()
       {
          setEnabled(false);
       }

       /**
        * Starts the timer. It has the exact effect like setting the {@code enabled} property to
        * {@code true}. Note that timer will not be activated unless the {@code interval} property
        * was set to a strictly positive value.
        */
       @ComMethod(name = "start")
       @Override
       public void start()
       {
          setEnabled(true);
       }

       /**
        * Perform actual delete of all resources. At the time of this call, it is assumed the resource
        * is valid for deletion (the handle and the resource are both valid).
        * <p>
        * This is called from a handle when working as FWD extension object.
        */
       @Override
       public void delete()
       {
          if (invalid)
          {
             return; // already deleted
          }

          destroy();

          log(() -> "Releasing");

          // end of lifetime: mark the object are invalid
          invalid = true;

          AssociatedThread save = this.timer;
          if (save == null)
          {
             return;
          }

          this.timer = null;
          enabled = false;
          interval = 0;

          save.interrupt();
          SessionManager.get().deregisterAsyncThread(save);
       }

       /**
        * Checks whether this object is valid. The object is valid from the moment it was created
        * (even if not active) and until expressly deleted.
        *
        * @return  {@code true} if this object is valid.
        */
       @Override
       public boolean valid()
       {
          return !invalid;
       }

       /**
        * This callback will be called after the security context has been set up and is valid for
        * the life of this thread and before any of the client code it will be executed.
        * <p>
        * Implement this method if you some work must be done from the thread that has the proper
        * security context set up.
        * <p>
        * No-op for the {@link PSTimer} case.
        *
        * @param    session
        *           The session that is starting.
        */
       @Override
       public void initialize(Session session)
       {
          // no-op
       }

       /**
        * This method is called when the session is ending.  This is useful for
        * cleaning up resources or state.
        * <p>
        * It ensures the runner thread gets terminated.
        *
        * @param    session
        *           The session that is ending.
        */
       @Override
       public void terminate(Session session)
       {
          delete();
       }

       /**
        * Worker method. Checks the internal state and start the timer. On the first call, it makes
        * sure the {@link AssociatedThread} is created and started. If already armed, calling this
        * will cause the current interval to be dropped, and a new schedule is performed for the
        * current {@code interval}.
        */
       private void startWorker()
       {
          if (invalid)
          {
             return; // object already deleted/released cannot be brought back to life
          }

          if (enabled && interval > 0)
          {
             if (timer == null)
             {
                SessionManager sm = SessionManager.get();
                log(() -> "Creating timing thread.");
                if (!sm.getSession().addSessionListener(this, true))
                {
                   // the session doesn't exist...
                   invalid = true;
                   return;
                }

                timer = new AssociatedThread(this);
                sm.registerAsyncThread(timer);
                timer.setDaemon(true);
                timer.setName("PSTimer");
                timer.start();
             }
             else
             {
                // restart: abort current sleep and restart with new
                log(() -> "Notify / restart (" + timer + ")");
                timerReset = true; // let it know the reason for interruption
                timer.interrupt();
             }
          }
       }

       /**
        * The background worker thread. It get repeatably suspended for {@code interval} milliseconds
        * and, if not interrupted, will trigger the event. In case of interruption, it will analyze
        * the internal state and rearm or wait for the timer to be re-enabled.
        */
       @Override
       public void run()
       {
          timerLoops:
          while (timer != null)
          {
             if (interval > 0 && enabled)
             {
                long sleepTo = System.currentTimeMillis() + interval;

                log(() -> "Arming timer (" + timer + "). See you in " + interval / 1000f + " secs.");
                do
                {
                   try
                   {
                      synchronized (timer)
                      {
                         long timeout = sleepTo - System.currentTimeMillis();
                         if (timeout > 0)
                         {
                            timer.wait(timeout);
                         }
                      }
                   }
                   catch (InterruptedException e)
                   {
                      log(() -> "Timer interrupted.");
                      if (this.timer == null)
                      {
                         log(() -> "Timer released (1). Quitting thread.");
                         break timerLoops;
                      }
                   }

                   if (!enabled || interval <= 0)
                   {
                      // something changed, firing is not allowed in these conditions
                      // let the big loop decide what to do
                      continue timerLoops;
                   }
                   else if (timerReset) // && enabled && interval > 0
                   {
                      log(() -> "Timer reset detected.");
                      timerReset = false;
                      // loop with the new interval
                      continue timerLoops;
                   }
                }
                while (System.currentTimeMillis() <= sleepTo);

                log(() -> "Timer completed. Firing event.");
                if (getParentProcedure()._isValid())
                {
                   fireEvent();
                }
                else
                {
                   // if the external procedure of callback was deleted we cannot invoke the callback
                   // anymore, end of object and thread lifetime
                   delete();
                   return;
                }
             }
             else
             {
                log(() -> "Waiting for restart...");
                try
                {
                   synchronized (timer)
                   {
                      timer.wait(0);
                   }
                }
                catch (InterruptedException e)
                {
                   if (this.timer == null)
                   {
                      log(() -> "Timer released (2). Quitting thread.");
                      break;
                   }
                }
             }
          }
       }

       /**
        * Calls the callback or the event procedure asynchronously. In both cases a
        * {@link ServerEvent} will be posted with {@link LogicalTerminal#postServerEvent}. When
        * running as FWD extension, the {@code callback} procedure is invoked, if set. When running as
        * a COM/ActiveX, a new {@code ComEvent} event is dispatched through {@code ComServer}, which
        * will use the {@code ControlFrame} container, the name of this {@code ComObject} and the
        * declared event name to compute the legacy name of teh procedure to call.
        */
       private void fireEvent()
       {
          if (isCom)
          {
             // if the object ws created as a COM object, fire using COM protocol
             ComServer.emit(new ComEvent(PSTimer.this, "Tick"), false);
             return;
          }

          // this is a TIMER FWD extension. Fire event only if callback attribute was set.
          if (callback != null)
          {
             try
             {
                CountDownLatch latch = new CountDownLatch(1);

                LogicalTerminal.postServerEvent(
                      new ServerEvent(id(),
                                      "TIMER-EVENT",
                                      () ->
                                      {
                                         try
                                         {
                                            if (ComServer.checkValid(this))
                                            {
                                               ControlFlowOps.invokeIn(callback, getParentProcedure());
                                            }
                                         }
                                         finally
                                         {
                                            latch.countDown();
                                         }
                                      }));

                try
                {
                   latch.await();
                }
                catch (InterruptedException ie)
                {
                   // ignore
                }
             }
             catch (Exception e)
             {
                if (LOG.isLoggable(Level.WARNING))
                {
                   LOG.log(Level.WARNING, "Fail to asynchronously call '" + callback + ".", e);
                }
             }
          }
       }

       /**
        * Logs information mostly for debugging purposes. The method will automatically insert a
        * timestamp relative to creation of this object (for easy tracking) and the thread id this
        * call is performed.
        *
        * @param   s
        *          The message to be logged.
        */
       private void log(Supplier<String> s)
       {
          if (LOG_ENABLED)
          {
             float dt = (System.currentTimeMillis() - t0) / 1000f;
             LOG.log(Level.FINE, dt + "\tT" + Thread.currentThread().getId() + "\t" + s.get());
          }
       }
    }