/*
 * Copyright (c) 2006, Swedish Institute of Computer Science. All rights
 * reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 * 1. Redistributions of source code must retain the above copyright notice,
 * this list of conditions and the following disclaimer. 2. Redistributions in
 * binary form must reproduce the above copyright notice, this list of
 * conditions and the following disclaimer in the documentation and/or other
 * materials provided with the distribution. 3. Neither the name of the
 * Institute nor the names of its contributors may be used to endorse or promote
 * products derived from this software without specific prior written
 * permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE INSTITUTE AND CONTRIBUTORS ``AS IS'' AND ANY
 * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 * DISCLAIMED. IN NO EVENT SHALL THE INSTITUTE OR CONTRIBUTORS BE LIABLE FOR ANY
 * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 *
 * $Id: GUI.java,v 1.144 2009/09/18 16:13:31 fros4943 Exp $
 */

package se.sics.cooja;

import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Component;
import java.awt.Container;
import java.awt.Dialog;
import java.awt.Dimension;
import java.awt.Frame;
import java.awt.GraphicsDevice;
import java.awt.GraphicsEnvironment;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.Window;
import java.awt.Dialog.ModalityType;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.ComponentAdapter;
import java.awt.event.ComponentEvent;
import java.awt.event.KeyEvent;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.beans.PropertyVetoException;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.FileReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.PrintStream;
import java.io.StringReader;
import java.lang.reflect.InvocationTargetException;
import java.net.URL;
import java.net.URLClassLoader;
import java.security.AccessControlException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Enumeration;
import java.util.List;
import java.util.Observable;
import java.util.Observer;
import java.util.Properties;
import java.util.Vector;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import javax.swing.AbstractAction;
import javax.swing.Action;
import javax.swing.BorderFactory;
import javax.swing.Box;
import javax.swing.DefaultDesktopManager;
import javax.swing.InputMap;
import javax.swing.JApplet;
import javax.swing.JButton;
import javax.swing.JCheckBoxMenuItem;
import javax.swing.JComponent;
import javax.swing.JDesktopPane;
import javax.swing.JDialog;
import javax.swing.JFileChooser;
import javax.swing.JFrame;
import javax.swing.JInternalFrame;
import javax.swing.JLabel;
import javax.swing.JMenu;
import javax.swing.JMenuBar;
import javax.swing.JMenuItem;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
import javax.swing.JProgressBar;
import javax.swing.JScrollPane;
import javax.swing.JSeparator;
import javax.swing.JTabbedPane;
import javax.swing.JTextPane;
import javax.swing.KeyStroke;
import javax.swing.SwingUtilities;
import javax.swing.ToolTipManager;
import javax.swing.UIManager;
import javax.swing.event.MenuEvent;
import javax.swing.event.MenuListener;
import javax.swing.filechooser.FileFilter;

import org.apache.log4j.BasicConfigurator;
import org.apache.log4j.Logger;
import org.apache.log4j.xml.DOMConfigurator;
import org.jdom.Document;
import org.jdom.Element;
import org.jdom.JDOMException;
import org.jdom.input.SAXBuilder;
import org.jdom.output.Format;
import org.jdom.output.XMLOutputter;

import se.sics.cooja.MoteType.MoteTypeCreationException;
import se.sics.cooja.VisPlugin.PluginRequiresVisualizationException;
import se.sics.cooja.contikimote.ContikiMoteType;
import se.sics.cooja.dialogs.AddMoteDialog;
import se.sics.cooja.dialogs.BufferSettings;
import se.sics.cooja.dialogs.ConfigurationWizard;
import se.sics.cooja.dialogs.CreateSimDialog;
import se.sics.cooja.dialogs.ExternalToolsDialog;
import se.sics.cooja.dialogs.MessageList;
import se.sics.cooja.dialogs.ProjectDirectoriesDialog;
import se.sics.cooja.plugins.MoteTypeInformation;
import se.sics.cooja.plugins.ScriptRunner;
import se.sics.cooja.plugins.SimControl;
import se.sics.cooja.plugins.SimInformation;

/**
 * Main file of COOJA Simulator. Typically contains a visualizer for the
 * simulator, but can also be started without visualizer.
 *
 * This class loads external Java classes (in project directories), and handles the
 * COOJA plugins as well as the configuration system. If provides a number of
 * help methods for the rest of the COOJA system, and is the starting point for
 * loading and saving simulation configs.
 *
 * @author Fredrik Osterlind
 */
public class GUI extends Observable {

  /**
   * External tools default Win32 settings filename.
   */
  public static final String EXTERNAL_TOOLS_WIN32_SETTINGS_FILENAME = "/external_tools_win32.config";

  /**
   * External tools default Mac OS X settings filename.
   */
  public static final String EXTERNAL_TOOLS_MACOSX_SETTINGS_FILENAME = "/external_tools_macosx.config";

  /**
   * External tools default FreeBSD settings filename.
   */
  public static final String EXTERNAL_TOOLS_FREEBSD_SETTINGS_FILENAME = "/external_tools_freebsd.config";

  /**
   * External tools default Linux/Unix settings filename.
   */
  public static final String EXTERNAL_TOOLS_LINUX_SETTINGS_FILENAME = "/external_tools_linux.config";

  /**
   * External tools default Linux/Unix settings filename for 64 bit architectures.
   * Tested on Intel 64-bit Gentoo Linux.
   */
  public static final String EXTERNAL_TOOLS_LINUX_64_SETTINGS_FILENAME = "/external_tools_linux_64.config";

  /**
   * External tools user settings filename.
   */
  public static final String EXTERNAL_TOOLS_USER_SETTINGS_FILENAME = ".cooja.user.properties";
  public static File externalToolsUserSettingsFile;
  private static boolean externalToolsUserSettingsFileReadOnly = false;

  private static String specifiedContikiPath = null;

  /**
   * Logger settings filename.
   */
  public static final String LOG_CONFIG_FILE = "log4j_config.xml";

  /**
   * Default project configuration filename.
   */
  public static String PROJECT_DEFAULT_CONFIG_FILENAME = null;

  /**
   * User project configuration filename.
   */
  public static final String PROJECT_CONFIG_FILENAME = "cooja.config";

  /**
   * File filter only showing saved simulations files (*.csc).
   */
  public static final FileFilter SAVED_SIMULATIONS_FILES = new FileFilter() {
    public boolean accept(File file) {
      if (file.isDirectory()) {
        return true;
      }

      if (file.getName().endsWith(".csc")) {
        return true;
      }

      return false;
    }

    public String getDescription() {
      return "COOJA Configuration files";
    }

    public String toString() {
      return ".csc";
    }
  };

  private static JFrame frame = null;

  private static JApplet applet = null;

  private static final long serialVersionUID = 1L;

  private static Logger logger = Logger.getLogger(GUI.class);

  // External tools setting names
  private static Properties defaultExternalToolsSettings;
  private static Properties currentExternalToolsSettings;

  private static final String externalToolsSettingNames[] = new String[] {
    "PATH_CONTIKI", "PATH_COOJA_CORE_RELATIVE",

    "PATH_MAKE",
    "PATH_SHELL",
    "PATH_C_COMPILER", "COMPILER_ARGS",
    "PATH_LINKER", "LINK_COMMAND_1", "LINK_COMMAND_2",
    "PATH_AR", "AR_COMMAND_1", "AR_COMMAND_2",
    "PATH_OBJDUMP", "OBJDUMP_ARGS",
    "PATH_JAVAC",

    "CONTIKI_STANDARD_PROCESSES",
    "CONTIKI_MAIN_TEMPLATE_FILENAME",

    "CMD_GREP_PROCESSES", "REGEXP_PARSE_PROCESSES",
    "CMD_GREP_INTERFACES", "REGEXP_PARSE_INTERFACES",
    "CMD_GREP_SENSORS", "REGEXP_PARSE_SENSORS",

    "DEFAULT_PROJECTDIRS",
    "CORECOMM_TEMPLATE_FILENAME",

    "MAPFILE_DATA_START", "MAPFILE_DATA_SIZE",
    "MAPFILE_BSS_START", "MAPFILE_BSS_SIZE",
    "MAPFILE_VAR_NAME",
    "MAPFILE_VAR_ADDRESS_1", "MAPFILE_VAR_ADDRESS_2",
    "MAPFILE_VAR_SIZE_1", "MAPFILE_VAR_SIZE_2",

    "PARSE_WITH_COMMAND",
    "PARSE_COMMAND",
    "COMMAND_VAR_NAME_ADDRESS",
    "COMMAND_DATA_START", "COMMAND_DATA_END",
    "COMMAND_BSS_START", "COMMAND_BSS_END",
  };

  private static final int FRAME_NEW_OFFSET = 30;

  private static final int FRAME_STANDARD_WIDTH = 150;

  private static final int FRAME_STANDARD_HEIGHT = 300;

  private GUI myGUI;

  private Simulation mySimulation;

  protected GUIEventHandler guiEventHandler = new GUIEventHandler();

  private JMenu menuPlugins, menuMoteTypeClasses, menuMoteTypes;

  private JMenu menuOpenSimulation, menuConfOpenSimulation;
  private boolean hasFileHistoryChanged;

  private Vector<Class<? extends Plugin>> menuMotePluginClasses;

  private JDesktopPane myDesktopPane;

  private Vector<Plugin> startedPlugins = new Vector<Plugin>();

  private ArrayList<GUIAction> guiActions = new ArrayList<GUIAction>();
  
  // Platform configuration variables
  // Maintained via method reparseProjectConfig()
  private ProjectConfig projectConfig;

  private Vector<File> currentProjectDirs = new Vector<File>();

  private ClassLoader projectDirClassLoader;

  private Vector<Class<? extends MoteType>> moteTypeClasses = new Vector<Class<? extends MoteType>>();

  private Vector<Class<? extends Plugin>> pluginClasses = new Vector<Class<? extends Plugin>>();

  private Vector<Class<? extends Plugin>> pluginClassesTemporary = new Vector<Class<? extends Plugin>>();

  private Vector<Class<? extends RadioMedium>> radioMediumClasses = new Vector<Class<? extends RadioMedium>>();

  private Vector<Class<? extends IPDistributor>> ipDistributorClasses = new Vector<Class<? extends IPDistributor>>();

  private Vector<Class<? extends Positioner>> positionerClasses = new Vector<Class<? extends Positioner>>();

  private class HighlightObservable extends Observable {
    private void setChangedAndNotify(Mote mote) {
      setChanged();
      notifyObservers(mote);
    }
  }
  private HighlightObservable moteHighlightObservable = new HighlightObservable();

  private class MoteRelationsObservable extends Observable {
    private void setChangedAndNotify() {
      setChanged();
      notifyObservers();
    }
  }
  private MoteRelationsObservable moteRelationObservable = new MoteRelationsObservable();

  private JTextPane quickHelpTextPane;
  private JScrollPane quickHelpScroll;
  private Properties quickHelpProperties = null; /* quickhelp.txt */

  /**
   * Mote relation (directed).
   */
  public static class MoteRelation {
    public Mote source;
    public Mote dest;
    public MoteRelation(Mote source, Mote dest) {
      this.source = source;
      this.dest = dest;
    }
  }
  private ArrayList<MoteRelation> moteRelations = new ArrayList<MoteRelation>();

  /**
   * Creates a new COOJA Simulator GUI.
   *
   * @param desktop Desktop pane
   */
  public GUI(JDesktopPane desktop) {
    myGUI = this;
    mySimulation = null;
    myDesktopPane = desktop;
    if (menuPlugins == null) {
      menuPlugins = new JMenu("Plugins");
      menuPlugins.removeAll();

      /* COOJA/GUI plugins at top, simulation plugins in middle, mote plugins at bottom */
      menuPlugins.addSeparator();
      menuPlugins.addSeparator();
    }
    if (menuMotePluginClasses == null) {
      menuMotePluginClasses = new Vector<Class<? extends Plugin>>();
    }

    /* Help panel */
    quickHelpTextPane = new JTextPane();
    quickHelpTextPane.setContentType("text/html");
    quickHelpTextPane.setEditable(false);
    quickHelpTextPane.setVisible(false);
    quickHelpScroll = new JScrollPane(quickHelpTextPane, JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED, JScrollPane.HORIZONTAL_SCROLLBAR_NEVER);
    quickHelpScroll.setPreferredSize(new Dimension(200, 0));
    quickHelpScroll.setBorder(BorderFactory.createCompoundBorder(
        BorderFactory.createLineBorder(Color.GRAY), 
        BorderFactory.createEmptyBorder(0, 3, 0, 0)
    ));
    quickHelpScroll.setVisible(false);
    loadQuickHelp("KEYBOARD_SHORTCUTS");
    
    // Load default and overwrite with user settings (if any)
    loadExternalToolsDefaultSettings();
    loadExternalToolsUserSettings();
    if (specifiedContikiPath != null) {
      setExternalToolsSetting("PATH_CONTIKI", specifiedContikiPath);
    }

    /* Debugging - Break on repaints outside EDT */
    /*RepaintManager.setCurrentManager(new RepaintManager() {
      public void addDirtyRegion(JComponent comp, int a, int b, int c, int d) {
        if(!java.awt.EventQueue.isDispatchThread()) {
          throw new RuntimeException("Repainting outside EDT");
        }
        super.addDirtyRegion(comp, a, b, c, d);
      }
    });*/

    // Register default project directories
    String defaultProjectDirs = getExternalToolsSetting("DEFAULT_PROJECTDIRS", null);
    if (defaultProjectDirs != null && defaultProjectDirs.length() > 0) {
      String[] arr = defaultProjectDirs.split(";");
      for (String p : arr) {
        File projectDir = new File(p);
        currentProjectDirs.add(projectDir);
      }

      // Load extendable parts (using current project config)
      try {
        reparseProjectConfig();
      } catch (ParseProjectsException e) {
        if (isVisualized()) {
          JOptionPane.showMessageDialog(GUI.getTopParentContainer(),
              "Default projects could not load, reconfigure project directories:" +
              "\n\tMenu->Settings->Manage project directories" +
              "\n\nSee console for stack trace with more information.",
              "Project loading error", JOptionPane.ERROR_MESSAGE);
        }
        logger.fatal("Error when loading projects", e);
      }
    }

    // Start all standard GUI plugins
    for (Class<? extends Plugin> pluginClass : pluginClasses) {
      int pluginType = pluginClass.getAnnotation(PluginType.class).value();
      if (pluginType == PluginType.COOJA_STANDARD_PLUGIN) {
        tryStartPlugin(pluginClass, this, null, null);
      }
    }
  }


  /**
   * Add mote highlight observer.
   *
   * @see #deleteMoteHighlightObserver(Observer)
   * @param newObserver
   *          New observer
   */
  public void addMoteHighlightObserver(Observer newObserver) {
    moteHighlightObservable.addObserver(newObserver);
  }

  /**
   * Delete mote highlight observer.
   *
   * @see #addMoteHighlightObserver(Observer)
   * @param observer
   *          Observer to delete
   */
  public void deleteMoteHighlightObserver(Observer observer) {
    moteHighlightObservable.deleteObserver(observer);
  }

  /**
   * @return True if simulator is visualized
   */
  public static boolean isVisualized() {
    return isVisualizedInFrame() || isVisualizedInApplet();
  }

  public static Container getTopParentContainer() {
    if (isVisualizedInFrame()) {
      return frame;
    }

    if (isVisualizedInApplet()) {
      /* Find parent frame for applet */
      Container container = applet;
      while((container = container.getParent()) != null){
        if (container instanceof Frame) {
          return container;
        }
        if (container instanceof Dialog) {
          return container;
        }
        if (container instanceof Window) {
          return container;
        }
      }

      logger.fatal("Returning null top owner container");
    }

    return null;
  }

  public static boolean isVisualizedInFrame() {
    return frame != null;
  }

  public static URL getAppletCodeBase() {
    return applet.getCodeBase();
  }

  public static boolean isVisualizedInApplet() {
    return applet != null;
  }

  /**
   * Tries to create/remove simulator visualizer.
   *
   * @param visualized Visualized
   */
  public void setVisualizedInFrame(boolean visualized) {
    if (visualized) {
      if (!isVisualizedInFrame()) {
        configureFrame(myGUI, false);
      }
    } else {
      if (frame != null) {
        frame.setVisible(false);
        frame.dispose();
        frame = null;
      }
    }
  }

  public File getLastOpenedFile() {
    // Fetch current history
    String[] historyArray = getExternalToolsSetting("SIMCFG_HISTORY", "").split(";");
    return historyArray.length > 0 ? new File(historyArray[0]) : null;
  }

  public File[] getFileHistory() {
    // Fetch current history
    String[] historyArray = getExternalToolsSetting("SIMCFG_HISTORY", "").split(";");
    File[] history = new File[historyArray.length];
    for (int i = 0; i < historyArray.length; i++) {
      history[i] = new File(historyArray[i]);
    }
    return history;
  }

