package nxm.sys.libxm;

import java.awt.Frame;
import java.awt.GraphicsEnvironment;
import java.awt.Insets;
import java.awt.Rectangle;
import java.io.File;
import java.io.FilenameFilter;

import nxm.sys.inc.CommandParent;
import nxm.sys.inc.Commandable;
import nxm.sys.inc.InternalUseOnly;
import nxm.sys.inc.ShellParent;
import nxm.sys.lib.Args;
import nxm.sys.lib.Command;
import nxm.sys.lib.Convert;
import nxm.sys.lib.Data;
import nxm.sys.lib.DataFile;
import nxm.sys.lib.FileName;
import nxm.sys.lib.Foreign;
import nxm.sys.lib.Import;
import nxm.sys.lib.Intrinsic;
import nxm.sys.lib.Message;
import nxm.sys.lib.Midas;
import nxm.sys.lib.MidasException;
import nxm.sys.lib.Parser;
import nxm.sys.lib.Privileges;
import nxm.sys.lib.Shell;
import nxm.sys.lib.Terminal;
import nxm.sys.lib.Time;
import nxm.sys.lib.XmUtil;
import nxm.sys.libg.GPrimitive;
import nxm.sys.libg.MWindow;

/** A class to interface to an X-Midas session.<br>
    <span style="color:red">Do NOT use this class directly, this class is for use by the SYS option tree ONLY
    and is subject to change without notice!</span>
    @author  Jay Hallam / J. Czechowski IV
    @since   NeXtMidas 2.1.0
 */
@InternalUseOnly
public class XmShellParent implements ShellParent {
  // Set the next line to true to use the workaround for Pete that fixes his particular
  // exit problem, but causes other things not to work correctly. This is a temporary
  // workaround that will be addressed in future releases.
  private static final boolean EXIT_BUG_WORKAROUND = true;

  /** The parent thread group for all JSM threads. */
  private static ThreadGroup jsmThreadGroup = new ThreadGroup("jsmThreadGroup");

  private static long         cmdCounter   = 0;    // Counts the number of commands started.
  private static Insets       defInsets    = null; // The window insets (used ONLY by getDefaultInsets())
  private static Insets       scrInsets    = null; // The screen insets (used ONLY by getScreenInsets())
  private static Rectangle    maxBounds    = null; // The screen bounds (used ONLY by getScreenBounds())
  private static XmMsgHandler xmMsgHandler = null; // The message handler to use.

  private Shell    nxmShell = null; // The NeXtMidas shell class
  private Terminal terminal = null; // The Terminal to use

  /** No argument constructor */
  public XmShellParent() {
    nxmShell = new XmShell((ShellParent)this);
    terminal = new Terminal(); // FYI: CANNOT use NTerminal as it registers SIGINT (ctrl+c) handler
                               //      (replacing the X-Midas handler). See BUG 2274 for details
    Shell.setPriv(Privileges.PRIV_ALL);
    nxmShell.open();
  }

  /** Get the NeXtMidas shell reference
   *  @return the NeXtMidas shell reference */
  public Shell getShell() {
    return nxmShell;
  }

  /** Get the property defining the path for the given X-Midas option tree.
      @param option The option tree.
      @return The value of the environment variable XMAREA_[opt] or null if not found.
  */
  String getOptionPath(String option) {
    return Import.getEnv("XMAREA_"+option.toUpperCase());
  }

  /** Get the X-Midas path definition.
      @return The value of the environment variable XM_PATH or null if not found.
   */
  public String getXmPath() {
    return Import.getEnv("XM_PATH");
  }

