// ============================================================================
// File:               Utility.java
//
// Project:            General.
//
// Purpose:            Different useful functions.
//
// Author:             Rammi
//-----------------------------------------------------------------------------
// Copyright Notice:   (c) 2002  Rammi (rammi@caff.de)
//                     This code is in the public domain.
//                     Use at own risk.
//                     No guarantees given.
//
// Latest change:      $Date$
//
// History:	       $Log$
//=============================================================================

package de.caff.util;

import javax.swing.*;
import java.applet.Applet;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.awt.image.ImageProducer;
import java.io.BufferedReader;
import java.io.File;
import java.io.InputStreamReader;
import java.net.URL;
import java.util.ResourceBundle;

/**
 *  Utility contains some helpful functionality.
 *  @author Rammi
 *  @version $Revision$
 */  
public class Utility
{
  /** Physiolocigal brightness value for red. */
  public static final float PHYS_RED_SCALE   = 0.30f;
  /** Physiolocigal brightness value for green. */
  public static final float PHYS_GREEN_SCALE = 0.53f;
  /** Physiolocigal brightness value for blue. */
  public static final float PHYS_BLUE_SCALE  = 1.0f-PHYS_RED_SCALE-PHYS_GREEN_SCALE;
  /** Component used for access to a prepared image. */
  private static Component preparer = new Canvas();
  /** Are we on Windows? */
  private static boolean   weAreOnDOS = (File.separatorChar == '\\'); // (too?) simple
  /** The default resource directory.. */
  private static String    resourceDir   = "resources/";
  /** Applet if we are running as an applet? */
  private static Applet	   applet = null;
  /** The event queue exception wrapper if one is used. */
  private static EventQueueExceptionWrapper exceptionWrapper = null;


  /** Debugging mode. */
  private static boolean   debugging = false;

  /**
   *  Set the debugging mode.
   *  @param mode new mode
   */
  public static void setDebug(boolean mode) {
    debugging = mode;
  }

  /**
   *  Get the debug mode.
   *  @return debug mode
   */
  public static boolean isDebug() {
    return debugging;
  }

  /**
   *  Load an image and prepare a representation. Used for static images to be loaded
   *  in an very early stage of program execution.
   *  @param   path   path of the image file
   *  @return  the loaded image
   */
  public static Image loadImage(String path) {
    return loadImage(path, preparer);
  }

  /**
   *  Load an image and prepare a representation. Used for static images to be loaded
   *  in an very early stage of program execution.
   *  @param   path      path of the image file
   *  @param   renderer  renderer component for image creation
   *  @return  the loaded image
   */
  public static Image loadImage(String path, Component renderer) {
    if (resourceDir != null  &&  !path.startsWith("/")) {
      path = resourceDir + /*File.separator +*/ path;
    }
    return (new Utility()).loadAnImage(path, renderer);
  }

  /**
   *  Loads an image from a jar file. Be careful to always use /
   *  for dirs packed in jar!
   *  @param   path   path of file (e.g.. images/icon.gif)
   *  @param   renderer  component used for image rendering
   *  @return  the image
   */
  private Image loadAnImage(String path, Component renderer) {
    Image img = null;
    try {
      URL url = getClass().getResource(path);
      if (url == null) {
	// workaround for netscape problem
	if (applet != null) {
	  url = new URL(applet.getDocumentBase(), "de/caff/gimmicks/"+path);
	}
	else {
	  return null;
	}
      }

      if (applet != null) {
	img = applet.getImage(url);
      }
      else {
	img = Toolkit.getDefaultToolkit().createImage( (ImageProducer) url.getContent() );
      }
    } catch (Exception x) {
      debug(x);
    }
          
    if (img != null) {
      /* --- load it NOW --- */
      renderer.prepareImage(img, null);
    }

    return img;
  } 


  /**
   *  Load a text file into a string. 
   *  @param   path   name of the text file
   *  @return  the loaded text
   */
  public static String loadText(String path) {
    if (resourceDir != null) {
      path = resourceDir + /*File.separator +*/ path;
    }
    return (new Utility()).loadAText(path);
  }

  /**
   *  Loads a text file from a jar file. Be careful to always use /
   *  for dirs packed in jar!
   *  @param   path   path of file (e.g.. images/foo.txt)
   *  @return  the text
   */
  private String loadAText(String path) {
    String txt = "";
    try {
      String line;
      //      System.out.println("Loading "+path);
      
      //      System.out.println("URL = "+url);
      BufferedReader reader = new BufferedReader(new InputStreamReader(getClass().getResourceAsStream(path)));
      while ((line = reader.readLine()) != null) {
	txt += line +"\n";
      }
      reader.close();
    } catch (Exception x) {
      debug(x);
    }
          
    return txt;
  } 


