package nxm.sys.libg;

import nxm.sys.inc.Commandable;
import nxm.sys.inc.MessageHandler;
import nxm.sys.lib.Args;
import nxm.sys.lib.Command;
import nxm.sys.lib.Message;
import nxm.sys.lib.Midas;
import nxm.sys.lib.MidasThread;
import nxm.sys.lib.Shell;
import nxm.sys.lib.Table;
import nxm.sys.lib.Time;

/** This class is used to create JavaBeans of existing primitives and macros.
    The {@link #createBean(Midas,String,MidasDisplay,String)} method is the one
    most commonly used to create a new bean. <br>
    <br>
    Usage Example:
    <pre>
      javax.swing.JFrame frame;
        :
      javax.swing.JPanel panel = new JPanel();
      frame.setContentPane(panel);
        :
      MidasDisplay world = new MidasDisplay();  panel.add(world);
      MidasDisplay list  = new MidasDisplay();  panel.add(list);
      MidasDisplay sd360 = new MidasDisplay();  panel.add(sd360);

      // The displays must be visible before creating the beans.
      frame.setVisible(true);

      // create MidasBeans and save reference later for closing
      bean1 = MidasBean.createBean(M, "plot world",   world, "worldID");
      bean2 = MidasBean.createBean(M, "list fftdemo", list,  "listID" );
      bean2 = MidasBean.createBean(M, "sd360 3e10",   sd360, "sd360ID");

        :
      // later, when application is closing/exiting
      frame.setVisible(false);
        :
      // close the bean to release resources and cleanup
      bean1.close();
      bean2.close();
      bean3.close();
    </pre>

    @author  J. Czechowski IV / Neon Ngo
    @version $Id: MidasBean.java,v 1.24 2014/03/25 19:45:11 nmg Exp $
    @since   NeXtMidas 1.9.2
 */
public class MidasBean implements MessageHandler, Runnable, Commandable {

  /** Amount of time (seconds) to sleep while waiting for {@link #cmd} to OPEN, PROCESS, CLOSE, etc.
      This is larger than SLEEP_INTERVAL in {@link Shell#execCommand(Midas, Command)} b/c this
      class mostly waits for the embedded command.
   */
  private static final double SLEEP_INTERVAL = 0.1;

  /** The command being used. */
  private Command      cmd;
  /** Indicates the status of the bean. */
  private int          state;
  /** This is the panel being used. */
  private MidasDisplay panel;
  /** The id the current bean/cmd. */
  private String       id;
  /** The Midas context to use. */
  private Midas        midas;
  /** The command (string) to run. */
  private String       command;
  /** Max wait time (seconds) for command to OPEN (to get out of INIT state), negative is effectively infinite. */
  private double cmdOpenTimeout; // Since 2.7.4 for BUG 2056
  /** Max wait time (seconds) for command to CLOSE, negative is effectively infinite. */
  private double cmdCloseTimeout;// Since 3.1.1 for BUG 2536
  /** reference to thread started in {@link #start()}, so that it does not block current thread. */
  private Thread       thread;   // since 3.1.1 for BUG 2536

  /** Creates a new bean for an existing command. In most cases it is best to use the
      {@link #createBean(Midas,String,MidasDisplay,String)} method rather than
      calling the constructor directly.
      @param midas   The midas context to run the command in (cannot be null).
      @param command The command to run.
      @param panel   The panel to use (can be <code>null</code> for a
                     non-graphical command or a graphical command that will
                     use its own window).
      @param id      The id of the bean (can only be <code>null</code> if
                     panel is <code>null</code>).
      @see #createBean(Midas,String,MidasDisplay,String)
   */
  public MidasBean (final Midas        midas,
                    final String       command,
                    final MidasDisplay panel,
                    final String       id) {
    this.midas   = midas;
    this.command = command;
    this.state   = INIT;
    this.panel   = panel;
    this.id      = ((id == null)? null: id.toUpperCase());

    Args args = Args.parseCommand(midas, command);
    int cmdLgth = args.getName().length();

    if ((panel != null) && (id == null)) {
      midas.warning("MidasBean: Creating a graphical MidasBean but given a null ID.");
    }
    else if (id == null) {
      /* Don't set ID */
    }
    else if (args.isPresent("/ID")) {
      if (!args.getS("/ID").equalsIgnoreCase(id)) {
        midas.warning("MidasBean: ID of command '"+ command +"' does not "+ "match '"+ id +"'.");
      }
    }
    else {
      this.command = this.command.substring(0, cmdLgth)
                   + "/ID=" + this.id
                   + this.command.substring(cmdLgth);
    }

    if (!args.isPresent("/BG")) {
      this.command = this.command.substring(0, cmdLgth)
                   + "/BG"
                   + this.command.substring(cmdLgth);
    }

    // the open/close timeout switches below mirrors the ones in Shell.execCommand(..)
    cmdOpenTimeout  = args.getD("/CMDOPENTIMEOUT", -1.0); // negative for infinite (default); in seconds; since 2.7.4 for Bug 2056
    cmdCloseTimeout = args.getD("/CMDCLOSETIMEOUT", 1.0); // negative for infinite (DEF=1.0); in seconds; since 3.1.1 for BUG 2536
  }

