/*
** Module   : SendKey.java
** Abstract : base class for sending keys to the terminal
**
** Copyright (c) 2009-2024, Golden Code Development Corporation.
**
** -#- -I- --Date-- -----------------------Description------------------------
** 001 GES 20090114 Base class for sending keys to the terminal.
** 002 IAS 20150814 Fixed SPEC3 format literal.
** 003 TJD 20220817 Implement performance test mode
** 004 TJD 20220818 Drain mode optimization for performance mode
** 005 TJD 20220819 Allow configuring max timeout for single test that require idle waiting
** 014 TJD 20240201 Benchmark mode corrections
*/

/*
** 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 should have received a copy of the GNU Affero General Public License
** along with this program.  If not, see <http://www.gnu.org/licenses/>.
*/

package com.goldencode.harness.test;

import com.goldencode.harness.*;
import com.goldencode.harness.terminal.*;
import com.goldencode.util.LogHelper;

/**
 * Base class for sending keys to the terminal.
 * <p>
 * The interpretation and specification of the key(s) to send is specific
 * to the sub-class.
 * <p>
 * As with all key processing, it is possible to specify modifier keys that
 * are sent along with each key. Modifier keys include the control, alt and
 * shift keys. This allows a key that is sent to be interpreted differently.
 * For example, the F1 key will be interpreted as CTRL-F1 is the control
 * modifier is included. Zero or more modifiers can be sent with each key.
 * <p>
 * Modifiers cannot be sent as a separate key (by themselves).
 * <p>
 * The following symbolic modifier names are valid:
 * <p>
 * <pre>
 * CTRL
 * ALT
 * SHIFT
 * </pre>
 * <p>
 * This directive offers a useful synchronization primitive, called drain mode.
 * This facility allows the directive to detect when screen changes have been
 * made in response to the sent keys. When all screen changes have been
 * processed and the terminal has no more screen output to read, then the
 * directive will complete. This draining facility is purely optional and is
 * specified by providing a maximum timeout and the number of bytes to watch
 * for.
 * <p> 
 * If the timeout value is specified, drain mode synching will be enabled. In
 * this mode, before any keys are sent, the terminal's counter of read bytes
 * (the drain counter) will be reset to zero. Then after the keys are sent, the
 * drain counter will be polled to detect when any bytes are read. Once the
 * minimum number of bytes have been read and one polling interval has elapsed
 * without any additional bytes being read, then the terminal will be
 * determined to have drained all output that was caused by the sent keys. At
 * that point the directive will complete. This is a useful synching mechanism
 * to use in place of a specific screen check. In particular, it allows a more
 * complex set of key input to be specified in between screen checks without
 * danger of overrunning the terminal or getting out of synch. It also can
 * replace the less flexible fixed pause directive when used for the same
 * purpose (waiting for the expected output of some sent keys before doing
 * something else like sending more keys).
 * <p>
 * To provide some safety, a timeout is also specified. This is the maximum
 * number of milliseconds to wait for draining to occur. If the expected bytes
 * are not read by that time, then the step will be placed into FAILED status.
 * It is ignored if drain mode is not active.
 */
