/*
 * Open Source Physics software is free software as described near the bottom of this code file.
 *
 * For additional information and documentation on Open Source Physics please see:
 * <https://www.compadre.org/osp/>
 */

package org.opensourcephysics.controls;
import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Rectangle;
import java.awt.RenderingHints;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.io.OutputStream;
import java.io.PrintStream;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.util.Enumeration;
import java.util.logging.FileHandler;
import java.util.logging.Handler;
import java.util.logging.Level;
import java.util.logging.LogRecord;
import java.util.logging.Logger;
import java.util.logging.SimpleFormatter;
import java.util.logging.XMLFormatter;

import javax.swing.AbstractAction;
import javax.swing.AbstractButton;
import javax.swing.Action;
import javax.swing.ButtonGroup;
import javax.swing.JCheckBoxMenuItem;
import javax.swing.JFileChooser;
import javax.swing.JFrame;
import javax.swing.JMenu;
import javax.swing.JMenuBar;
import javax.swing.JMenuItem;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
import javax.swing.JPopupMenu;
import javax.swing.JRadioButtonMenuItem;
import javax.swing.JScrollPane;
import javax.swing.JTextPane;
import javax.swing.WindowConstants;
import javax.swing.text.BadLocationException;
import javax.swing.text.Document;
import javax.swing.text.Style;
import javax.swing.text.StyleConstants;
import javax.swing.text.StyleContext;

import org.opensourcephysics.display.OSPRuntime;
import org.opensourcephysics.tools.FontSizer;

/**
 * This is a viewable file-based message log for a java package.
 * It displays log records in a text pane and saves them in a temp file.
 *
 * @author Douglas Brown
 * @author Wolfgang Christian
 * @version 1.0
 */
public class OSPLog extends JFrame {
  /**
   * A logger that can be used by any OSP program to log and show messages.
   */
  private static OSPLog OSPLOG;
  private static JFileChooser chooser;
  protected static Style black, red, blue, green, magenta, gray;
  protected static final Color DARK_GREEN = new Color(0, 128, 0), DARK_BLUE = new Color(0, 0, 128), DARK_RED = new Color(128, 0, 0);
  public static final Level[] levels = new Level[] {Level.OFF, Level.SEVERE, Level.WARNING, Level.INFO, ConsoleLevel.ERR_CONSOLE, ConsoleLevel.OUT_CONSOLE, Level.CONFIG, Level.FINE, Level.FINER, Level.FINEST, Level.ALL};
  private static Level defaultLevel = ConsoleLevel.OUT_CONSOLE;
	public static final int OUT_OF_MEMORY_ERROR=1;
	protected static boolean logConsole = true;
	
  // instance fields
  private Logger logger;
  private Handler fileHandler;
  private OSPLogHandler logHandler;
  private JTextPane textPane;
  private String logFileName;
  private String tempFileName;
  private JPanel logPanel;
  private JPopupMenu popup;
  private ButtonGroup popupGroup, menubarGroup;
  private String pkgName;
  private String bundleName;
  private JMenuItem logToFileItem;
  private boolean hasPermission = true;
  private static LogRecord[] messageStorage = new LogRecord[128];
  private static int messageIndex = 0;
  static String eol = "\n";   //$NON-NLS-1$
  static String logdir = "."; //$NON-NLS-1$
  static String slash = "/";  //$NON-NLS-1$

  static {
    try {                                                // system properties may not be readable in some environments
      eol = System.getProperty("line.separator", eol);   //$NON-NLS-1$
      logdir = System.getProperty("user.dir", logdir);  //$NON-NLS-1$
      slash = System.getProperty("file.separator", "/"); //$NON-NLS-1$//$NON-NLS-2$
    } catch(Exception ex) {
      /** empty block */
    }
  }

  /**
   * Gets the OSPLog that can be shared by multiple OSP packages.
   *
   * @return the shared OSPLog
   */
  public static OSPLog getOSPLog() {
    if(OSPLOG==null) {
      if(!OSPRuntime.appletMode&&(OSPRuntime.applet==null)) { // cannot redirect applet streams
        OSPLOG = new OSPLog("org.opensourcephysics", null);   //$NON-NLS-1$
        try {
          System.setOut(new PrintStream(new LoggerOutputStream(ConsoleLevel.OUT_CONSOLE, System.out)));
          System.setErr(new PrintStream(new LoggerOutputStream(ConsoleLevel.ERR_CONSOLE, System.err)));
        } catch(SecurityException ex) {
          /** empty block */
        }
        setLevel(defaultLevel);
      }
    }
    return OSPLOG;
  }