  /** Gets the default insets for a window. This is used to adjust coordinates for LEARN. */
  private static Insets getDefaultInsets() {
    if (defInsets == null) {
      Frame  frame  = new Frame();

      frame.setBounds(100,100,100,100);
//    frame.setVisible(true);  // prior to 3.1.1
      frame.pack();            // since 3.1.1: calling pack() is enough to get window sizes without
                               //              having to actually make them visible (BUG 2564)
      defInsets = frame.getInsets();
      frame.setVisible(false);
      frame.dispose();
    }

    return defInsets;
  }

//JC4: This may be useful in the future.
//  /** Gets the default insets for the screen. This is used to adjust coordinates for LEARN. */
//  private static Insets getScreenInsets() {
//    if (scrInsets == null) {
//      GraphicsEnvironment   ge = GraphicsEnvironment.getLocalGraphicsEnvironment();
//      GraphicsConfiguration gc = ge.getDefaultScreenDevice().getDefaultConfiguration();
//
//      scrInsets = Toolkit.getDefaultToolkit().getScreenInsets(gc);
//    }
//
//    return scrInsets;
//  }

  /** Gets the bounds of the screen. This is used to adjust coordinates for LEARN. */
  private static Rectangle getScreenBounds() {
    if (maxBounds == null) {
      GraphicsEnvironment ge = GraphicsEnvironment.getLocalGraphicsEnvironment();
      maxBounds = ge.getMaximumWindowBounds();
    }

    return maxBounds;
  }

  /** Sends a message to the X-Midas message queue.
      @param id  The command ID.
      @param msg The message.
      @return status true if successful
   */
  public static boolean sendXmMessage(int id, Message msg) {
    final int MQK_SENDER = 2;
    boolean status = true;

    XmMsgHandler.XmMessage xmMsg = getXmMsgHandler().nmMsgToXm(msg);
    if (xmMsg == null) return false; // unable to convert

    XMidas.sendMsg(xmMsg.getHeader(), xmMsg.getData(), id, MQK_SENDER);

    return status;
  }

  /** Gets a message from the X-Midas message queue.
      @param id The command ID.
   */
  private static synchronized Message getXmMessage(int id) {
    final int MQK_READER = 4;

    Message msg = null;
    byte[]  mqh = new byte[XmUtil.LENGTH_MQH_H];
    byte[]  mqd = new byte[XmUtil.LENGTH_MQD];

    Convert.packL(mqh,0,id); // Pre-set the ID in the header M$GET_MSG uses it as a filter.

    if (XMidas.getMsg(mqh, mqd, 0.0f, MQK_READER) == 1) {
      msg = getXmMsgHandler().xmMsgToNm(mqh, mqd);
    }

    return msg;
  }

  /** Gets the message handler to use for the command. This is only used by processMessageXM. */
  private static XmMsgHandler getXmMsgHandler() {
    if (xmMsgHandler == null) {
      String opt = Shell.getProperty("MSGOPT");

      if ((opt == null) || (opt.length() == 0)) {
        opt = "SYS";
      } else {
        opt = opt.toUpperCase();

        if (opt.equals("NXM")) {
          opt = "SYS";
        } else if (opt.equals("SYS")) {
          throw new MidasException("XmShellParent: Error can not map to messages in X-Midas SYS, "
                                  +"found /MSGOPT="+opt+", use /MSGOPT=NXM to use messages in "
                                  +"NeXtMidas SYS option.");
        }
      }

      xmMsgHandler = XmMsgHandler.getHandlerFor(opt,true);

      if (xmMsgHandler == null) {
        throw new MidasException("XmShellParent: Message definitions not found for '"+opt+"', "
                               + "need to run GENNMMSG for "+opt+" or specify another option "
                               + "with /MSGOPT=<opt>.");
      }
    }

    return xmMsgHandler;
  }

  //////////////////////////////////////////////////////////////////////////////////////////////////
  // ShellParent methods
  //////////////////////////////////////////////////////////////////////////////////////////////////