public abstract class SendKey
extends SubstitutionReport
{
   /** Milliseconds to wait in polling loop. */
   private static final int INTERVAL = 500;
   
   /** Format specification for creating a description of this step. */
   private static final String SPEC = "value = '%s'; modifiers = '%s';";
   
   /** Format specification for drain mode. */
   private static final String SPEC2 = "%s\ntimeout = %d; minimum = %d;";
   
   /** Another format specification for drain mode. */
   private static final String SPEC3 = "timeout occurred before all output " +
                                       "could be read [%s] (%d bytes read)";
   
   /** Yet another format specification for drain mode. */
   private static final String SPEC4 = "%s actual (bytes read) = %d;";
                                       
   /** Text to send, with each character as a separate key. */
   private String value = null;
   
   /** Modifier keys to be passed. */
   private String modifiers = null;
   
   /** Drain mode flag. */
   private boolean drain = false;
   
   /** Number of polling intervals in drain mode until failure. */ 
   private int timeout = -1;
   
   /** Minimum number of bytes to read in drain mode for success. */ 
   private int minimum = 0;
   
   /**
    * Construct an instance of a step which sends keys to the terminal.
    *
    * @param    name
    *           The short name of the specific directive being implemented.
    * @param    value
    *           The text to send. The data format is specific to the
    *           sub-class.
    * @param    modifiers
    *           Zero or more modifier key names (CTRL, ALT and/or SHIFT).
    *           These can be separated by spaces, commas, plus signs or any
    *           other separator (e.g. "CTRL,ALT"). Case will be ignored. This
    *           may be empty or <code>null</code> which is the equivalent of
    *           no modifiers. If not <code>null</code>, this specifies
    *           modifier keys that are to be treated as "pressed" when the
    *           key(s) are sent.
    */
   public SendKey(String name, String value, String modifiers)
   {
      super(name, "");
      this.value     = value;
      this.modifiers = modifiers;
   }
   
   /**
    * Set the keystroke data to send.
    *
    * @param    value
    *           The text to send. The data format is specific to the
    *           sub-class.
    */
   public void setValue(String value)
   {
      this.value = value;
   }
   
   /**
    * Set the keystroke modifiers to send.
    *
    * @param    modifiers
    *           Zero or more modifier key names (CTRL, ALT and/or SHIFT).
    *           These can be separated by spaces, commas, plus signs or any
    *           other separator (e.g. "CTRL,ALT"). Case will be ignored. This
    *           may be empty or <code>null</code> which is the equivalent of
    *           no modifiers. If not <code>null</code>, this specifies
    *           modifier keys that are to be treated as "pressed" when the
    *           key(s) are sent.
    */
   public void setModifiers(String modifiers)
   {
      this.modifiers = modifiers;
   }
   
   /**
    * Enable drain mode.
    *
    * @param    timeout
    *           Maximum number of milliseconds to wait for draining to occur.
    *           Use -1 for an indefinite wait (it never times out). Otherwise
    *           this needs to be a positive number large enough to safely
    *           read the minimum number of expected bytes of output.
    * @param    minimum
    *           The minimum number of bytes to read from the back end before
    *           the timeout occurs.
    */
   public void enableDrainMode(int timeout, int minimum)
   {
      this.drain    = true;
      this.timeout  = timeout;
      this.minimum  = minimum;
   }
   
   /**
    * Enable drain mode and set the minimum bytes read. The timeout value
    * will be unaffected.
    *
    * @param    minimum
    *           The minimum number of bytes to read from the back end before
    *           the timeout occurs.
    */
   public void setMinimum(int minimum)
   {
      this.drain    = true;
      this.minimum  = minimum;
   }
   
   /**
    * Enable drain mode and set the timeout. The minimum bytes read value
    * will be unaffected.
    *
    * @param    timeout
    *           Maximum number of milliseconds to wait for draining to occur.
    *           Use -1 for an indefinite wait (it never times out). Otherwise
    *           this needs to be a positive number large enough to safely
    *           read the minimum number of expected bytes of output.
    */
   public void setTimeout(int timeout)
   {
      this.drain    = true;
      this.timeout  = timeout;
   }
   
   /**
    * Finds the setter associated with the given field name and this current
    * instance (such a setter can only be used to set the field for this
    * specific instance).
    *
    * @param    parm
    *           The field name.
    *
    * @return   The delegated setter or <code>null</code> on any failure.
    */
   public Setter obtainSetter(String parm)
   {
      Setter setter = null;
      
      if (parm != null)
      {
         if (parm.equals("value"))
         {
            setter = getValueSetter();
         }
         else if (parm.equals("modifiers"))
         {
            setter = getModifiersSetter();
         }
         else if (parm.equals("minimum"))
         {
            setter = getMinimumSetter();
         }
         else if (parm.equals("timeout"))
         {
            setter = getTimeoutSetter();
         }
      }
      
      return setter;
   }
   
   /**
    * Obtain an instance that can delegate the calling of the {@link #setValue}
    * method.
    *
    * @return   The delegated instance.
    */
   public Setter getValueSetter()
   {
      return new Setter()
      {
         public void set(String value)
         {
            setValue(value);
         }
      };
   }
   
   /**
    * Obtain an instance that can delegate the calling of the
    * {@link #setModifiers} method.
    *
    * @return   The delegated instance.
    */
   public Setter getModifiersSetter()
   {
      return new Setter()
      {
         public void set(String value)
         {
            setModifiers(value);
         }
      };
   }
   
   /**
    * Obtain an instance that can delegate the calling of the
    * {@link #setMinimum} method.
    *
    * @return   The delegated instance.
    */
   public Setter getMinimumSetter()
   {
      return new Setter()
      {
         public void set(String value)
         {
            try
            {
               setMinimum(Integer.parseInt(value));
            }
            
            catch (NumberFormatException nfe)
            {
               throw new RuntimeException(nfe);
            }
         }
      };
   }
   
   /**
    * Obtain an instance that can delegate the calling of the
    * {@link #setTimeout} method.
    *
    * @return   The delegated instance.
    */
   public Setter getTimeoutSetter()
   {
      return new Setter()
      {
         public void set(String value)
         {
            try
            {
               setTimeout(Integer.parseInt(value));
            }
            
            catch (NumberFormatException nfe)
            {
               throw new RuntimeException(nfe);
            }
         }
      };
   }
   
   /**
    * Called after substitutions are processed to allow the description
    * to be updated if necessary.
    */
   protected void updateDescription()
   {
      String txt = String.format(SPEC, value, modifiers);
      
      if (drain)
      {
         txt = String.format(SPEC2, txt, timeout, minimum);
      }
      
      setDescription(txt);
   }
   /**
    * Execute the directive of the test using the given terminal as the data
    * sink and/or source for the operation.
    * Performance mode version - single character is enough to proceed when in drain mode
    *
    * @param    term
    *           The terminal that represents the system under test.
    */
   protected void executeWorkerPM(Terminal term)
   {
      String reason = null;
      minimum = 1; //Math.min(minimum, value.length());
      // prepare for drain
      if (drain)
      {
         term.resetDrain();
      }
      
      // call the specific test implementation (in our sub-class)
      send(term, value, modifiers);
      
      // wait for drain to occur
      if (drain)
      {
         boolean success    = false;
         boolean indefinite = (timeout == -1);
         int     millis     = Harness.scaleTimeoutMultiplier(timeout);
         int     newCount   = 0;
//         int     oldCount   = 0;

         // break if any bytes have been received
         while (indefinite || millis > 0)
         {
            long waitStartTime = System.currentTimeMillis();
            try
            {
               newCount += term.resetDrain(minimum-newCount,millis);
            }
            catch (InterruptedException ie)
            {
               reason = "thread interrupted";
               break;
            }
            long waitEndTime = System.currentTimeMillis();
             // if we have read enough data and a full interval has passed
            // without more data being read, then we consider the terminal
            // drained and we exit the polling loop
            if (newCount >= minimum)
            {
               success = true;
               break;
            }
            
            
            
            if (!indefinite)
               millis -= (waitEndTime-waitStartTime);
         }
         
         // check for successful synching
         if (!success)
         {
            if (reason == null)
            {
               if (newCount > 0)
               {
                  reason = String.format(SPEC3, reason, newCount);
               }
               else
               {
                  reason = "no output read before timeout occurred";
               }
            }
            
            setStatus(Status.FAILED, reason);
         }
         else
         {
            // update the description with some useful data about the drain op
            String descr = getDescription();
            setDescription(String.format(SPEC4, descr, newCount));
         }
      }
   }
   
   /**
    * Execute the directive of the test using the given terminal as the data
    * sink and/or source for the operation.
    *
    * @param    term
    *           The terminal that represents the system under test.
    */
   protected void executeWorker(Terminal term)
   {
      if (Harness.isBenchmarkMode())
      {
         executeWorkerPM(term);
         return;
      }
      String reason = null;
      
      // prepare for drain
      if (drain)
      {
         term.resetDrain();
      }
      
      // call the specific test implementation (in our sub-class)
      send(term, value, modifiers);
      
      // wait for drain to occur
      if (drain)
      {
         boolean success    = false;
         boolean indefinite = (timeout == -1);
         int     millis     = Harness.scaleTimeoutMultiplier(timeout);
         int     newCount   = 0;
         int     oldCount   = 0;
         int     amt        = 0;
         
         // wait for output to be fully read (for the output from these sent
         // keys to be fully drained from the terminal)
         while (indefinite || millis > 0)
         {
            // if we have read enough data and a full interval has passed
            // without more data being read, then we consider the terminal
            // drained and we exit the polling loop
            if (oldCount == newCount && oldCount > minimum)
            {
               success = true;
               break;
            }
            
            oldCount = newCount;
            newCount += term.resetDrain();
            
            try
            {
               amt = indefinite ? INTERVAL : Math.min(millis, INTERVAL);
               // make check more often for benchmark mode
               if (Harness.isBenchmarkMode())
                  amt = 10;
               Thread.sleep(amt);
            }
            
            catch (InterruptedException ie)
            {
               reason = "thread interrupted";
               break;
            }
         
            if (!indefinite)
               millis -= amt;
         }
         
         // check for successful synching
         if (!success)
         {
            if (reason == null)
            {
               if (newCount > 0)
               {
                  reason = String.format(SPEC3, reason, newCount);
               }
               else
               {
                  reason = "no output read before timeout occurred";
               }
            }
            
            setStatus(Status.FAILED, reason);
         }
         else
         {
            // update the description with some useful data about the drain op
            String descr = getDescription();
            setDescription(String.format(SPEC4, descr, newCount));
         }
      }
   }
   
   /**
    * Worker to actually send the key(s).
    *
    * @param    term
    *           The terminal that represents the system under test.
    * @param    value
    *           The text to send. The data format is specific to the
    *           sub-class.
    * @param    modifiers
    *           Zero or more modifier key names (CTRL, ALT and/or SHIFT).
    *           These can be separated by spaces, commas, plus signs or any
    *           other separator (e.g. "CTRL,ALT"). Case will be ignored. This
    *           may be empty or <code>null</code> which is the equivalent of
    *           no modifiers. If not <code>null</code>, this specifies
    *           modifier keys that are to be treated as "pressed" when the
    *           key(s) are sent.
    */
   protected abstract void send(Terminal term, String value, String modifiers);
}