  /**
   * Sets the directory where the log file will be saved if logging is enabled.
   * @param dir String
   */
  public void setLogDir(String dir) {
    logdir = dir;
  }

  /**
   * Gets the directory where the log file will be saved if logging is enabled.
   * @param dir String
   */
  public String getLogDir() {
    return logdir;
  }

  /**
   * Determines if the shared log is visible.
   *
   * @return true if visible
   */
  public static boolean isLogVisible() {
    if((OSPRuntime.appletMode||(OSPRuntime.applet!=null))&&(MessageFrame.APPLET_MESSAGEFRAME!=null)) {
      return MessageFrame.APPLET_MESSAGEFRAME.isVisible();
    } else if(OSPLOG!=null) {
      return OSPLOG.isVisible();
    }
    return false;
  }

  /**
   * Sets the visibility of this log.
   *
   * @param true to set visible
   */
  public void setVisible(boolean visible) {
    if(OSPRuntime.appletMode||(OSPRuntime.applet!=null)) {
      org.opensourcephysics.controls.MessageFrame.showLog(visible);
    } else {
      super.setVisible(visible);
    }
  }

  /**
   * Determines if the log is visible.
   *
   * @return true if visible
   */
  public boolean isVisible() {
    if(OSPRuntime.appletMode||(OSPRuntime.applet!=null)) {
      return org.opensourcephysics.controls.MessageFrame.isLogVisible();
    }
    return super.isVisible();
  }

  /**
   * Shows the log when it is invoked from the event queue.
   */
  public static JFrame showLog() {
    if(OSPRuntime.appletMode||(OSPRuntime.applet!=null)) {
      return org.opensourcephysics.controls.MessageFrame.showLog(true);
    }
    getOSPLog().setVisible(true);
    Logger logger = OSPLOG.getLogger();
    for(int i = 0, n = messageStorage.length; i<n; i++) {
      LogRecord record = messageStorage[(i+messageIndex)%n];
      if(record!=null) {
        logger.log(record);
      }
    }
    messageIndex = 0;
    return getOSPLog();
  }

  /**
   * Shows the log.
   */
  public static void showLogInvokeLater() {
    Runnable doLater = new Runnable() {
      public void run() {
        showLog();
      }

    };
    java.awt.EventQueue.invokeLater(doLater);
  }

  /**
   * Gets the logger level value.
   * @return the current level value
   */
  public static int getLevelValue() {
    if(OSPRuntime.appletMode||(OSPRuntime.applet!=null)) {
      return org.opensourcephysics.controls.MessageFrame.getLevelValue();
    }
    try {
      Level level = getOSPLog().getLogger().getLevel();
      if (level!=null) return level.intValue();
    } catch(Exception ex) { // throws security exception if the caller does not have LoggingPermission("control").
    }
    return -1;
  }

  /**
   * Sets the logger level.
   *
   * @param level the Level
   */
  public static void setLevel(Level level) {
    if(OSPRuntime.appletMode||(OSPRuntime.applet!=null)) {
      org.opensourcephysics.controls.MessageFrame.setLevel(level);
    } else {
      try {
        getOSPLog().getLogger().setLevel(level);
      } catch(Exception ex) { // throws security exception if the caller does not have LoggingPermission("control").
        // keep the current level
      }
      // refresh the level menus if the menubar exists
      if((getOSPLog()==null)||(getOSPLog().menubarGroup==null)) {
        return;
      }
      for(int i = 0; i<2; i++) {
        Enumeration<AbstractButton> e = getOSPLog().menubarGroup.getElements();
        if(i==1) {
          e = getOSPLog().popupGroup.getElements();
        }
        while(e.hasMoreElements()) {
          JMenuItem item = (JMenuItem) e.nextElement();
          if(getOSPLog().getLogger().getLevel().toString().equals(item.getActionCommand())) {
            item.setSelected(true);
            break;
          }
        }
      }
    }
  }

  /**
   * Returns the Level with the specified name, or null if none.
   *
   * @param level the Level
   */
  public static Level parseLevel(String level) {
    for(int i = 0; i<levels.length; i++) {
      if(levels[i].getName().equals(level)) {
        return levels[i];
      }
    }
    return null;
  }

  /**
   * Logs a severe error message.
   *
   * @param msg the message
   */
  public static void severe(String msg) {
    if(OSPRuntime.appletMode||(OSPRuntime.applet!=null)) {
      org.opensourcephysics.controls.MessageFrame.severe(msg);
    } else {
      log(Level.SEVERE, msg);
    }
  }

  /**
   * Logs a warning message.
   *
   * @param msg the message
   */
  public static void warning(String msg) {
    if(OSPRuntime.appletMode||(OSPRuntime.applet!=null)) {
      org.opensourcephysics.controls.MessageFrame.warning(msg);
    } else {
      log(Level.WARNING, msg);
    }
  }

