package nxm.sys.libg;

import java.awt.AWTPermission;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.EventQueue;
import java.awt.Graphics2D;
import java.awt.GraphicsConfiguration;
import java.awt.GraphicsDevice;
import java.awt.GraphicsEnvironment;
import java.awt.Image;
import java.awt.Rectangle;
import java.awt.Toolkit;
import java.awt.Transparency;
import java.awt.datatransfer.Clipboard;
import java.awt.datatransfer.ClipboardOwner;
import java.awt.datatransfer.DataFlavor;
import java.awt.datatransfer.StringSelection;
import java.awt.datatransfer.Transferable;
import java.awt.datatransfer.UnsupportedFlavorException;
import java.awt.event.MouseEvent;
import java.awt.image.BufferedImage;
import java.io.IOException;
import java.io.OutputStream;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;

import javax.imageio.ImageIO;
import javax.swing.JFrame;
import javax.swing.SwingUtilities;

import nxm.sys.inc.InternalUseOnly;
import nxm.sys.lib.BaseFile;
import nxm.sys.lib.Convert;
import nxm.sys.lib.Data;
import nxm.sys.lib.DataFile;
import nxm.sys.lib.DataOp;
import nxm.sys.lib.ImageFile;
import nxm.sys.lib.Message;
import nxm.sys.lib.Shell;
import nxm.sys.lib.StringUtil;
import nxm.sys.lib.Table;

/** Various graphical utility methods.

    @author  J. Czechowski IV
    @since   NeXtMidas 2.5.0
 */
public final class GraphicsUtil {
  private static final ClipboardManager CLIPBOARD_OWNER = new ClipboardManager();

  /** All methods are static -- prevent instantiation. */
  private GraphicsUtil() { }

  /** Check if we need to call paint in paintcomponent for backwards compatibility
   *  @param clazz Java Class to check to see if is a legacy Class (implements paint rather than painComponent)
   *  @return true if we need to call paint in paintcomponent for backwards compatibility
   *  @since NeXtMidas 3.9.0 for NXM-3213
   */
  public static boolean checkIfNeedLegacyPaint (Object clazz) {
    if (Theme.getOption(Theme.SWINGLEGACY)) {
      try {
        Class<?> implementingClassPaintComponent = clazz.getClass().getMethod("paintComponent",java.awt.Graphics.class).getDeclaringClass();
        Class<?> implementingClassPaint = clazz.getClass().getMethod("paint").getDeclaringClass();
        return !implementingClassPaint.equals(implementingClassPaintComponent) &&
            (implementingClassPaintComponent.isAssignableFrom(implementingClassPaint));
      } catch (SecurityException | NoSuchMethodException e) {
        return false;
      }
    }
    return false;
  }

// JC4: we may need to keep this method under Java 5 and Java 6, or check Theme.isHeadless() for
// cases where X11 forwarding is not configured correctly, see Bug 1443.
  /** Gets the default toolkit to use. */
  static Toolkit getDefaultToolkit () { // package-private can be removed after moving to Java 5
    try {
      return Toolkit.getDefaultToolkit(); // <-- Throws Exception or Error in Java 1.4.2 if headless
    } catch (Throwable e) {
      return null;
    }
  }

  /** Invokes a given call in the event dispatch thread but does not for completion.
   *  @param r The call to invoke
   *  @since NextMidas 3.9.0 to solve graphical timing issues (NXM-3213)
   */
  public static void invokeLater (Runnable r) {
    if (SwingUtilities.isEventDispatchThread()) {
      r.run();
    }
    else {
      SwingUtilities.invokeLater(r);
    }
  }

  /** Invokes a given call in the event dispatch thread and waits for completion.
   *  @param r The call to invoke
   *  @since NextMidas 3.9.0 to solve graphical timing issues (NXM-3213)
   */
  public static void invokeAndWait (Runnable r) {
    // If it is the swing thread, do not wait
    if (SwingUtilities.isEventDispatchThread()) {
      r.run();
    }
    else {
      // If it is not the swing thread, use the invoke and wait
      try {
        SwingUtilities.invokeAndWait(r);
      }
      catch (InterruptedException|InvocationTargetException e) {
        Shell.printStackTrace("Interrupted",e);
      }
    }
  }