  public String getAux()    { return null; }
  public String getNmRoot() {
    String nmroot = System.getProperty("NMROOT");

    // JC4: Try to resolve NMROOT to its shortest form. Otherwise if someone does "home test sys"
    //      they will set the home directory to ".../nxm220/nxm/sys/host/../../../nxm/sys/test/"
    //      wich will cause problems due to X-Midas's 80 character file name limits.
    if (nmroot.endsWith("../")) {
      try {
        File file = new File(nmroot);
        nmroot = file.getCanonicalFile().toString();
      } catch(Exception e) {
        System.err.println("WARN: Error resolving NMROOT='"+nmroot+"': "+e);
      }
    }

    return FileName.terminatePath(nmroot);
  }

  /** Get the option trees useful for NeXtMidas. Any option tree named NXM or SYS is removed. */
  public String getOpts() {
    boolean doNotImportXMidasPath = Convert.s2state(System.getProperty("NOIMPORTXMPATH"), false);
    if (doNotImportXMidasPath) { // do not auto import X-Midas path (for BUG 2279)
      return null;
    }

    String       xmpath  = getXmPath();
    StringBuffer nxmpath = new StringBuffer();

    if (xmpath == null) {
      System.out.println("WARNING: XM_PATH property is NULL (XmShellParent.getXmPath())");
    } else {
      Parser parser = new Parser(xmpath);

      for(int i = 1; i <= parser.elements(); i++) {
        String opt = parser.get(i);

        if (opt.equals("SYS") || opt.equals("NXM")) {
          // Do not include NeXtMidas SYS (NXM) or X-Midas SYS (SYS) on path.
        } else {
          if (nxmpath.length() > 0) { nxmpath.append(","); }
          nxmpath.append(opt);
        }
      }
    }
    return nxmpath.toString();
  }

  public String getCWD()    { return null; }
  public String getNMS()    { return null; }
  public String getHome()   { return null; }
  public String getServer() { return null; }
  public String getTheme () { return null; }
  public String getStyle () { return null; }

  /**  @since NeXtMidas 3.3.0 */
  @Override
  public Terminal getTerminal () {
    return terminal;
  }

  /** Runs a command. This should ONLY be used from JNI.
      @param command     The command to run.
      @param responseId  The IPC response ID to use.
   */
  private void runCommand (String command, int responseId) {
    JsmThread thread = new JsmThread(command, responseId);

    thread.start();
  }

  //////////////////////////////////////////////////////////////////////////////////////////////////
  // JsmThread Class
  //////////////////////////////////////////////////////////////////////////////////////////////////

  /** A thread used for starting commands from the JSM. */
  public class JsmThread extends Thread implements CommandParent {
    private final String  command;            // The command to run.
    private final int     responseId;         // The IPC response ID.
    private final boolean inXmPipe;           // Indicates if running in an X-Midas pipe.
    private final double  startTime;          // The start time of the command (used for LEARN)
    private       int     xsSwitch   = -1;    // The X-Midas /XS switch being used (ignore if < 0).
    private       String  macroName;          // The name of the macro (not set if xsSwitch < 0).
    private       boolean syncCalled = false; // Indicates M$SYNC has been called
    private       int     xMidasID   = -1;    // The X-Midas integer ID.


    /** Creates a new thread.
        @param command     The command to run.
        @param responseId  The IPC response ID to use.
     */
    private JsmThread(String command, int responseId) {
      this(command, responseId, cmdCounter+1);
    }

    /** Creates a new thread.
        @param command     The command to run.
        @param responseId  The IPC response ID to use.
        @param index       The command counter.
     */
    private JsmThread(String command, int responseId, long index) {
      super(new ThreadGroup(jsmThreadGroup,"ThreadGroup for JsmThread #"+index+": "+command),
                                           "JsmThread #"+index+": "+command);
      cmdCounter++;

      if (command.trim().length() == 0) {
        // JC4: Make the ERROR intrinsic handle the case where no command is specified, otherwise
        //      we need to manually do M$SYNC and all that stuff.
        command = "ERROR \"No command specified for NeXtMidas to run.\"";
      }

      this.command    = command;
      this.responseId = responseId;
      this.inXmPipe   = (Mt.getNpipe() >= 0) || (Mt.getNwidget() >= 0);
      this.startTime  = Time.current();
    }

