package nxm.sys.libg;

import java.awt.AWTEvent;
import java.awt.Color;
import java.awt.Container;
import java.awt.Font;
import java.awt.event.InputEvent;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;
import java.awt.event.MouseEvent;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;

import javax.swing.BorderFactory;
import javax.swing.JTextArea;
import javax.swing.SwingUtilities;
import javax.swing.border.Border;

import nxm.sys.inc.InternalUseOnly;
import nxm.sys.inc.MessageHandler;
import nxm.sys.lib.Message;
import nxm.sys.lib.Shell;

/** A class to handle a SWING text area.  Extends JTextArea to handle mouse clicks.
   <br><br>
    This text area will trap all <code>CONTROL+X</code>, <code>CONTROL+C</code>,
    and <code>CONTROL+V</code> key presses (see {@link #keyPressed(KeyEvent)})
    and will pass the respective messages
    (<code>new Message("TEXTBOX",0,"CUT")</code>,
     <code>new Message("TEXTBOX",0,"COPY")</code>, and
     <code>new Message("TEXTBOX",0,"PASTE")</code>) to the current message
    handler (if defined). <i>(This was initially provided for use by
    {@link nxm.sys.prim.textdisplay}.)</i>

    NOTE: In Swing, the scrollbar policy is set in the JScrollPane, not here.

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

    @since  NeXtMidas 2.7.1
    @author Jim Bauer / Jay Hallam
    @author Originally based on MTextArea
*/
public class MJTextArea extends JTextArea implements MessageHandler, KeyListener {
  /** NOTE: this should be changed if the structure/fields of this class changes. */
  private static final long serialVersionUID = 2012020960311L; // for Serializable interface

  /** Message given when user tries to serialize an MJTextArea object */
  @InternalUseOnly
  public static final String UNSERIALIZABLE_MJTEXTAREA_OBJECT_MESSAGE =
      "nxm.sys.libg.MJTextArea does NOT currently support Java Serialization";

  /** The message handler to use. */
  private transient MessageHandler mh;

  /** The local system line separator, e.g. "\r\n" on Windows, and "\n" on Unix. */
  static final String lineSep = getLineSep();

  /** Get the System Line separator used (on error, defaults to "\n" for *nix systems) */
  private static String getLineSep() {
    // 2010-07-22 NTN: Note that JTextArea uses "\n" internally, so unless we need to
    //                 save back the output of the JTextArea to a file, we don't even
    //                 need to do this.
    try {
      // 2010-07-22 NTN: "line.separator" system property is always accessible from any client
      return System.getProperty("line.separator");
    } catch (Exception e) {
      return "\n"; // fall-back to Unix line-seperator (newline "\n")
    }
  }

  /** Constructor which uses the default number of rows/columns.
      @param text The text to initially display.
   */
  public MJTextArea (String text) {
    this(text, 20, 80);
  }

  /** Constructor which uses the the specified text, rows, and columns to display.
      @param text The text to initially display.
      @param rows The number of rows to initially display.
      @param cols The number of columns to initially display.
   */
  public MJTextArea (String text, int rows, int cols) {
    super(text, rows, cols);
    // in Swing, the scrollbar policy is set in the scrollpane, not here
    open(rows, cols);
    this.addKeyListener(this);
  }

  /** Constructor helper, sets text area attributes.
      @param rows            The number of rows to initially display.
      @param cols            The number of columns to initially display.
   */
  public void open (int rows, int cols) {
    // Get the fontsize
    Font font = new Font ("monospaced", Font.PLAIN, 12);

    // Create the output text area
    super.setRows (rows);
    super.setColumns (cols);
    super.setEditable(true);
    super.setEnabled(true);
    super.setFont(font);
    super.setLineWrap(true);
    super.setWrapStyleWord(true);
    Border outer = BorderFactory.createEtchedBorder(Theme.global().cwbh,Theme.global().cwbs );
    Border inner = BorderFactory.createEtchedBorder(Theme.global().cwbh,Theme.global().cwbs );
    super.setBorder(BorderFactory.createCompoundBorder(outer, inner));
    //setLocation(100, 100);
    // Specify the events of interest
    enableEvents();
  }

  /** Do not want this to be public, want user to use the AWT event methods. */
  private void enableEvents () {
    enableEvents(AWTEvent.MOUSE_EVENT_MASK);
    enableEvents(AWTEvent.KEY_EVENT_MASK);
  }

  /** Processes events on this text component.
      @param e The event.
   */
  public void processEvent (AWTEvent e) {
    if (e.getID()==MouseEvent.MOUSE_RELEASED) {
      InputEvent ie = (InputEvent)e;
      String msgName = null;
      if (SwingUtilities.isLeftMouseButton((MouseEvent) e)) { // updated in 3.9.0 to use SwingUtilities to find mouse button
        //System.out.println ("Got an event button 1, ID = "+e.getID());
      }
      else if (SwingUtilities.isMiddleMouseButton((MouseEvent) e)) { // updated in 3.9.0 to use SwingUtilities to find mouse button
        //processActionEvent(new ActionEvent(this,0,"EXIT"));
        //System.out.println ("Got an event button 2, ID = "+e.getID());
        msgName = "MENU";
      }
      else if (SwingUtilities.isRightMouseButton((MouseEvent) e)) { // updated in 3.9.0 to use SwingUtilities to find mouse button
        msgName = "MENU";
      }
      else {
        Shell.warning("Got an event, ? button ID = "+e.getID());
      }
      if (mh != null && msgName != null) {
        mh.processMessage(new Message (msgName,0,null,mh,this));
      }
    } else if (e.getID()==MouseEvent.MOUSE_PRESSED) {
      if (SwingUtilities.isMiddleMouseButton((MouseEvent) e)) { // updated in 3.9.0 to use SwingUtilities to find mouse button
        /* This is to prevent the automatic middle-button paste that
           typically happens when the middle mouse button is clicked
           within X-Windows.
        */
        ((MouseEvent)e).consume();
      }
    }
    super.processEvent(e);
  }