  /** Emits an audio beep. Unlike {@link Toolkit#beep()} this falls back to using a system beep
      (via the ASCII bell character) on headless systems.
      @see nxm.sys.lib.Shell#beep()
      @since NeXtMidas 2.5.5
   */
  public static void beep () {
    Shell.beep(); // Since 3.9.2 - moved logic to shell to provide more visibility. If this is headless, we may not use the graphics module. for Java 11 modularity
  }

  /** Copies text to the system clipboard.
      @param str The text to copy to the clipboard.
      @return true if successful, false otherwise (i.e. clipboard is not available).
      @see #toSystemSelection(String)
   */
  public static boolean toSystemClipboard (String str) {
    return toClipboard(str, getDefaultToolkit().getSystemClipboard());
  }

  /** Copies text to the system "selection". This is the auxiliary copy-paste buffer common to
      many Linux installations that allow copy/paste via the mouse. This method will just return
      false on those systems that do not have a "selection" clipboard.
      @param str The text to copy to the clipboard.
      @return true if successful, false otherwise (i.e. clipboard is not available).
      @see #toSystemClipboard(String)
   */
  public static boolean toSystemSelection (String str) {
    return toClipboard(str, getDefaultToolkit().getSystemSelection());
  }

  /** Pastes text from the system clipboard.
      @return The contents of the clipboard as a string or null if unavailable.
      @see #fromSystemSelection()
   */
  public static String fromSystemClipboard () {
    return fromClipboard(getDefaultToolkit().getSystemClipboard());
  }

  /** Pastes text from the system "selection". This is the auxiliary copy-paste buffer common to
      many Linux installations that allow copy/paste via the mouse. This method will just return
      null on those systems that do not have a "selection" clipboard.
      @return The contents of the clipboard as a string or null if not available.
      @see #fromSystemClipboard()
   */
  public static String fromSystemSelection () {
    return fromClipboard(getDefaultToolkit().getSystemSelection());
  }

  /** Coppies text to a given clipboard. */
  private static boolean toClipboard (String str, Clipboard clipboard) {
    boolean status = false;

    try {
      if (clipboard != null) {
        clipboard.setContents(new StringSelection(str), CLIPBOARD_OWNER);
        status = true;
      }
    }
    catch (IllegalStateException e) { } // Do Nothing - Clipboard is not available
    return status;
  }

  /** Pastes text from a given clipboard. */
  private static String fromClipboard (Clipboard clipboard) {
    String str = null;

    try {
      if (clipboard != null) {
        Transferable xfer = clipboard.getContents(null);
        if ((xfer != null) && xfer.isDataFlavorSupported(DataFlavor.stringFlavor)) {
          str = (String)xfer.getTransferData(DataFlavor.stringFlavor);
        }
      }
    }
    catch (UnsupportedFlavorException e) { } // Do Nothing - Flavor not supported
    catch (IOException e) { }                // Do Nothing - Error converting to String
    catch (IllegalStateException e) { }      // Do Nothing - Clipboard is not available
    return str;
  }

  /** This will create a thread-safe interface of a given class. This is identical to
   *  {@link #getThreadSafeInstance(Object,Class[])}, except that only one interface is used to specify the public API of
   *  the class. Please see {@link #getThreadSafeInstance(Object,Class[])} for a full explanation of this method.
   *  @param obj The object to wrap.
   *  @param api The interface defining the public methods in the class.
   *  @return The instance created for the given object
   *  @see #getThreadSafeInstance(Object,Class[])
   */
  public static final Object getThreadSafeInstance (Object obj, Class<?> api) {
    Class<?>[] asArray = new Class<?>[] { api };
    return getThreadSafeInstance(obj, asArray) ;
  }