  public void addToFileHistory(File file) {
    // Fetch current history
    String[] history = getExternalToolsSetting("SIMCFG_HISTORY", "").split(";");
    String newFile = file.getAbsolutePath();
    if (history.length > 0 && history[0].equals(newFile)) {
      // File already added
      return;
    }
    // Create new history
    StringBuilder newHistory = new StringBuilder();
    newHistory.append(newFile);
    for (int i = 0, count = 1; i < history.length && count < 10; i++) {
      String historyFile = history[i];
      if (newFile.equals(historyFile) || historyFile.length() == 0) {
        // File already added or empty file name
      } else {
        newHistory.append(';').append(historyFile);
        count++;
      }
    }
    setExternalToolsSetting("SIMCFG_HISTORY", newHistory.toString());
    saveExternalToolsUserSettings();
    hasFileHistoryChanged = true;
  }

  private void updateOpenHistoryMenuItems() {
    if (isVisualizedInApplet()) {
      return;
    }
    if (!hasFileHistoryChanged) {
      // No need to update menu because file history has not changed
      return;
    }
    hasFileHistoryChanged = false;

    File[] openFilesHistory = getFileHistory();
    updateOpenHistoryMenuItems("confopen", menuConfOpenSimulation, openFilesHistory);
    updateOpenHistoryMenuItems("open", menuOpenSimulation, openFilesHistory);
  }

  private void updateOpenHistoryMenuItems(String type, JMenu menu, File[] openFilesHistory) {
    menu.removeAll();
    JMenuItem browseItem = new JMenuItem("Browse...");
    browseItem.setActionCommand(type + " sim");
    browseItem.addActionListener(guiEventHandler);
    menu.add(browseItem);
    menu.add(new JSeparator());

    String command = type + " last sim";
    int index = 0;
    JMenuItem lastItem;
    for (File file: openFilesHistory) {
      if (index < 10) {
        char mnemonic = (char) ('0' + (++index % 10));
        lastItem = new JMenuItem(mnemonic + " " + file.getName());
        lastItem.setMnemonic(mnemonic);
      } else {
        lastItem = new JMenuItem(file.getName());
      }
      lastItem.setActionCommand(command);
      lastItem.putClientProperty("file", file);
      lastItem.setToolTipText(file.getAbsolutePath());
      lastItem.addActionListener(guiEventHandler);
      menu.add(lastItem);
    }
  }

  /**
   * Enables/disables menues and menu items depending on whether a simulation is loaded etc.
   */
  private void updateGUIComponentState() {
    if (!isVisualized()) {
      return;
    }

    /* Update action state */
    for (GUIAction a: guiActions) {
      a.setEnabled(a.shouldBeEnabled());
    }
    
    /* Mote and mote type menues */
    if (menuMoteTypeClasses != null) {
      menuMoteTypeClasses.setEnabled(getSimulation() != null);
    }
    if (menuMoteTypes != null) {
      menuMoteTypes.setEnabled(getSimulation() != null);
    }
  }
  
  private JMenuBar createMenuBar() {
    JMenuBar menuBar = new JMenuBar();
    JMenu menu;
    JMenuItem menuItem;

    /* Prepare GUI actions */
    guiActions.add(newSimulationAction);
    guiActions.add(closeSimulationAction);
    guiActions.add(reloadSimulationAction);
    guiActions.add(reloadRandomSimulationAction);
    guiActions.add(saveSimulationAction);
    guiActions.add(closePluginsAction);
    guiActions.add(exitCoojaAction);
    guiActions.add(startStopSimulationAction);
    guiActions.add(removeAllMotesAction);
    guiActions.add(showBufferSettingsAction);
    
    /* File menu */
    menu = new JMenu("File");
    menu.addMenuListener(new MenuListener() {
      public void menuSelected(MenuEvent e) {
        updateGUIComponentState();
        updateOpenHistoryMenuItems();
      }
      public void menuDeselected(MenuEvent e) {
      }

      public void menuCanceled(MenuEvent e) {
      }
    });
    menu.setMnemonic(KeyEvent.VK_F);
    menuBar.add(menu);

    menu.add(new JMenuItem(newSimulationAction));

    menuItem = new JMenu("Reload simulation");
    menuItem.add(new JMenuItem(reloadSimulationAction));
    menuItem.add(new JMenuItem(reloadRandomSimulationAction));
    menu.add(menuItem);

    menu.add(new JMenuItem(closeSimulationAction));

    menuOpenSimulation = new JMenu("Open simulation");
    menuOpenSimulation.setMnemonic(KeyEvent.VK_O);
    menu.add(menuOpenSimulation);
    if (isVisualizedInApplet()) {
      menuOpenSimulation.setEnabled(false);
      menuOpenSimulation.setToolTipText("Not available in applet version");
    }

    menuConfOpenSimulation = new JMenu("Open & Reconfigure simulation");
    menuConfOpenSimulation.setMnemonic(KeyEvent.VK_R);
    menu.add(menuConfOpenSimulation);
    if (isVisualizedInApplet()) {
      menuConfOpenSimulation.setEnabled(false);
      menuConfOpenSimulation.setToolTipText("Not available in applet version");
    }
    hasFileHistoryChanged = true;

    menu.add(new JMenuItem(saveSimulationAction));

    menu.addSeparator();

    menu.add(new JMenuItem(closePluginsAction));

    menu.addSeparator();

    menu.add(new JMenuItem(exitCoojaAction));

    /* Simulation menu */
    menu = new JMenu("Simulation");
    menu.addMenuListener(new MenuListener() {
      public void menuSelected(MenuEvent e) {
        updateGUIComponentState();
      }
      public void menuDeselected(MenuEvent e) {
      }
      public void menuCanceled(MenuEvent e) {
      }
    });
    menu.setMnemonic(KeyEvent.VK_S);
    menuBar.add(menu);
    
    menu.add(new JMenuItem(startStopSimulationAction));

    GUIAction guiAction = new StartPluginGUIAction("Control panel");
    menuItem = new JMenuItem(guiAction);
    guiActions.add(guiAction);
    menuItem.setMnemonic(KeyEvent.VK_C);
    menuItem.putClientProperty("class", SimControl.class);
    menu.add(menuItem);

    guiAction = new StartPluginGUIAction("Information");
    menuItem = new JMenuItem(guiAction);
    guiActions.add(guiAction);
    menuItem.setMnemonic(KeyEvent.VK_I);
    menuItem.putClientProperty("class", SimInformation.class);
    menu.add(menuItem);

    // Mote type menu
    menu = new JMenu("Mote Types");
    menu.addMenuListener(new MenuListener() {
      public void menuSelected(MenuEvent e) {
        updateGUIComponentState();
      }
      public void menuDeselected(MenuEvent e) {
      }
      public void menuCanceled(MenuEvent e) {
      }
    });
    menu.setMnemonic(KeyEvent.VK_T);
    menuBar.add(menu);

    // Mote type classes sub menu
    menuMoteTypeClasses = new JMenu("Create mote type");
    menuMoteTypeClasses.setMnemonic(KeyEvent.VK_C);
    menuMoteTypeClasses.addMenuListener(new MenuListener() {
      public void menuSelected(MenuEvent e) {
        // Clear menu
        menuMoteTypeClasses.removeAll();

        // Recreate menu items
        JMenuItem menuItem;

        for (Class<? extends MoteType> moteTypeClass : moteTypeClasses) {
          /* Sort mote types according to abstraction level */
          String abstractionLevelDescription = GUI.getAbstractionLevelDescriptionOf(moteTypeClass);
          if(abstractionLevelDescription == null) {
            abstractionLevelDescription = "[unknown cross-level]";
          }

          /* Check if abstraction description already exists */
          JSeparator abstractionLevelSeparator = null;
          for (Component component: menuMoteTypeClasses.getMenuComponents()) {
            if (component == null || !(component instanceof JSeparator)) {
              continue;
            }
            JSeparator existing = (JSeparator) component;
            if (abstractionLevelDescription.equals(existing.getToolTipText())) {
              abstractionLevelSeparator = existing;
              break;
            }
          }
          if (abstractionLevelSeparator == null) {
            abstractionLevelSeparator = new JSeparator();
            abstractionLevelSeparator.setToolTipText(abstractionLevelDescription);
            menuMoteTypeClasses.add(abstractionLevelSeparator);
          }

          String description = GUI.getDescriptionOf(moteTypeClass);
          menuItem = new JMenuItem(description);
          menuItem.setActionCommand("create mote type");
          menuItem.putClientProperty("class", moteTypeClass);
          menuItem.setToolTipText(abstractionLevelDescription);
          menuItem.addActionListener(guiEventHandler);
          if (isVisualizedInApplet() && moteTypeClass.equals(ContikiMoteType.class)) {
            menuItem.setEnabled(false);
            menuItem.setToolTipText("Not available in applet version");
          }

          /* Add new item directly after cross level separator */
          for (int i=0; i < menuMoteTypeClasses.getMenuComponentCount(); i++) {
            if (menuMoteTypeClasses.getMenuComponent(i) == abstractionLevelSeparator) {
              menuMoteTypeClasses.add(menuItem, i+1);
              break;
            }
          }
        }
      }

      public void menuDeselected(MenuEvent e) {
      }

      public void menuCanceled(MenuEvent e) {
      }
    });
    menu.add(menuMoteTypeClasses);

    guiAction = new StartPluginGUIAction("Information");
    menuItem = new JMenuItem(guiAction);
    guiActions.add(guiAction);
    menuItem.putClientProperty("class", MoteTypeInformation.class);

    menu.add(menuItem);

    // Mote menu
    menu = new JMenu("Motes");
    menu.addMenuListener(new MenuListener() {
      public void menuSelected(MenuEvent e) {
        updateGUIComponentState();
      }
      public void menuDeselected(MenuEvent e) {
      }
      public void menuCanceled(MenuEvent e) {
      }
    });
    menu.setMnemonic(KeyEvent.VK_M);
    menuBar.add(menu);

    // Mote types sub menu
    menuMoteTypes = new JMenu("Add motes of type");
    menuMoteTypes.setMnemonic(KeyEvent.VK_A);
    menuMoteTypes.addMenuListener(new MenuListener() {
      public void menuSelected(MenuEvent e) {
        // Clear menu
        menuMoteTypes.removeAll();

        if (mySimulation == null) {
          return;
        }

        // Recreate menu items
        JMenuItem menuItem;

        for (MoteType moteType : mySimulation.getMoteTypes()) {
          menuItem = new JMenuItem(moteType.getDescription());
          menuItem.setActionCommand("add motes");
          menuItem.setToolTipText(getDescriptionOf(moteType.getClass()));
          menuItem.putClientProperty("motetype", moteType);
          menuItem.addActionListener(guiEventHandler);
          menuMoteTypes.add(menuItem);
        }
      }

      public void menuDeselected(MenuEvent e) {
      }

      public void menuCanceled(MenuEvent e) {
      }
    });
    menu.add(menuMoteTypes);

    menu.add(new JMenuItem(removeAllMotesAction));

    // Plugins menu
    if (menuPlugins == null) {
      menuPlugins = new JMenu("Plugins");
      menuPlugins.removeAll();

      /* COOJA/GUI plugins at top, simulation plugins in middle, mote plugins at bottom */
      menuPlugins.addSeparator();
      menuPlugins.addSeparator();
    } else {
      menuPlugins.setText("Plugins");
    }
    menuPlugins.setMnemonic(KeyEvent.VK_P);
    menuBar.add(menuPlugins);

    // Settings menu
    menu = new JMenu("Settings");
    menu.addMenuListener(new MenuListener() {
      public void menuSelected(MenuEvent e) {
        updateGUIComponentState();
      }
      public void menuDeselected(MenuEvent e) {
      }
      public void menuCanceled(MenuEvent e) {
      }
    });
    menuBar.add(menu);

    menuItem = new JMenuItem("External tools paths");
    menuItem.setActionCommand("edit paths");
    menuItem.addActionListener(guiEventHandler);
    menu.add(menuItem);
    if (isVisualizedInApplet()) {
      menuItem.setEnabled(false);
      menuItem.setToolTipText("Not available in applet version");
    }

    menuItem = new JMenuItem("Manage project directories");
    menuItem.setActionCommand("manage projects");
    menuItem.addActionListener(guiEventHandler);
    menu.add(menuItem);
    if (isVisualizedInApplet()) {
      menuItem.setEnabled(false);
      menuItem.setToolTipText("Not available in applet version");
    }

    menuItem = new JMenuItem("Compiler configuration wizard");
    menuItem.setActionCommand("configuration wizard");
    menuItem.addActionListener(guiEventHandler);
    menu.add(menuItem);
    if (isVisualizedInApplet()) {
      menuItem.setEnabled(false);
      menuItem.setToolTipText("Not available in applet version");
    }

    menu.add(new JMenuItem(showBufferSettingsAction));
    
    menu.addSeparator();

    menuItem = new JMenuItem("Java version: "
        + System.getProperty("java.version") + " ("
        + System.getProperty("java.vendor") + ")");
    menuItem.setEnabled(false);
    menu.add(menuItem);

    /* Help */
    menu = new JMenu("Help");
    menu.setMnemonic(KeyEvent.VK_H);
    menu.add(new JMenuItem(showKeyboardShortcutsAction));
    JCheckBoxMenuItem checkBox = new JCheckBoxMenuItem(showQuickHelpAction);
    showQuickHelpAction.putValue("checkbox", checkBox);
    menu.add(checkBox);
    menuBar.add(menu);

    // Mote plugins popup menu (not available via menu bar)
    if (menuMotePluginClasses == null) {
      menuMotePluginClasses = new Vector<Class<? extends Plugin>>();
    }
    return menuBar;
  }

  private static void configureFrame(final GUI gui, boolean createSimDialog) {

    if (frame == null) {
      frame = new JFrame("COOJA Simulator");
    }
    frame.setDefaultCloseOperation(JFrame.DO_NOTHING_ON_CLOSE);

    /* Menu bar */
    frame.setJMenuBar(gui.createMenuBar());

    /* Scrollable desktop */
    JComponent desktop = gui.getDesktopPane();
    desktop.setOpaque(true);

    JScrollPane scroll = new JScrollPane(desktop);
    scroll.setBorder(null);

    JPanel container = new JPanel(new BorderLayout());
    container.add(BorderLayout.CENTER, scroll);
    container.add(BorderLayout.EAST, gui.quickHelpScroll);
    frame.setContentPane(container);

    frame.setSize(700, 700);
    frame.setLocationRelativeTo(null);
    frame.addWindowListener(new WindowAdapter() {
      public void windowClosing(WindowEvent e) {
        gui.doQuit(true);
      }
    });
    frame.addComponentListener(new ComponentAdapter() {
      public void componentResized(ComponentEvent e) {
        updateDesktopSize(gui.getDesktopPane());
      }
    });

    /* Restore frame size and position */
    int framePosX = Integer.parseInt(getExternalToolsSetting("FRAME_POS_X", "0"));
    int framePosY = Integer.parseInt(getExternalToolsSetting("FRAME_POS_Y", "0"));
    int frameWidth = Integer.parseInt(getExternalToolsSetting("FRAME_WIDTH", "0"));
    int frameHeight = Integer.parseInt(getExternalToolsSetting("FRAME_HEIGHT", "0"));
    String frameScreen = getExternalToolsSetting("FRAME_SCREEN", "");

    /* Restore position to the same graphics device */
    GraphicsDevice device = null;
    GraphicsDevice all[] = GraphicsEnvironment.getLocalGraphicsEnvironment().getScreenDevices();
    for (GraphicsDevice gd : all) {
      if (gd.getIDstring().equals(frameScreen)) {
        device = gd;
      }
    }

    /* Check if frame should be maximized */
    if (device != null) {
      if (frameWidth == Integer.MAX_VALUE && frameHeight == Integer.MAX_VALUE) {
        frame.setLocation(device.getDefaultConfiguration().getBounds().getLocation());
        frame.setExtendedState(JFrame.MAXIMIZED_BOTH);
      } else if (frameWidth > 0 && frameHeight > 0) {

        /* Sanity-check: will Cooja be visible on screen? */
        boolean intersects =
          device.getDefaultConfiguration().getBounds().intersects(
              new Rectangle(framePosX, framePosY, frameWidth, frameHeight));

        if (intersects) {
          frame.setLocation(framePosX, framePosY);
          frame.setSize(frameWidth, frameHeight);
        }

      }
    }

    frame.setVisible(true);

    if (createSimDialog) {
      SwingUtilities.invokeLater(new Runnable() {
        public void run() {
          gui.doCreateSimulation(true);
        }
      });
    }
  }