  /** Set attributes to those of an MWindow.  For example, use <code>setAttributes(panel)</code>
      to set the foreground and background colors of this text area to match those specified in
      the theme for <code>panel</code>.
      @param mw The MWindow to mimic the attributes of.
      @see #setAttributes(Container)
      @see #setAttributesGW(GWidget)
   */
  public void setAttributes (MWindow mw) {
    super.setBackground(mw.theme.cbg);
    super.setForeground(mw.theme.cfg);
  }

  /** Set attributes to those of a GWidget.  For example, use <code>setAttributes(panel)</code> to
      set the foreground and background colors of this text area to match those specified in the
      theme for <code>panel</code>.
      @param mw The GWidget to mimic the attributes of.
      @see #setAttributes(MWindow)
      @see #setAttributes(Container)
      @see #setAttributesGW(GWidget)
   */
  public void setAttributesGW (GWidget mw) {
    super.setBackground(mw.theme.cwbs);
    super.setForeground(mw.theme.cwfg);
  }

  /** Set attributes to those of a Container.  For example, use <code>setAttributes(panel)</code>
      to set the foreground and background colors of this text area to match those of the Component,
      <code>panel</code>.
      @param panel The Container to mimic the attributes of.
      @see #setAttributes(MWindow)
      @see #setAttributesGW(GWidget)
   */
  public void setAttributes (Container panel) {
    Color cb = panel.getBackground();
    super.setBackground(cb);
    Color cf = panel.getForeground();
    super.setForeground(cf);
  }

  /** Set the message handler for this instance.
      @param mh  The message handler to use.
      @todo (JPH ???? Why do I need this???)
  */
  public void setMessageHandler (MessageHandler mh) {
    this.mh = mh;
  }

  public int processMessage (Message msg) {
    return processMessage (msg,null,null);
  }

  /** Process a message from parent process or config menu.
      @param msg    The message to process.
      @param msgid  The message ID (or <code>null</code> if not known).
      @param parent The message's parent (or <code>null</code> if not known).
      @return <code>1</code> on success, <code>-1</code> on failure.
      @todo Remove this method since it is not useful.
   */
  public int processMessage (Message msg, Object msgid, Object parent) {

    int stat=1;	/* assume its for me */

    if      (msg.name.equals("MJTEXT.CFG"))  { }
    else if (msg.name.equals("AXIS"))       { }
    else if (msg.name.equals("MODE"))       { }
    else if (msg.name.equals("SCALE"))      { }
    else if (msg.name.equals("SCALERANGE")) { }
    else if (msg.name.equals("OPTIONS"))    { }
    else {
      stat=-1;
    }
    return stat;
  }

  /** This will append a line to the text displayed in the text area followed by
      a line separator (such as '\n').
      @param str The string to append to the displayed text.
   */
  public void appendLine(String str) {
    super.append(str+lineSep);
  }

  public void keyPressed(KeyEvent e) {
    /* Change CTL-X, CTL-C, CTL-V into "Cut", "Copy", "Paste" */
    if ((e.getModifiersEx() & KeyEvent.CTRL_DOWN_MASK) != 0x0) {// updated in 3.9.0 to use extended modifiers
      if (e.getKeyCode() == KeyEvent.VK_X) {
        if (mh != null) {
          this.mh.processMessage(new Message("TEXTBOX", 0, "CUT"));
        }
        e.consume();

      } else if (e.getKeyCode() == KeyEvent.VK_C) {
        if (mh != null) {
          this.mh.processMessage(new Message("TEXTBOX", 0, "COPY"));
        }
        e.consume();

      } else if (e.getKeyCode() == KeyEvent.VK_V) {

        if (mh != null) {
          this.mh.processMessage(new Message("TEXTBOX", 0, "PASTE"));
        }
        e.consume();
      }
    }
  }

  public void keyReleased (KeyEvent e) {
    /* Do nothing. */
  }

  public void keyTyped (KeyEvent e) {
    /* Do nothing. */
  }

  public void setRows (int rows) {
    if (rows > 0) {
      this.setSize(this.getWidth(), rows * this.getFont().getSize());
    } else {
      throw new IllegalArgumentException("Invalid size given (" + rows + "), "
                                       + "size must be no less than 0.");
    }
  }

  public void setColumns (int cols) {
    if (cols > 0) {
      this.setSize(cols * this.getFont().getSize(), this.getHeight());
    } else {
      throw new IllegalArgumentException("Invalid size given (" + cols + "), "
                                       + "size must be no less than 0.");
    }
  }

  /** Overwrite default serialization for writeObject
   *  to throw an UnsupportedOperationException since Java Serialization currently not supported
   *
   *  @param oos Object Output Stream
   *  @throws IOException Exception thrown by writeObject
   */
  private void writeObject (ObjectOutputStream oos) throws IOException {
    throw new UnsupportedOperationException(UNSERIALIZABLE_MJTEXTAREA_OBJECT_MESSAGE);
  }

  /** Overwrite default deserialization for readObject
   *  to throw an UnsupportedOperationException since Java Serialization currently not supported
   *
   *  @param ois Object Input Stream
   *  @throws ClassNotFoundException Exception thrown by readObject
   *  @throws IOException Exception thrown by readObject
   */
  private void readObject (ObjectInputStream ois) throws IOException, ClassNotFoundException {
    throw new UnsupportedOperationException(UNSERIALIZABLE_MJTEXTAREA_OBJECT_MESSAGE);
  }
}