  /** This will create a thread-safe interface of a given class. This creates a {@link Proxy} object that implements the
      given API interfaces. The methods called on the returned class will be run in the AWT/Swing event thread (i.e. they
      are thread-safe with respect to the AWT/Swing event dispatch thread). <br>
     <br>
      A proxy is like a fake (non-existent) class that magically implements the interface, but uses reflection (via the
      invoke method in the handler) to call any methods. This allows run-time alteration of the method calls as they are
      made. <br>
      <br>
      Typical usage:

     <pre>
      public class Foo implements FooIF {
        ...

        public FooIF getThreadSafeInstance () {
          return (FooIF)GraphicsUtil.getThreadSafeInstance(this, FooIF.class);
        }
      }
      </pre>

      @param obj The object to wrap.
      @param api The interfaces defining the public methods in the class.
      @return The instance created for the given object
      @see #getThreadSafeInstance(Object,Class)
   */
  public static final Object getThreadSafeInstance (Object obj, Class<?>[] api) {
    AwtInvocationProxy handler = new AwtInvocationProxy(obj);
    return Proxy.newProxyInstance(obj.getClass().getClassLoader(), api, handler);
  }

  /** Call a given method on the Swing/AWT event thread. This will execute the method as follows:
      <pre>
        (1) If already in the Swing/AWT thread, execute the method immediately.
        (2) If the method is a void method, call it later on the Swing/AWT
            thread using {@link EventQueue#invokeLater} (do not
            wait for it to finish before returning),
        (3) Otherwise, call the method on the Swing/AWT thread using
            {@link EventQueue#invokeAndWait} and wait for it to finish.
            When it is done, return its value.
      </pre>
      <b>Implementation note:</b> The <code>isEventDispatchThread()</code>, <code>invokeAndWait(..)</code>,
      and <code>invokeLater(..)</code> methods in {@link SwingUtilities} just call the similarly-named
      methods in {@link EventQueue}. To avoid the extra method call, this method directly calls the
      methods in {@link EventQueue}.
      </pre>
      @param obj    The object to call the method on.
      @param method The method to call in the AWT thread.
      @param args   The arguments to use.
      @return The results of calling the given method (possibly null) or null if it was a void
              method.
   */
  private static final Object invoke (Object obj, Method method, Object[] args) {
    Object value = null;

    try {
      if (EventQueue.isDispatchThread()) {
        value = method.invoke(obj,args);

      } else {
        // JC4: The invokeLater(..) and invokeAndWait(..) methods wrap the Runnable (i.e. the
        //      SwingTask) inside an InvocationEvent object. I tried to skip this step by making
        //      SwingTask extend AWTEvent and implement ActiveEvent (same as InvocationEvent) rather
        //      than implement Runnable. However, they rely on a package-private method in Toolkit,
        //      that I can't access. Maybe future versions of Java will remove this restriction.
        boolean   isVoid = (method.getReturnType() == Void.TYPE);
        SwingTask task   = new SwingTask(obj,method,args,isVoid);

        if (isVoid) {
          EventQueue.invokeLater(task);
        } else {
          EventQueue.invokeAndWait(task);
          value = task.getValue();
        }
      }
    } catch (Throwable t) {
      throw toRuntimeException(t); // <-- Strips off the extra InvocationTargetException
    }
    return value;
  }

  /** Converts an arbitrary exception into a RuntimeException, used only by invoke(..). */
  private static final RuntimeException toRuntimeException (Throwable throwable) {
    if (throwable instanceof InvocationTargetException) {
      // JC4: This occurs ~99% of the time.
      throwable = ((InvocationTargetException)throwable).getTargetException();
    }

    RuntimeException exception = null;
    if (throwable instanceof RuntimeException) {
      exception = (RuntimeException)throwable;

    } else {
      String msg = throwable.getClass().getName() + ": "+ throwable.getMessage();
      exception = new RuntimeException(msg,throwable);
      exception.setStackTrace(new StackTraceElement[0]); // <-- Go directly to nested stack trace.
    }
    return exception;
  }

  /** Create an image out of a given DataFile<br><br>
      NOTE: This is only suitable for making JPGs,GIFs,and PNGs.
      @param df      The DataFile
      @param colors  The colormap to use
      @param format  The format format for the output File
      @param file    The ImageFile to be saved
      @param cthru   Whether or not to make the background color (black) transparent
                     NOTE: As of 3.1.0, colors were fixed and this no longer adjusts the lowest color to transparent.
                           Use a custom colormap to achieve that effect.
   */
  public static void createImageFile (DataFile df, Color[] colors, String format, ImageFile file, boolean cthru) {
    createImageFile(df, colors,-Double.MAX_VALUE,Double.MAX_VALUE,format,file,-1,-1, -1,false,"MAG",false);
  }