  private static void configureApplet(final GUI gui, boolean createSimDialog) {
    applet = CoojaApplet.applet;

    // Add menu bar
    JMenuBar menuBar = gui.createMenuBar();
    applet.setJMenuBar(menuBar);

    JComponent newContentPane = gui.getDesktopPane();
    newContentPane.setOpaque(true);
    applet.setContentPane(newContentPane);
    applet.setSize(700, 700);

    if (createSimDialog) {
      SwingUtilities.invokeLater(new Runnable() {
        public void run() {
          gui.doCreateSimulation(true);
        }
      });
    }
  }

  /**
   * @return Current desktop pane (simulator visualizer)
   */
  public JDesktopPane getDesktopPane() {
    return myDesktopPane;
  }

  private static void setLookAndFeel() {

    JFrame.setDefaultLookAndFeelDecorated(true);
    JDialog.setDefaultLookAndFeelDecorated(true);

    ToolTipManager.sharedInstance().setDismissDelay(60000);

    /* Nimbus */
    try {
      String osName = System.getProperty("os.name").toLowerCase();
      if (osName.startsWith("linux")) {
        UIManager.setLookAndFeel(UIManager.getCrossPlatformLookAndFeelClassName());
      } else {
        UIManager.setLookAndFeel("com.sun.java.swing.plaf.nimbus.NimbusLookAndFeel");
      }
      return;
    } catch (Exception e) {
    }

    /* System */
    try {
      UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
      return;
    } catch (Exception e) {
    }
  }

  private static void updateDesktopSize(final JDesktopPane desktop) {
    if (desktop == null || !desktop.isVisible() || desktop.getParent() == null) {
      return;
    }

    Rectangle rect = desktop.getVisibleRect();
    Dimension pref = new Dimension(rect.width - 1, rect.height - 1);
    for (JInternalFrame frame : desktop.getAllFrames()) {
      if (pref.width < frame.getX() + frame.getWidth() - 20) {
        pref.width = frame.getX() + frame.getWidth();
      }
      if (pref.height < frame.getY() + frame.getHeight() - 20) {
        pref.height = frame.getY() + frame.getHeight();
      }
    }
    desktop.setPreferredSize(pref);
    desktop.revalidate();
  }

  private static JDesktopPane createDesktopPane() {
    final JDesktopPane desktop = new JDesktopPane() {
      public void setBounds(int x, int y, int w, int h) {
        super.setBounds(x, y, w, h);
        updateDesktopSize(this);
      }
      public void remove(Component c) {
        super.remove(c);
        updateDesktopSize(this);
      }
      public Component add(Component comp) {
        Component c = super.add(comp);
        updateDesktopSize(this);
        return c;
      }
    };
    desktop.setDesktopManager(new DefaultDesktopManager() {
      public void endResizingFrame(JComponent f) {
        super.endResizingFrame(f);
        updateDesktopSize(desktop);
      }
      public void endDraggingFrame(JComponent f) {
        super.endDraggingFrame(f);
        updateDesktopSize(desktop);
      }
    });
    desktop.setDragMode(JDesktopPane.OUTLINE_DRAG_MODE);
    return desktop;
  }

  private static Simulation quickStartSimulationConfig(File config, boolean vis) {
    logger.info("> Starting COOJA");
    JDesktopPane desktop = createDesktopPane();
    if (vis) {
      frame = new JFrame("COOJA Simulator");
    }
    GUI gui = new GUI(desktop);
    if (vis) {
      configureFrame(gui, false);
    }

    if (vis) {
      gui.doLoadConfig(false, true, config);
      return gui.getSimulation();
    } else {
      try {
        Simulation newSim = gui.loadSimulationConfig(config, true);
        if (newSim == null) {
          return null;
        }
        gui.setSimulation(newSim, false);
        return newSim;
      } catch (Exception e) {
        logger.fatal("Exception when loading simulation: ", e);
        return null;
      }
    }
  }

  /**
   * Allows user to create a simulation with a single mote type.
   *
   * @param source Contiki application file name
   * @return True if simulation was created
   */
  private static boolean quickStartSimulation(String source) {
    logger.info("> Starting COOJA");
    JDesktopPane desktop = createDesktopPane();
    frame = new JFrame("COOJA Simulator");
    GUI gui = new GUI(desktop);
    configureFrame(gui, false);

    logger.info("> Creating simulation");
    Simulation simulation = new Simulation(gui);
    simulation.setTitle("Quickstarted simulation: " + source);
    boolean simOK = CreateSimDialog.showDialog(GUI.getTopParentContainer(), simulation);
    if (!simOK) {
      logger.fatal("No simulation, aborting quickstart");
      System.exit(1);
    }
    gui.setSimulation(simulation, true);

    logger.info("> Creating mote type");
    ContikiMoteType moteType = new ContikiMoteType();
    moteType.setContikiSourceFile(new File(source));
    moteType.setDescription("Contiki Mote Type (" + source + ")");

    try {
      boolean compileOK = moteType.configureAndInit(GUI.getTopParentContainer(), simulation, true);
      if (!compileOK) {
        logger.fatal("Mote type initialization failed, aborting quickstart");
        return false;
      }
    } catch (MoteTypeCreationException e1) {
      logger.fatal("Mote type initialization failed, aborting quickstart");
      return false;
    }
    simulation.addMoteType(moteType);

    logger.info("> Adding motes");
    gui.doAddMotes(moteType);
    return true;
  }

  //// PROJECT CONFIG AND EXTENDABLE PARTS METHODS ////

  /**
   * Register new mote type class.
   *
   * @param moteTypeClass
   *          Class to register
   */
  public void registerMoteType(Class<? extends MoteType> moteTypeClass) {
    moteTypeClasses.add(moteTypeClass);
  }

  /**
   * Unregister all mote type classes.
   */
  public void unregisterMoteTypes() {
    moteTypeClasses.clear();
  }

  /**
   * @return All registered mote type classes
   */
  public Vector<Class<? extends MoteType>> getRegisteredMoteTypes() {
    return moteTypeClasses;
  }

  /**
   * Register new IP distributor class
   *
   * @param ipDistributorClass
   *          Class to register
   * @return True if class was registered
   */
  public boolean registerIPDistributor(
      Class<? extends IPDistributor> ipDistributorClass) {
    // Check that vector constructor exists
    try {
      ipDistributorClass.getConstructor(new Class[] { Vector.class });
    } catch (Exception e) {
      logger.fatal("No vector constructor found of IP distributor: "
          + ipDistributorClass);
      return false;
    }

    ipDistributorClasses.add(ipDistributorClass);
    return true;
  }

  /**
   * Unregister all IP distributors.
   */
  public void unregisterIPDistributors() {
    ipDistributorClasses.clear();
  }

  /**
   * @return All registered IP distributors
   */
  public Vector<Class<? extends IPDistributor>> getRegisteredIPDistributors() {
    return ipDistributorClasses;
  }

  /**
   * Register new positioner class.
   *
   * @param positionerClass
   *          Class to register
   * @return True if class was registered
   */
  public boolean registerPositioner(Class<? extends Positioner> positionerClass) {
    // Check that interval constructor exists
    try {
      positionerClass
      .getConstructor(new Class[] { int.class, double.class, double.class,
          double.class, double.class, double.class, double.class });
    } catch (Exception e) {
      logger.fatal("No interval constructor found of positioner: "
          + positionerClass);
      return false;
    }

    positionerClasses.add(positionerClass);
    return true;
  }

  /**
   * Unregister all positioner classes.
   */
  public void unregisterPositioners() {
    positionerClasses.clear();
  }

  /**
   * @return All registered positioner classes
   */
  public Vector<Class<? extends Positioner>> getRegisteredPositioners() {
    return positionerClasses;
  }

  /**
   * Register new radio medium class.
   *
   * @param radioMediumClass
   *          Class to register
   * @return True if class was registered
   */
  public boolean registerRadioMedium(
      Class<? extends RadioMedium> radioMediumClass) {
    // Check that simulation constructor exists
    try {
      radioMediumClass.getConstructor(new Class[] { Simulation.class });
    } catch (Exception e) {
      logger.fatal("No simulation constructor found of radio medium: "
          + radioMediumClass);
      return false;
    }

    radioMediumClasses.add(radioMediumClass);
    return true;
  }

  /**
   * Unregister all radio medium classes.
   */
  public void unregisterRadioMediums() {
    radioMediumClasses.clear();
  }

  /**
   * @return All registered radio medium classes
   */
  public Vector<Class<? extends RadioMedium>> getRegisteredRadioMediums() {
    return radioMediumClasses;
  }

  /**
   * Builds new project configuration using current project directories settings.
   * Reregisters mote types, plugins, IP distributors, positioners and radio
   * mediums. This method may still return true even if all classes could not be
   * registered, but always returns false if all project directory configuration
   * files were not parsed correctly.
   *
   * Any registered temporary plugins will be saved and reregistered.
   */
  public void reparseProjectConfig() throws ParseProjectsException {
    if (PROJECT_DEFAULT_CONFIG_FILENAME == null) {
      if (isVisualizedInApplet()) {
        PROJECT_DEFAULT_CONFIG_FILENAME = "/cooja_applet.config";
      } else {
        PROJECT_DEFAULT_CONFIG_FILENAME = "/cooja_default.config";
      }
    }

    // Backup temporary plugins
    Vector<Class<? extends Plugin>> oldTempPlugins = 
      (Vector<Class<? extends Plugin>>) pluginClassesTemporary.clone();

    // Reset current configuration
    unregisterMoteTypes();
    unregisterPlugins();
    unregisterIPDistributors();
    unregisterPositioners();
    unregisterRadioMediums();

    try {
      // Read default configuration
      projectConfig = new ProjectConfig(true);
    } catch (FileNotFoundException e) {
      logger.fatal("Could not find default project config file: "
          + PROJECT_DEFAULT_CONFIG_FILENAME);
      throw (ParseProjectsException) new ParseProjectsException(
          "Could not find default project config file: "
          + PROJECT_DEFAULT_CONFIG_FILENAME).initCause(e);
    } catch (IOException e) {
      logger.fatal("Error when reading default project config file: "
          + PROJECT_DEFAULT_CONFIG_FILENAME);
      throw (ParseProjectsException) new ParseProjectsException(
          "Error when reading default project config file: "
          + PROJECT_DEFAULT_CONFIG_FILENAME).initCause(e);
    }

    if (!isVisualizedInApplet()) {
      // Append project directory configurations
      for (File projectDir : currentProjectDirs) {
        try {
          // Append config to general config
          projectConfig.appendProjectDir(projectDir);
        } catch (FileNotFoundException e) {
          logger.fatal("Could not find project config file: " + projectDir);
          throw (ParseProjectsException) new ParseProjectsException(
              "Could not find project config file: " + projectDir).initCause(e);
        } catch (IOException e) {
          logger.fatal("Error when reading project config file: " + projectDir);
          throw (ParseProjectsException) new ParseProjectsException(
              "Error when reading project config file: " + projectDir).initCause(e);
        }
      }

      // Create class loader
      try {
        projectDirClassLoader = createClassLoader(currentProjectDirs);
      } catch (ClassLoaderCreationException e) {
        throw (ParseProjectsException) new ParseProjectsException(
        "Error when creating class loader").initCause(e);
      }
    } else {
      projectDirClassLoader = null;
    }

    // Register mote types
    String[] moteTypeClassNames = projectConfig.getStringArrayValue(GUI.class,
    "MOTETYPES");
    if (moteTypeClassNames != null) {
      for (String moteTypeClassName : moteTypeClassNames) {
        Class<? extends MoteType> moteTypeClass = tryLoadClass(this,
            MoteType.class, moteTypeClassName);

        if (moteTypeClass != null) {
          registerMoteType(moteTypeClass);
          // logger.info("Loaded mote type class: " + moteTypeClassName);
        } else {
          logger.warn("Could not load mote type class: " + moteTypeClassName);
        }
      }
    }

    // Register plugins
    registerPlugin(SimControl.class, false); // Not in menu
    registerPlugin(SimInformation.class, false); // Not in menu
    registerPlugin(MoteTypeInformation.class, false); // Not in menu
    String[] pluginClassNames = projectConfig.getStringArrayValue(GUI.class,
    "PLUGINS");
    if (pluginClassNames != null) {
      for (String pluginClassName : pluginClassNames) {
        Class<? extends Plugin> pluginClass = tryLoadClass(this, Plugin.class,
            pluginClassName);

        if (pluginClass != null) {
          registerPlugin(pluginClass);
          // logger.info("Loaded plugin class: " + pluginClassName);
        } else {
          logger.warn("Could not load plugin class: " + pluginClassName);
        }
      }
    }

    // Reregister temporary plugins again
    if (oldTempPlugins != null) {
      for (Class<? extends Plugin> pluginClass : oldTempPlugins) {
        if (registerTemporaryPlugin(pluginClass)) {
          // logger.info("Reregistered temporary plugin class: " +
          // getDescriptionOf(pluginClass));
        } else {
          logger.warn("Could not reregister temporary plugin class: "
              + getDescriptionOf(pluginClass));
        }
      }
    }

    // Register IP distributors
    String[] ipDistClassNames = projectConfig.getStringArrayValue(GUI.class,
    "IP_DISTRIBUTORS");
    if (ipDistClassNames != null) {
      for (String ipDistClassName : ipDistClassNames) {
        Class<? extends IPDistributor> ipDistClass = tryLoadClass(this,
            IPDistributor.class, ipDistClassName);

        if (ipDistClass != null) {
          registerIPDistributor(ipDistClass);
          // logger.info("Loaded IP distributor class: " + ipDistClassName);
        } else {
          logger
          .warn("Could not load IP distributor class: " + ipDistClassName);
        }
      }
    }

    // Register positioners
    String[] positionerClassNames = projectConfig.getStringArrayValue(
        GUI.class, "POSITIONERS");
    if (positionerClassNames != null) {
      for (String positionerClassName : positionerClassNames) {
        Class<? extends Positioner> positionerClass = tryLoadClass(this,
            Positioner.class, positionerClassName);

        if (positionerClass != null) {
          registerPositioner(positionerClass);
          // logger.info("Loaded positioner class: " + positionerClassName);
        } else {
          logger
          .warn("Could not load positioner class: " + positionerClassName);
        }
      }
    }

    // Register radio mediums
    String[] radioMediumsClassNames = projectConfig.getStringArrayValue(
        GUI.class, "RADIOMEDIUMS");
    if (radioMediumsClassNames != null) {
      for (String radioMediumClassName : radioMediumsClassNames) {
        Class<? extends RadioMedium> radioMediumClass = tryLoadClass(this,
            RadioMedium.class, radioMediumClassName);

        if (radioMediumClass != null) {
          registerRadioMedium(radioMediumClass);
          // logger.info("Loaded radio medium class: " + radioMediumClassName);
        } else {
          logger.warn("Could not load radio medium class: "
              + radioMediumClassName);
        }
      }
    }

  }

  /**
   * Returns the current project configuration common to the entire simulator.
   *
   * @return Current project configuration
   */
  public ProjectConfig getProjectConfig() {
    return projectConfig;
  }

  /**
   * Returns the current project directories common to the entire simulator.
   *
   * @return Current project directories.
   */
  public Vector<File> getProjectDirs() {
    return currentProjectDirs;
  }

  // // PLUGIN METHODS ////

  /**
   * Show a started plugin in working area.
   *
   * @param plugin Plugin
   */
  public void showPlugin(final Plugin plugin) {
    new RunnableInEDT<Boolean>() {
      public Boolean work() {
        JInternalFrame pluginFrame = plugin.getGUI();
        if (pluginFrame == null) {
          logger.fatal("Failed trying to show plugin without visualizer!");
          return false;
        }

        int nrFrames = myDesktopPane.getAllFrames().length;
        myDesktopPane.add(pluginFrame);

        /* Set size if not already specified by plugin */
        if (pluginFrame.getWidth() <= 0 || pluginFrame.getHeight() <= 0) {
          pluginFrame.setSize(FRAME_STANDARD_WIDTH, FRAME_STANDARD_HEIGHT);
        }

        /* Set location if not already visible */
        if (pluginFrame.getLocation().x <= 0 && pluginFrame.getLocation().y <= 0) {
          pluginFrame.setLocation(
              nrFrames * FRAME_NEW_OFFSET,
              nrFrames * FRAME_NEW_OFFSET);
        }

        pluginFrame.setVisible(true);

        /* Select plugin */
        try {
          for (JInternalFrame existingPlugin : myDesktopPane.getAllFrames()) {
            existingPlugin.setSelected(false);
          }
          pluginFrame.setSelected(true);
        } catch (Exception e) { }
        myDesktopPane.moveToFront(pluginFrame);

        return true;
      }
    }.invokeAndWait();
  }