    public void run() {
      try {
        Shell.runCommand(nxmShell.M, command, this);

      } catch(Throwable t) {
        System.err.println("ERROR: "+t);
        t.printStackTrace();
        nxm.sys.libxm.XMidas.return_(responseId);
      }
    }

    /** Gets the X-Midas arguments used when starting the most recent command. The Args returned
        should only be used to get switch values anything else may be inaccurate. WARNING: This
        method will produce BAD DATA after M$SYNC is called.
     */
    private Args getJsmArgs() {
      String cmdString = Mc.getCommandString();

      /* Strip off the NeXtMidas command */
      if (cmdString.indexOf(',') > 0) {
        cmdString = cmdString.substring(0,cmdString.indexOf(','));
      }
      /* Strip off XM option tree name (in case via NeXtOpt) */
      if (cmdString.indexOf(';') >= 0) {
        String xmSwitches = "";
        /* Preserve XM command switches */
        if (cmdString.indexOf('/') >= 0) {
          xmSwitches = cmdString.substring(cmdString.indexOf('/'));
        }
        cmdString = cmdString.substring(0,cmdString.indexOf(';')) + xmSwitches;
      }

      /* JSM is not recognized, so prefix it with a '%' so it looks like a user macro. */
      cmdString = "%" + cmdString;

      return Shell.parseCommand(Shell.getSharedMidasContext(), cmdString);
    }

    public Args setArgs(Command cmd, Args args) {
      Args xmCommand = getJsmArgs();

      if (args.find("/BG")) {
        // Note: Can not print to cmd.M.warn(..) here since M is not yet set.
        Shell.warning("When running NeXtMidas commands from X-Midas the "
                         + "/BG switch should be placed immediately after the 'NM' (e.g. "
                         + "'NM/BG <NeXtMidas Command>').");
      }

      /* Handle the window positioning for those cases where the user gives the /XS switch to the
         NM;NXM command. This will not override any manually entered /WPOS= values.
      */
      if (GPrimitive.isInstance(cmd)) {
        xsSwitch  = xmCommand.getL("/XS",-1);
        macroName = xmCommand.getS("/MACRONAME", null);

        if (xsSwitch > 0) {
          if ((macroName == null) || (macroName.trim().length() == 0)) {
            Shell.warning("Can not determine name of parent macro, "
                                          + "LEARN will be disabled.");
          }

          if (!args.find("/WPOS")) {
            int screenWidth  = getScreenBounds().width;
            int screenHeight = getScreenBounds().height;

            /* Compute the /WPOS values which are in pixels from the X-Midas values which are in...
                 n > 0: 1000ths of screen width
                 n < 0: pixels (rare!)
            */
            int xLeft   = xmCommand.getL("/XL", -MWindow.DEFAULT_WINDOW_POSITION.x);
            int xTop    = xmCommand.getL("/XT", -MWindow.DEFAULT_WINDOW_POSITION.y);
            int xWidth  = xmCommand.getL("/XW", -MWindow.DEFAULT_WINDOW_POSITION.w);
            int xHeight = xmCommand.getL("/XH", -MWindow.DEFAULT_WINDOW_POSITION.h);

            if (xLeft  >0) { xLeft  =(int)(xLeft  /1000.0*screenWidth ); } else { xLeft  =-xLeft;   }
            if (xTop   >0) { xTop   =(int)(xTop   /1000.0*screenHeight); } else { xTop   =-xTop;    }
            if (xWidth >0) { xWidth =(int)(xWidth /1000.0*screenWidth ); } else { xWidth =-xWidth;  }
            if (xHeight>0) { xHeight=(int)(xHeight/1000.0*screenHeight); } else { xHeight=-xHeight; }

            // JC4: Adding getDefaultInsets().right on next lines is a hack to make it line up
            //      correctly on Gnome/Linux, but is will probably work for other window managers.
            xLeft = xLeft + getDefaultInsets().left + getDefaultInsets().right;
            xTop  = xTop  + getDefaultInsets().top  - getDefaultInsets().right;

            args.put("/WPOS", "("+xLeft+","+xTop+","+xWidth+","+xHeight+")");
          }
        }
      }

      return args;
    }