  /** Create an image out of a given DataFile<br><br>
      NOTE: This is only suitable for making JPGs,GIFs,and PNGs.
      @param df      The DataFile
      @param colors  The colormap to use (see {@link MColor#getColorMap(int, int)}
      @param minVal  Min allowable value in the data (prevents skewed picture from out-riders)
      @param maxVal  Max allowable value in the data (prevents skewed picture from out-riders)
      @param format  The format format for the output File
      @param file    The ImageFile to be saved
      @param cthru   Whether or not to make the background color (black) transparent
                     NOTE: As of 3.1.0, colors were fixed and cthru no longer adjusts the lowest color to transparent.
                           Use a custom colormap to achieve that effect.
      @param cnt     Percent Contrast adjustment on the image (0 to 100, -1 or 50f == no change)
      @param brt     Percent brightness adjustment on the image (0 to 100, -1 or 50f == no change)
      @param alpha   Transparency for the whole image (0 (trans) to 255 (opaque), -1 == no change)
      @param grayscale Whether or not to make the image grayscale

   */
  public static void createImageFile (DataFile df, Color[] colors,
      double minVal, double maxVal, String format, ImageFile file, boolean cthru,
      int cnt, int brt, int alpha, boolean grayscale) {
    createImageFile(df, colors, minVal, maxVal, format, file, cnt, brt, alpha, grayscale, "MAG", false);
  }