  /**
   * Close all mote plugins for given mote.
   *
   * @param mote Mote
   */
  public void closeMotePlugins(Mote mote) {
    Vector<Plugin> pluginsToRemove = new Vector<Plugin>();

    for (Plugin startedPlugin : startedPlugins) {
      int pluginType = startedPlugin.getClass().getAnnotation(PluginType.class).value();

      if (pluginType != PluginType.MOTE_PLUGIN) {
        continue;
      }

      Mote pluginMote = (Mote) startedPlugin.getTag();
      if (pluginMote == mote) {
        pluginsToRemove.add(startedPlugin);
      }
    }

    for (Plugin pluginToRemove: pluginsToRemove) {
      removePlugin(pluginToRemove, false);
    }
  }

  /**
   * Remove a plugin from working area.
   *
   * @param plugin
   *          Plugin to remove
   * @param askUser
   *          If plugin is the last one, ask user if we should remove current
   *          simulation also?
   */
  public void removePlugin(final Plugin plugin, final boolean askUser) {
    new RunnableInEDT<Boolean>() {
      public Boolean work() {
        /* Free resources */
        plugin.closePlugin();
        startedPlugins.remove(plugin);
        updateGUIComponentState();

        /* Dispose visualized components */
        if (plugin.getGUI() != null) {
          plugin.getGUI().dispose();
        }

        /* (OPTIONAL) Remove simulation if all plugins are closed */
        if (getSimulation() != null && askUser && startedPlugins.isEmpty()) {
          doRemoveSimulation(true);
        }

        return true;
      }
    }.invokeAndWait();
  }

  /**
   * Same as the {@link #startPlugin(Class, GUI, Simulation, Mote)} method,
   * but does not throw exceptions. If COOJA is visualised, an error dialog
   * is shown if plugin could not be started.
   * 
   * @see #startPlugin(Class, GUI, Simulation, Mote)
   * @param pluginClass Plugin class
   * @param argGUI Plugin GUI argument
   * @param argSimulation Plugin simulation argument
   * @param argMote Plugin mote argument
   * @return Started plugin
   */
  public Plugin tryStartPlugin(final Class<? extends Plugin> pluginClass,
      final GUI argGUI, final Simulation argSimulation, final Mote argMote) {
    try {
      return startPlugin(pluginClass, argGUI, argSimulation, argMote);
    } catch (PluginConstructionException ex) {
      if (GUI.isVisualized()) {
        GUI.showErrorDialog(GUI.getTopParentContainer(), "Error when starting plugin", ex, false);
      } else {
        /* If the plugin requires visualization, inform user */
        Throwable cause = ex.getCause();
        do {
          if (cause instanceof PluginRequiresVisualizationException) {
            logger.info("Visualized plugin was not started: " + pluginClass);
            return null;
          }
        } while ((cause=cause.getCause()) != null);
        
        logger.fatal("Error when starting plugin", ex);
      }
    }
    return null;
  }

  /**
   * Starts given plugin. If visualized, the plugin is also shown.
   * 
   * @see PluginType
   * @param pluginClass Plugin class
   * @param argGUI Plugin GUI argument
   * @param argSimulation Plugin simulation argument
   * @param argMote Plugin mote argument
   * @return Started plugin
   * @throws PluginConstructionException At errors
   */
  public Plugin startPlugin(final Class<? extends Plugin> pluginClass,
      final GUI argGUI, final Simulation argSimulation, final Mote argMote)
  throws PluginConstructionException
  {

    // Check that plugin class is registered
    if (!pluginClasses.contains(pluginClass)) {
      throw new PluginConstructionException("Plugin class not registered: " + pluginClass);
    }

    // Construct plugin depending on plugin type
    int pluginType = pluginClass.getAnnotation(PluginType.class).value();
    Plugin plugin;

    try {
      if (pluginType == PluginType.MOTE_PLUGIN) {
        if (argGUI == null) {
          throw new PluginConstructionException("No GUI argument for mote plugin");
        }
        if (argSimulation == null) {
          throw new PluginConstructionException("No simulation argument for mote plugin");
        }
        if (argMote == null) {
          throw new PluginConstructionException("No mote argument for mote plugin");
        }

        plugin = 
          pluginClass.getConstructor(new Class[] { Mote.class, Simulation.class, GUI.class })
          .newInstance(argMote, argSimulation, argGUI);

        /* Tag plugin with mote */
        plugin.tagWithObject(argMote);

      } else if (pluginType == PluginType.SIM_PLUGIN
          || pluginType == PluginType.SIM_STANDARD_PLUGIN) {
        if (argGUI == null) {
          throw new PluginConstructionException("No GUI argument for simulation plugin");
        }
        if (argSimulation == null) {
          throw new PluginConstructionException("No simulation argument for simulation plugin");
        }

        plugin = 
          pluginClass.getConstructor(new Class[] { Simulation.class, GUI.class })
          .newInstance(argSimulation, argGUI);

      } else if (pluginType == PluginType.COOJA_PLUGIN
          || pluginType == PluginType.COOJA_STANDARD_PLUGIN) {
        if (argGUI == null) {
          throw new PluginConstructionException("No GUI argument for GUI plugin");
        }

        plugin =
          pluginClass.getConstructor(new Class[] { GUI.class })
          .newInstance(argGUI);

      } else {
        throw new PluginConstructionException("Bad plugin type: " + pluginType);
      }
    } catch (PluginRequiresVisualizationException e) {
      PluginConstructionException ex = new PluginConstructionException("Plugin class requires visualization: " + pluginClass.getName());
      ex.initCause(e);
      throw ex;
    } catch (Exception e) {
      PluginConstructionException ex = new PluginConstructionException("Construction error for plugin of class: " + pluginClass.getName());
      ex.initCause(e);
      throw ex;
    }

    // Add to active plugins list
    startedPlugins.add(plugin);
    updateGUIComponentState();

    // Show plugin if visualizer type
    if (plugin.getGUI() != null) {
      myGUI.showPlugin(plugin);
    }

    return plugin;
  }

  /**
   * Register a plugin to be included in the GUI. The plugin will be visible in
   * the menubar.
   *
   * @param newPluginClass
   *          New plugin to register
   * @return True if this plugin was registered ok, false otherwise
   */
  public boolean registerPlugin(Class<? extends Plugin> newPluginClass) {
    return registerPlugin(newPluginClass, true);
  }

  /**
   * Register a temporary plugin to be included in the GUI. The plugin will be
   * visible in the menubar. This plugin will automatically be unregistered if
   * the current simulation is removed.
   *
   * @param newPluginClass
   *          New plugin to register
   * @return True if this plugin was registered ok, false otherwise
   */
  public boolean registerTemporaryPlugin(Class<? extends Plugin> newPluginClass) {
    if (pluginClasses.contains(newPluginClass)) {
      return false;
    }

    boolean returnVal = registerPlugin(newPluginClass, true);
    if (!returnVal) {
      return false;
    }

    pluginClassesTemporary.add(newPluginClass);
    return true;
  }

  /**
   * Unregister a plugin class. Removes any plugin menu items links as well.
   *
   * @param pluginClass
   *          Plugin class to unregister
   */
  public void unregisterPlugin(Class<? extends Plugin> pluginClass) {

    // Remove (if existing) plugin class menu items
    for (Component menuComponent : menuPlugins.getMenuComponents()) {
      if (menuComponent.getClass().isAssignableFrom(JMenuItem.class)) {
        JMenuItem menuItem = (JMenuItem) menuComponent;
        if (menuItem.getClientProperty("class").equals(pluginClass)) {
          menuPlugins.remove(menuItem);
        }
      }
    }
    if (menuMotePluginClasses.contains(pluginClass)) {
      menuMotePluginClasses.remove(pluginClass);
    }

    // Remove from plugin vectors (including temporary)
    if (pluginClasses.contains(pluginClass)) {
      pluginClasses.remove(pluginClass);
    }
    if (pluginClassesTemporary.contains(pluginClass)) {
      pluginClassesTemporary.remove(pluginClass);
    }
  }

  /**
   * Register a plugin to be included in the GUI.
   *
   * @param newPluginClass
   *          New plugin to register
   * @param addToMenu
   *          Should this plugin be added to the dedicated plugins menubar?
   * @return True if this plugin was registered ok, false otherwise
   */
  private boolean registerPlugin(final Class<? extends Plugin> newPluginClass,
      boolean addToMenu) {

    // Get description annotation (if any)
    final String description = getDescriptionOf(newPluginClass);

    // Get plugin type annotation (required)
    final int pluginType;
    if (newPluginClass.isAnnotationPresent(PluginType.class)) {
      pluginType = newPluginClass.getAnnotation(PluginType.class).value();
    } else {
      pluginType = PluginType.UNDEFINED_PLUGIN;
    }

    // Check that plugin type is valid and constructor exists
    try {
      if (pluginType == PluginType.MOTE_PLUGIN) {
        newPluginClass.getConstructor(new Class[] { Mote.class,
            Simulation.class, GUI.class });
      } else if (pluginType == PluginType.SIM_PLUGIN
          || pluginType == PluginType.SIM_STANDARD_PLUGIN) {
        newPluginClass.getConstructor(new Class[] { Simulation.class, GUI.class });
      } else if (pluginType == PluginType.COOJA_PLUGIN
          || pluginType == PluginType.COOJA_STANDARD_PLUGIN) {
        newPluginClass.getConstructor(new Class[] { GUI.class });
      } else {
        logger.fatal("Could not find valid plugin type annotation in class " + newPluginClass);
        return false;
      }
    } catch (NoSuchMethodException e) {
      logger.fatal("Could not find valid constructor in class " + newPluginClass + ": " + e);
      return false;
    }

    if (addToMenu && menuPlugins != null) {
      new RunnableInEDT<Boolean>() {
        public Boolean work() {
          // Create 'start plugin'-menu item
          JMenuItem menuItem;
          String tooltip = "<html>";
          
          /* Sort menu according to plugin type */
          int itemIndex=0;
          if (pluginType == PluginType.COOJA_PLUGIN || pluginType == PluginType.COOJA_STANDARD_PLUGIN) {
            for (; itemIndex < menuPlugins.getItemCount(); itemIndex++) {
              if (menuPlugins.getItem(itemIndex) == null /* separator */) {
                break;
              }
            }
            tooltip += "COOJA plugin: " + newPluginClass.getName();
            menuItem = new JMenuItem(description);
            menuItem.addActionListener(new ActionListener() {
              public void actionPerformed(ActionEvent e) {
                tryStartPlugin(newPluginClass, myGUI, null, null);
              }
            });
          } else if (pluginType == PluginType.SIM_PLUGIN || pluginType == PluginType.SIM_STANDARD_PLUGIN) {
            for (; itemIndex < menuPlugins.getItemCount(); itemIndex++) {
              if (menuPlugins.getItem(itemIndex) == null /* separator */) {
                break;
              }
            }
            itemIndex++;
            for (; itemIndex < menuPlugins.getItemCount(); itemIndex++) {
              if (menuPlugins.getItem(itemIndex) == null /* separator */) {
                break;
              }
            }
            tooltip += "Simulation plugin: " + newPluginClass.getName();
            GUIAction guiAction = new StartPluginGUIAction(description);
            menuItem = new JMenuItem(guiAction);
            guiActions.add(guiAction);
          } else if (pluginType == PluginType.MOTE_PLUGIN) {
            // Disable previous menu item and add new item to mote plugins menu
            menuItem = new JMenuItem(description);
            menuItem.setEnabled(false);
            tooltip += "Mote plugin: " + newPluginClass.getName();
            tooltip += "<br>Start mote plugins by right-clicking a mote in the simulation visualizer";
            menuMotePluginClasses.add(newPluginClass);
            itemIndex = menuPlugins.getItemCount();
          } else {
            logger.warn("Unknown plugin type: " + pluginType);
            return false;
          }

          /* Check if plugin was imported by a project directory */
          File project =
            getProjectConfig().getUserProjectDefining(GUI.class, "PLUGINS", newPluginClass.getName());
          if (project != null) {
            tooltip += "<br>Loaded by project: " + project.getPath();
          }

          tooltip += "</html>";
          menuItem.setToolTipText(tooltip);
          menuItem.putClientProperty("class", newPluginClass);

          menuPlugins.add(menuItem, itemIndex);
          return true;
        }
      }.invokeAndWait();
    }

    pluginClasses.add(newPluginClass);
    return true;
  }

  /**
   * Unregister all plugin classes, including temporary plugins.
   */
  public void unregisterPlugins() {
    if (menuPlugins != null) {
      menuPlugins.removeAll();

      /* COOJA/GUI plugins at top, simulation plugins in middle, mote plugins at bottom */
      menuPlugins.addSeparator();
      menuPlugins.addSeparator();
    }
    if (menuMotePluginClasses != null) {
      menuMotePluginClasses.clear();
    }
    pluginClasses.clear();
    pluginClassesTemporary.clear();
  }

  /**
   * Return a mote plugins submenu for given mote.
   *
   * @param mote Mote
   * @return Mote plugins menu
   */
  public JMenu createMotePluginsSubmenu(Mote mote) {
    JMenu menuMotePlugins = new JMenu("Open mote plugin for " + mote);

    for (Class<? extends Plugin> motePluginClass: menuMotePluginClasses) {
      GUIAction guiAction = new StartPluginGUIAction(getDescriptionOf(motePluginClass));
      JMenuItem menuItem = new JMenuItem(guiAction);
      menuItem.putClientProperty("class", motePluginClass);
      menuItem.putClientProperty("mote", mote);
      menuMotePlugins.add(menuItem);
    }
    return menuMotePlugins;
  }

  // // GUI CONTROL METHODS ////

  /**
   * @return Current simulation
   */
  public Simulation getSimulation() {
    return mySimulation;
  }

  public void setSimulation(Simulation sim, boolean startPlugins) {
    if (sim != null) {
      doRemoveSimulation(false);
    }
    mySimulation = sim;
    updateGUIComponentState();

    // Set frame title
    if (frame != null) {
      frame.setTitle("COOJA Simulator" + " - " + sim.getTitle());
    }

    // Open standard plugins (if none opened already)
    if (startPlugins) {
      for (Class<? extends Plugin> pluginClass : pluginClasses) {
        int pluginType = pluginClass.getAnnotation(PluginType.class).value();
        if (pluginType == PluginType.SIM_STANDARD_PLUGIN) {
          tryStartPlugin(pluginClass, this, sim, null);
        }
      }
    }

    setChanged();
    notifyObservers();
  }

  /**
   * Creates a new mote type of the given mote type class.
   *
   * @param moteTypeClass
   *          Mote type class
   */
  public void doCreateMoteType(Class<? extends MoteType> moteTypeClass) {
    if (mySimulation == null) {
      logger.fatal("Can't create mote type (no simulation)");
      return;
    }

    // Stop simulation (if running)
    mySimulation.stopSimulation();

    // Create mote type
    MoteType newMoteType = null;
    boolean moteTypeOK = false;
    try {
      newMoteType = moteTypeClass.newInstance();
      moteTypeOK = newMoteType.configureAndInit(GUI.getTopParentContainer(), mySimulation, isVisualized());
    } catch (Exception e) {
      logger.fatal("Exception when creating mote type", e);
      if (isVisualized()) {
        showErrorDialog(getTopParentContainer(), "Mote type creation error", e, false);
      }
      return;
    }

    // Add mote type to simulation
    if (moteTypeOK) {
      mySimulation.addMoteType(newMoteType);

      /* Allow user to immediately add motes */
      doAddMotes(newMoteType);
    }
  }

  /**
   * Remove current simulation
   *
   * @param askForConfirmation
   *          Should we ask for confirmation if a simulation is already active?
   * @return True if no simulation exists when method returns
   */
  public boolean doRemoveSimulation(boolean askForConfirmation) {

    if (mySimulation == null) {
      return true;
    }

    if (askForConfirmation) {
      boolean ok = new RunnableInEDT<Boolean>() {
        public Boolean work() {
          String s1 = "Remove";
          String s2 = "Cancel";
          Object[] options = { s1, s2 };
          int n = JOptionPane.showOptionDialog(GUI.getTopParentContainer(),
              "You have an active simulation.\nDo you want to remove it?",
              "Remove current simulation?", JOptionPane.YES_NO_OPTION,
              JOptionPane.QUESTION_MESSAGE, null, options, s1);
          if (n != JOptionPane.YES_OPTION) {
            return false;
          }
          return true;
        }
      }.invokeAndWait();

      if (!ok) {
        return false;
      }
    }

    // Close all started non-GUI plugins
    for (Object startedPlugin : startedPlugins.toArray()) {
      int pluginType = startedPlugin.getClass().getAnnotation(PluginType.class).value();
      if (pluginType != PluginType.COOJA_PLUGIN
          && pluginType != PluginType.COOJA_STANDARD_PLUGIN) {
        removePlugin((Plugin) startedPlugin, false);
      }
    }

    // Delete simulation
    mySimulation.deleteObservers();
    mySimulation.stopSimulation();

    /* Clear current mote relations */
    MoteRelation relations[] = getMoteRelations();
    for (MoteRelation r: relations) {
      removeMoteRelation(r.source, r.dest);
    }

    mySimulation = null;
    updateGUIComponentState();

    // Unregister temporary plugin classes
    Class<? extends Plugin>[] pluginClasses =
      new Class[pluginClassesTemporary.size()];
    pluginClassesTemporary.toArray(pluginClasses);
    for (Class<? extends Plugin> pClass: pluginClasses) {
      unregisterPlugin(pClass);
    }

    // Reset frame title
    if (isVisualizedInFrame()) {
      frame.setTitle("COOJA Simulator");
    }

    setChanged();
    notifyObservers();

    return true;
  }