    public void runEnter(Command cmd) {
      /* In X-Midas IDs are integer values.  See if the user has given us an ID we can convert. */
      String idString = cmd.getID().trim();

      if (idString.length() > 0) {
        try {
          xMidasID = Integer.parseInt(idString);
        } catch (Exception e) {
          System.err.println("XmShellParent: can not convert "+idString+" to a X-Midas number id");
        }
      }

      if (cmd.isPipable() && inXmPipe) {
        cmd.isPiped    = true;
        cmd.M.pipeMode = Midas.PRUN;
      }
    }

    public void runExit(Command cmd) {
      /* If running with /XS set then update any setup files modified since the macro started. */
      if ((xsSwitch > 0) && (macroName != null) && (GPrimitive.isInstance(cmd))) {
        doLearn(cmd);
      }

      if ((cmd instanceof Intrinsic) || (cmd instanceof Foreign)) {
        // JC4: NeXtMidas intrinsics normally run to completion (open,process,close) in a single
        //      pass and then get cleaned up when the macro exits (control statements -- if, while,
        //      etc. -- operate a little different though -- but no one should be using those from
        //      X-Midas).
        //
        //      The problem is that X-Midas thinks it is running a primitive (the NM;NXM) and will
        //      wait for the M$SYNC if it is in a piped section. So we need to run the NeXtMidas
        //      intrinsic to completion and then call M$SYNC.
        nxm.sys.libxm.XMidas.sync(responseId);
        if (inXmPipe) {
          // If inside a pipe decrease Mc.syncCount by one so that when we exit and Mc.count is
          // decreased by one (this is done by M$RETURN) it looks as if an intrinsic exited, not
          // a primitive.
// JC4: We can't just do this... we would need to get a M$LOCK on LS_midas before we did this.
//          Mc.setSyncCount(Mc.getSyncCount() - 1);
        }
      }
      nxm.sys.libxm.XMidas.return_(responseId);
    }

    private int countAfterSync = -1;
    public int runSingle(Command cmd, int state, int setState) {
      if (!syncCalled) {
        if ((cmd.state != Command.INIT) && (cmd.state != Command.OPEN)) {
          nxm.sys.libxm.XMidas.sync(responseId);
          countAfterSync = Mc.getCount();
          syncCalled = true;
        }

        if (cmd.isPiped) {
          cmd.M.pipeMode = (syncCalled)? Midas.PRUN: Midas.PINIT;
        }
      }

      if (cmd.isPiped) {
        switch(Mc.getMode()) {
          case -3: cmd.M.pipeMode = Midas.PINIT;  break;
          case  1: cmd.M.pipeMode = Midas.PPAUSE; break;
          case  3: cmd.M.pipeMode = Midas.PRUN;   break;
        }

        if (EXIT_BUG_WORKAROUND) {
          if (Mc.getBreak()) {
            cmd.M.pipeMode = Midas.PSTOP;
            cmd.setState(Command.FINISH);
          }
        } else {
          if (Mc.getBreak() || (syncCalled && (Mc.getCount() < countAfterSync))) {
            cmd.M.pipeMode = Midas.PSTOP;
            cmd.setState(Command.FINISH);
          }
        }
      } else {
        if (Mc.getBreak()) {
          cmd.setState(Command.FINISH);
        }
      }
      return cmd.runSingle();
    }

