package nxm.vnc.libvnc.tightvnc;

import java.awt.FlowLayout;
import java.awt.GridBagConstraints;
import java.awt.GridBagLayout;
import java.awt.Panel;
import java.awt.ScrollPane;
import java.awt.event.WindowEvent;
import java.io.EOFException;
import java.io.IOException;
import java.net.ConnectException;
import java.net.NoRouteToHostException;
import java.net.UnknownHostException;
import java.util.Map;

import nxm.sys.lib.Convert;
import nxm.sys.lib.Midas;
import nxm.sys.lib.MidasException;

/** Override the standard <code>VncViewer</code> to allow it to be used in NeXtMidas. The primary
    changes made here are:
    <pre>
      (1) Parameters are passed in via the constructor. They are all
          uppercase and have underscores in place of spaces.

      (2) VncViewer calls System.exit(..) when it closes. Need to
          override this so that NeXtMidas does not close on exit.
    </pre>

    NOTE: NxmViewer does NOT currently support Java Serialization, even though it extends classes that support it.

    @author  Warren Knight
    @author  Neon Ngo  2011-12-14: Support HOST=<host>:<display_number> syntax (BUG 2527)
                       2011-05-27: Use replacement APIs for deprecated graphics methods
    @author  J. Czechowski IV  2006-09-22 Added support for using display number (:1) in addition to port (5901) / added docs
    @since   NeXtMidas 2.3.0
 */
public class NxmViewer extends VncViewer {
  /** NOTE: this should be changed if the structure/fields of this class changes.
   *  This is being left to avoid warnings, though this class is not truly serializable.
   *  We are not overwriting readObject and writeObject because serialization will error out on VncViewer */
  private static final long serialVersionUID = 2013030805332L; // for Serializable interface

  private NxmCanvas vnc;
  private boolean   shutdown = false;
  private boolean   debug    = true;  // Turn on debug?
  private transient Midas     midas;            // The midas context for warning messages (can be null).
  private transient Map<String,? extends Object>  args;   // The parameters passed in.

  /** This replaces the init() call in VncViewer also.
      @param midas The Midas context to use.
      @param args  The table of arguments to use. This takes in an instance of Map so that
                   a NeXtMidas table (normal NeXtMidas operation) or a Java hashtable (if
                   used outside of NeXtMidas) can be used.
   */
  public NxmViewer(Midas midas, Map<String,? extends Object> args) {
    this.inSeparateFrame = false;
    this.midas           = midas;
    this.args            = args;
  }

  public void setShutdown() {
    shutdown=true;
    if (rfbThread != null) {rfbThread.interrupt();};
  }

  public boolean isShutdown() {
    return(shutdown);
  }

  /* Override of readParameter to use the NeXtMidas routines */
  public String readParameter(String name, boolean required) {
    Object value = null;
    name  = name.replace(' ','_').toUpperCase();

    if (args == null) {
      warn("Unable to get parameter "+name+"=, no parameters table found.");
    } else {
      // NextMidas does not like space in the paramater names
      // So we change them to _'s
      value = args.get(name);
      if ((value == null) && required) {
        fatalError(name + " parameter not specified");
      }

      // since 3.1.0: allow <host>:<display_number> syntax for HOST= argument for BUG 2527
      if (name.equals("HOST") && (value != null)) {
        value = (Object) parseHost(value.toString());
      }

      // Allow port number to be provided using "PORT=5901" or "PORT=:1" syntax.
      if (name.equals("PORT")) {
        int port;
        if ((value == null) || value.equals("")) {  // since 3.1.0 (when PORT= is not specified), then
          port = this.port;                         // use port that may have been saved by parseHost(String) method
        }
        else {
          port = displayNumberToPort(value.toString());
        }
        value = Integer.toString(port);

        if (port < 100) {
          warn("Port number "+port+" appears to be invalid; use PORT=5901 or PORT=:1 syntax.");
        }
      }
    }
    debug("Arg: "+name+"="+value);
    return (value == null)? null: value.toString();
  }

  /** Converts a display number (like ":1") to a port number. If a port number (like "5901") is
      given it is kept as is.
   */
  private int displayNumberToPort(String disp) {
    int port; // since 3.1.0, do not overwrite this.port in this utility method
    if (disp.startsWith(":")) {
      port = 5900 + Convert.o2l(disp.substring(1));
    } else {
      port = Convert.o2l(disp);
    }
    return port;
  }