  /** Create an image out of a given DataFile<br><br>
      NOTE: This is only suitable for making JPGs,GIFs,and PNGs.
      @param df      The DataFile
      @param colors  The colormap to use (see {@link MColor#getColorMap(int, int)}
      @param minVal  Min allowable value in the data (prevents skewed picture from out-riders)
      @param maxVal  Max allowable value in the data (prevents skewed picture from out-riders)
      @param format  The format format for the output File
      @param file    The ImageFile to be saved
      @param cnt     Percent Contrast adjustment on the image (0 to 100, -1 or 50f == no change)
      @param brt     Percent brightness adjustment on the image (0 to 100, -1 or 50f == no change)
      @param alpha   Transparency for the whole image (0 (trans) to 255 (opaque), -1 == no change)
      @param grayscale Whether or not to make the image grayscale
      @param cxMode  The mode to use when the datafile contains complex data (Must be in "MAG","PHASE","REAL","IMAG","10LOG","20LOG")
      @param terrain Whether or not the data is terrain-based (requires special handling so that Alt=0 is always val=0)
      @since NeXtMidas 3.1.0 Added support for terrain data handling and complex data
   */
  public static void createImageFile (DataFile df, Color[] colors,
    double minVal, double maxVal, String format, ImageFile file, int cnt,
    int brt, int alpha, boolean grayscale, String cxMode, boolean terrain ) {

    float  min       = Float.MAX_VALUE;  // min value in the data
    float  max       = -min;             // max value in the data
    double  scaled    = 0.0; // the value after it has been scaled to the colormap and ncolors
    int     ncolors   = colors.length - 1;
    String  defFormat = "png";

    df.open(); // DataFile must be open to get info from it
    boolean positiveXDelta = (df.getXDelta() > 0)? true: false;
    boolean positiveYDelta = (df.getYDelta() > 0)? true: false;
    int width              = df.getFrameSize();
    int height             = (int) df.getNumberOfRows();
    int imageType          = (format.equalsIgnoreCase("JPG")) ? // JDK11 dropped JPG Alpha support
                             BufferedImage.TYPE_INT_RGB : BufferedImage.TYPE_INT_ARGB;
    BufferedImage bi       = new BufferedImage(width, height, imageType);
    int[] rgb              = new int[height*width];  // buffer to hold the RGB values
    float[] buff           = new float[height*width]; // buffer to hold the data
    int index              = 0;  // current location in the buffer
    float adjVal           = 0;  // value to scale off of (usually the min value)
    float scaleFactor      = 1;  // multiplier to apply to the scale value (usually (ncolors/(max-min))
    Data  data             = null; // hold a row of the data from the file
    float value            = 0.0f;  // individual data value from the file

    if (cnt == -1) cnt = 50;  // 50 is what MColor uses for no change
    if (brt == -1) brt = 50;  // 50 is what MColor uses for no change

    float[] fbuf = new float[width]; // buffer to hold data values pulled from the row

    // load the buffer with the data in the file
    for (int h=0, rowNum=1; h<height; h++,rowNum++) {
      data = df.getData(h);  // get a row of the data into a Data object

      if (df.getFormatMode()=='C') convertComplexData(data, cxMode);  // complex data needs to be converted to scalar values
      Convert.bb2ja (data.buf,0,data.type, fbuf,0,Data.FLOAT, width); // put the data into a float array

      for (int w=0; w<width; w++) {
        value = fbuf[w];
        if      (value < minVal) fbuf[w] = (float)minVal;
        else if (value > maxVal) fbuf[w] = (float)maxVal;

        if      (value < min)    min=value;
        else if (value > max)    max=value;
      }
      // The if...else statements below arrange the data in the image buffer
      // so that the resulting image looks like Plot would have plotted it.
      if      (positiveXDelta && !positiveYDelta)  index = (rowNum-1) * width;                    // left-to-right, top-to-bottom
      else if (positiveXDelta && positiveYDelta)   index = (width*height) - (width*rowNum);       // left-to-right, bottom-to-top
      else if (!positiveXDelta && positiveYDelta)  index = (width*height) - ((rowNum-1) * width); // right-to-left, bottom-to-top
      else if (!positiveXDelta && !positiveYDelta) index = ((rowNum-1) * width) + width;          // right-to-left, top-to-bottom
      for (int r=0; r<fbuf.length; r++) {
        if (positiveXDelta) buff[index++] = fbuf[r];
        else                buff[--index] = fbuf[r];  // load backwards in the buffer
      }
    }

    scaled = 0;  // the value after it has been scaled to the colormap and ncolors

    // compute a scale factor for the data to be able to "bin" it into a color later
    if (terrain) {  // terrain needs special handling to make sure Alt=0 is always 0
      adjVal      = Math.min(min, -max);
      scaleFactor = ncolors/(-2*adjVal);
    } else {
      adjVal      = min;
      scaleFactor = ncolors/(max-min);
    }

    for (int i=0; i<buff.length; i++) {
      // the next three lines match Layer2d.loadRasterBuffer() logic
      scaled =( (buff[i]-adjVal)*scaleFactor );
      if (scaled<0) scaled=0;
      if (++scaled>ncolors) scaled = ncolors;

      buff[i] = colors[(int)scaled].getRGB();
      rgb[i] = MColor.adjustColor((int)buff[i], cnt, brt, alpha, grayscale);
    }

    // set the RGB in the BufferedImage to the data
    bi.setRGB(0, 0, width, height, rgb, 0, width);

    // write the image file
    try {
      file.open(BaseFile.OUTPUT);                   // open file for output
      OutputStream os = file.io.getOutputStream();  // get OutputStream for ImageIO use
      boolean boo = ImageIO.write(bi, format, os);
      if (!boo) {
        Shell.warning("GraphicsUtil: Unable to create appropriate image "+
            "writer from format given, using "+defFormat+".");
        ImageIO.write(bi, defFormat, os);
      }
      os.close();
      file.close();
    } catch (IOException e) {
      e.printStackTrace();
    }
    df.close();  // clean up
  }

  /** Do the conversions of complex data to scalar for plotting. Uses DataOp.java methods for conversion.
      @param data The Data object to be converted
      @param cxmode The mode for the data (Must be in "MAG","PHASE","REAL","IMAG","10LOG","20LOG", though others
                    may be supported in future versions.<br><br>
                    Note: 10LOG and 20LOG use the bounded logarithm formulas (see DataOp.java)
      @since NeXtMidas 3.1.0
   */
  private static void convertComplexData (Data data, String cxmode) {

    if      (cxmode==null)                      DataOp.mag(data, data,-1,DataOp.CS);  // default to MAG
    else if (cxmode.equalsIgnoreCase("PHASE"))  DataOp.phase(data, data,-1,DataOp.CS);
    else if (cxmode.equalsIgnoreCase("REAL"))   DataOp.real(data, data,-1,DataOp.CS);
    else if (cxmode.equalsIgnoreCase("IMAG"))   DataOp.imag(data, data,-1,DataOp.CS);
    else if (cxmode.equalsIgnoreCase("10LOG"))  DataOp.dblogb(data, data,-1,DataOp.CS);
    else if (cxmode.equalsIgnoreCase("20LOG"))  DataOp.dblogb2(data, data,-1,DataOp.CS);
    else                                        DataOp.mag(data, data,-1,DataOp.CS);  // default to MAG

  }