  /**
   *  Test wether our System is a DOS.
   *  @return   true   we are on DOS
   */
  public static boolean areWeOnDOS() {
    return weAreOnDOS;
  }


  /**
   *  Set the resource directory.
   *  @param   dir   the image drirectory
   */
  public static void setResourceDir(String dir) {
    resourceDir = dir;
  }


  /**
   *  Compile a formatted string with maximum 10 args.
   *  <pre>
   *  Special signs:
   *     %#  where hash is a digit from 0 to 9 means insert arg #
   *     &#64;#  where hash is a digit from 0 to 9 means insert localized arg #
   *     %%  means %
   *     &#64;&@64;  means &#64; 
   *  </pre>
   *  @param   tag    resource tag for format string
   *  @param   args   arguments for insertion
   *  @param   res    active resource bundle
   *  @return  String with inserted args.
   */
  public static String compileString(String tag, Object[] args, ResourceBundle res) {
    String       format  = res.getString(tag);
    StringBuffer ret     = new StringBuffer(format.length());
    int          i;
    char         c;

    for (i = 0;   i < format.length();   i++) {
      c = format.charAt(i);
      
      if (c == '%'   ||   c == '@') {
	int argNum = -1;

        if (i < format.length()-1) {
	  // this implies that there are never more than 10 args
	  switch (format.charAt(i+1)) {
	  case '%':  
	    if (c == '%') { // "%%" means "%"
	      ret.append('%');
	      i++;
	    }
	    break;

	  case '@':
	    if (c == '@') { // "@@" means "@"
	      ret.append("@");
	      i++;
	    }
	    break;

	  case '0':
	    argNum = 0;
	    break;

	  case '1':
	    argNum = 1;
	    break;

	  case '2':
	    argNum = 2;
	    break;

	  case '3':
	    argNum = 3;
	    break;
	    
	  case '4':
	    argNum = 4;
	    break;

	  case '5':
	    argNum = 5;
	    break;

	  case '6':
	    argNum = 6;
	    break;

	  case '7':
	    argNum = 7;
	    break;
	    
	  case '8':
	    argNum = 8;
	    break;

	  case '9':
	    argNum = 9;
	    break;

	  default:
	    break;
	  }
	}
	if (argNum >= 0   &&   argNum < args.length) {
	  if (c == '%') {
	    // arg is a non-localized string
	    ret.append(args[argNum]);
	  }
	  else { // c == '@'
	    // arg is a tag for localization
	    ret.append(res.getString(args[argNum].toString()));
	  }
	  i++;
	}
      }
      else {
	ret.append(c);
      }
    }

    return new String(ret);
  }


  /**
   *  Method to get the frame parent of any component.
   *  @param   comp   the component to search the frame for
   *  @return  the frame parent of the component
   */
  public static Frame getFrame(Component comp) {
    for (   ;  comp != null;   comp = comp.getParent()) {
      if (comp instanceof Frame) {
	return (Frame)comp;
      }
    }
    /* --- Not found. Ugly workaround: --- */
    return new Frame();
  }


  /**
   *  Compare two byte arrays.
   *  Compare <code>len</code> bytes from array 1 starting with offset 1 
   *  with <code>len</code> bytes from array 2 starting with offset 2.
   *  Will return always <code>true</code> for <code>len &le;= 0</code>
   *  @param arr1    array 1
   *  @param off1    offset 1
   *  @param arr2    array 2
   *  @param off2    offset 2
   *  @param len     length to compare
   *  @return <code>true</code> if both chunks are equal<br>
   *          <code>false</code> otherwise
   */
  public static boolean equalBytes(byte[] arr1, int off1, byte[] arr2, int off2, int len) {
    while (len-- > 0) {
      //      System.out.println(arr1[off1] + " == "+arr2[off2]);
      if (arr1[off1++] != arr2[off2++]) {
	//	System.out.println();
	return false;		// not equal
      }
    }
    return true;		// equal
  }


  /**
   *  Set the applet we are running in (if any).
   *  @param applet   applet we are running in (if <code>null</code> then we
   *                  are running in an application
   */
  public static void setApplet(Applet applet) {
    Utility.applet = applet;
  }

  /**
   *  Get the applet we are running in (if any).
   *  @return applet or <code>null</code>
   */
  public static Applet getApplet() {
    return applet;
  }

  /**
   *  Are we running an applet?
   *  @return the answer
   */
  public static boolean areWeInAnApplet() {
    return applet != null;
  }