  /**
   * Load a simulation configuration file from disk
   *
   * @param askForConfirmation Ask for confirmation before removing any current simulation
   * @param quick Quick-load simulation
   * @param configFile Configuration file to load, if null a dialog will appear
   */
  public void doLoadConfig(boolean askForConfirmation, final boolean quick, File configFile) {
    if (isVisualizedInApplet()) {
      return;
    }

    /* Remove current simulation */
    if (!doRemoveSimulation(true)) {
      return;
    }

    /* Use provided configuration, or open File Chooser */
    if (configFile != null && !configFile.isDirectory()) {
      if (!configFile.exists() || !configFile.canRead()) {
        logger.fatal("No read access to file: " + configFile.getAbsolutePath());
        /* File does not exist, open dialog */
        doLoadConfig(askForConfirmation, quick, null);
        return;
      }
    } else {
      final File suggestedFile = configFile;
      configFile = new RunnableInEDT<File>() {
        public File work() {
          JFileChooser fc = new JFileChooser();

          fc.setFileFilter(GUI.SAVED_SIMULATIONS_FILES);

          if (suggestedFile != null && suggestedFile.isDirectory()) {
            fc.setCurrentDirectory(suggestedFile);
          } else {
            /* Suggest file using file history */
            File suggestedFile = getLastOpenedFile();
            if (suggestedFile != null) {
              fc.setSelectedFile(suggestedFile);
            }
          }

          int returnVal = fc.showOpenDialog(GUI.getTopParentContainer());
          if (returnVal != JFileChooser.APPROVE_OPTION) {
            logger.info("Load command cancelled by user...");
            return null;
          }

          File file = fc.getSelectedFile();

          if (!file.exists()) {
            /* Try default file extension */
            file = new File(file.getParent(), file.getName() + SAVED_SIMULATIONS_FILES);
          }

          if (!file.exists() || !file.canRead()) {
            logger.fatal("No read access to file");
            return null;
          }

          return file;
        }
      }.invokeAndWait();

      if (configFile == null) {
        return;
      }
    }

    final JDialog progressDialog;
    final String progressTitle = configFile == null
    ? "Loading" : ("Loading " + configFile.getAbsolutePath());

    if (quick) {
      final Thread loadThread = Thread.currentThread();

      progressDialog = new RunnableInEDT<JDialog>() {
        public JDialog work() {
          final JDialog progressDialog;

          if (GUI.getTopParentContainer() instanceof Window) {
            progressDialog = new JDialog((Window) GUI.getTopParentContainer(), progressTitle, ModalityType.APPLICATION_MODAL);
          } else if (GUI.getTopParentContainer() instanceof Frame) {
            progressDialog = new JDialog((Frame) GUI.getTopParentContainer(), progressTitle, ModalityType.APPLICATION_MODAL);
          } else if (GUI.getTopParentContainer() instanceof Dialog) {
            progressDialog = new JDialog((Dialog) GUI.getTopParentContainer(), progressTitle, ModalityType.APPLICATION_MODAL);
          } else {
            logger.warn("No parent container");
            progressDialog = new JDialog((Frame) null, progressTitle, ModalityType.APPLICATION_MODAL);
          }

          JPanel progressPanel = new JPanel(new BorderLayout());
          JProgressBar progressBar;
          JButton button;

          progressBar = new JProgressBar(0, 100);
          progressBar.setValue(0);
          progressBar.setIndeterminate(true);

          PROGRESS_BAR = progressBar; /* Allow various parts of COOJA to show messages */

          button = new JButton("Cancel");
          button.addActionListener(new ActionListener() {
            public void actionPerformed(ActionEvent e) {
              if (loadThread.isAlive()) {
                loadThread.interrupt();
                doRemoveSimulation(false);
              }
              if (progressDialog.isDisplayable()) {
                progressDialog.dispose();
              }
            }
          });

          progressPanel.add(BorderLayout.CENTER, progressBar);
          progressPanel.add(BorderLayout.SOUTH, button);
          progressPanel.setBorder(BorderFactory.createEmptyBorder(20, 20, 20, 20));

          progressPanel.setVisible(true);

          progressDialog.getContentPane().add(progressPanel);
          progressDialog.setSize(400, 200);

          progressDialog.getRootPane().setDefaultButton(button);
          progressDialog.setLocationRelativeTo(GUI.getTopParentContainer());
          progressDialog.setDefaultCloseOperation(JDialog.DO_NOTHING_ON_CLOSE);

          java.awt.EventQueue.invokeLater(new Runnable() {
            public void run() {
              progressDialog.setVisible(true);
            }
          });

          return progressDialog;
        }
      }.invokeAndWait();
    } else {
      progressDialog = null;
    }

    // Load simulation in this thread, while showing progress monitor
    final File fileToLoad = configFile;
    Simulation newSim = null;
    boolean shouldRetry = false;
    do {
      try {
        shouldRetry = false;
        myGUI.doRemoveSimulation(false);
        newSim = loadSimulationConfig(fileToLoad, quick);
        myGUI.setSimulation(newSim, false);
        addToFileHistory(fileToLoad);
      } catch (UnsatisfiedLinkError e) {
        shouldRetry = showErrorDialog(GUI.getTopParentContainer(), "Simulation load error", e, true);
      } catch (SimulationCreationException e) {
        shouldRetry = showErrorDialog(GUI.getTopParentContainer(), "Simulation load error", e, true);
      }
    } while (shouldRetry);

    if (progressDialog != null && progressDialog.isDisplayable()) {
      progressDialog.dispose();
    }
    return;
  }

  /**
   * Reload currently configured simulation.
   * Reloading a simulation may include recompiling Contiki.
   *
   * @param autoStart Start executing simulation when loaded
   * @param newSeed Change simulation seed
   */
  public void reloadCurrentSimulation(final boolean autoStart, final long randomSeed) {
    if (getSimulation() == null) {
      logger.fatal("No simulation to reload");
      return;
    }

    final JDialog progressDialog = new JDialog(frame, "Reloading", true);
    final Thread loadThread = new Thread(new Runnable() {
      public void run() {

        /* Get current simulation configuration */
        Element root = new Element("simconf");
        Element simulationElement = new Element("simulation");

        simulationElement.addContent(getSimulation().getConfigXML());
        root.addContent(simulationElement);
        Collection<Element> pluginsConfig = getPluginsConfigXML();
        if (pluginsConfig != null) {
          root.addContent(pluginsConfig);
        }

        /* Remove current simulation, and load config */
        boolean shouldRetry = false;
        do {
          try {
            shouldRetry = false;
            myGUI.doRemoveSimulation(false);
            Simulation newSim = loadSimulationConfig(root, true, new Long(randomSeed));
            myGUI.setSimulation(newSim, false);

            if (autoStart) {
              newSim.startSimulation();
            }
          } catch (UnsatisfiedLinkError e) {
            shouldRetry = showErrorDialog(frame, "Simulation reload error", e, true);

            myGUI.doRemoveSimulation(false);
          } catch (SimulationCreationException e) {
            shouldRetry = showErrorDialog(frame, "Simulation reload error", e, true);

            myGUI.doRemoveSimulation(false);
          }
        } while (shouldRetry);

        if (progressDialog.isDisplayable()) {
          progressDialog.dispose();
        }
      }
    });

    // Display progress dialog while reloading
    JProgressBar progressBar = new JProgressBar(0, 100);
    progressBar.setValue(0);
    progressBar.setIndeterminate(true);

    PROGRESS_BAR = progressBar; /* Allow various parts of COOJA to show messages */

    JButton button = new JButton("Cancel");
    button.addActionListener(new ActionListener() {
      public void actionPerformed(ActionEvent e) {
        if (loadThread.isAlive()) {
          loadThread.interrupt();
          doRemoveSimulation(false);
        }
        if (progressDialog.isDisplayable()) {
          progressDialog.dispose();
        }
      }
    });

    JPanel progressPanel = new JPanel(new BorderLayout());
    progressPanel.add(BorderLayout.CENTER, progressBar);
    progressPanel.add(BorderLayout.SOUTH, button);
    progressPanel.setBorder(BorderFactory.createEmptyBorder(20, 20, 20, 20));

    progressPanel.setVisible(true);

    progressDialog.getContentPane().add(progressPanel);
    progressDialog.setSize(400, 200);

    progressDialog.getRootPane().setDefaultButton(button);
    progressDialog.setLocationRelativeTo(frame);
    progressDialog.setDefaultCloseOperation(JDialog.DO_NOTHING_ON_CLOSE);

    loadThread.start();
    progressDialog.setVisible(true);
  }

  /**
   * Reload currently configured simulation.
   * Reloading a simulation may include recompiling Contiki.
   * The same random seed is used.
   *
   * @see #reloadCurrentSimulation(boolean, boolean)
   * @param autoStart Start executing simulation when loaded
   */
  public void reloadCurrentSimulation(boolean autoStart) {
    reloadCurrentSimulation(autoStart, getSimulation().getRandomSeed());
  }

  /**
   * Save current simulation configuration to disk
   *
   * @param askForConfirmation
   *          Ask for confirmation before overwriting file
   */
  public File doSaveConfig(boolean askForConfirmation) {
    if (isVisualizedInApplet()) {
      return null;
    }

    if (mySimulation == null) {
      return null;
    }

    mySimulation.stopSimulation();

    JFileChooser fc = new JFileChooser();

    fc.setFileFilter(GUI.SAVED_SIMULATIONS_FILES);

    // Suggest file using history
    File suggestedFile = getLastOpenedFile();
    if (suggestedFile != null) {
      fc.setSelectedFile(suggestedFile);
    }

    int returnVal = fc.showSaveDialog(myDesktopPane);
    if (returnVal == JFileChooser.APPROVE_OPTION) {
      File saveFile = fc.getSelectedFile();
      if (!fc.accept(saveFile)) {
        saveFile = new File(saveFile.getParent(), saveFile.getName()
            + SAVED_SIMULATIONS_FILES);
      }

      if (saveFile.exists()) {
        if (askForConfirmation) {
          String s1 = "Overwrite";
          String s2 = "Cancel";
          Object[] options = { s1, s2 };
          int n = JOptionPane.showOptionDialog(
              GUI.getTopParentContainer(),
              "A file with the same name already exists.\nDo you want to remove it?",
              "Overwrite existing file?", JOptionPane.YES_NO_OPTION,
              JOptionPane.QUESTION_MESSAGE, null, options, s1);
          if (n != JOptionPane.YES_OPTION) {
            return null;
          }
        }
      }

      if (!saveFile.exists() || saveFile.canWrite()) {
        saveSimulationConfig(saveFile);
        addToFileHistory(saveFile);
        return saveFile;
      } else {
        logger.fatal("No write access to file");
      }
    } else {
      logger.info("Save command cancelled by user...");
    }
    return null;
  }

  /**
   * Add new mote to current simulation
   */
  public void doAddMotes(MoteType moteType) {
    if (mySimulation != null) {
      mySimulation.stopSimulation();

      Vector<Mote> newMotes = AddMoteDialog.showDialog(getTopParentContainer(), mySimulation,
          moteType);
      if (newMotes != null) {
        for (Mote newMote : newMotes) {
          mySimulation.addMote(newMote);
        }
      }

    } else {
      logger.warn("No simulation active");
    }
  }

  /**
   * Create a new simulation
   *
   * @param askForConfirmation
   *          Should we ask for confirmation if a simulation is already active?
   */
  public void doCreateSimulation(boolean askForConfirmation) {
    /* Remove current simulation */
    if (!doRemoveSimulation(askForConfirmation)) {
      return;
    }

    // Create new simulation
    Simulation newSim = new Simulation(this);
    boolean createdOK = CreateSimDialog.showDialog(GUI.getTopParentContainer(), newSim);
    if (createdOK) {
      myGUI.setSimulation(newSim, true);
    }
  }

  /**
   * Quit program
   *
   * @param askForConfirmation
   *          Should we ask for confirmation before quitting?
   */
  public void doQuit(boolean askForConfirmation) {
    if (isVisualizedInApplet()) {
      return;
    }

    if (askForConfirmation) {
      String s1 = "Quit";
      String s2 = "Cancel";
      Object[] options = { s1, s2 };
      int n = JOptionPane.showOptionDialog(GUI.getTopParentContainer(),
          "Sure you want to quit?",
          "Close COOJA Simulator", JOptionPane.YES_NO_OPTION,
          JOptionPane.QUESTION_MESSAGE, null, options, s1);
      if (n != JOptionPane.YES_OPTION) {
        return;
      }
    }

    // Clean up resources
    Object[] plugins = startedPlugins.toArray();
    for (Object plugin : plugins) {
      removePlugin((Plugin) plugin, false);
    }

    /* Store frame size and position */
    if (isVisualizedInFrame()) {
      setExternalToolsSetting("FRAME_SCREEN", frame.getGraphicsConfiguration().getDevice().getIDstring());
      setExternalToolsSetting("FRAME_POS_X", "" + frame.getLocationOnScreen().x);
      setExternalToolsSetting("FRAME_POS_Y", "" + frame.getLocationOnScreen().y);

      if (frame.getExtendedState() == JFrame.MAXIMIZED_BOTH) {
        setExternalToolsSetting("FRAME_WIDTH", "" + Integer.MAX_VALUE);
        setExternalToolsSetting("FRAME_HEIGHT", "" + Integer.MAX_VALUE);
      } else {
        setExternalToolsSetting("FRAME_WIDTH", "" + frame.getWidth());
        setExternalToolsSetting("FRAME_HEIGHT", "" + frame.getHeight());
      }
    }
    saveExternalToolsUserSettings();

    System.exit(0);
  }

  // // EXTERNAL TOOLS SETTINGS METHODS ////

  /**
   * @return Number of external tools settings
   */
  public static int getExternalToolsSettingsCount() {
    return externalToolsSettingNames.length;
  }

  /**
   * Get name of external tools setting at given index.
   *
   * @param index
   *          Setting index
   * @return Name
   */
  public static String getExternalToolsSettingName(int index) {
    return externalToolsSettingNames[index];
  }

  /**
   * @param name
   *          Name of setting
   * @return Value
   */
  public static String getExternalToolsSetting(String name) {
    return currentExternalToolsSettings.getProperty(name);
  }

  /**
   * @param name
   *          Name of setting
   * @param defaultValue
   *          Default value
   * @return Value
   */
  public static String getExternalToolsSetting(String name, String defaultValue) {
    return currentExternalToolsSettings.getProperty(name, defaultValue);
  }

  /**
   * @param name
   *          Name of setting
   * @param defaultValue
   *          Default value
   * @return Value
   */
  public static String getExternalToolsDefaultSetting(String name, String defaultValue) {
    return defaultExternalToolsSettings.getProperty(name, defaultValue);
  }

  /**
   * @param name
   *          Name of setting
   * @param newVal
   *          New value
   */
  public static void setExternalToolsSetting(String name, String newVal) {
    currentExternalToolsSettings.setProperty(name, newVal);
  }

  /**
   * Load external tools settings from default file.
   */
  public static void loadExternalToolsDefaultSettings() {
    String osName = System.getProperty("os.name").toLowerCase();
    String osArch = System.getProperty("os.arch").toLowerCase();

    String filename = null;
    if (osName.startsWith("win")) {
      filename = GUI.EXTERNAL_TOOLS_WIN32_SETTINGS_FILENAME;
    } else if (osName.startsWith("mac os x")) {
      filename = GUI.EXTERNAL_TOOLS_MACOSX_SETTINGS_FILENAME;
    } else if (osName.startsWith("freebsd")) {
      filename = GUI.EXTERNAL_TOOLS_FREEBSD_SETTINGS_FILENAME;
    } else if (osName.startsWith("linux")) {
      filename = GUI.EXTERNAL_TOOLS_LINUX_SETTINGS_FILENAME;
      if (osArch.startsWith("amd64")) {
        filename = GUI.EXTERNAL_TOOLS_LINUX_64_SETTINGS_FILENAME;
      }
    } else {
      logger.warn("Unknown system: " + osName);
      logger.warn("Using default linux external tools configuration");
      filename = GUI.EXTERNAL_TOOLS_LINUX_SETTINGS_FILENAME;
    }

    logger.info("Loading external tools user settings from: " + filename);

    try {
      InputStream in = GUI.class.getResourceAsStream(filename);
      if (in == null) {
        throw new FileNotFoundException(filename + " not found");
      }
      Properties settings = new Properties();
      settings.load(in);
      in.close();

      currentExternalToolsSettings = settings;
      defaultExternalToolsSettings = (Properties) currentExternalToolsSettings.clone();
    } catch (IOException e) {
      // Error while importing default properties
      logger.warn(
          "Error when reading external tools settings from " + filename, e);
    } finally {
      if (currentExternalToolsSettings == null) {
        defaultExternalToolsSettings = new Properties();
        currentExternalToolsSettings = new Properties();
      }
    }
  }