  /**  Rotate image by given degrees
       @param image Source image
       @param angle Angle to rotate (in radians)
       @return Rotated image
  */
  public static BufferedImage rotateImage(BufferedImage image, double angle) {
    double sin = Math.abs(Math.sin(angle));
    double cos = Math.abs(Math.cos(angle));
    int w      = image.getWidth();
    int h      = image.getHeight();
    int neww   = (int)Math.floor(w*cos+h*sin), newh = (int)Math.floor(h*cos+w*sin);
    GraphicsEnvironment ge = GraphicsEnvironment.getLocalGraphicsEnvironment();
    GraphicsDevice gd = ge.getDefaultScreenDevice();
    GraphicsConfiguration gc = gd.getDefaultConfiguration();
    BufferedImage result = gc.createCompatibleImage(neww, newh, Transparency.TRANSLUCENT);
    Graphics2D g = result.createGraphics();
    g.translate((neww-w)/2, (newh-h)/2);
    g.rotate(angle, w/2.0, h/2.0);
    g.drawRenderedImage(image, null);
    g.dispose();
    return result;
  }


  //////////////////////////////////////////////////////////////////////////////////////////////////
  // Nested Classes
  //////////////////////////////////////////////////////////////////////////////////////////////////
  /** Clipboard Manager. */
  private static class ClipboardManager implements ClipboardOwner {
    public void lostOwnership(Clipboard clipboard, Transferable contents) {
      // Do Nothing -- Just implements ClipboardOwner
    }
  }

  /** A simple implementation of InvocationHandler that calls GraphicsUtil.invoke(..); this is used
      by the getThreadSafeInstance(..) method.
   */
  private static final class AwtInvocationProxy implements InvocationHandler {
    private final Object  obj;    // The object to call methods on.