  /** Creates a new bean for an existing command and starts it running. This is a convenience method
      that is identical to using: <code>createBean(midas,command,null,null)</code>
      WARNING: This SHOULD NOT be run on the EventDispatchThread (EDT) (i.e. the AWT/Swing thread)
      as it can cause running a PLOT command to lockup since PLOT depends on initializing its
      graphics panels on the EDT.
      @param midas   The midas context to run the command in.
      @param command The command to run.
      @return the new bean
   */
  public static MidasBean createBean (final Midas midas, final String command) {
    return createBean(midas, command, null, null);
  }

  /** Creates a new bean for an existing command and starts it running. This is a convenience method
      that is identical to using: <code>(new MidasBean(midas,command,panel,id)).start()</code>
      EXCEPT that it will not return until the command's <code>init()</code> method has called;
      this ensures that everything has been initialized.
      WARNING: This SHOULD NOT be run on the EventDispatchThread (EDT) (i.e. the AWT/Swing thread)
      as it can cause running a PLOT command to lockup since PLOT depends on initializing its
      graphics panels on the EDT. But if you must, then you may want to add the /CMDOPENTIMEOUT switch
      to specify a timeout as a crude workaround.
      @param midas   The midas context to run the command in.
      @param command The command to run.
      @param panel   The panel to use.
      @param id      The ID to use.
      @return the new bean
   */
  public static MidasBean createBean (final Midas        midas,
                                      final String       command,
                                      final MidasDisplay panel,
                                      final String       id) {
    MidasBean bean = new MidasBean(midas,command,panel,id);
    bean.start(); // start MidasBean thread

    // following variables (cmdOpenTimeout, numLoops) are for Bug 2056
    int numLoops = Integer.MAX_VALUE; // Integer.MAX_VALUE is effectively infinity
    if (bean.cmdOpenTimeout >=0 ) numLoops = (int)(bean.cmdOpenTimeout/SLEEP_INTERVAL); // 0 or positive values will effect timeout

    // Wait for open() to be run unless it timeout
    while ((bean.getState() == INIT) && (numLoops-- > 0)) {
      Time.sleep(SLEEP_INTERVAL);
    }

    return bean;
  }

  /** Gets the panel being used.
      @return The panel being used.
   */
  public synchronized MidasDisplay getPanel () {
    return panel;
  }

  /** Gets the state of the bean.
      @return The status of the bean. This will be one of the state indicators
              specified in {@link Commandable}
   */
  public synchronized final int getState () {
    return state;
  }

  @Override
  public int open () {
    Table win = (Table)midas.registry.get("WIN");
    if (win == null) {
      win = new Table();
      midas.registry.put("WIN", win);
    }
    win.put(id, panel);
    cmd = Shell.runCommand(midas, command);
    //cmd = Shell.runCommand(midas, command, null, panel); // since 3.3.2: use new API
    if ((cmd != null) && (thread != null) ) {            // since 3.1.1: name MidasBean thread (BUG 2536)
      String currentName = thread.getName();
      thread.setName(currentName + "-" + cmd.getName()); // append command name to thread name
    }
    state = PROCESS;

    return NORMAL;
  }

