package nxm.sys.libg;

import static nxm.sys.libg.GraphicsUtil.invokeLater;

import java.awt.Color;
import java.awt.Dimension;
import java.awt.GraphicsConfiguration;
import java.awt.GraphicsEnvironment;
import java.awt.HeadlessException;
import java.awt.Insets;
import java.awt.Point;
import java.awt.event.ComponentEvent;
import java.awt.event.WindowEvent;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;

import javax.swing.JFrame;
import javax.swing.RepaintManager;

import nxm.sys.inc.InternalUseOnly;
import nxm.sys.lib.Shell;
import nxm.sys.lib.Time;

/** A top-level window for displaying graphical panels and widgets. <br>
    <br>
    This class uses swing while its sister class ({@link MFrame}) uses AWT.

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

    @see MWindow
    @see MFrame

    @author  Jeff Schoen
*/
public class MJFrame extends JFrame implements nxm.sys.libg.MJFrameModSwap {
  /** NOTE: this should be changed if the structure/fields of this class changes. */
  private static final long serialVersionUID = 2012021010311L; // for Serializable interface

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

  /** MWindow associate with this MJFrame */
  transient MWindow MW;
  /** position */
  private transient MBox pos;
  /** borders (insets) */
  private static Insets insets;

  /**
     Creates a new MJFrame with the given registry name and sized and
     located according to dimensions and coordinates of the given
     MBox parameter.
     @param name the registry name
     @param pos the size and location of the MJFrame
   */
  public MJFrame (String name, MBox pos) {
    this(name,pos,(String)null);
  }

  /** Creates a new frame.  <br>
      This is identical to <code>MJFrame(name,pos,false,MFrame.getDisplay(display))</code>.
      @param name    The name/title of the frame.
      @param pos     The position of the frame.
      @param display The display to show the frame on (or <code>null</code> for the current display).
      @see MJFrame#MJFrame(String,MBox,GraphicsConfiguration)
      @since NeXtMidas 2.5.2
   */
  public MJFrame (String name, MBox pos, String display) {
    this(name,pos,getDisplay(display));
  }

  /** Creates a new frame.  This is NOT visible by default.
      @param name    The name/title of the frame.
      @param pos     The position of the frame.
      @param display The display to show the frame on (or <code>null</code> for the current display).
      @since NeXtMidas 2.5.2
   */
  public MJFrame (String name, MBox pos, GraphicsConfiguration display) {
    super(name,display);
    setupFrame(pos, name);
  }

  private void setupFrame (MBox pos, String name) {
    this.pos = pos;
    //registryName = name; //not used


    // BUG 1762 - Unless user has set the SWINGDB (SWING double-buffering) graphics option, disable it.
    if (!Theme.getOption(Theme.SWINGDB)) RepaintManager.currentManager(this).setDoubleBufferingEnabled(false);

    if (Theme.graphicsDelay > 0) Time.pause(Theme.graphicsDelay); // Pause (in seconds) to let the JVM graphics initialize (BUG 2499)

    setBackground(Color.black);
    setForeground(Color.white);

    /* Specify the events of interest */
    addWindowListener(this);
    addComponentListener(this);
    setBounds(pos);

    setDefaultCloseOperation(JFrame.DO_NOTHING_ON_CLOSE);
  }

  @Override
  public void setBounds (MBox pos) {
    if (insets==null) {
      super.setBounds (pos.x,pos.y,pos.w,pos.h);
    } else {
      super.setBounds (pos.x-insets.left, pos.y-insets.top,
        pos.w+insets.left+insets.right, pos.h+insets.top+insets.bottom);
    }
  }

  @Override
  public void resizePanel () {
    invokeLater(() -> { // Since 3.9.0 use invokeLater for graphical timing issues (NXM-3213)
      if (MW==null || MW.panel==null) return;
      if (insets==null) MW.panel.setBounds(0,0,pos.w,pos.h);
      else MW.panel.setBounds(insets.left,insets.top,pos.w,pos.h);
    });
  }

  @Override
  public Dimension getSize () {
    Dimension d = super.getSize();
    if (insets==null) return d;
//    This needs to be removed for resizing windows - very noticeable when running from webswing since 3.9.2
//    d.width -= (insets.left+insets.right);
//    d.height -= (insets.top+insets.bottom);
    return d;
  }