  /**
   * Logs an information message.
   *
   * @param msg the message
   */
  public static void info(String msg) {
    if(OSPRuntime.appletMode||(OSPRuntime.applet!=null)) {
      org.opensourcephysics.controls.MessageFrame.info(msg);
    } else {
      log(Level.INFO, msg);
    }
  }

  /**
   * Logs a configuration message.
   *
   * @param msg the message
   */
  public static void config(String msg) {
    if(OSPRuntime.appletMode||(OSPRuntime.applet!=null)) {
      org.opensourcephysics.controls.MessageFrame.config(msg);
    } else {
      log(Level.CONFIG, msg);
    }
  }

  /**
   * Logs a fine debugging message.
   *
   * @param msg the message
   */
  public static void fine(String msg) {
    if(OSPRuntime.appletMode||(OSPRuntime.applet!=null)) {
      org.opensourcephysics.controls.MessageFrame.fine(msg);
    } else {
      log(Level.FINE, msg);
    }
  }

  /**
   * Clears the Log.
   *
   * @param msg the message
   */
  public static void clearLog() {
    messageIndex = 0;
    if(OSPRuntime.appletMode||(OSPRuntime.applet!=null)) {
      org.opensourcephysics.controls.MessageFrame.clear();
    } else {
      OSPLOG.clear();
    }
  }

  /**
   * Logs a finer debugging message.
   *
   * @param msg the message
   */
  public static void finer(String msg) {
    if(OSPRuntime.appletMode||(OSPRuntime.applet!=null)) {
      org.opensourcephysics.controls.MessageFrame.finer(msg);
    } else {
      log(Level.FINER, msg);
    }
  }

  /**
   * Logs a finest debugging message.
   *
   * @param msg the message
   */
  public static void finest(String msg) {
    if(OSPRuntime.appletMode||(OSPRuntime.applet!=null)) {
      org.opensourcephysics.controls.MessageFrame.finest(msg);
    } else {
      log(Level.FINEST, msg);
    }
  }

  /**
   * Sets whether console messages are logged.
   *
   * @param log true to log console messages
   */
  public static void setConsoleMessagesLogged(boolean log) {
  	logConsole = log;
  }

  /**
   * Gets whether console messages are logged.
   *
   * @return true if console messages are logged
   */
  public static boolean isConsoleMessagesLogged() {
  	return logConsole;
  }

  /**
   * Constructs an OSPLog for a specified package.
   *
   * @param pkg the package
   */
  public OSPLog(Package pkg) {
    this(pkg.getName(), null);
  }

  /**
   * Constructs an OSPLog for a specified package and resource bundle.
   *
   * @param pkg the package
   * @param resourceBundleName the name of the resource bundle
   */
  public OSPLog(Package pkg, String resourceBundleName) {
    this(pkg.getName(), resourceBundleName);
  }

  /**
   * Constructs an OSPLog for a specified class.
   *
   * @param type the class
   */
  public OSPLog(Class<?> type) {
    this(type, null);
  }

  /**
   * Constructs an OSPLog for a specified class and resource bundle.
   *
   * @param type the class
   * @param resourceBundleName the name of the resource bundle
   */
  public OSPLog(Class<?> type, String resourceBundleName) {
    this(type.getPackage().getName(), resourceBundleName);
  }

  /**
   * Gets the log panel so it can be displayed in a dialog or other container.
   *
   * @return a JPanel containing the log
   */
  public JPanel getLogPanel() {
    return logPanel;
  }

  /**
   * Clears the log.
   */
  public void clear() {
    textPane.setText(null);
  }

  /**
   * Saves the log to a text file specified by name.
   *
   * @param fileName the file name
   * @return the name of the file
   */
  public String saveLog(String fileName) {
    if((fileName==null)||fileName.trim().equals("")) { //$NON-NLS-1$
      return saveLogAs();
    }
    try {
      BufferedWriter out = new BufferedWriter(new FileWriter(fileName));
      out.write(textPane.getText());
      out.flush();
      out.close();
      return fileName;
    } catch(IOException ex) {
      return null;
    }
  }