  @Override
  public int process () {
    if ((cmd != null) && (cmd.state != DONE)) {
      return NOOP;
    } else {
      return FINISH;
    }
  }

  /** Put EXIT message on command's message queue and wait for it to process
      and change the command's state to DONE.
      Wait up to {@link #cmdCloseTimeout} second(s) for command to close, otherwise continue.
      @since NeXtMidas 3.1.1 for BUG 2453
   */
  synchronized void closeCommand () {
    if ((cmd != null) && (cmd.MQ != null)) {
      //cmd.MQ.put("EXIT",0,null); // Send "EXIT" message to command
      cmd.setState("FINISH");  // Set the state manually instead of via msg (since 3.3.0 - Bug 2686)

      // Integer.MAX_VALUE is effectively infinity for numLoops; only positive values will effect timeout
      int numLoops = (cmdCloseTimeout < 0)? Integer.MAX_VALUE : (int)(cmdCloseTimeout/SLEEP_INTERVAL);
      for (int ii=0; ii<numLoops && cmd.state!=Command.DONE; ii++) {
        Time.sleep(SLEEP_INTERVAL);
      }

      // if command's state is still not DONE, call command.close() directly
      if (cmd.state != Command.DONE) {
        midas.warning("Command still not DONE, so closing directly: " + cmd);
        cmd.close();
      }

      // if command is still in registry, then remove it
      Object objInRegistry = midas.registry.get(cmd.id);
      if (objInRegistry != null) {
        midas.warning("Command is still in registry, removing: " + cmd.id);
        midas.registry.remove(cmd.id);
      }

      // since 3.3.0: cleanup WIN.<cmdID> (MidasDisplay panel reference) in registry (BUG 2453)
      Object winPanel4Cmd = midas.registry.get("WIN."+id);
      if (winPanel4Cmd != null && winPanel4Cmd == panel) {
        midas.registry.remove("WIN."+id);
      }
    }
    cmd = null; // clear out reference to command
  }

  @Override
  public int close () {
    closeCommand(); // since 3.1.1 to close command to cleanup for BUG 2453

    return NORMAL;
  }

  /** This runs the command sequence. In most cases it is better to use {@link #start()}, rather
      than this method.
      @see #start()
   */
  @Override
  public final void run () {
    int status = NORMAL;

    status = open();

    this.state = PROCESS;
    while (status != FINISH) {
      if (status == NOOP) {
        Time.sleep(SLEEP_INTERVAL);
      }

      status = process();
    }
    this.state = CLOSE;

    status = close();
    this.state = DONE;
  }

  /** This starts the command running in a separate thread. This is the same as
      <pre>(new MidasThread(midas, this, threadName)).start()
      where threadName is "MidasBean-Thread-" + &lt; this.id OR counter &gt; (since 3.1.1).
      If this.id is null, then an anonymous incrementing counting number is used.</pre>
      @return <code>this</code> - This permits method chaining.
      @see #run()
   */
  public final MidasBean start () {
    // since 3.1.1: name thread for BUG 2536
    String threadName = "MidasBean-" + ((id==null)? MidasThread.nextThreadNum() : id);
    thread = new MidasThread(midas, this, threadName);
    thread.start();

    return this;
  }

  /** Gets the command being run.
      @return The command being run (or <code>null</code> if not yet started).
   */
  public synchronized final Command getCommand () {
    return cmd;
  }

  /** Gets the message handler to use. By default this is just the command being run
      (see {@link #getCommand()}), but may be overridden by subclasses.
      @return The message handler to use (or <code>null</code> if not set).
   */
  public synchronized MessageHandler getMessageHandler () {
    return getCommand();
  }

  @Override
  public int processMessage (Message msg) {
    if (getMessageHandler() != null) {
      return getMessageHandler().processMessage(msg);
    } else {
      return 0;
    }
  }

  /** Get the ID of the current bean.
   *  @return the ID of the current bean
   *  @since NeXtMidas 3.1.1
   */
  public String getId () {
    return id;
  }

  /** @since NeXtMidas 3.1.1 */
  @Override
  public String toString () {
    return "MidasBean: " + cmd + " ID=" + id;
  }
}