  /**
   * Load user values from external properties file
   */
  public static void loadExternalToolsUserSettings() {
    if (externalToolsUserSettingsFile == null) {
      return;
    }

    try {
      FileInputStream in = new FileInputStream(externalToolsUserSettingsFile);
      Properties settings = new Properties();
      settings.load(in);
      in.close();

      Enumeration en = settings.keys();
      while (en.hasMoreElements()) {
        String key = (String) en.nextElement();
        setExternalToolsSetting(key, settings.getProperty(key));
      }

    } catch (FileNotFoundException e) {
      // No default configuration file found, using default
    } catch (IOException e) {
      // Error while importing saved properties, using default
      logger.warn("Error when reading default settings from " + externalToolsUserSettingsFile);
    }
  }

  /**
   * Save external tools user settings to file.
   */
  public static void saveExternalToolsUserSettings() {
    if (isVisualizedInApplet()) {
      return;
    }

    if (externalToolsUserSettingsFileReadOnly) {
      return;
    }

    try {
      FileOutputStream out = new FileOutputStream(externalToolsUserSettingsFile);

      Properties differingSettings = new Properties();
      Enumeration keyEnum = currentExternalToolsSettings.keys();
      while (keyEnum.hasMoreElements()) {
        String key = (String) keyEnum.nextElement();
        String defaultSetting = getExternalToolsDefaultSetting(key, "");
        String currentSetting = getExternalToolsSetting(key, "");
        if (!defaultSetting.equals(currentSetting)) {
          differingSettings.setProperty(key, currentSetting);
        }
      }

      differingSettings.store(out, "COOJA External Tools (User specific)");
      out.close();
    } catch (FileNotFoundException ex) {
      // Could not open settings file for writing, aborting
      logger.warn("Could not save external tools user settings to "
          + externalToolsUserSettingsFile + ", aborting");
    } catch (IOException ex) {
      // Could not open settings file for writing, aborting
      logger.warn("Error while saving external tools user settings to "
          + externalToolsUserSettingsFile + ", aborting");
    }
  }

  // // GUI EVENT HANDLER ////

  private class GUIEventHandler implements ActionListener {
    public void actionPerformed(ActionEvent e) {
      if (e.getActionCommand().equals("confopen sim")) {
        new Thread(new Runnable() {
          public void run() {
            myGUI.doLoadConfig(true, false, null);
          }
        }).start();
      } else if (e.getActionCommand().equals("confopen last sim")) {
        final File file = (File) ((JMenuItem) e.getSource()).getClientProperty("file");
        new Thread(new Runnable() {
          public void run() {
            myGUI.doLoadConfig(true, false, file);
          }
        }).start();
      } else if (e.getActionCommand().equals("open sim")) {
        new Thread(new Runnable() {
          public void run() {
            myGUI.doLoadConfig(true, true, null);
          }
        }).start();
      } else if (e.getActionCommand().equals("open last sim")) {
        final File file = (File) ((JMenuItem) e.getSource()).getClientProperty("file");
        new Thread(new Runnable() {
          public void run() {
            myGUI.doLoadConfig(true, true, file);
          }
        }).start();
      } else if (e.getActionCommand().equals("create mote type")) {
        myGUI.doCreateMoteType((Class<? extends MoteType>) ((JMenuItem) e
            .getSource()).getClientProperty("class"));
      } else if (e.getActionCommand().equals("add motes")) {
        myGUI.doAddMotes((MoteType) ((JMenuItem) e.getSource())
            .getClientProperty("motetype"));
      } else if (e.getActionCommand().equals("edit paths")) {
        ExternalToolsDialog.showDialog(GUI.getTopParentContainer());
      } else if (e.getActionCommand().equals("manage projects")) {
        Vector<File> newProjects = ProjectDirectoriesDialog.showDialog(
            GUI.getTopParentContainer(), currentProjectDirs, null);
        if (newProjects != null) {
          currentProjectDirs = newProjects;
          try {
            reparseProjectConfig();
          } catch (ParseProjectsException e2) {
            logger.fatal("Error when loading projects: " + e2.getMessage());
            e2.printStackTrace();
            if (myGUI.isVisualized()) {
              JOptionPane.showMessageDialog(GUI.getTopParentContainer(),
                  "Error when loading projects.\nStack trace printed to console.",
                  "Error", JOptionPane.ERROR_MESSAGE);
            }
            return;
          }
        }
      } else if (e.getActionCommand().equals("configuration wizard")) {
        ConfigurationWizard.startWizard(GUI.getTopParentContainer(), GUI.this);
      } else {
        logger.warn("Unhandled action: " + e.getActionCommand());
      }
    }
  }

  // // VARIOUS HELP METHODS ////

  /**
   * Help method that tries to load and initialize a class with given name.
   *
   * @param <N>
   *          Class extending given class type
   * @param classType
   *          Class type
   * @param className
   *          Class name
   * @return Class extending given class type or null if not found
   */
  public <N extends Object> Class<? extends N> tryLoadClass(
      Object callingObject, Class<N> classType, String className) {

    if (callingObject != null) {
      try {
        return callingObject.getClass().getClassLoader().loadClass(className).asSubclass(classType);
      } catch (ClassNotFoundException e) {
      } catch (UnsupportedClassVersionError e) {
      }
    }

    try {
      return Class.forName(className).asSubclass(classType);
    } catch (ClassNotFoundException e) {
    } catch (UnsupportedClassVersionError e) {
    }

    if (!isVisualizedInApplet()) {
      try {
        if (projectDirClassLoader != null) {
          return projectDirClassLoader.loadClass(className).asSubclass(
              classType);
        }
      } catch (NoClassDefFoundError e) {
      } catch (ClassNotFoundException e) {
      } catch (UnsupportedClassVersionError e) {
      }
    }

    return null;
  }

  public ClassLoader createProjectDirClassLoader(Vector<File> projectsDirs)
  throws ParseProjectsException, ClassLoaderCreationException {
    if (projectDirClassLoader == null) {
      reparseProjectConfig();
    }
    return createClassLoader(projectDirClassLoader, projectsDirs);
  }

  private ClassLoader createClassLoader(Vector<File> currentProjectDirs)
  throws ClassLoaderCreationException
  {
    return createClassLoader(ClassLoader.getSystemClassLoader(),
        currentProjectDirs);
  }

  private File findJarFile(File projectDir, String jarfile) {
    File fp = new File(jarfile);
    if (!fp.exists()) {
      fp = new File(projectDir, jarfile);
    }
    if (!fp.exists()) {
      fp = new File(projectDir, "java/" + jarfile);
    }
    if (!fp.exists()) {
      fp = new File(projectDir, "java/lib/" + jarfile);
    }
    if (!fp.exists()) {
      fp = new File(projectDir, "lib/" + jarfile);
    }
    return fp.exists() ? fp : null;
  }

  private ClassLoader createClassLoader(ClassLoader parent,
      Vector<File> projectDirs) throws ClassLoaderCreationException {
    if (projectDirs == null || projectDirs.isEmpty()) {
      return parent;
    }

    // Combine class loader from all project directories (including any
    // specified JAR files)
    ArrayList<URL> urls = new ArrayList<URL>();
    for (int j = projectDirs.size() - 1; j >= 0; j--) {
      File projectDir = projectDirs.get(j);
      try {
        urls.add((new File(projectDir, "java")).toURI().toURL());

        // Read configuration to check if any JAR files should be loaded
        ProjectConfig projectConfig = new ProjectConfig(false);
        projectConfig.appendProjectDir(projectDir);
        String[] projectJarFiles = projectConfig.getStringArrayValue(
            GUI.class, "JARFILES");
        if (projectJarFiles != null && projectJarFiles.length > 0) {
          for (String jarfile : projectJarFiles) {
            File jarpath = findJarFile(projectDir, jarfile);
            if (jarpath == null) {
              throw new FileNotFoundException(jarfile);
            }
            urls.add(jarpath.toURI().toURL());
          }
        }

      } catch (Exception e) {
        logger.fatal("Error when trying to read JAR-file in " + projectDir
            + ": " + e);
        throw (ClassLoaderCreationException) new ClassLoaderCreationException(
            "Error when trying to read JAR-file in " + projectDir).initCause(e);
      }
    }

    URL[] urlsArray = urls.toArray(new URL[urls.size()]);
    /* TODO Load from webserver if applet */
    return new URLClassLoader(urlsArray, parent);
  }

  /**
   * Help method that returns the description for given object. This method
   * reads from the object's class annotations if existing. Otherwise it returns
   * the simple class name of object's class.
   *
   * @param object
   *          Object
   * @return Description
   */
  public static String getDescriptionOf(Object object) {
    return getDescriptionOf(object.getClass());
  }

  /**
   * Help method that returns the description for given class. This method reads
   * from class annotations if existing. Otherwise it returns the simple class
   * name.
   *
   * @param clazz
   *          Class
   * @return Description
   */
  public static String getDescriptionOf(Class<? extends Object> clazz) {
    if (clazz.isAnnotationPresent(ClassDescription.class)) {
      return clazz.getAnnotation(ClassDescription.class).value();
    }
    return clazz.getSimpleName();
  }

  /**
   * Help method that returns the abstraction level description for given mote type class.
   *
   * @param clazz
   *          Class
   * @return Description
   */
  public static String getAbstractionLevelDescriptionOf(Class<? extends MoteType> clazz) {
    if (clazz.isAnnotationPresent(AbstractionLevelDescription.class)) {
      return clazz.getAnnotation(AbstractionLevelDescription.class).value();
    }
    return null;
  }

  /**
   * Load configurations and create a GUI.
   *
   * @param args
   *          null
   */
  public static void main(String[] args) {

    try {
      // Configure logger
      if ((new File(LOG_CONFIG_FILE)).exists()) {
        DOMConfigurator.configure(LOG_CONFIG_FILE);
      } else {
        // Used when starting from jar
        DOMConfigurator.configure(GUI.class.getResource("/" + LOG_CONFIG_FILE));
      }

      externalToolsUserSettingsFile = new File(System.getProperty("user.home"), EXTERNAL_TOOLS_USER_SETTINGS_FILENAME);
    } catch (AccessControlException e) {
      BasicConfigurator.configure();
      externalToolsUserSettingsFile = null;
    }

    /* Look and Feel: Nimbus */
    setLookAndFeel();

    // Parse general command arguments
    for (String element : args) {
      if (element.startsWith("-contiki=")) {
        String arg = element.substring("-contiki=".length());
        GUI.specifiedContikiPath = arg;
      }

      if (element.startsWith("-external_tools_config=")) {
        String arg = element.substring("-external_tools_config=".length());
        File specifiedExternalToolsConfigFile = new File(arg);
        if (!specifiedExternalToolsConfigFile.exists()) {
          logger.fatal("Specified external tools configuration not found: " + specifiedExternalToolsConfigFile);
          specifiedExternalToolsConfigFile = null;
          System.exit(1);
        } else {
          GUI.externalToolsUserSettingsFile = specifiedExternalToolsConfigFile;
          GUI.externalToolsUserSettingsFileReadOnly = true;
        }
      }
    }

    // Check if simulator should be quick-started
    if (args.length > 0 && args[0].startsWith("-quickstart=")) {
      String contikiApp = args[0].substring("-quickstart=".length());

      /* Cygwin fix */
      if (contikiApp.startsWith("/cygdrive/")) {
        char driveCharacter = contikiApp.charAt("/cygdrive/".length());
        contikiApp = contikiApp.replace("/cygdrive/" + driveCharacter + "/", driveCharacter + ":/");
      }

      boolean ok = false;
      if (contikiApp.endsWith(".csc")) {

        ok = quickStartSimulationConfig(new File(contikiApp), true) != null;

      } else {
        if (contikiApp.endsWith(".cooja")) {
          contikiApp = contikiApp.substring(0, contikiApp.length() - ".cooja".length());
        }
        if (!contikiApp.endsWith(".c")) {
          contikiApp += ".c";
        }

        ok = quickStartSimulation(contikiApp);
      }

      if (!ok) {
        System.exit(1);
      }

    } else if (args.length > 0 && args[0].startsWith("-nogui=")) {

      /* Load simulation */
      String config = args[0].substring("-nogui=".length());
      final File configFile = new File(config);
      Simulation sim = quickStartSimulationConfig(configFile, false);
      if (sim == null) {
        System.exit(1);
      }
      GUI gui = sim.getGUI();
      
      /* Make sure at least one test editor is controlling the simulation */
      boolean hasEditor = false;
      for (Plugin startedPlugin : gui.startedPlugins) {
        if (startedPlugin instanceof ScriptRunner) {
          hasEditor = true;
          break;
        }
      }

      /* Backwards compatibility:
       * simulation has no test editor, but has external (old style) test script.
       * We will manually start a test editor from here. */
      if (!hasEditor) {
        File scriptFile = new File(config.substring(0, config.length()-4) + ".js");
        if (scriptFile.exists()) {
          logger.info("Detected old simulation test, starting test editor manually from: " + scriptFile);
          ScriptRunner plugin = (ScriptRunner) gui.tryStartPlugin(ScriptRunner.class, gui, sim, null);
          if (plugin == null) {
            System.exit(1);
          }
          plugin.updateScript(scriptFile);
          plugin.setScriptActive(true);
          sim.setDelayTime(0);
          sim.startSimulation();
        } else {
          logger.fatal("No test editor controlling simulation, aborting");
          System.exit(1);
        }
      }
      
    } else if (args.length > 0 && args[0].startsWith("-applet")) {

      String tmpWebPath=null, tmpBuildPath=null, tmpEsbFirmware=null, tmpSkyFirmware=null;
      for (int i = 1; i < args.length; i++) {
        if (args[i].startsWith("-web=")) {
          tmpWebPath = args[i].substring("-web=".length());
        } else if (args[i].startsWith("-sky_firmware=")) {
          tmpSkyFirmware = args[i].substring("-sky_firmware=".length());
        } else if (args[i].startsWith("-esb_firmware=")) {
          tmpEsbFirmware = args[i].substring("-esb_firmware=".length());
        } else if (args[i].startsWith("-build=")) {
          tmpBuildPath = args[i].substring("-build=".length());
        }
      }

      // Applet start-up
      final String webPath = tmpWebPath, buildPath = tmpBuildPath;
      final String skyFirmware = tmpSkyFirmware, esbFirmware = tmpEsbFirmware;
      javax.swing.SwingUtilities.invokeLater(new Runnable() {
        public void run() {
          JDesktopPane desktop = createDesktopPane();

          applet = CoojaApplet.applet;
          GUI gui = new GUI(desktop);

          GUI.setExternalToolsSetting("PATH_CONTIKI_BUILD", buildPath);
          GUI.setExternalToolsSetting("PATH_CONTIKI_WEB", webPath);

          GUI.setExternalToolsSetting("SKY_FIRMWARE", skyFirmware);
          GUI.setExternalToolsSetting("ESB_FIRMWARE", esbFirmware);

          configureApplet(gui, false);
        }
      });

    } else {

      // Frame start-up
      javax.swing.SwingUtilities.invokeLater(new Runnable() {
        public void run() {
          JDesktopPane desktop = createDesktopPane();
          frame = new JFrame("COOJA Simulator");
          GUI gui = new GUI(desktop);
          configureFrame(gui, false);
        }
      });

    }
  }

  /**
   * Loads a simulation configuration from given file.
   *
   * When loading Contiki mote types, the libraries must be recompiled. User may
   * change mote type settings at this point.
   *
   * @see #saveSimulationConfig(File)
   * @param file
   *          File to read
   * @return New simulation or null if recompiling failed or aborted
   * @throws UnsatisfiedLinkError
   *           If associated libraries could not be loaded
   */
  public Simulation loadSimulationConfig(File file, boolean quick)
  throws UnsatisfiedLinkError, SimulationCreationException {
    this.currentConfigFile = file; /* Used to generate config relative paths */

    try {
      SAXBuilder builder = new SAXBuilder();
      Document doc = builder.build(file);
      Element root = doc.getRootElement();

      return loadSimulationConfig(root, quick, null);
    } catch (JDOMException e) {
      logger.fatal("Config not wellformed: " + e.getMessage());
      return null;
    } catch (IOException e) {
      logger.fatal("Load simulation error: " + e.getMessage());
      return null;
    }
  }