  /**
   * Saves a log to a text file selected with a chooser.
   *
   * @return the name of the file
   */
  public String saveLogAs() {
    int result = getChooser().showSaveDialog(null);
    if(result==JFileChooser.APPROVE_OPTION) {
      File file = getChooser().getSelectedFile();
      // check to see if file already exists
      if(file.exists()) {
        int selected = JOptionPane.showConfirmDialog(this, ControlsRes.getString("OSPLog.ReplaceExisting_dialog_message")+file.getName() //$NON-NLS-1$
          +ControlsRes.getString("OSPLog.question_mark"), ControlsRes.getString("OSPLog.ReplaceFile_dialog_title"), //$NON-NLS-1$ //$NON-NLS-2$
            JOptionPane.YES_NO_CANCEL_OPTION);
        if(selected!=JOptionPane.YES_OPTION) {
          return null;
        }
      }
      String fileName = XML.getRelativePath(file.getAbsolutePath());
      return saveLog(fileName);
    }
    return null;
  }

  /**
   * Saves the xml-formatted log records to a file specified by name.
   *
   * @param fileName the file name
   * @return the name of the file
   */
  public String saveXML(String fileName) {
    if(OSPRuntime.appletMode||(OSPRuntime.applet!=null)) {
      logger.log(Level.FINE, "Cannot save XML file when running as an applet."); //$NON-NLS-1$
      return null;                                                               // cannot log to file in applet mode
    }
    if((fileName==null)||fileName.trim().equals("")) { //$NON-NLS-1$
      return saveXMLAs();
    }
    // open temp file and get xml string
    String xml = read(tempFileName);
    // add closing tag to xml
    Handler fileHandler = getFileHandler();
    String tail = fileHandler.getFormatter().getTail(fileHandler);
    xml = xml+tail;
    // write the xml
    try {
      BufferedWriter out = new BufferedWriter(new FileWriter(fileName));
      out.write(xml);
      out.flush();
      out.close();
      return fileName;
    } catch(IOException ex) {
      return null;
    }
  }

  /**
   * Saves the xml-formatted log records to a file selected with a chooser.
   *
   * @return the name of the file
   */
  public String saveXMLAs() {
    int result = getChooser().showSaveDialog(null);
    if(result==JFileChooser.APPROVE_OPTION) {
      File file = getChooser().getSelectedFile();
      // check to see if file already exists
      if(file.exists()) {
        int selected = JOptionPane.showConfirmDialog(this, ControlsRes.getString("OSPLog.ReplaceExisting_dialog_message")+file.getName() //$NON-NLS-1$
          +ControlsRes.getString("OSPLog.question_mark"), ControlsRes.getString("OSPLog.ReplaceFile_dialog_title"), //$NON-NLS-1$ //$NON-NLS-2$
            JOptionPane.YES_NO_CANCEL_OPTION);
        if(selected!=JOptionPane.YES_OPTION) {
          return null;
        }
      }
      logFileName = XML.getRelativePath(file.getAbsolutePath());
      return saveXML(logFileName);
    }
    return null;
  }

  /**
   * Opens a text file selected with a chooser and writes the contents to the log.
   *
   * @return the name of the file
   */
  public String open() {
    int result = getChooser().showOpenDialog(null);
    if(result==JFileChooser.APPROVE_OPTION) {
      File file = getChooser().getSelectedFile();
      String fileName = XML.getRelativePath(file.getAbsolutePath());
      return open(fileName);
    }
    return null;
  }

  /**
   * Opens a text file specified by name and writes the contents to the log.
   *
   * @param fileName the file name
   * @return the file name
   */
  public String open(String fileName) {
    textPane.setText(read(fileName));
    return fileName;
  }

  /**
   * Gets the logger.
   *
   * @return the logger
   */
  public Logger getLogger() {
    return logger;
  }

  /**
   * Enables logging to a file.
   *
   * @param enable true to log to a file
   */
  public void setLogToFile(boolean enable) {
    if(OSPRuntime.appletMode||(OSPRuntime.applet!=null)) {
      logger.log(Level.FINE, "Cannot log to file when running as an applet."); //$NON-NLS-1$
      return;                                                                  // cannot log to file in applet mode
    }
    if(enable) {
      logToFileItem.setSelected(true);
      logger.addHandler(getFileHandler());
    } else {
      logToFileItem.setSelected(false);
      logger.removeHandler(fileHandler);
    }
  }