  /**
      @param hoststr string to be parsed containing either "host" or "host:display_number"
      @return hostname (i.e. everything before colon [':'] from hoststr)
      @see #displayNumberToPort(String)
      @since NeXtMidas 3.1.0 for BUG 2527
   */
  private String parseHost(String hoststr) {
    int iColon = hoststr.indexOf(':');
    if (iColon >= 0) {
      this.port = displayNumberToPort(hoststr.substring(iColon)); // save port to be used later
      return hoststr.substring(0,iColon);                         // return just hostname
    }
    return hoststr;
  }

  /** Prints a warning message out. */
  private void warn(String msg) {
    if (midas != null) {
      midas.warning("VNCVIEWER: "+msg);
    } else {
      System.err.println("WARN VNCVIEWER: "+msg);
    }
  }

  /** Prints a debug message out. */
  private void debug(String msg) {
    if (debug) {
      if (midas != null) {
        // 2011-12-14: updated to call midas.type(..) so that it's output gets to the right place (e.g. SHELLGUI)
        midas.type("VNCVIEWER: "+msg);
      } else {
        System.out.println("VNCVIEWER: "+msg);
      }
    }
  }

  /** Print stack trace using {@link Midas#printStackTrace(Throwable)} if midas is not null,
      otherwise just call {@link Throwable#printStackTrace()} to print it to STDOUT.
      @since 3.1.0
   */
  private void printStackTrace(Throwable t) {
    if (midas != null) midas.printStackTrace(t);
    else t.printStackTrace();
  }

  /* This is an exact duplicate of VncViewer.run() except that it
     starts a NxmCanvas instead of a VncCanvas
  */
  public void run() {
    gridbag = new GridBagLayout();
    vncContainer.setLayout(gridbag);

    GridBagConstraints gbc = new GridBagConstraints();
    gbc.gridwidth = GridBagConstraints.REMAINDER;
    gbc.anchor = GridBagConstraints.NORTHWEST;

    if (showControls) {
      buttonPanel = new ButtonPanel(this);
      gridbag.setConstraints(buttonPanel, gbc);
      vncContainer.add(buttonPanel);
    }

    try {
      connectAndAuthenticate();

      doProtocolInitialisation();

//    Changed whk
//      vc = new VncCanvas(this);
      vc = new NxmCanvas(this);
      gbc.weightx = 1.0;
      gbc.weighty = 1.0;

      if (inSeparateFrame) {

	// Create a panel which itself is resizeable and can hold
	// non-resizeable VncCanvas component at the top left corner.
	Panel canvasPanel = new Panel();
	canvasPanel.setLayout(new FlowLayout(FlowLayout.LEFT, 0, 0));
	canvasPanel.add(vc);

	// Create a ScrollPane which will hold a panel with VncCanvas
	// inside.
	desktopScrollPane = new ScrollPane(ScrollPane.SCROLLBARS_AS_NEEDED);
	gbc.fill = GridBagConstraints.BOTH;
	gridbag.setConstraints(desktopScrollPane, gbc);
	desktopScrollPane.add(canvasPanel);

	// Finally, add our ScrollPane to the Frame window.
	vncFrame.add(desktopScrollPane);
	vncFrame.setTitle(rfb.desktopName);
	vncFrame.pack();
	vc.resizeDesktopFrame();

      } else {

	// Just add the VncCanvas component to the end of this container
	gridbag.setConstraints(vc, gbc);
	add(vc);
	validate();

      }

      if (showControls)
	buttonPanel.enableButtons();

      vc.processNormalProtocol();

    } catch (NoRouteToHostException e) {
      fatalError("Network error: no route to server: " + host, e);
    } catch (UnknownHostException e) {
      fatalError("Network error: server name unknown: " + host, e);
    } catch (ConnectException e) {
      fatalError("Network error: could not connect to server: " +
		 host + ":" + port, e);
    } catch (EOFException e) {
      if (showOfflineDesktop) {
	printStackTrace(e);
	warn("Network error: remote side closed connection");
	if (vc != null) {
	  vc.enableInput(false);
	}
	if (inSeparateFrame) {
	  vncFrame.setTitle(rfb.desktopName + " [disconnected]");
	}
	if (rfb != null && !rfb.closed())
	  rfb.close();
	if (showControls && buttonPanel != null) {
	  buttonPanel.disableButtonsOnDisconnect();
	  if (inSeparateFrame) {
	    vncFrame.pack();
	  } else {
	    validate();
	  }
	}
      } else {
	fatalError("Network error: remote side closed connection", e);
      }
    } catch (IOException e) {
      String str = e.getMessage();
      if (str != null && str.length() != 0) {
	fatalError("Network Error: " + str, e);
      } else {
	fatalError(e.toString(), e);
      }
    } catch (Exception e) {
      String str = e.getMessage();
      if (str != null && str.length() != 0) {
	fatalError("Error: " + str, e);
      } else {
	fatalError(e.toString(), e);
      }
    }
  }