    /**
     * Creates a new instance wrapping the given object.
     * @param obj The object to be used
     */
    public AwtInvocationProxy(Object obj) {
      this.obj = obj;
    }

    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
      // JC4: Resist any temptation to call a method with the proxy, doing so creates
      //      an infinite loop. Always call the methods with the actual object.
      return GraphicsUtil.invoke(obj,method,args);
    }
  }

  /** Class used for calling methods on the Swing/AWT thread. */
  private static final class SwingTask implements Runnable {
    private final Object    obj;              // The object to call the method on
    private final Method    method;           // The method to call
    private final Object[]  args;             // Arguments to pass to the method
    private final boolean   showError;        // Immediately show any errors?
    private       Object    value     = null; // Value from calling the method
    private       Throwable throwable = null; // Any error/exception caused by the method call

    /** Creates a new instance.
        @param obj       The object to call the method on.
        @param method    The method to call.
        @param args      Any arguments to use when calling it.
        @param showError Immediately show any errors? (true for use with invokeLater(..),
                         false for use with invokeAndWait(..)).
     */
    public SwingTask(Object obj, Method method, Object[] args, boolean showError) {
      this.obj       = obj;
      this.method    = method;
      this.args      = args;
      this.showError = showError;
    }

    public void run() {
      try {
        value = method.invoke(obj,args);
      } catch (Throwable t) {
        if (showError) Shell.getSharedMidasContext().printStackTrace("Error running "+this, t);
        throwable = t;
      }
    }

    /** Gets the value of the call. This should only be called after SwingUtilities.invokeAndWait(..) has returned.
     *  @return The value of the call
     *  @throws Throwable Any exception that was raised when running the specified method.
     */
    public Object getValue() throws Throwable {
      if (throwable != null) throw throwable; // Throw the exception here so the caller gets it.
      return value;
    }

    @Override
    public String toString() {
      String argStr = (args == null)? "": StringUtil.join(args);
      String toCall = obj.getClass().getName() + "." + method.getName() + "(" + argStr + ")";
      return "GraphicsUtil.SwingTask: calling "+toCall+" in Swing/AWT event dispatch thread";
    }
  }

  /** Get the number of screens in the GraphicsEnvironment. Returns 0 if headless.
      @param virtuals true if you want to include virtual screens (e.g. those with xinerama on)
      @return the number of screens
      @since NeXtMidas 2.8.0
      @since NeXtMidas 3.9.2 moved here for jdk modularity.
   */
  public static int getNumScreens (boolean virtuals) {
    int numScreens = 0;
    if (!Theme.isHeadless()) {
      try {
        GraphicsDevice[] screens = GraphicsEnvironment.getLocalGraphicsEnvironment().getScreenDevices();
        if (virtuals) {
          numScreens = screens.length; // count all screens (including virtuals)
        }
        else {
          Rectangle virtualBounds;
          for (GraphicsDevice gd : screens) {
            GraphicsConfiguration[] gconfs = gd.getConfigurations();
            int maxx=0, maxy=0;
            for (GraphicsConfiguration gconf : gconfs) {
              virtualBounds = gconf.getBounds();
              if (virtualBounds.x != 0) maxx = virtualBounds.x;
              if (virtualBounds.y != 0) maxy = virtualBounds.y;
            }
            if (maxx == 0 && maxy == 0) numScreens++; // only count non-virtual screens
          }
        }
      } catch (Exception e) {
        Shell.warning("Unable to determine the number of screens"); // no stack trace required
      }
    }
    return numScreens;
  }

  static BufferedImage toBufferedImage(Image img)
  {
      if (img instanceof BufferedImage)
      {
          return (BufferedImage) img;
      }

      // Create a buffered image with transparency
      BufferedImage bimage = new BufferedImage(img.getWidth(null), img.getHeight(null), BufferedImage.TYPE_INT_ARGB);

      // Draw the image on to the buffered image
      Graphics2D bGr = bimage.createGraphics();
      bGr.drawImage(img, 0, 0, null);
      bGr.dispose();

      // Return the buffered image
      return bimage;
  }

  /**
   * Convenience method added for issues found with webswing.
   * This will resize the screen if it goes off screen.
   * @param frame This is the top level frame to be sized.
   * @since NeXtMidas 3.9.2
   */
  public static void fitFrameToScreen(JFrame frame) {
    Dimension window = frame.getToolkit().getScreenSize();
    int x = frame.getSize().width > window.width ? window.width : frame.getSize().width;
    int y = frame.getSize().width > window.height ? window.height : frame.getSize().height;
    frame.setPreferredSize(new Dimension(x,y));
    frame.pack();
  }

  /** Is ctrl and Left Mouse Button clicked
   *
   * @param msg The message that holds the mouse event
   * @return true if ctrl lmb is clicked
   */
  static boolean isCtrlLMB (Message msg) {
    Table quals = null;
    if (msg.quals instanceof Table) quals = (Table) msg.quals;
    if (quals != null) {
      Object temp = quals.getO("ORIG_MSG_DATA");
      if (temp != null) {
        if (temp instanceof MouseEvent) {
          MouseEvent me = (MouseEvent) temp;
          int onmask = MouseEvent.CTRL_DOWN_MASK;
          if ((me.getModifiersEx() & (onmask)) == onmask && me.getButton()==MouseEvent.BUTTON1) {
            return true;
          }
        }
      }
    }
    return false;
  }

  /** Is ctrl held down
  *
  * @param msg The message that holds the mouse event
  * @return true if ctrl was held down
  */
  @InternalUseOnly
 public static boolean isCtrl (Message msg) {
   Table quals = null;
   if (msg.quals instanceof Table) quals = (Table) msg.quals;
   if (quals != null) {
     Object temp = quals.getO("ORIG_MSG_DATA");
     if (temp != null) {
       if (temp instanceof MouseEvent) {
         MouseEvent me = (MouseEvent) temp;
         int onmask = MouseEvent.CTRL_DOWN_MASK;
         if ((me.getModifiersEx() & (onmask)) == onmask) {
           return true;
         }
       }
     }
   }
   return false;
 }
}