  private Simulation loadSimulationConfig(StringReader stringReader, boolean quick)
  throws SimulationCreationException {
    try {
      SAXBuilder builder = new SAXBuilder();
      Document doc = builder.build(stringReader);
      Element root = doc.getRootElement();

      return loadSimulationConfig(root, quick, null);
    } catch (JDOMException e) {
      throw (SimulationCreationException) new SimulationCreationException(
          "Configuration file not wellformed: " + e.getMessage()).initCause(e);
    } catch (IOException e) {
      throw (SimulationCreationException) new SimulationCreationException(
          "IO Exception: " + e.getMessage()).initCause(e);
    }
  }

  private Simulation loadSimulationConfig(Element root, boolean quick, Long manualRandomSeed)
  throws SimulationCreationException {
    Simulation newSim = null;

    try {
      // Check that config file version is correct
      if (!root.getName().equals("simconf")) {
        logger.fatal("Not a valid COOJA simulation config!");
        return null;
      }

      /* Verify project directories */
      boolean projectsOk = verifyProjects(root.getChildren(), !quick);

      /* GENERATE UNIQUE MOTE TYPE IDENTIFIERS */
      root.detach();
      String configString = new XMLOutputter().outputString(new Document(root));

      /* Locate Contiki mote types in config */
      Properties moteTypeIDMappings = new Properties();
      String identifierExtraction = ContikiMoteType.class.getName() + "[\\s\\n]*<identifier>([^<]*)</identifier>";
      Matcher matcher = Pattern.compile(identifierExtraction).matcher(configString);
      while (matcher.find()) {
        moteTypeIDMappings.setProperty(matcher.group(1), "");
      }

      /* Create old to new identifier mappings */
      Enumeration<Object> existingIdentifiers = moteTypeIDMappings.keys();
      while (existingIdentifiers.hasMoreElements()) {
        String existingIdentifier = (String) existingIdentifiers.nextElement();
        MoteType[] existingMoteTypes = null;
        if (mySimulation != null) {
          existingMoteTypes = mySimulation.getMoteTypes();
        }
        ArrayList<Object> reserved = new ArrayList<Object>();
        reserved.addAll(moteTypeIDMappings.keySet());
        reserved.addAll(moteTypeIDMappings.values());
        String newID = ContikiMoteType.generateUniqueMoteTypeID(existingMoteTypes, reserved);
        moteTypeIDMappings.setProperty(existingIdentifier, newID);
      }

      /* Create new config */
      existingIdentifiers = moteTypeIDMappings.keys();
      while (existingIdentifiers.hasMoreElements()) {
        String existingIdentifier = (String) existingIdentifiers.nextElement();
        configString = configString.replaceAll(
            "<identifier>" + existingIdentifier + "</identifier>",
            "<identifier>" + moteTypeIDMappings.get(existingIdentifier) + "</identifier>");
        configString = configString.replaceAll(
            "<motetype_identifier>" + existingIdentifier + "</motetype_identifier>",
            "<motetype_identifier>" + moteTypeIDMappings.get(existingIdentifier) + "</motetype_identifier>");
      }

      /* Replace existing config */
      root = new SAXBuilder().build(new StringReader(configString)).getRootElement();

      // Create new simulation from config
      for (Object element : root.getChildren()) {
        if (((Element) element).getName().equals("simulation")) {
          Collection<Element> config = ((Element) element).getChildren();
          newSim = new Simulation(this);
          System.gc();
          boolean createdOK = newSim.setConfigXML(config, !quick, manualRandomSeed);
          if (!createdOK) {
            logger.info("Simulation not loaded");
            return null;
          }
        }
      }

      // Restart plugins from config
      setPluginsConfigXML(root.getChildren(), newSim, !quick);

    } catch (JDOMException e) {
      throw (SimulationCreationException) new SimulationCreationException(
          "Configuration file not wellformed: " + e.getMessage()).initCause(e);
    } catch (IOException e) {
      throw (SimulationCreationException) new SimulationCreationException(
          "No access to configuration file: " + e.getMessage()).initCause(e);
    } catch (MoteTypeCreationException e) {
      throw (SimulationCreationException) new SimulationCreationException(
          "Mote type creation error: " + e.getMessage()).initCause(e);
    } catch (Exception e) {
      throw (SimulationCreationException) new SimulationCreationException(
          "Unknown error: " + e.getMessage()).initCause(e);
    }

    return newSim;
  }

  /**
   * Saves current simulation configuration to given file and notifies
   * observers.
   *
   * @see #loadSimulationConfig(File, boolean)
   * @param file
   *          File to write
   */
  public void saveSimulationConfig(File file) {
    this.currentConfigFile = file; /* Used to generate config relative paths */

    try {
      // Create simulation config
      Element root = new Element("simconf");

      /* Store project directories meta data */
      for (File project: currentProjectDirs) {
        Element projectElement = new Element("project");
        projectElement.addContent(project.getPath().replaceAll("\\\\", "/"));
        root.addContent(projectElement);
      }

      Element simulationElement = new Element("simulation");
      simulationElement.addContent(mySimulation.getConfigXML());
      root.addContent(simulationElement);

      // Create started plugins config
      Collection<Element> pluginsConfig = getPluginsConfigXML();
      if (pluginsConfig != null) {
        root.addContent(pluginsConfig);
      }

      // Create and write to document
      Document doc = new Document(root);
      FileOutputStream out = new FileOutputStream(file);
      XMLOutputter outputter = new XMLOutputter();
      outputter.setFormat(Format.getPrettyFormat());
      outputter.output(doc, out);
      out.close();

      logger.info("Saved to file: " + file.getAbsolutePath());
    } catch (Exception e) {
      logger.warn("Exception while saving simulation config: " + e);
      e.printStackTrace();
    }
  }

  /**
   * Returns started plugins config.
   *
   * @return Config or null
   */
  public Collection<Element> getPluginsConfigXML() {
    Vector<Element> config = new Vector<Element>();

    // Loop through all started plugins
    // (Only return config of non-GUI plugins)
    Element pluginElement, pluginSubElement;
    for (Plugin startedPlugin : startedPlugins) {
      int pluginType = startedPlugin.getClass().getAnnotation(PluginType.class).value();

      // Ignore GUI plugins
      if (pluginType == PluginType.COOJA_PLUGIN
          || pluginType == PluginType.COOJA_STANDARD_PLUGIN) {
        continue;
      }

      pluginElement = new Element("plugin");
      pluginElement.setText(startedPlugin.getClass().getName());

      // Create mote argument config (if mote plugin)
      if (pluginType == PluginType.MOTE_PLUGIN
          && startedPlugin.getTag() != null) {
        pluginSubElement = new Element("mote_arg");
        Mote taggedMote = (Mote) startedPlugin.getTag();
        for (int moteNr = 0; moteNr < mySimulation.getMotesCount(); moteNr++) {
          if (mySimulation.getMote(moteNr) == taggedMote) {
            pluginSubElement.setText(Integer.toString(moteNr));
            pluginElement.addContent(pluginSubElement);
            break;
          }
        }
      }

      // Create plugin specific configuration
      Collection<Element> pluginXML = startedPlugin.getConfigXML();
      if (pluginXML != null) {
        pluginSubElement = new Element("plugin_config");
        pluginSubElement.addContent(pluginXML);
        pluginElement.addContent(pluginSubElement);
      }

      // If plugin is visualizer plugin, create visualization arguments
      if (startedPlugin.getGUI() != null) {
        JInternalFrame pluginFrame = startedPlugin.getGUI();

        pluginSubElement = new Element("width");
        pluginSubElement.setText("" + pluginFrame.getSize().width);
        pluginElement.addContent(pluginSubElement);

        pluginSubElement = new Element("z");
        pluginSubElement.setText("" + getDesktopPane().getComponentZOrder(pluginFrame));
        pluginElement.addContent(pluginSubElement);

        pluginSubElement = new Element("height");
        pluginSubElement.setText("" + pluginFrame.getSize().height);
        pluginElement.addContent(pluginSubElement);

        pluginSubElement = new Element("location_x");
        pluginSubElement.setText("" + pluginFrame.getLocation().x);
        pluginElement.addContent(pluginSubElement);

        pluginSubElement = new Element("location_y");
        pluginSubElement.setText("" + pluginFrame.getLocation().y);
        pluginElement.addContent(pluginSubElement);

        pluginSubElement = new Element("minimized");
        pluginSubElement.setText(new Boolean(pluginFrame.isIcon()).toString());
        pluginElement.addContent(pluginSubElement);
      }

      config.add(pluginElement);
    }

    return config;
  }

  public boolean verifyProjects(Collection<Element> configXML, boolean visAvailable) {
    boolean allOk = true;

    /* Match current projects against projects in simulation config */
    for (final Element pluginElement : configXML.toArray(new Element[0])) {
      if (pluginElement.getName().equals("project")) {
        String project = pluginElement.getText();

        boolean found = false;
        for (File currentProject: currentProjectDirs) {
          if (project.equals(currentProject.getPath().replaceAll("\\\\", "/"))) {
            found = true;
            break;
          }
        }

        if (!found) {
          logger.warn("Loaded simulation may depend on external project: '" + project + "'");
          allOk = false;
        }
      }
    }

    return allOk;
  }


  /**
   * Starts plugins with arguments in given config.
   *
   * @param configXML
   *          Config XML elements
   * @param simulation
   *          Simulation on which to start plugins
   * @return True if all plugins started, false otherwise
   */
  public boolean setPluginsConfigXML(Collection<Element> configXML,
      Simulation simulation, boolean visAvailable) {

    for (final Element pluginElement : configXML.toArray(new Element[0])) {
      if (pluginElement.getName().equals("plugin")) {

        // Read plugin class
        String pluginClassName = pluginElement.getText().trim();

        /* Backwards compatibility: old visualizers were replaced */
        if (pluginClassName.equals("se.sics.cooja.plugins.VisUDGM") ||
            pluginClassName.equals("se.sics.cooja.plugins.VisBattery") ||
            pluginClassName.equals("se.sics.cooja.plugins.VisTraffic") ||
            pluginClassName.equals("se.sics.cooja.plugins.VisState") ||
            pluginClassName.equals("se.sics.cooja.plugins.VisUDGM")) {
          logger.warn("Old simulation config detected: visualizers have been remade");
          pluginClassName = "se.sics.cooja.plugins.Visualizer";
        }

        Class<? extends Plugin> pluginClass =
          tryLoadClass(this, Plugin.class, pluginClassName);
        if (pluginClass == null) {
          logger.fatal("Could not load plugin class: " + pluginClassName);
          return false;
        }

        // Parse plugin mote argument (if any)
        Mote mote = null;
        for (Element pluginSubElement : (List<Element>) pluginElement.getChildren()) {
          if (pluginSubElement.getName().equals("mote_arg")) {
            int moteNr = Integer.parseInt(pluginSubElement.getText());
            if (moteNr >= 0 && moteNr < simulation.getMotesCount()) {
              mote = simulation.getMote(moteNr);
            }
          }
        }

        /* Start plugin */
        final Plugin startedPlugin = tryStartPlugin(pluginClass, this, simulation, mote);
        if (startedPlugin == null) {
          continue;
        }

        /* Apply plugin specific configuration */
        for (Element pluginSubElement : (List<Element>) pluginElement.getChildren()) {
          if (pluginSubElement.getName().equals("plugin_config")) {
            startedPlugin.setConfigXML(pluginSubElement.getChildren(), visAvailable);
          }
        }

        /* If Cooja not visualized, ignore window configuration */
        if (startedPlugin.getGUI() == null) {
          continue;
        }

        // If plugin is visualizer plugin, parse visualization arguments
        new RunnableInEDT<Boolean>() {
          public Boolean work() {
            Dimension size = new Dimension(100, 100);
            Point location = new Point(100, 100);

            for (Element pluginSubElement : (List<Element>) pluginElement.getChildren()) {
              if (pluginSubElement.getName().equals("width")) {
                size.width = Integer.parseInt(pluginSubElement.getText());
                startedPlugin.getGUI().setSize(size);
              } else if (pluginSubElement.getName().equals("height")) {
                size.height = Integer.parseInt(pluginSubElement.getText());
                startedPlugin.getGUI().setSize(size);
              } else if (pluginSubElement.getName().equals("z")) {
                int zOrder = Integer.parseInt(pluginSubElement.getText());
                // Save z order as temporary client property
                startedPlugin.getGUI().putClientProperty("zorder", zOrder);
              } else if (pluginSubElement.getName().equals("location_x")) {
                location.x = Integer.parseInt(pluginSubElement.getText());
                startedPlugin.getGUI().setLocation(location);
              } else if (pluginSubElement.getName().equals("location_y")) {
                location.y = Integer.parseInt(pluginSubElement.getText());
                startedPlugin.getGUI().setLocation(location);
              } else if (pluginSubElement.getName().equals("minimized")) {
                try {
                  startedPlugin.getGUI().setIcon(Boolean.parseBoolean(pluginSubElement.getText()));
                } catch (PropertyVetoException e) { }
              }
            }

            // For all visualized plugins, check if they have a zorder property
            try {
              for (JInternalFrame plugin : getDesktopPane().getAllFrames()) {
                if (plugin.getClientProperty("zorder") != null) {
                  getDesktopPane().setComponentZOrder(plugin,
                      ((Integer) plugin.getClientProperty("zorder")).intValue());
                  plugin.putClientProperty("zorder", null);
                }
              }
            } catch (Exception e) { }

            return true;
          }
        }.invokeAndWait();

      }
    }

    return true;
  }

  public class ParseProjectsException extends Exception {
    public ParseProjectsException(String message) {
      super(message);
    }
  }

  public class ClassLoaderCreationException extends Exception {
    public ClassLoaderCreationException(String message) {
      super(message);
    }
  }

  public class SimulationCreationException extends Exception {
    public SimulationCreationException(String message) {
      super(message);
    }
  }

  public class PluginConstructionException extends Exception {
    public PluginConstructionException(String message) {
      super(message);
    }
  }