  /* Yet another method taken verbatim except the while(true) changed to while(!isShutdown()) */
  void connectAndAuthenticate() throws Exception {
    // If "ENCPASSWORD" parameter is set, decrypt the password into
    // the passwordParam string.

    if (encPasswordParam != null) {
      // ENCPASSWORD is hexascii-encoded. Decode.
      byte[] pw = {0, 0, 0, 0, 0, 0, 0, 0};
      int len = encPasswordParam.length() / 2;
      if (len > 8)
        len = 8;
      for (int i = 0; i < len; i++) {
        String hex = encPasswordParam.substring(i*2, i*2+2);
        Integer x = Integer.valueOf(hex, 16); // Update in 3.9.0 to use valueOf instad of new instance
        pw[i] = x.byteValue();
      }

      // Decrypt the password.
      byte[] key = {23, 82, 107, 6, 35, 78, 88, 7};
      DesCipher des = new DesCipher(key);
      des.decrypt(pw, 0, pw, 0);
      passwordParam = new String(pw);
    }

    // If a password parameter ("PASSWORD" or "ENCPASSWORD") is set,
    // don't ask user a password, get one from passwordParam instead.
    // Authentication failures would be fatal.

    if (passwordParam != null) {
      if (inSeparateFrame) {
        vncFrame.pack();
//      vncFrame.show();           // ORIG prior to 2.9.2
        vncFrame.setVisible(true); // since 2.9.2 to fix Deprecated warning
      } else {
        validate();
      }
      if (!tryAuthenticate(passwordParam)) {
        throw new Exception("VNC authentication failed");
      }
      return;
    }

    // There is no "PASSWORD" or "ENCPASSWORD" parameters -- ask user
    // for a password, try to authenticate, retry on authentication
    // failures.

    GridBagConstraints gbc = new GridBagConstraints();
    gbc.gridwidth = GridBagConstraints.REMAINDER;
    gbc.anchor = GridBagConstraints.NORTHWEST;
    gbc.weightx = 1.0;
    gbc.weighty = 1.0;
    gbc.ipadx = 100;
    gbc.ipady = 50;
    gridbag.setConstraints(authenticator, gbc);
    vncContainer.add(authenticator);

    if (inSeparateFrame) {
      vncFrame.pack();
//    vncFrame.show();           // ORIG prior to 2.9.2
      vncFrame.setVisible(true); // since 2.9.2 to fix Deprecated warning
    } else {
      validate();
      // FIXME: here moveFocusToPasswordField() does not always work
      // under Netscape 4.7x/Java 1.1.5/Linux. It seems like this call
      // is being executed before the password field of the
      // authenticator is fully drawn and activated, therefore
      // requestFocus() does not work. Currently, I don't know how to
      // solve this problem.
      //   -- const
      authenticator.moveFocusToPasswordField();
    }

    while (!isShutdown()) {
      // Wait for user entering a password.
      synchronized(authenticator) {
        try {
          authenticator.wait();
        } catch (InterruptedException e) {
        }
      }

      // Try to authenticate with a given password.
      if (tryAuthenticate(authenticator.password.getText()))
        break;

      // Retry on authentication failure.
      authenticator.retry();
    }

    vncContainer.remove(authenticator);
  }

  //////////////////////////////////////////////////////////////////////////////////////////////////
  // Override those methods that call System.exit()
  // Taken verbatim from VncViewer.java except that exit call
  //////////////////////////////////////////////////////////////////////////////////////////////////

  /** disconnect() - close connection to server. */
  synchronized public void disconnect() {
    debug("Disconnect called");

    if (rfb != null && !rfb.closed())
      rfb.close();
    options.dispose();
    clipboard.dispose();
    if (rec != null)
      rec.dispose();

    setShutdown();
  }

  /** fatalError() - print out a fatal error message. */
  synchronized public void fatalError(String str) {
    warn(str);
    setShutdown();
    throw new MidasException("FatalError");
  }


  synchronized public void fatalError(String str, Exception e) {
    if (rfb != null) {
      // Not necessary to show error message if the error was caused
      // by I/O problems after the rfb.close() method call.
      if (rfb.closed()) {
	debug("RFB thread finished");
	return;
      }
      rfb.close();
    }

    printStackTrace(e);
    warn(str);

    setShutdown();
    throw new MidasException("FatalError");
  }

  /** Close application properly on window close event. */
  public void windowClosing(WindowEvent evt) {
    debug("Closing window");
    if (rfb != null)
      disconnect();

    vncFrame.dispose();
    setShutdown();
  }
}