  /**
   *  Look for a boolean applet parameter or application property.
   *  @param  key  parameter key
   *  @param  def  default value
   *  @return the parameter value (if set) or the default
   */
  public static boolean getBooleanParameter(String key, boolean def) {
    String value = getStringParameter(key, null);
    if (value != null) {
      return "true".equals(value.toLowerCase());
    }
    else {
      return def;
    }
  }

  /**
   *  Look for a String applet parameter or application property.
   *  @param  key  parameter key
   *  @param  def  default value
   *  @return the parameter value (if set) or the default
   */
  public static String getStringParameter(String key, String def) {
    try {
      String value = areWeInAnApplet()  ?
	applet.getParameter(key)  :
	System.getProperty(key);

      if (value != null) {
	return value;
      }
    } catch (Exception x) {
      // do nothing
      debug(x);
    }

    // === return default ===
    return def;
  }

  /**
   *  Look for a color applet parameter or application property.
   *  @param  key  parameter key
   *  @param  def  default value
   *  @return the parameter value (if set) or the default
   */
  public static Color getColorParameter(String key, Color def) {
    String value = getStringParameter(key, null);
    if (value != null) {
      // try to decode color
      try {
	return Color.decode(value);
      } catch (Exception x) {
	// nothing
	debug(x);
      }
    }

    return def;
  }

  /**
   *  Look for a integer applet parameter or application property.
   *  @param  key  parameter key
   *  @param  def  default value
   *  @return the parameter value (if set) or the default
   */
  public static int getIntParameter(String key, int def) {
    return getIntParameter(key, def, 10);
  }


  /**
   *  Look for an integer applet parameter or application property.
   *  @param  key  parameter key
   *  @param  def  default value
   *  @param  base number base
   *  @return the parameter value (if set) or the default
   */
  public static int getIntParameter(String key, int def, int base) {
    String value = getStringParameter(key, null);
    if (value != null) {
      try {
	return Integer.parseInt(value, base);
      } catch (NumberFormatException x) {
	// nothing
	debug(x);
      }
    }

    return def;
  }

  /**
   *  Look for a double applet parameter or application property.
   *  @param  key  parameter key
   *  @param  def  default value
   *  @return the parameter value (if set) or the default
   */
  public static double getDoubleParameter(String key, double def) {
    String value = getStringParameter(key, null);
    if (value != null) {
      try {
	return Double.valueOf(value).doubleValue();
      } catch (NumberFormatException x) {
	// nothing
	debug(x);
      }
    }

    return def;
  }

  /**
   *  Look for a float applet parameter or application property.
   *  @param  key  parameter key
   *  @param  def  default value
   *  @return the parameter value (if set) or the default
   */
  public static float getFloatParameter(String key, float def) {
    String value = getStringParameter(key, null);
    if (value != null) {
      try {
	return Float.valueOf(value).floatValue();
      } catch (NumberFormatException x) {
	// nothing
	debug(x);
      }
    }

    return def;
  }

  /**
   *  Print message if debug mode is on.
   *  @param  x  object which's toString is called
   */
  public static void debug(Object x) {
    if (debugging) {
      System.out.println(x == null  ?  "<null>"  :  x.toString());
    }
  }

  /**
   *  Print the stack trace if debug mode is on.
   *  @param  x  exception
   */
  public static void debug(Throwable x) {
    if (debugging) {
      x.printStackTrace();
    }
  }

  /**
   *  Print a given property to the console. Catch possible Security exceptions.
   *  Does nothing if not in debug mode.
   *  @param prop poperty name
   */
  public static void printProperty(String prop) {
    try {
      debug(prop+"="+System.getProperty(prop));
    } catch (Throwable x) {
      // empty
    }
  }

  /**
   *  In debug mode: print properties to console.
   */
  public static void printProperties() {
    if (Utility.isDebug()) {
      String[] useful = new String[] {
	"java.version",
	"java.vendor",
	"os.name",
	"os.arch",
	"os.version"
      };

      for (int u = 0;  u < useful.length;  ++u) {
	printProperty(useful[u]);
      }
    }
  }

  /**
   *  An equal function which accepts globbing.
   *  This method accepts the glob chars <tt>'*'</tt>
   *  (for any number of chars) and <tt>'?'</tt> (any single char).
   *  @param  mask   glob mask (containing special chars)
   *  @param  str    string to be checked against mask
   *  @return wether the string matches the mask
   */
  public static boolean globEquals(String mask, String str)
  {
    int maskLen = mask.length();
    int strLen  = str.length();
    if (maskLen > 0) {
      char first = mask.charAt(0);
      switch (first) {
      case '*':
	return 
	  globEquals(mask.substring(1), str) ||
	  (strLen > 0  &&  globEquals(mask, str.substring(1)));

      case '?':
	return strLen > 0  &&
	  globEquals(mask.substring(1), str.substring(1));

      default:
	return strLen > 0   &&
	  first == str.charAt(0)   &&
	  globEquals(mask.substring(1), str.substring(1));
      }
    }
    else {
      return maskLen == strLen;
    }
  }