    public int processMessages(Command cmd, int maxmsg) {
      if (cmd.state > Command.INIT) {
        processXmMessages(cmd, maxmsg);
      }
      int status = cmd.processMessages(maxmsg);

      return status;
    }

    /** Process a message from the X-Midas message queue, convert it to a NeXtMidas Message object
        and send it to the NeXtMidas command.
        @param cmd    A Command object
        @param maxmsg Max number of messages to process (-1 to process all available)
        @return {@link Commandable#NOOP} if no message processed or {@link Commandable#NORMAL} if a
                message is processed.
        */
    private synchronized int processXmMessages(Command cmd, int maxmsg) {
      int status = Commandable.NOOP;
      if (xMidasID > 0) {
        while (maxmsg != 0) {
          maxmsg--;

          Message msg = getXmMessage(xMidasID);

          if (msg != null) {
            status = cmd.processMessage(msg);
          } else {
            maxmsg = 0; // If maxmsg was initially -1 this sets it to 0 to stop the loop.
          }
        }
      }
      return status;
    }

    // PRIVATE METHODS /////////////////////////////////////////////////////////////////////////////
    /** Update any setup files modified since the macro started.
        @param cmd The command.
     */
    private void doLearn(Command cmd) {
      MWindow mw     = GPrimitive.getMWindowViaReflection(cmd);
      Insets  insets = (new Frame()).getInsets();

      String filename = null;

      macroName = macroName.trim();

      if (macroName.startsWith("%")) {
        macroName = macroName.substring(1);
        filename  = cmd.M.getResults().get("AUX.98")+macroName.toLowerCase()+".set";
      } else {
        String opt = null;

        if (macroName.indexOf(';') > 0) {
          opt       = macroName.substring(macroName.indexOf(';')+1);
          macroName = macroName.substring(0,macroName.indexOf(';'));
        } else {
          opt = "SYS";
        }

        filename = nxm.sys.libxm.XMidas.fileNameOpt("mcr", macroName, "set", opt);
      }

      File   macroFile  = new File(filename);
      File[] setupFiles = macroFile.getParentFile().listFiles(makeSetFileFilter(macroName));

      GraphicsEnvironment ge = GraphicsEnvironment.getLocalGraphicsEnvironment();
      int screenWidth  = ge.getMaximumWindowBounds().width;
      int screenHeight = ge.getMaximumWindowBounds().height;

      int xLeft   = (int)((mw.pos.x-insets.left)*1000.0/screenWidth );
      int xTop    = (int)((mw.pos.y-insets.top )*1000.0/screenHeight);
      int xWidth  = (int)(mw.pos.w*1000.0/screenWidth );
      int xHeight = (int)(mw.pos.h*1000.0/screenHeight);

      for(int i = 0; i < setupFiles.length; i++) {
        double modTime = (setupFiles[i].lastModified()/1000.0) - Time.J1950TOJ1970;

        if (modTime > startTime) {
          DataFile setupFile = new DataFile(cmd.M, setupFiles[i].toString());

          if (setupFile.open(DataFile.INOUT)) {
            Data data = new Data(setupFile.getFormat(), 1);
            data.fromArray(new int[] {xLeft, xTop, xWidth, xHeight } );
            setupFile.write(data, (xsSwitch-1.0));
            setupFile.close();

            cmd.M.info("Wrote setup to "+setupFiles[i]);
          } else {
            cmd.M.warning("Unable to write setup to "+setupFiles[i]);
          }
        }
      }
    }

    /** Makes a FilenameFilter to filter setup files.
        @param macroName The name of the macro.
        @return A filter that matches all files that start with "[macroName].set".
     */
    private FilenameFilter makeSetFileFilter(String macroName) {
      final String matchName = macroName.toLowerCase() + ".set";

      return new java.io.FilenameFilter() {
        public boolean accept(File dir, String name) { return name.startsWith(matchName); }
      };
    }
  } // end class JsmThread

}