  /**
   * A simple error dialog with compilation output and stack trace.
   *
   * @param parentComponent
   *          Parent component
   * @param title
   *          Title of error window
   * @param exception
   *          Exception causing window to be shown
   * @param retryAvailable
   *          If true, a retry option is presented
   * @return Retry failed operation
   */
  public static boolean showErrorDialog(final Component parentComponent,
      final String title, final Throwable exception, final boolean retryAvailable) {

    return new RunnableInEDT<Boolean>() {
      public Boolean work() {
        JTabbedPane tabbedPane = new JTabbedPane();
        final JDialog errorDialog;
        if (parentComponent instanceof Dialog) {
          errorDialog = new JDialog((Dialog) parentComponent, title, true);
        } else if (parentComponent instanceof Frame) {
          errorDialog = new JDialog((Frame) parentComponent, title, true);
        } else {
          errorDialog = new JDialog((Frame) null, title);
        }
        Box buttonBox = Box.createHorizontalBox();

        if (exception != null) {
          /* Compilation output */
          MessageList compilationOutput = null;
          if (exception instanceof MoteTypeCreationException
              && ((MoteTypeCreationException) exception).hasCompilationOutput()) {
            compilationOutput = ((MoteTypeCreationException) exception).getCompilationOutput();
          } else if (exception.getCause() != null
              && exception.getCause() instanceof MoteTypeCreationException
              && ((MoteTypeCreationException) exception.getCause()).hasCompilationOutput()) {
            compilationOutput = ((MoteTypeCreationException) exception.getCause()).getCompilationOutput();
          }
          if (compilationOutput != null) {
            compilationOutput.addPopupMenuItem(null, true);
            tabbedPane.addTab("Compilation output", null, new JScrollPane(compilationOutput), null);
          }

          /* Stack trace */
          MessageList stackTrace = new MessageList();
          PrintStream printStream = stackTrace.getInputStream(MessageList.NORMAL);
          exception.printStackTrace(printStream);
          stackTrace.addPopupMenuItem(null, true);
          tabbedPane.addTab("Java stack trace", null, new JScrollPane(stackTrace), null);

          /* Exception message */
          buttonBox.add(Box.createHorizontalStrut(10));
          buttonBox.add(new JLabel(exception.getMessage()));
          buttonBox.add(Box.createHorizontalStrut(10));
        }

        buttonBox.add(Box.createHorizontalGlue());

        if (retryAvailable) {
          Action retryAction = new AbstractAction() {
            public void actionPerformed(ActionEvent e) {
              errorDialog.setTitle("-RETRY-");
              errorDialog.dispose();
            }
          };
          JButton retryButton = new JButton(retryAction);
          retryButton.setText("Retry Ctrl+R");
          buttonBox.add(retryButton);

          InputMap inputMap = errorDialog.getRootPane().getInputMap(
              JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT);
          inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_R, KeyEvent.CTRL_DOWN_MASK, false), "retry");
          errorDialog.getRootPane().getActionMap().put("retry", retryAction);
        }

        AbstractAction closeAction = new AbstractAction(){
          public void actionPerformed(ActionEvent e) {
            errorDialog.dispose();
          }
        };

        JButton closeButton = new JButton(closeAction);
        closeButton.setText("Close");
        buttonBox.add(closeButton);

        InputMap inputMap = errorDialog.getRootPane().getInputMap(
            JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT);
        inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0, false), "close");
        errorDialog.getRootPane().getActionMap().put("close", closeAction);


        errorDialog.getRootPane().setDefaultButton(closeButton);
        errorDialog.getContentPane().add(BorderLayout.CENTER, tabbedPane);
        errorDialog.getContentPane().add(BorderLayout.SOUTH, buttonBox);
        errorDialog.setSize(700, 500);
        errorDialog.setLocationRelativeTo(parentComponent);
        errorDialog.setVisible(true); /* BLOCKS */

        if (errorDialog.getTitle().equals("-RETRY-")) {
          return true;
        }
        return false;

      }
    }.invokeAndWait();

  }

  /**
   * Runs work method in event dispatcher thread.
   * Worker method returns a value.
   *
   * @author Fredrik Osterlind
   */
  public static abstract class RunnableInEDT<T> {
    private T val;

    /**
     * Work method to be implemented.
     *
     * @return Return value
     */
    public abstract T work();

    /**
     * Runs worker method in event dispatcher thread.
     *
     * @see #work()
     * @return Worker method return value
     */
    public T invokeAndWait() {
      if(java.awt.EventQueue.isDispatchThread()) {
        return RunnableInEDT.this.work();
      }

      try {
        java.awt.EventQueue.invokeAndWait(new Runnable() {
          public void run() {
            val = RunnableInEDT.this.work();
          }
        });
      } catch (InterruptedException e) {
        e.printStackTrace();
      } catch (InvocationTargetException e) {
        e.printStackTrace();
      }

      return val;
    }
  }

  /**
   * This method can be used by various different modules in the simulator to
   * indicate for example that a mote has been selected. All mote highlight
   * listeners will be notified. An example application of mote highlightinh is
   * a simulator visualizer that highlights the mote.
   *
   * @see #addMoteHighlightObserver(Observer)
   * @param m
   *          Mote to highlight
   */
  public void signalMoteHighlight(Mote m) {
    moteHighlightObservable.setChangedAndNotify(m);
  }

  /**
   * Adds directed relation between given motes.
   *
   * @param source Source mote
   * @param dest Destination mote
   */
  public void addMoteRelation(Mote source, Mote dest) {
    if (source == null || dest == null) {
      return;
    }
    removeMoteRelation(source, dest); /* Unique relations */
    moteRelations.add(new MoteRelation(source, dest));
    moteRelationObservable.setChangedAndNotify();
  }

  /**
   * Removes the relations between given motes.
   *
   * @param source Source mote
   * @param dest Destination mote
   */
  public void removeMoteRelation(Mote source, Mote dest) {
    if (source == null || dest == null) {
      return;
    }
    MoteRelation[] arr = getMoteRelations();
    for (MoteRelation r: arr) {
      if (r.source == source && r.dest == dest) {
        moteRelations.remove(r);
      }
    }
    moteRelationObservable.setChangedAndNotify();
  }

  /**
   * @return All current mote relations.
   *
   * @see #addMoteRelationsObserver(Observer)
   */
  public MoteRelation[] getMoteRelations() {
    MoteRelation[] arr = new MoteRelation[moteRelations.size()];
    moteRelations.toArray(arr);
    return arr;
  }

  /**
   * Adds mote relation observer.
   * Typically used by visualizer plugins.
   *
   * @param newObserver Observer
   */
  public void addMoteRelationsObserver(Observer newObserver) {
    moteRelationObservable.addObserver(newObserver);
  }

  /**
   * Removes mote relation observer.
   * Typically used by visualizer plugins.
   *
   * @param observer Observer
   */
  public void deleteMoteRelationsObserver(Observer observer) {
    moteRelationObservable.deleteObserver(observer);
  }

  public File createPortablePath(File file) {
    try {
      File contikiPath = new File(GUI.getExternalToolsSetting("PATH_CONTIKI", null));
      String contikiRelative = contikiPath.getPath();
      String contikiCanonical = contikiPath.getCanonicalPath();

      String fileCanonical = file.getCanonicalPath();
      if (!fileCanonical.startsWith(contikiCanonical)) {
        /*logger.warn("Error when converting to Contiki relative path: file is not in Contiki: " + file.getAbsolutePath());*/
        return createConfigPath(file);
      }

      /* Replace Contiki's canonical path with Contiki's relative path */
      String newFilePath = fileCanonical.replaceFirst(
          java.util.regex.Matcher.quoteReplacement(contikiCanonical), 
          java.util.regex.Matcher.quoteReplacement(contikiRelative));

      File newFile = new File(newFilePath);
      if (!newFile.exists()) {
        /*logger.warn("Error when converting to Contiki relative path: new file does not exist: " + newFile.getAbsolutePath());*/
        return createConfigPath(file);
      }

      logger.info("Generated Contiki relative path '" + file.getPath() + "' to '" + newFile.getPath() + "'");
      return newFile;

    } catch (IOException e1) {
      /*logger.warn("Error when converting to Contiki relative path: " + e1.getMessage());*/
    }

    return createConfigPath(file);
  }

  public File restorePortablePath(File file) {
    return restoreConfigPath(file);
  }

  public File currentConfigFile = null; /* Used to generate config relative paths */
  public File createConfigPath(File file) {
    if (currentConfigFile == null) {
      return file;
    }

    try {
      File configPath = currentConfigFile.getParentFile();
      String configIdentifier = "[CONFIG_DIR]";
      if (configPath == null) {
        return file;
      }
      String configCanonical = configPath.getCanonicalPath();

      String fileCanonical = file.getCanonicalPath();
      if (!fileCanonical.startsWith(configCanonical)) {

        /* SPECIAL CASE: Allow one parent directory */
        configCanonical = configPath.getParentFile().getCanonicalPath();
        configIdentifier += "/..";
        if (!fileCanonical.startsWith(configCanonical)) {
          /*logger.warn("Error when converting to config relative path: file not in config directory: " + file.getAbsolutePath());*/
          return file;
        }
      }

      /* Replace config's canonical path with config identifier */
      String newFilePath = fileCanonical.replaceFirst(
          java.util.regex.Matcher.quoteReplacement(configCanonical), 
          java.util.regex.Matcher.quoteReplacement(configIdentifier));

      File newFile = new File(newFilePath);
      /*logger.info("Generated config relative path: '" + file.getPath() + "' to '" + newFile.getPath() + "'");*/
      
      /* Verify that new file exists */
      File tmpFile = restorePortablePath(newFile);
      if (!tmpFile.exists()) {
        /*logger.warn("Failed generating config relative path, using absolute path: " + file);*/
        return file;
      }

      return newFile;

    } catch (IOException e1) {
      /*logger.warn("Error when converting to config relative path: " + e1.getMessage());*/
    }

    return file;
  }

  public File restoreConfigPath(File file) {
    if (currentConfigFile == null) {
      return file;
    }

    File configPath = currentConfigFile.getParentFile();
    if (configPath == null) {
      return file;
    }

    String path = file.getPath();
    if (!path.startsWith("[CONFIG_DIR]")) {
      /*logger.info("Not config relative path: " + file.getAbsolutePath());*/
      return file;
    }

    File newFile = new File(path.replace("[CONFIG_DIR]", configPath.getAbsolutePath()));
    /*logger.info("Reverted config relative path: '" + path + "' to '" + newFile.getPath() + "'");*/
    return newFile;
  }

  private static JProgressBar PROGRESS_BAR = null;
  public static void setProgressMessage(String msg) {
    if (PROGRESS_BAR != null && PROGRESS_BAR.isShowing()) {
      PROGRESS_BAR.setString(msg);
      PROGRESS_BAR.setStringPainted(true);
    }
  }

  public interface HasQuickHelp {
    /**
     * @return Quick help. May be HTML formatted, but must not include the
     *         document html-tags.
     */
    public String getQuickHelp();
  }
  
  /**
   * Load quick help for given object or identifier. Note that this method does not
   * show the quick help pane.
   *  
   * @param obj If string: help identifier. Else, the class name of the argument
   * is used as help identifier.
   */
  public void loadQuickHelp(final Object obj) {
    if (obj == null) {
      return;
    }

    String key;
    if (obj instanceof String) {
      key = (String) obj;
    } else {
      key = obj.getClass().getName();
    }
    
    String help = null;
    if (obj instanceof HasQuickHelp) {
      help = ((HasQuickHelp) obj).getQuickHelp();
    } else {
      if (quickHelpProperties == null) {
        /* Load quickhelp.txt */
        try {
          quickHelpProperties = new Properties();
          quickHelpProperties.load(new FileReader("quickhelp.txt"));
        } catch (Exception e) {
          quickHelpProperties = null;
          help = "<html><b>Failed to read quickhelp.txt:</b><p>" + e.getMessage() + "</html>";
        }
      }

      if (quickHelpProperties != null) {
        help = quickHelpProperties.getProperty(key);
      }
    }

    if (help != null) {
      quickHelpTextPane.setText("<html>" + help + "</html>");
    } else {
      quickHelpTextPane.setText(
          "<html><b>" + getDescriptionOf(obj) +"</b>" +
          "<p>No help available</html>");
    }
    quickHelpTextPane.setCaretPosition(0);
  }

  /* GUI actions */
  abstract class GUIAction extends AbstractAction {
    public GUIAction(String name) {
      super(name);
    }
    public GUIAction(String name, int nmenomic) {
      this(name);
      putValue(Action.MNEMONIC_KEY, nmenomic);
    }
    public GUIAction(String name, KeyStroke accelerator) {
      this(name);
      putValue(Action.ACCELERATOR_KEY, accelerator);
    }
    public GUIAction(String name, int nmenomic, KeyStroke accelerator) {
      this(name, nmenomic);
      putValue(Action.ACCELERATOR_KEY, accelerator);
    }
    public abstract boolean shouldBeEnabled();
  }
  GUIAction newSimulationAction = new GUIAction("New simulation", KeyEvent.VK_N, KeyStroke.getKeyStroke(KeyEvent.VK_N, ActionEvent.CTRL_MASK)) {
    public void actionPerformed(ActionEvent e) {
      myGUI.doCreateSimulation(true);
    }
    public boolean shouldBeEnabled() {
      return true;
    }
  };
  GUIAction closeSimulationAction = new GUIAction("Close simulation", KeyEvent.VK_C) {
    public void actionPerformed(ActionEvent e) {
      myGUI.doRemoveSimulation(true);
    }
    public boolean shouldBeEnabled() {
      return getSimulation() != null;
    }
  };
  GUIAction reloadSimulationAction = new GUIAction("keep random seed", KeyEvent.VK_K, KeyStroke.getKeyStroke(KeyEvent.VK_R, ActionEvent.CTRL_MASK)) {
    public void actionPerformed(ActionEvent e) {
      if (getSimulation() == null) {
        /* Reload last opened simulation */
        final File file = getLastOpenedFile();
        new Thread(new Runnable() {
          public void run() {
            myGUI.doLoadConfig(true, true, file);
          }
        }).start();
        return;
      }

      /* Reload current simulation */
      long seed = getSimulation().getRandomSeed();
      reloadCurrentSimulation(getSimulation().isRunning(), seed);
    }
    public boolean shouldBeEnabled() {
      return true;
    }
  };
  GUIAction reloadRandomSimulationAction = new GUIAction("new random seed", KeyEvent.VK_N, KeyStroke.getKeyStroke(KeyEvent.VK_R, ActionEvent.CTRL_MASK | ActionEvent.SHIFT_MASK)) {
    public void actionPerformed(ActionEvent e) {
      /* Replace seed before reloading */
      if (getSimulation() != null) {
        getSimulation().setRandomSeed(getSimulation().getRandomSeed()+1);
        reloadSimulationAction.actionPerformed(null);
      }
    }
    public boolean shouldBeEnabled() {
      return getSimulation() != null;
    }
  };
  GUIAction saveSimulationAction = new GUIAction("Save simulation", KeyEvent.VK_S) {
    public void actionPerformed(ActionEvent e) {
      myGUI.doSaveConfig(true);
    }
    public boolean shouldBeEnabled() {
      if (isVisualizedInApplet()) {
        return false;
      }
      return getSimulation() != null;
    }
  };
  GUIAction closePluginsAction = new GUIAction("Close all plugins") {
    public void actionPerformed(ActionEvent e) {
      Object[] plugins = startedPlugins.toArray();
      for (Object plugin : plugins) {
        removePlugin((Plugin) plugin, false);
      }
    }
    public boolean shouldBeEnabled() {
      return !startedPlugins.isEmpty();
    }
  };
  GUIAction exitCoojaAction = new GUIAction("Exit", KeyEvent.VK_X, KeyStroke.getKeyStroke(KeyEvent.VK_X, ActionEvent.CTRL_MASK)) {
    public void actionPerformed(ActionEvent e) {
      myGUI.doQuit(true);
    }
    public boolean shouldBeEnabled() {
      if (isVisualizedInApplet()) {
        return false;
      }
      return true;
    }
  };
  GUIAction startStopSimulationAction = new GUIAction("Start/Stop simulation", KeyStroke.getKeyStroke(KeyEvent.VK_S, ActionEvent.CTRL_MASK)) {
    public void actionPerformed(ActionEvent e) {
      /* Start/Stop current simulation */
      Simulation s = getSimulation();
      if (s == null) {
        return;
      }
      if (s.isRunning()) {
        s.stopSimulation();
      } else {
        s.startSimulation();
      }
    }
    public void setEnabled(boolean newValue) {
      if (getSimulation() == null) {
        putValue(NAME, "Start/Stop simulation");
      } else if (getSimulation().isRunning()) {
        putValue(NAME, "Stop simulation");
      } else {
        putValue(NAME, "Start simulation");
      }
      super.setEnabled(newValue);
    }
    public boolean shouldBeEnabled() {
      return getSimulation() != null;
    }
  };
  class StartPluginGUIAction extends GUIAction {
    public StartPluginGUIAction(String name) {
      super(name);
    }
    public void actionPerformed(final ActionEvent e) {
      new Thread(new Runnable() {
        public void run() {
          Class<Plugin> pluginClass = 
            (Class<Plugin>) ((JMenuItem) e.getSource()).getClientProperty("class");
          Mote mote = (Mote) ((JMenuItem) e.getSource()).getClientProperty("mote");
          tryStartPlugin(pluginClass, myGUI, mySimulation, mote);
        }
      }).start();
    }
    public boolean shouldBeEnabled() {
      return getSimulation() != null;
    }
  }
  GUIAction removeAllMotesAction = new GUIAction("Remove all motes") {
    public void actionPerformed(ActionEvent e) {
      Simulation s = getSimulation();
      if (s.isRunning()) {
        s.stopSimulation();
      }

      while (s.getMotesCount() > 0) {
        s.removeMote(getSimulation().getMote(0));
      }
    }
    public boolean shouldBeEnabled() {
      Simulation s = getSimulation();
      return s != null && s.getMotesCount() > 0;
    }
  };
  GUIAction showQuickHelpAction = new GUIAction("Quick help", KeyStroke.getKeyStroke(KeyEvent.VK_F1, 0)) {
    public void actionPerformed(ActionEvent e) {
      if (!(e.getSource() instanceof JCheckBoxMenuItem)) {
        return;
      }
      boolean show = ((JCheckBoxMenuItem) e.getSource()).isSelected();
      quickHelpTextPane.setVisible(show);
      quickHelpScroll.setVisible(show);
      ((JPanel)frame.getContentPane()).revalidate();
      updateDesktopSize(getDesktopPane());
    }

    public boolean shouldBeEnabled() {
      return true;
    }
  };
  GUIAction showKeyboardShortcutsAction = new GUIAction("Keyboard shortcuts") {
    public void actionPerformed(ActionEvent e) {
      loadQuickHelp("KEYBOARD_SHORTCUTS");
      JCheckBoxMenuItem checkBox = ((JCheckBoxMenuItem)showQuickHelpAction.getValue("checkbox"));
      if (checkBox == null) {
        return;
      }
      if (checkBox.isSelected()) {
        return;
      }
      checkBox.doClick();
    }

    public boolean shouldBeEnabled() {
      return true;
    }
  };
  GUIAction showBufferSettingsAction = new GUIAction("Buffer sizes") {
    public void actionPerformed(ActionEvent e) {
      if (mySimulation == null) {
        return;
      }
      BufferSettings.showDialog(myDesktopPane, mySimulation);
    }
    public boolean shouldBeEnabled() {
      return mySimulation != null;
    }
  };
  
}