  @Override
  public void display (int mode) {
    // Note that this should match MFrame.display(int)
    if (mode>=MWindow.BACK)  { setVisible(true); }
    if (mode==MWindow.EXIT)  { setVisible(false); }
    if (mode==MWindow.FRONT) { setExtendedState(NORMAL); setState(NORMAL); toFront(); }
    if (mode==MWindow.BACK)  { toBack();  }
    if (mode==MWindow.ICON)  { setState(ICONIFIED); }
    if (mode==MWindow.MAXIM) { setExtendedState(MAXIMIZED_BOTH); setState(NORMAL); toFront(); }
  }

  private void process (String name, int info, Object data) {
    if (MW!=null) MW.process(name,info,data);
  }

  @Override
  public void setMWindow (MWindow mw) {
    MW = mw;
  }

  @Override
  public MWindow getMWindow () {
    return MW;
  }

  @Override
  public void componentMoved (ComponentEvent e) {
    if (MW==null || insets==null) return;
    Point p = getLocation();
    MW.pos.x = p.x+insets.left;
    MW.pos.y = p.y+insets.top;
  }

  @Override
  public void componentResized (ComponentEvent e) {
    if (MW != null) {
      invokeLater(() -> {  // Since 3.9.0 use invokeLater for graphical timing issues (NXM-3213)
        if (MW.panel != null) {
          MW.panel.setSize(getSize());
        }
      });
    }
  }

  @Override
  public void componentShown (ComponentEvent e) {
//  System.out.println("MJFrame Comp shown event="+e+"\n ==> this="+this);
  }

  @Override
  public void componentHidden (ComponentEvent e) {}

  @Override
  public void windowOpened (WindowEvent e) {
    insets = getInsets();
    setBounds(pos);
    process("WINDOW",1,"OPENED");
  }

  @Override
  public void windowActivated (WindowEvent e) {
//  System.out.println("MJFrame windowActivatedevent="+e+"\n ==> this="+this);
  }

  @Override
  public void windowDeactivated (WindowEvent e) { }

  @Override
  public void windowIconified (WindowEvent e) {
    process("WINDOW",-1,"ICONIFIED");
  }

  @Override
  public void windowDeiconified (WindowEvent e) {
// System.out.println("MJFrame windowDeiconified="+e+"\n ==> this="+this);
    process("WINDOW",1,"DEICONIFIED");
  }

  @Override
  public void windowClosing (WindowEvent e) {
    process("WINDOW",0,"CLOSING");
  }

  @Override
  public void windowClosed (WindowEvent e) {
  }

  /** Gets the {@link GraphicsConfiguration} for the given display.
    <br>
    At this time this only supports simple screen device selection on the same system.
    In the future this may be extended to permit more advanced display device selection.
    <br>
    For example within X (running on display <code>':0'</code>) this works as:
    <pre>
      getDisplay(null)     --&gt; null  (the default display)
      getDisplay("0")      --&gt; <i>X-Display ':0.0'</i>
      getDisplay("1")      --&gt; <i>X-Display ':0.1'</i> (if available)
      getDisplay(???)      --&gt; null (if ??? does not match a valid display)
                                  plus a warning printed to {@link System#out}
    </pre>
    @param display The number of the display to get (or <code>null</code> for the default).

    @return The {@link GraphicsConfiguration} for the specified display or
            <code>null</code> on error.
    @since NeXtMidas 3.1.0 (Moved from MFrame)
  */
  public static final GraphicsConfiguration getDisplay (String display) {
    GraphicsConfiguration gconf = null; // Default to null (i.e. same display)

    if (display != null) {
      try {
        final int device = Integer.parseInt(display);
        final int config = 0;
        gconf = GraphicsEnvironment.getLocalGraphicsEnvironment()
                                   .getScreenDevices()[device]
                                   .getConfigurations()[config];
      }
      catch (HeadlessException|NumberFormatException exception) {
        Shell.warning("Unable to connect to display '" + display + "':" + exception);
      }
    }

    return gconf;
  }

  /** 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_MJFRAME_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_MJFRAME_OBJECT_MESSAGE);
  }
}