  /**
   *  Add an exception listener which is called when an exception occurs during the
   *  dispatch of an AWT event.
   *  @param listener listener to add
   */
  public static void addEventQueueExceptionListener(EventQueueExceptionListener listener)
  {
    if (exceptionWrapper == null) {
      exceptionWrapper = new EventQueueExceptionWrapper();
    }
    exceptionWrapper.addEventQueueExceptionListener(listener);
  }

  /**
   *  Remove an exception listener which was called when an exception occurs during the
   *  dispatch of an AWT event.
   *  @param listener listener to remove
   */
  public static void removeEventQueueExceptionListener(EventQueueExceptionListener listener)
  {
    if (exceptionWrapper != null) {
      exceptionWrapper.removeEventQueueExceptionListener(listener);
    }
  }

  /**
   *  Try to create a custom cursor from an icon. The hot spot is set to the icon center.
   *  @param icon      icon from which to create the cursor
   *  @param bgColor   background color for cursor
   *  @param name      name for accessibility
   *  @param fallback  fallback cursor taken if the image size is not supported
   *  @return the new cursor if it was possible to create one with the required size,
   *          or the fallback cursor
   */
  public static Cursor createCustomCursor(Icon icon, Color bgColor, String name, Cursor fallback)
  {
    return createCustomCursor(icon, bgColor, null, name, fallback);
  }

  /**
   *  Try to create a custom cursor from an icon.
   *  @param icon      icon from which to create the cursor
   *  @param bgColor   background color for cursor
   *  @param hotspot   hot spot of cursor (if <code>null</code> the center of the icon is taken)
   *  @param name      name for accessibility
   *  @param fallback  fallback cursor taken if the image size is not supported
   *  @return the new cursor if it was possible to create one with the required size,
   *          or the fallback cursor
   */
  public static Cursor createCustomCursor(Icon icon, Color bgColor, Point hotspot, String name, Cursor fallback)
  {
    Toolkit toolkit = Toolkit.getDefaultToolkit();
    Dimension size = toolkit.getBestCursorSize(icon.getIconWidth(), icon.getIconHeight());
    if (size.width >= icon.getIconWidth()  &&   size.height >= icon.getIconHeight()) {
      int x = (size.width -icon.getIconWidth())/2;
      int y = (size.height-icon.getIconHeight())/2;
      BufferedImage image = new BufferedImage(size.width, size.height, BufferedImage.TYPE_INT_ARGB);
      preparer.setBackground(bgColor);
      icon.paintIcon(preparer, image.getGraphics(), x, y);
      if (hotspot == null) {
        hotspot = new Point(icon.getIconWidth()/2, icon.getIconHeight()/2);
      }
      Cursor cursor = toolkit.createCustomCursor(image, new Point(hotspot.x+x, hotspot.y+y), name);
      if (cursor != null) {
        return cursor;
      }
    }
    return fallback;
  }

  /**
   * Get the brightness of a color.
   * Humans do perceive the different color components differently in respect of their brightness,
   * which this method tries to take care off.
   * @param color color which brightness is needed
   * @return brightness value between <code>0</code> (black) and <code>1.0</code> (white)
   */
  public static float getPhysiologicalBrightness(Color color)
  {
    return (0.30f*color.getRed()/255.0f +
            0.53f*color.getGreen()/255.0f +
            0.17f*color.getBlue()/255.0f);
  }

  /**
   * Get a gray with the same brightness as a given color.
   * Humans do perceive the different color components differently in respect of their brightness,
   * which this method tries to take care off.
   * @param color color to convert to gray
   * @return gray color with the same brightness as the given color
   */
  public static Color getPhysiologicalGray(Color color)
  {
    float brightness = getPhysiologicalBrightness(color);
    return new Color(brightness,
                     brightness,
                     brightness);
  }

  /**
   *  Get the distance between two colors by comparing their brightness.
   *  @param color1 first color
   *  @param color2 second color
   *  @return the distance of the colors as the absolute difference of their colors,
   *          a number between <code>0</code> (same brightness) and <code>1</code>
   *          (completely different brightness)
   */
  public static float getColorBrightnessDistance(Color color1, Color color2)
  {
    return Math.abs(getPhysiologicalBrightness(color1) - getPhysiologicalBrightness(color2));
  }
}