  /*
   *  //Uncomment this method to test the OSPLog.
   * public static void main(String[] args) {
   *   JMenuBar menubar = getOSPLog().getJMenuBar();
   *   JMenu menu = new JMenu("Test");
   *   menubar.add(menu);
   *   String[] levels = new String[] {
   *     "SEVERE", "WARNING", "INFO", "CONFIG", "FINE", "FINER", "FINEST"
   *   };
   *   for(int i = 0;i<levels.length;i++) {
   *     JMenuItem item = new JMenuItem(levels[i]);
   *     menu.add(item, 0);
   *     item.setActionCommand(levels[i]);
   *     item.addActionListener(new ActionListener() {
   *       public void actionPerformed(ActionEvent e) {
   *         OSPLOG.getLogger().log(Level.parse(e.getActionCommand()), "Testing "+e.getActionCommand());
   *       }
   *     });
   *   }
   *   menu = new JMenu("Console");
   *   menubar.add(menu);
   *   JMenuItem item = new JMenuItem("Console Out");
   *   menu.add(item, 0);
   *   item.addActionListener(new ActionListener() {
   *     public void actionPerformed(ActionEvent e) {
   *       System.out.println("Out console message.");
   *     }
   *   });
   *   item = new JMenuItem("Console Err");
   *   menu.add(item, 0);
   *   item.addActionListener(new ActionListener() {
   *     public void actionPerformed(ActionEvent e) {
   *       System.err.println("Error console message.");
   *     }
   *   });
   *   item = new JMenuItem("Exception");
   *   menu.add(item, 0);
   *   item.addActionListener(new ActionListener() {
   *     public void actionPerformed(ActionEvent e) {
   *       double[] x = null;
   *       x[0] = 1;
   *     }
   *   });
   *   OSPLOG.setVisible(true);
   *   OSPLOG.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
   * }
   */

  /**
   * Fires a property change event. Needed to expose protected method.
   */
  protected void firePropertyChange(String propertyName,
      Object oldValue, Object newValue) {
  	super.firePropertyChange(propertyName, oldValue, newValue);
  }
  
  /**
   * Creates the GUI.
   */
  protected void createGUI() {
    // create the panel, text pane and scroller
    logPanel = new JPanel(new BorderLayout());
    logPanel.setPreferredSize(new Dimension(480, 240));
    setContentPane(logPanel);
    textPane = new JTextPane() {
      public void paintComponent(Graphics g) {
        if(OSPRuntime.antiAliasText) {
          Graphics2D g2 = (Graphics2D) g;
          RenderingHints rh = g2.getRenderingHints();
          rh.put(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
          rh.put(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
        }
        super.paintComponent(g);
      }

    };
    textPane.setEditable(false);
    textPane.setAutoscrolls(true);
    JScrollPane textScroller = new JScrollPane(textPane);
    textScroller.setWheelScrollingEnabled(true);
    logPanel.add(textScroller, BorderLayout.CENTER);
    // create the colored styles
    black = StyleContext.getDefaultStyleContext().getStyle(StyleContext.DEFAULT_STYLE);
    red = textPane.addStyle("red", black); //$NON-NLS-1$
    StyleConstants.setForeground(red, DARK_RED);
    blue = textPane.addStyle("blue", black); //$NON-NLS-1$
    StyleConstants.setForeground(blue, DARK_BLUE);
    green = textPane.addStyle("green", black); //$NON-NLS-1$
    StyleConstants.setForeground(green, DARK_GREEN);
    magenta = textPane.addStyle("magenta", black); //$NON-NLS-1$
    StyleConstants.setForeground(magenta, Color.MAGENTA);
    gray = textPane.addStyle("gray", black); //$NON-NLS-1$
    StyleConstants.setForeground(gray, Color.GRAY);
    // create the logger
    createLogger();
    // create the menus
    createMenus();
    pack();
    textPane.addMouseListener(new MouseAdapter() {
      public void mousePressed(MouseEvent e) {
        try {
          if(OSPRuntime.isPopupTrigger(e)) {
            // show popup menu
            if(popup!=null) {
            	FontSizer.setFonts(popup, FontSizer.getLevel());
              popup.show(textPane, e.getX(), e.getY()+8);
            }
          }
        } catch(Exception ex) {
          System.err.println("Error in mouse action."); //$NON-NLS-1$
          System.err.println(ex.toString());
          ex.printStackTrace();
        }
      }

    });
  }

  /**
   * Creates and initializes the logger.
   *
   * @return the logger
   */
  protected Logger createLogger() {
    // get the package logger and reference the ResourceBundle it will use
    if(bundleName!=null) {
      try {
        logger = Logger.getLogger(pkgName, bundleName);
      } catch(Exception ex) {
        logger = Logger.getLogger(pkgName);
      }
    } else {
      logger = Logger.getLogger(pkgName);
    }
    try {
      logger.setLevel(defaultLevel);
      // add a log handler for this log
      logHandler = new OSPLogHandler(textPane, this);
      logHandler.setFormatter(new ConsoleFormatter());
      logHandler.setLevel(Level.ALL);
      // ignore parent handlers (specifically root console handler)
      OSPRuntime.class.getClass(); // force the static methods to execute
      logger.setUseParentHandlers(false);
      logger.addHandler(logHandler);
    } catch(SecurityException ex) {
      hasPermission = false;
    }
    return logger;
  }

  /**
   * Gets the file handler using lazy instantiation.
   *
   * @return the Handler
   */
  protected synchronized Handler getFileHandler() {
    if(fileHandler!=null) {
      return fileHandler;
    }
    try {
      // add a file handler with file name equal to short package name
      int i = pkgName.lastIndexOf(".");                                        //$NON-NLS-1$
      if(i>-1) {
        pkgName = pkgName.substring(i+1);
      }
      if(logdir.endsWith(slash)) {
        tempFileName = logdir+pkgName+".log";                                  //$NON-NLS-1$
      } else {
        tempFileName = logdir+slash+pkgName+".log";                            //$NON-NLS-1$
      }
      fileHandler = new FileHandler(tempFileName);
      fileHandler.setFormatter(new XMLFormatter());
      fileHandler.setLevel(Level.ALL);
      logger.addHandler(fileHandler);
      logger.log(Level.INFO, "Logging to file enabled. File = "+tempFileName); //$NON-NLS-1$
    } catch(Exception ex) {
      ex.printStackTrace();
    }
    return fileHandler;
  }

  /**
   * Creates the popup menu.
   */
  protected void createMenus() {
    if(!hasPermission) {
      return;
    }
    popup = new JPopupMenu();
    JMenu menu = new JMenu(ControlsRes.getString("OSPLog.Level_menu")); //$NON-NLS-1$
    popup.add(menu);
    popupGroup = new ButtonGroup();
    for(int i = 0; i<levels.length; i++) {
      JRadioButtonMenuItem item = new JRadioButtonMenuItem(levels[i].getName());
      menu.add(item, 0);
      popupGroup.add(item);
      if(logger.getLevel().toString().equals(levels[i].toString())) {
        item.setSelected(true);
      }
      item.setActionCommand(levels[i].getName());
      item.addActionListener(new ActionListener() {
        public void actionPerformed(ActionEvent e) {
          logger.setLevel(Level.parse(e.getActionCommand()));
          Enumeration<AbstractButton> e2 = menubarGroup.getElements();
          while(e2.hasMoreElements()) {
            JMenuItem item = (JMenuItem) e2.nextElement();
            if(logger.getLevel().toString().equals(item.getActionCommand())) {
              item.setSelected(true);
              break;
            }
          }
        }

      });
    }
    popup.addSeparator();
    Action openAction = new AbstractAction(ControlsRes.getString("OSPLog.Open_popup")) { //$NON-NLS-1$
      public void actionPerformed(ActionEvent e) {
        open();
      }

    };
    openAction.setEnabled(!OSPRuntime.appletMode&&(OSPRuntime.applet==null));
    popup.add(openAction);
    Action saveAsAction = new AbstractAction(ControlsRes.getString("OSPLog.SaveAs_popup")) { //$NON-NLS-1$
      public void actionPerformed(ActionEvent e) {
        saveLogAs();
      }

    };
    saveAsAction.setEnabled(!OSPRuntime.appletMode&&(OSPRuntime.applet==null));
    popup.add(saveAsAction);
    popup.addSeparator();
    Action clearAction = new AbstractAction(ControlsRes.getString("OSPLog.Clear_popup")) { //$NON-NLS-1$
      public void actionPerformed(ActionEvent e) {
        clear();
      }

    };
    popup.add(clearAction);
    // create menubar
    JMenuBar menubar = new JMenuBar();
    setJMenuBar(menubar);
    menu = new JMenu(ControlsRes.getString("OSPLog.File_menu")); //$NON-NLS-1$
    menubar.add(menu);
    menu.add(openAction);
    menu.add(saveAsAction);
    menu = new JMenu(ControlsRes.getString("OSPLog.Edit_menu")); //$NON-NLS-1$
    menubar.add(menu);
    menu.add(clearAction);
    menu = new JMenu(ControlsRes.getString("OSPLog.Level_menu")); //$NON-NLS-1$
    menubar.add(menu);
    menubarGroup = new ButtonGroup();
    for(int i = 0; i<levels.length; i++) {
      JRadioButtonMenuItem item = new JRadioButtonMenuItem(levels[i].getName());
      menu.add(item, 0);
      menubarGroup.add(item);
      if(logger.getLevel().toString().equals(levels[i].toString())) {
        item.setSelected(true);
      }
      item.setActionCommand(levels[i].getName());
      item.addActionListener(new ActionListener() {
        public void actionPerformed(ActionEvent e) {
          logger.setLevel(Level.parse(e.getActionCommand()));
          Enumeration<AbstractButton> e2 = popupGroup.getElements();
          while(e2.hasMoreElements()) {
            JMenuItem item = (JMenuItem) e2.nextElement();
            if(logger.getLevel().toString().equals(item.getActionCommand())) {
              item.setSelected(true);
              break;
            }
          }
        }

      });
    }
    JMenu prefMenu = new JMenu(ControlsRes.getString("OSPLog.Options_menu")); //$NON-NLS-1$
    menubar.add(prefMenu);
    logToFileItem = new JCheckBoxMenuItem(ControlsRes.getString("OSPLog.LogToFile_check_box")); //$NON-NLS-1$
    logToFileItem.setSelected(false);
    logToFileItem.setEnabled(!OSPRuntime.appletMode&&(OSPRuntime.applet==null));
    logToFileItem.addActionListener(new ActionListener() {
      public void actionPerformed(ActionEvent e) {
        JCheckBoxMenuItem item = (JCheckBoxMenuItem) e.getSource();
        setLogToFile(item.isSelected());
      }

    });
    prefMenu.add(logToFileItem);
  }

  /**
   * Gets a file chooser.
   *
   * @return the chooser
   */
  protected static JFileChooser getChooser() {
    if(chooser==null) {
      chooser = new JFileChooser(new File(OSPRuntime.chooserDir));
    }
  	FontSizer.setFonts(chooser, FontSizer.getLevel());
    return chooser;
  }

  /**
   * Reads a file.
   *
   * @param fileName the name of the file
   * @return the file contents as a String
   */
  protected String read(String fileName) {
    File file = new File(fileName);
    StringBuffer buffer = null;
    try {
      BufferedReader in = new BufferedReader(new FileReader(file));
      buffer = new StringBuffer();
      String line = in.readLine();
      while(line!=null) {
        buffer.append(line+XML.NEW_LINE);
        line = in.readLine();
      }
      in.close();
    } catch(IOException ex) {
      logger.warning(ex.toString());
    }
    return buffer.toString();
  }
  
  private OSPLog(String name, String resourceBundleName) {
    super(ControlsRes.getString("OSPLog.DefaultTitle")); //$NON-NLS-1$
    this.setName("LogTool"); // identify this as a tool //$NON-NLS-1$
    bundleName = resourceBundleName;
    pkgName = name;
    ConsoleLevel.class.getName(); // force ConsoleLevel to load static constants
    createGUI();
    setDefaultCloseOperation(WindowConstants.HIDE_ON_CLOSE);
  }

  private static void log(Level level, String msg) {
    LogRecord record = new LogRecord(level, msg);
    // get the stack trace
    StackTraceElement stack[] = (new Throwable()).getStackTrace();
    // find the first method not in class OSPLog
    for(int i = 0; i<stack.length; i++) {
      StackTraceElement el = stack[i];
      String className = el.getClassName();
      if(!className.equals("org.opensourcephysics.controls.OSPLog")) { //$NON-NLS-1$
        // set the source class and method
        record.setSourceClassName(className);
        record.setSourceMethodName(el.getMethodName());
        break;
      }
    }
    if(OSPLOG!=null) {
      OSPLOG.getLogger().log(record);
    } else {
      messageStorage[messageIndex] = record;
      messageIndex++;
      messageIndex = messageIndex%messageStorage.length;
    }
  }

}

/**
 * A class that formats a record as if this were the console.
 */
class ConsoleFormatter extends SimpleFormatter {
  /**
   * Formats the record as if this were the console.
   *
   * @param record LogRecord
   * @return String
   */
  public String format(LogRecord record) {
    String message = formatMessage(record);
    if((record.getLevel().intValue()==ConsoleLevel.OUT_CONSOLE.intValue())||(record.getLevel().intValue()==ConsoleLevel.ERR_CONSOLE.intValue())) {
      StringBuffer sb = new StringBuffer();
      if((message.length()>0)&&(message.charAt(0)=='\t')) {
        message = message.replaceFirst("\t", "    "); //$NON-NLS-1$//$NON-NLS-2$
      } else {
        sb.append("CONSOLE: ");                       //$NON-NLS-1$
      }
      sb.append(message);
      sb.append(OSPLog.eol);
      // new line after message
      if(record.getThrown()!=null) {
        try {
          StringWriter sw = new StringWriter();
          PrintWriter pw = new PrintWriter(sw);
          record.getThrown().printStackTrace(pw);
          pw.close();
          sb.append(sw.toString());
        } catch(Exception ex) {
          ex.printStackTrace();
        }
      }
      return sb.toString();
    }
    return super.format(record);
  }

}

/**
 * A class that writes an output stream to the logger.
 */
class LoggerOutputStream extends OutputStream {
  StringBuffer buffer = new StringBuffer();
  OutputStream oldStream;
  ConsoleLevel level;

  LoggerOutputStream(ConsoleLevel level, OutputStream oldStream) {
    this.level = level;
    this.oldStream = oldStream;
  }

  public void write(int c) throws IOException {  	
    oldStream.write(c);
    if(c=='\n') {
      LogRecord record = new LogRecord(level, buffer.toString());
      OSPLog.getOSPLog().getLogger().log(record);
      buffer = new StringBuffer();
    } else {
      buffer.append((char) c);
    }
  }

}

/**
 * A handler class for a text log.
 */
class OSPLogHandler extends Handler {
	
  JTextPane logPane;
  OSPLog ospLog;

  /**
   * Constructor OSPLogHandler
   * @param textPane
   */
  public OSPLogHandler(JTextPane textPane, OSPLog log) {
    logPane = textPane;
    ospLog = log;
  }
  
  public void publish(LogRecord record) {
    if(!isLoggable(record)) {
      return;
    }
    String msg = getFormatter().format(record);
    Style style = OSPLog.green; // default style
    int val = record.getLevel().intValue();
    if(val==ConsoleLevel.ERR_CONSOLE.intValue()) {
    	if (msg.indexOf("OutOfMemory")>-1) //$NON-NLS-1$
    		ospLog.firePropertyChange("error", -1, OSPLog.OUT_OF_MEMORY_ERROR); //$NON-NLS-1$
    	if (!OSPLog.logConsole) return;
      style = OSPLog.magenta;
    } else if(val==ConsoleLevel.OUT_CONSOLE.intValue()) {
    	if (msg.indexOf("ERROR org.ffmpeg")>-1) //$NON-NLS-1$
    		ospLog.firePropertyChange("ffmpeg_error", null, msg); //$NON-NLS-1$
    	else if (msg.indexOf("JNILibraryLoader")>-1) {//$NON-NLS-1$
    		ospLog.firePropertyChange("xuggle_error", null, msg); //$NON-NLS-1$
    	}
    	if (!OSPLog.logConsole) return;
      style = OSPLog.gray;
    } else if(val>=Level.WARNING.intValue()) {
      style = OSPLog.red;
    } else if(val>=Level.INFO.intValue()) {
      style = OSPLog.black;
    } else if(val>=Level.CONFIG.intValue()) {
      style = OSPLog.green;
    } else if(val>=Level.FINEST.intValue()) {
      style = OSPLog.blue;
    }
    try {
      Document doc = logPane.getDocument();
      doc.insertString(doc.getLength(), msg+'\n', style);
      // scroll to display new message
      Rectangle rect = logPane.getBounds();
      rect.y = rect.height;
      logPane.scrollRectToVisible(rect);
    } catch(BadLocationException ex) {
      System.err.println(ex);
    }
  }

  public void flush() {
    /** empty block */
  }

  public void close() {
    /** empty block */
  }

}

/**
 * A formatter class that formats a log record as an osp xml string
 */
class OSPLogFormatter extends java.util.logging.Formatter {
  XMLControl control = new XMLControlElement();

  /**
   * Format the given log record and return the formatted string.
   *
   * @param record the log record to be formatted
   * @return the formatted log record
   */
  public String format(LogRecord record) {
    control.saveObject(record);
    return control.toXML();
  }

}

/**
 * A class to save and load LogRecord data in an XMLControl.
 * Note: this is in a very primitive state for testing only
 */
class OSPLogRecordLoader extends XMLLoader {
  public void saveObject(XMLControl control, Object obj) {
    LogRecord record = (LogRecord) obj;
    control.setValue("message", record.getMessage());        //$NON-NLS-1$
    control.setValue("level", record.getLevel().toString()); //$NON-NLS-1$
  }

  public Object createObject(XMLControl control) {
    String message = control.getString("message"); //$NON-NLS-1$
    String level = control.getString("level");     //$NON-NLS-1$
    return new LogRecord(Level.parse(level), message);
  }

  public Object loadObject(XMLControl control, Object obj) {
    return obj;
  }

}

/*
 * Open Source Physics software is free software; you can redistribute
 * it and/or modify it under the terms of the GNU General Public License (GPL) as
 * published by the Free Software Foundation; either version 2 of the License,
 * or(at your option) any later version.
 *
 * Code that uses any portion of the code in the org.opensourcephysics package
 * or any subpackage (subdirectory) of this package must must also be be released
 * under the GNU GPL license.
 *
 * This software is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston MA 02111-1307 USA
 * or view the license online at http://www.gnu.org/copyleft/gpl.html
 *
 * Copyright (c) 2019  The Open Source Physics project
 *                     https://www.compadre.org/osp
 */