public abstract class Agent implements Cloneable, Serializable {
  private static final long serialVersionUID = 1L;
  private static int s_agent_count = 0;

  private transient int m_agent_id;
  private transient String m_name;
  private Color m_color;

  /**
   * Entorno en el que reside el agente.
   */
  protected transient Environment m_env;

  /**
   * Posición en la que se encuentra el agente.
   */
  protected transient Point m_pos;

  /**
   * Crea un nuevo agente en el entorno. Le asigna un ID único y lo coloca en el
   * punto (0,0).
   *
   * @param env
   *          Entorno al que va a ser asignado dicho agente.
   */
  protected Agent (Environment env) {
    m_agent_id = s_agent_count++;
    m_name = getAlgorithmName() + " " + String.valueOf(m_agent_id);
    m_color = getAlgorithmColor();
    m_pos = new Point();
    setEnvironment(env);
  }

  /**
   * Establece la posición del agente.
   * <br><br>
   * Este método se puede sobrecargar en las clases derivadas para mantener
   * coherente la memoria de la que disponga el mismo.
   *
   * @param pos
   *          Nueva posición del agente.
   */
  public void setPosition (Point pos) {
    m_pos.x = pos.x;
    m_pos.y = pos.y;
  }

  /**
   * Obtiene la posición en el eje X del agente.
   *
   * @return Posición en el eje X del agente.
   */
  public int getX () {
    return m_pos.x;
  }

  /**
   * Obtiene la posición en el eje Y del agente.
   *
   * @return Posición en el eje Y de agente.
   */
  public int getY () {
    return m_pos.y;
  }

  /**
   * Obtiene la posición del agente.
   *
   * @return Posición del agente.
   */
  public Point getPos () {
    return new Point(m_pos);
  }

  /**
   * Cambia el entorno en el que se sitúa el agente.
   * <br><br>
   * Este método debería sobrecargarse en las clases derivadas que contengan
   * información acerca del camino a seguir por el agente (un plan) de forma que
   * éste siga siendo coherente tras el cambio de laberinto.
   *
   * @param env
   *          Entorno donde colocar el agente.
   */
  public void setEnvironment (Environment env) {
    if (env != null)
      m_env = env;
    else
      throw new IllegalArgumentException(
          MainWindow.getTranslations().exception().invalidEnvironment());
  }

  /**
   * Obtiene el entorno en el que se encuentra el agente.
   *
   * @return Entorno en el que se encuentra el agente.
   */
  public Environment getEnvironment () {
    return m_env;
  }

  /**
   * Obtiene la visión que tiene el agente al mirar en la dirección indicada.
   *
   * @param dir
   *          Dirección hacia la que mirar.
   * @return Lo que vería el agente si mira en la dirección especificada.
   */
  public MazeCell.Vision look (Direction dir) {
    return m_env.look(m_pos, dir);
  }

  /*
   * (non-Javadoc)
   *
   * @see java.lang.Object#equals(java.lang.Object)
   */
  @Override
  public boolean equals (Object obj) {
    return this == obj;
  }

  /*
   * (non-Javadoc)
   *
   * @see java.lang.Object#hashCode()
   */
  @Override
  public int hashCode () {
    return m_env.hashCode();
  }

  /**
   * Hace que el agente realice el movimiento especificado, sin comprobar que se
   * trata de un movimiento válido.
   * <br><br>
   * La clase base sólo cambia la posición del agente, si se desea más
   * funcionalidad, se debe sobrecargar en las clases derivadas.
   *
   * @param dir
   *          Dirección hacia la que mover el agente.
   */
  public void doMovement (Direction dir) {
    m_pos = dir.movePoint(m_pos);
  }

  /**
   * Cambia el nombre del agente.
   *
   * @param name
   *          Nombre del agente.
   */
  public void setAgentName (String name) {
    if (name != null && !name.isEmpty())
      m_name = name;
  }

  /**
   * Pregunta al agente el nombre que le identifica.
   *
   * @return Nombre identificador del agente. Por defecto incluye el nombre del
   *         algoritmo que implementa.
   */
  public String getAgentName () {
    return m_name;
  }

  /**
   * Cambia el color del agente.
   *
   * @param color
   *          Nuevo color del agente.
   */
  public void setAgentColor (Color color) {
    if (color != null)
      m_color = color;
  }

  /**
   * Pregunta al agente su color.
   *
   * @return El color del agente.
   */
  public Color getAgentColor () {
    return m_color;
  }

  /**
   * Carga la descripción serializada del agente de un fichero, lo crea y lo
   * devuelve.
   *
   * @param filename
   *          Nombre del fichero de entrada.
   * @param env
   *          Entorno en el que cargar el agente.
   * @return El agente creado.
   * @throws IOException
   *           Si no es posible leer el fichero.
   */
  public static Agent loadFile (String filename, Environment env) throws IOException {
    try {
      Agent ag;
      FileInputStream file_in = new FileInputStream(filename);
      ObjectInputStream in = new ObjectInputStream(file_in);
      ag = (Agent) in.readObject();
      in.close();
      file_in.close();

      ag.m_agent_id = s_agent_count++;
      ag.m_name = ag.getAlgorithmName() + " " + String.valueOf(ag.m_agent_id);
      ag.m_pos = new Point();
      ag.setEnvironment(env);

      return ag;
    }
    catch (ClassNotFoundException c) {
      throw new IOException(c);
    }
  }

  /**
   * Guarda la instancia del agente en un fichero utilizando su serialización.
   *
   * @param filename
   *          Nombre del fichero de salida.
   * @throws IOException
   *           Si no es posible guardar el fichero.
   */
  public void saveFile (String filename) throws IOException {
    FileOutputStream file_out = new FileOutputStream(filename);
    ObjectOutputStream out = new ObjectOutputStream(file_out);
    out.writeObject(this);
    out.close();
    file_out.close();
  }

  /**
   * Pregunta al agente el nombre del algoritmo que implementa.
   *
   * @return Nombre del algoritmo que implementa.
   */
  public abstract String getAlgorithmName ();


  /**
   * Pregunta al agente el color por defecto de los agentes de su tipo.
   *
   * @return Color por defecto de los agentes que implementan ese algoritmo.
   */
  public abstract Color getAlgorithmColor ();

  /**
   * Obtiene el siguiente movimiento dado el estado actual del agente.
   *
   * @return La dirección en la que el agente quiere realizar el siguiente
   *         movimiento.
   */
  public abstract Direction getNextMovement ();

  /**
   * Elimina la memoria que el agente tenga sobre el entorno. No elimina su
   * configuración, sino que lo deja en el estado inicial.
   */
  public abstract void resetMemory ();

  /**
   * Obtiene el panel de configuración asociado al agente.
   *
   * @return Un panel de configuración para el agente.
   */
  public abstract AgentConfigurationPanel getConfigurationPanel ();

  /*
   * (non-Javadoc)
   *
   * @see java.lang.Object#clone()
   */
  @Override
  public abstract Object clone ();

} public class MainWindow extends JFrame implements Observer, Translatable {
  private static String APP_NAME = "Maze Solver";
  private static String VERSION_STRING = "1.2";
  private static int DEFAULT_WIDTH = 640;
  private static int DEFAULT_HEIGHT = 480;

  private static int MINIMUM_ZOOM_VAL = 1;
  private static int MAXIMUM_ZOOM_VAL = 100;
  private static double MINIMUM_ZOOM_AUG = 1;
  private static double MAXIMUM_ZOOM_AUG = 10;

  private static final String USER_DOCS_URL = "https://github.com/MazeSolver/MazeSolver/wiki";
  private static final String API_DOCS_URL = "https://mazesolver.github.io/MazeSolver/";
  private static final String ISSUES_URL = "https://github.com/MazeSolver/MazeSolver/labels/bug";

  private static final long serialVersionUID = 1L;
  private static MainWindow s_instance;
  private static Translations s_tr;

  /**
   * Inicializa la interfaz gráfica y la muestra por pantalla.
   *
   * @param args
   *          No utilizados.
   */
  public static void main (String [] args) {
    try {
      UIManager.setLookAndFeel(WebLookAndFeel.class.getCanonicalName());
    }
    catch (Exception e) {
    }
    MainWindow wnd = MainWindow.getInstance();

    wnd.setTitle(APP_NAME);
    wnd.setSize(new Dimension(DEFAULT_WIDTH, DEFAULT_HEIGHT));
    wnd.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
    wnd.setMinimumSize(new Dimension(DEFAULT_WIDTH, DEFAULT_HEIGHT));
    wnd.setLocationRelativeTo(null);
    wnd.setVisible(true);
  }

  /**
   * Obtiene la instancia única de la clase o la crea si no existe.
   *
   * @return Instancia única de la clase.
   */
  public static MainWindow getInstance () {
    if (s_instance == null)
      s_instance = new MainWindow();
    return s_instance;
  }

  /**
   * Obtiene la instancia de traductor de la aplicación o la crea si no había
   * sido creada antes.
   *
   * @return El traductor de la aplicación.
   */
  public static Translations getTranslations () {
    if (s_tr == null)
      s_tr = C10N.get(Translations.class);
    return s_tr;
  }

  // Panel que contiene tanto el panel con los laberintos como el panel de
  // configuración, de tal manera que se pueden cambiar de tamaño.
  private JSplitPane m_split_panel;

  // Barra de menús
  private JMenuBar m_menu_bar;

  // Barra de acciones
  private WebToolBar m_toolbar;

  // Panel de configuración. Por defecto está oculto, pero cuando el usuario
  // vaya a configurar un agente la interfaz adecuada aparecerá en su lugar.
  private AgentConfigurationPanel m_config_panel;

  // Otros elementos en la interfaz
  private JLabel m_zoom_lb;
  private JButton m_run, m_step, m_pause, m_stop;
  private JSlider m_zoom;
  private JMenu m_menu_file, m_menu_env, m_menu_agent, m_menu_sim,
                m_menu_config, m_menu_language, m_menu_help;
  private JMenuItem m_itm_exit;
  private JMenuItem m_itm_env_new, m_itm_env_save, m_itm_env_config,
                    m_itm_env_close;
  private JMenuItem m_itm_agent_new, m_itm_agent_open, m_itm_agent_save,
                    m_itm_agent_copy, m_itm_agent_config, m_itm_agent_remove;
  private JMenuItem m_itm_sim_run, m_itm_sim_step, m_itm_sim_pause,
                    m_itm_sim_stop, m_itm_sim_fastrun;
  private JRadioButtonMenuItem m_itm_mode_sim, m_itm_mode_edit;
  private JMenuItem m_itm_language_spanish, m_itm_language_english,
                    m_itm_language_german, m_itm_language_russian,
                    m_itm_language_french, m_itm_language_italian,
                    m_itm_language_slovenian, m_itm_language_romanian,
                    m_itm_language_greek;
  private JMenuItem m_itm_userdoc, m_itm_apidoc, m_itm_bugs, m_itm_about;

  // Representación del modelo
  private EnvironmentSet m_environments;
  private SimulationManager m_simulation;

  // Interacción con el usuario
  private LoggingConsole m_console;
  private Agent m_agent_clipboard;

  /**
   * Constructor de la clase. Crea la interfaz y configura su estado interno
   * para permitir el uso del programa.
   */
  private MainWindow () {
    C10N.configure(new C10NConfigBase() {
      @Override
      protected void configure () {
        // install default annotations
        install(new DefaultC10NAnnotations());

        bindBundle("translations.Translations");

        /**
         * setup c10n to fallback to @En annotation when locale does not match
         * any other registered annotation, by binding it without specifying the
         * locale.
         */
        bindAnnotation(En.class);
      }
    });

    // Lo llamamos para asegurarnos de que las traducciones están cargadas antes
    // de crear los mensajes de error.
    MainWindow.getTranslations();

    createInterface();
    m_simulation = new SimulationManager(m_environments);
    m_simulation.addObserver(this);

    // Una vez todo está creado traducimos la aplicación
    translate();
  }

  /**
   * Construye los objetos de los que se compone la interfaz.
   */
  private void createInterface () {
    setLayout(new BorderLayout());

    createMenus();
    createToolbar();

    JPanel global_panel = new JPanel(new BorderLayout());
    global_panel.add(m_toolbar, BorderLayout.NORTH);
    m_environments = new EnvironmentSet();

    m_split_panel = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT, true, null, m_environments);

    m_console = new LoggingConsole();
    final JSplitPane console_split =
        new JSplitPane(JSplitPane.VERTICAL_SPLIT, true, m_split_panel, m_console);
    m_console.addPropertyChangeListener("ConsoleDisplay", new PropertyChangeListener() {
      @Override
      public void propertyChange (PropertyChangeEvent e) {
        // Si antes estaba visible, ahora no lo está, así que ajustamos el
        // separador para que se minimice la consola
        if ((Boolean) e.getOldValue())
          console_split.resetToPreferredSizes();
        // Si no, redimensionamos el panel para mostrar la consola
        else
          console_split.setDividerLocation(0.75);
      }
    });

    console_split.setResizeWeight(1);

    global_panel.add(console_split, BorderLayout.CENTER);
    add(m_menu_bar, BorderLayout.NORTH);
    add(global_panel, BorderLayout.CENTER);

    // Creamos las acciones de Ctrl+C y Ctrl+V para copiar agentes y enlazamos
    // la tecla "delete" para eliminar el agente seleccionado
    InputMap env_input = m_environments.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW);
    ActionMap env_action = m_environments.getActionMap();
    env_input.put(KeyStroke.getKeyStroke(KeyEvent.VK_C, InputEvent.CTRL_DOWN_MASK), "AgentCopy");
    env_input.put(KeyStroke.getKeyStroke(KeyEvent.VK_V, InputEvent.CTRL_DOWN_MASK), "AgentPaste");
    env_input.put(KeyStroke.getKeyStroke(KeyEvent.VK_DELETE, 0), "AgentDelete");

    env_action.put("AgentCopy", new AbstractAction() {
      private static final long serialVersionUID = 1L;

      @Override
      public void actionPerformed (ActionEvent e) {
        m_agent_clipboard = null;

        Environment env = m_environments.getSelectedEnvironment();
        if (env != null)
          m_agent_clipboard = env.getSelectedAgent();
      }
    });

    env_action.put("AgentPaste", new AbstractAction() {
      private static final long serialVersionUID = 1L;

      @Override
      public void actionPerformed (ActionEvent e) {
        Environment env = m_environments.getSelectedEnvironment();
        if (env != null && m_agent_clipboard != null)
          env.addAgent((Agent) m_agent_clipboard.clone());
      }
    });

    env_action.put("AgentDelete", new AbstractAction() {
      private static final long serialVersionUID = 1L;

      @Override
      public void actionPerformed (ActionEvent e) {
        Environment env = m_environments.getSelectedEnvironment();
        if (env != null) {
          Agent agent = env.getSelectedAgent();
          if (agent != null)
            env.removeAgent(agent);
        }
      }
    });

    closeConfigurationPanel();
    pack();
  }

  /**
   * Crea la barra de menús y sus elementos.
   */
  private void createMenus () {
    m_menu_bar = new JMenuBar();

    m_menu_file = new JMenu();
    m_menu_env = new JMenu();
    m_menu_agent = new JMenu();
    m_menu_sim = new JMenu();
    m_menu_config = new JMenu();
    m_menu_language = new JMenu();
    m_menu_help = new JMenu();

    // Menú "File"
    m_itm_exit = new JMenuItem();

    m_menu_file.add(m_itm_exit);

    // Menú "Environment"
    m_itm_env_new = new JMenuItem();
    m_itm_env_config = new JMenuItem();
    m_itm_env_save = new JMenuItem();
    m_itm_env_close = new JMenuItem();

    m_menu_env.add(m_itm_env_new);
    m_menu_env.add(m_itm_env_config);
    m_menu_env.addSeparator();
    m_menu_env.add(m_itm_env_save);
    m_menu_env.addSeparator();
    m_menu_env.add(m_itm_env_close);

    // Menú "Agent"
    m_itm_agent_new = new JMenuItem();
    m_itm_agent_config = new JMenuItem();
    m_itm_agent_copy = new JMenuItem();
    m_itm_agent_open = new JMenuItem();
    m_itm_agent_save = new JMenuItem();
    m_itm_agent_remove = new JMenuItem();

    m_menu_agent.add(m_itm_agent_new);
    m_menu_agent.add(m_itm_agent_config);
    m_menu_agent.add(m_itm_agent_copy);
    m_menu_agent.addSeparator();
    m_menu_agent.add(m_itm_agent_open);
    m_menu_agent.add(m_itm_agent_save);
    m_menu_agent.addSeparator();
    m_menu_agent.add(m_itm_agent_remove);

    // Menú "Simulation"
    m_itm_sim_run = new JMenuItem();
    m_itm_sim_step = new JMenuItem();
    m_itm_sim_pause = new JMenuItem();
    m_itm_sim_stop = new JMenuItem();
    m_itm_sim_fastrun = new JMenuItem();

    m_menu_sim.add(m_itm_sim_run);
    m_menu_sim.add(m_itm_sim_step);
    m_menu_sim.add(m_itm_sim_pause);
    m_menu_sim.add(m_itm_sim_stop);
    m_menu_sim.addSeparator();
    m_menu_sim.add(m_itm_sim_fastrun);

    // Menú "Configuration"
    m_itm_mode_sim = new JRadioButtonMenuItem();
    m_itm_mode_edit = new JRadioButtonMenuItem();

    m_itm_mode_sim.setSelected(true);

    m_menu_config.add(m_itm_mode_sim);
    m_menu_config.add(m_itm_mode_edit);

    ButtonGroup group = new ButtonGroup();
    group.add(m_itm_mode_sim);
    group.add(m_itm_mode_edit);

    // Menú "Language"
    m_itm_language_spanish = new JMenuItem();
    m_itm_language_english = new JMenuItem();
    m_itm_language_german = new JMenuItem();
    m_itm_language_russian = new JMenuItem();
    m_itm_language_french = new JMenuItem();
    m_itm_language_italian = new JMenuItem();
    m_itm_language_slovenian = new JMenuItem();
    m_itm_language_romanian = new JMenuItem();
    m_itm_language_greek = new JMenuItem();

    m_menu_language.add(m_itm_language_spanish);
    m_menu_language.add(m_itm_language_english);
    m_menu_language.add(m_itm_language_german);
    m_menu_language.add(m_itm_language_russian);
    m_menu_language.add(m_itm_language_french);
    m_menu_language.add(m_itm_language_italian);
    m_menu_language.add(m_itm_language_slovenian);
    m_menu_language.add(m_itm_language_romanian);
    m_menu_language.add(m_itm_language_greek);

    // Menú "Help"
    ImageIcon world = new ImageIcon(getClass().getResource("/images/world.png"));
    ImageIcon info = new ImageIcon(getClass().getResource("/images/info.png"));
    m_itm_userdoc = new JMenuItem("", world);
    m_itm_apidoc = new JMenuItem("", world);
    m_itm_bugs = new JMenuItem("", world);
    m_itm_about = new JMenuItem("", info);

    m_menu_help.add(m_itm_userdoc);
    m_menu_help.add(m_itm_apidoc);
    m_menu_help.add(m_itm_bugs);
    m_menu_help.addSeparator();
    m_menu_help.add(m_itm_about);

    m_menu_bar.add(m_menu_file);
    m_menu_bar.add(m_menu_env);
    m_menu_bar.add(m_menu_agent);
    m_menu_bar.add(m_menu_sim);
    m_menu_bar.add(m_menu_config);
    m_menu_bar.add(m_menu_language);
    m_menu_bar.add(m_menu_help);

    setupMenuListeners();
  }

  /**
   * Crea la barra de acciones.
   */
  private void createToolbar () {
    m_toolbar = new WebToolBar();
    m_toolbar.setFloatable(false);

    m_run = new JButton();
    m_step = new JButton();
    m_pause = new JButton();
    m_stop = new JButton();
    m_zoom = new JSlider(MINIMUM_ZOOM_VAL, MAXIMUM_ZOOM_VAL);
    m_zoom_lb = new JLabel();

    m_pause.setEnabled(false);
    m_stop.setEnabled(false);
    m_zoom.setValue(MINIMUM_ZOOM_VAL);

    m_toolbar.add(m_run);
    m_toolbar.add(m_step);
    m_toolbar.add(m_pause);
    m_toolbar.add(m_stop);
    m_toolbar.addToEnd(m_zoom_lb);
    m_toolbar.addToEnd(m_zoom);

    setupToolbarListeners();
  }

  /**
   * Crea los ActionListeners para los menús.
   */
  private void setupMenuListeners () {
    // Menú "File"
    // /////////////////////////////////////////////////////////////////////////
    m_itm_exit.addActionListener(new ActionListener() {
      @Override
      public void actionPerformed (ActionEvent e) {
        dispatchEvent(new WindowEvent(MainWindow.getInstance(), WindowEvent.WINDOW_CLOSING));
      }
    });

    // Menú "Environment"
    // /////////////////////////////////////////////////////////////////////////
    m_itm_env_new.addActionListener(new ActionListener() {
      @Override
      public void actionPerformed (ActionEvent e) {
        EnvSelectorDialog dialog = new EnvSelectorDialog(MainWindow.getInstance(),m_environments);
        dialog.setLocationRelativeTo(MainWindow.this);
        Environment generated = dialog.showDialog();

        if (generated != null)
          m_environments.addEnvironment(generated);
      }
    });

    m_itm_env_config.addActionListener(new ActionListener() {
      @Override
      public void actionPerformed (ActionEvent e) {
        Environment actual_env = m_environments.getSelectedEnvironment();
        if (actual_env == null) {
          JOptionPane.showMessageDialog(null,
              s_tr.message().noEnvironmentSelected(),
              s_tr.message().mazeChangeFailed(), JOptionPane.WARNING_MESSAGE);
          return;
        }

        EnvSelectorDialog dialog = new EnvSelectorDialog(MainWindow.this,
                                                         m_environments,
                                                         actual_env);
        dialog.setLocationRelativeTo(MainWindow.this);
        Environment new_env = dialog.showDialog();

        if (new_env != null) {
          for (int i = 0; i < actual_env.getAgentCount(); i++)
            new_env.addAgent(actual_env.getAgent(i));

          m_environments.exchangeEnvironments(actual_env, new_env);
        }
      }
    });

    m_itm_env_save.addActionListener(new ActionListener() {
      @Override
      public void actionPerformed (ActionEvent e) {
        try {
          FileDialog.saveMaze(m_environments.getSelectedEnvironment().getMaze());
        }
        catch (IOException exc) {
          JOptionPane.showMessageDialog(null, exc.getMessage(), s_tr.message().fileSaveFailed(),
              JOptionPane.ERROR_MESSAGE);
        }
        catch (Exception exc) {
          JOptionPane.showMessageDialog(null, s_tr.message().noEnvironmentSelected(), s_tr
              .message().fileSaveFailed(), JOptionPane.WARNING_MESSAGE);
        }
      }
    });

    m_itm_env_close.addActionListener(new ActionListener() {
      @Override
      public void actionPerformed (ActionEvent e) {
        m_environments.removeSelectedEnvironment();
      }
    });

    // Menú "Agent"
    // /////////////////////////////////////////////////////////////////////////
    m_itm_agent_new.addActionListener(new ActionListener() {
      @Override
      public void actionPerformed (ActionEvent e) {
        Environment env = m_environments.getSelectedEnvironment();
        if (env == null) {
          JOptionPane.showMessageDialog(null, s_tr.message().noEnvironmentSelected(), s_tr
              .message().agentCreationFailed(), JOptionPane.WARNING_MESSAGE);
          return;
        }

        Maze maze = env.getMaze();
        AgentSelectorDialog dialog =
            new AgentSelectorDialog(MainWindow.this, (maze.getHeight() * maze.getWidth())
                - env.getAgentCount());
        dialog.setLocationRelativeTo(MainWindow.this);
        Agent [] agents = dialog.showDialog();

        if (agents != null) {
          for (Agent ag: agents)
            m_environments.addAgentToSelectedEnvironment(ag);
        }
      }
    });

    m_itm_agent_open.addActionListener(new ActionListener() {
      @Override
      public void actionPerformed (ActionEvent e) {
        Environment env = m_environments.getSelectedEnvironment();

        if (env != null) {
          try {
            Agent ag = FileDialog.loadAgent(env);
            if (ag != null)
              m_environments.addAgentToSelectedEnvironment(ag);
          }
          catch (IOException exc) {
            JOptionPane.showMessageDialog(null, exc.getMessage(),
                s_tr.message().fileOpenFailed(), JOptionPane.ERROR_MESSAGE);
          }
        }
        else {
          JOptionPane.showMessageDialog(null, s_tr.message().noEnvironmentSelected(),
              s_tr.message().fileOpenFailed(), JOptionPane.WARNING_MESSAGE);
        }
      }
    });

    m_itm_agent_save.addActionListener(new ActionListener() {
      @Override
      public void actionPerformed (ActionEvent e) {
        Environment env = m_environments.getSelectedEnvironment();

        if (env != null) {
          Agent ag = env.getSelectedAgent();

          if (ag != null) {
            try {
              FileDialog.saveAgent(ag);
            }
            catch (IOException exc) {
              JOptionPane.showMessageDialog(null, exc.getMessage(),
                  s_tr.message().fileSaveFailed(), JOptionPane.ERROR_MESSAGE);
            }
          }
          else {
            JOptionPane.showMessageDialog(null, s_tr.message().noAgentSelected(),
                s_tr.message().fileSaveFailed(), JOptionPane.WARNING_MESSAGE);
          }
        }
        else {
          JOptionPane.showMessageDialog(null, s_tr.message().noEnvironmentSelected(),
              s_tr.message().fileSaveFailed(), JOptionPane.WARNING_MESSAGE);
        }
      }
    });

    m_itm_agent_copy.addActionListener(new ActionListener() {
      @Override
      public void actionPerformed (ActionEvent e) {
        try {
          Environment env = m_environments.getSelectedEnvironment();
          m_environments.addAgentToSelectedEnvironment((Agent) env.getSelectedAgent().clone());
        }
        catch (Exception exc) {
          JOptionPane.showMessageDialog(null, s_tr.message().noAgentSelected(),
              s_tr.message().cloningFailed(), JOptionPane.WARNING_MESSAGE);
        }
      }
    });

    m_itm_agent_config.addActionListener(new ActionListener() {
      @Override
      public void actionPerformed (ActionEvent e) {
        try {
          Agent ag = m_environments.getSelectedEnvironment().getSelectedAgent();
          setConfigurationPanel(ag.getConfigurationPanel());
        }
        catch (Exception exc) {
          JOptionPane.showMessageDialog(null, s_tr.message().noAgentSelected(), s_tr.message()
              .agentConfigFailed(), JOptionPane.WARNING_MESSAGE);
        }
      }
    });

    m_itm_agent_remove.addActionListener(new ActionListener() {
      @Override
      public void actionPerformed (ActionEvent e) {
        try {
          Environment env = m_environments.getSelectedEnvironment();
          m_environments.removeAgentFromEnvironment(env.getSelectedAgent(), env);
        }
        catch (Exception exc) {
          JOptionPane.showMessageDialog(null, s_tr.message().noAgentSelected(), s_tr.message()
              .agentRemovalFailed(), JOptionPane.WARNING_MESSAGE);
        }
      }
    });

    // Menú "Simulation"
    // /////////////////////////////////////////////////////////////////////////
    m_itm_sim_run.addActionListener(new RunAction());
    m_itm_sim_step.addActionListener(new StepAction());
    m_itm_sim_pause.addActionListener(new PauseAction());
    m_itm_sim_stop.addActionListener(new StopAction());

    m_itm_sim_fastrun.addActionListener(new ActionListener() {
      @Override
      public void actionPerformed (ActionEvent e) {
        FastSimulationDialog dialog = new FastSimulationDialog(MainWindow.this);
        dialog.setLocationRelativeTo(MainWindow.this);
        int steps = dialog.showDialog();

        if (steps > 0)
          startFastSimulation(steps);
      }
    });

    // Menú "Configuration"
    // /////////////////////////////////////////////////////////////////////////
    m_itm_mode_sim.addActionListener(new ActionListener() {
      @Override
      public void actionPerformed (ActionEvent e) {
        setInteractionModeState(InteractionMode.SIMULATION);
        m_environments.setInteractionMode(InteractionMode.SIMULATION);
      }
    });

    m_itm_mode_edit.addActionListener(new ActionListener() {
      @Override
      public void actionPerformed (ActionEvent e) {
        setInteractionModeState(InteractionMode.EDITION);
        m_environments.setInteractionMode(InteractionMode.EDITION);
      }
    });

    // Menú "Languages"
    // /////////////////////////////////////////////////////////////////////////
    class LanguageAction implements ActionListener {
      private String m_lang_code;

      public LanguageAction (String lang_code) {
        m_lang_code = lang_code;
      }

      @Override
      public void actionPerformed (ActionEvent e) {
        s_tr = C10N.get(Translations.class, new Locale(m_lang_code));
        translate();
      }
    }

    m_itm_language_spanish.addActionListener(new LanguageAction("es"));
    m_itm_language_english.addActionListener(new LanguageAction("en"));
    m_itm_language_german.addActionListener(new LanguageAction("de"));
    m_itm_language_russian.addActionListener(new LanguageAction("ru"));
    m_itm_language_french.addActionListener(new LanguageAction("fr"));
    m_itm_language_italian.addActionListener(new LanguageAction("it"));
    m_itm_language_slovenian.addActionListener(new LanguageAction("si"));
    m_itm_language_romanian.addActionListener(new LanguageAction("ro"));
    m_itm_language_greek.addActionListener(new LanguageAction("el"));

    // Menú "Help"
    // /////////////////////////////////////////////////////////////////////////
    class URLLauncher implements ActionListener {
      private String m_url;

      public URLLauncher (String url) {
        m_url = url;
      }

      @Override
      public void actionPerformed (ActionEvent e) {
        try {
          Desktop.getDesktop().browse(URI.create(m_url));
        }
        catch (Exception exc) {
          JOptionPane.showMessageDialog(null, exc.getMessage(), s_tr.message().urlLaunchFailed(),
              JOptionPane.ERROR_MESSAGE);
        }
      }
    }

    m_itm_userdoc.addActionListener(new URLLauncher(USER_DOCS_URL));
    m_itm_apidoc.addActionListener(new URLLauncher(API_DOCS_URL));
    m_itm_bugs.addActionListener(new URLLauncher(ISSUES_URL));

    m_itm_about.addActionListener(new ActionListener() {
      @Override
      public void actionPerformed (ActionEvent e) {
        JOptionPane
            .showMessageDialog(
                null,
                "<html><h2>" + APP_NAME + " " + VERSION_STRING + "</h2>"
                    + "Copyright &copy; 2014 - 2015<br>Sergio M. Afonso Fumero and Kevin I. Robayna Hernández<br><br>"
                    + "This program is free software: you can redistribute it and/or modify<br>"
                    + "it under the terms of the GNU General Public License as published by<br>"
                    + "the Free Software Foundation, either version 3 of the License, or<br>"
                    + "(at your option) any later version.<br><br>"
                    + "This program is distributed in the hope that it will be useful,<br>"
                    + "but WITHOUT ANY WARRANTY; without even the implied warranty of<br>"
                    + "MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the<br>"
                    + "GNU General Public License for more details.<br><br>"
                    + "You should have received a copy of the GNU General Public License<br>"
                    + "along with this program. If not, see &lt;<a href=\"http://www.gnu.org/licenses/\">http://www.gnu.org/licenses/</a>&gt;.</html>",
                s_tr.menu().about(), JOptionPane.INFORMATION_MESSAGE);
      }
    });
  }

  /**
   * Configura los listeners de los botones en la barra de herramientas.
   */
  private void setupToolbarListeners () {
    m_run.addActionListener(new RunAction());
    m_step.addActionListener(new StepAction());
    m_pause.addActionListener(new PauseAction());
    m_stop.addActionListener(new StopAction());

    m_zoom.addChangeListener(new ChangeListener() {
      @Override
      public void stateChanged (ChangeEvent e) {
        JSlider src = (JSlider) e.getSource();

        // Ajuste lineal del zoom
        double a = (MAXIMUM_ZOOM_AUG - MINIMUM_ZOOM_AUG) / (MAXIMUM_ZOOM_VAL - MINIMUM_ZOOM_VAL);
        double b = MAXIMUM_ZOOM_AUG - a * MAXIMUM_ZOOM_VAL;
        m_environments.setZoom(a * src.getValue() + b);
      }
    });
  }

  /**
   * Devuelve una referencia a la consola.
   *
   * @return Referencia a la consola de la ventana.
   */
  public LoggingConsole getConsole () {
    return m_console;
  }

  /**
   * Devuelve una referencia al conjunto de entornos.
   *
   * @return Referencia al conjunto de entornos de la ventana.
   */
  public EnvironmentSet getEnvironments () {
    return m_environments;
  }

  /**
   * Abre el panel de configuración.
   *
   * @param ag_panel
   *          Panel de configuración que se quiere abrir.
   */
  public void setConfigurationPanel (final AgentConfigurationPanel ag_panel) {
    if (m_config_panel != null)
      closeConfigurationPanel();

    if (ag_panel != null) {
      ((BasicSplitPaneUI) m_split_panel.getUI()).getDivider().setVisible(true);

      m_config_panel = ag_panel;

      ag_panel.addEventListener(new AgentConfigurationPanel.EventListener() {
        @Override
        public void onSuccess (ArrayList <String> msgs) {
          for (String msg: msgs)
            m_console.writeInfo(msg);
          closeConfigurationPanel();
        }

        @Override
        public void onError (ArrayList <String> errors) {
          for (String error: errors)
            m_console.writeError(error);
        }

        @Override
        public void onCancel () {
          closeConfigurationPanel();
        }
      });

      m_split_panel.add(m_config_panel);
      revalidate();
      repaint();

      m_itm_env_close.setEnabled(false);
      m_itm_agent_remove.setEnabled(false);
    }
  }

  /**
   * Cierra el panel de configuración.
   */
  public void closeConfigurationPanel () {
    ((BasicSplitPaneUI) m_split_panel.getUI()).getDivider().setVisible(false);
    if (m_config_panel != null) {
      m_split_panel.remove(m_config_panel);
      m_config_panel = null;
      revalidate();
      repaint();
    }
    m_itm_env_close.setEnabled(true);
    m_itm_agent_remove.setEnabled(true);
  }

  /*
   * (non-Javadoc)
   *
   * @see java.util.Observer#update(java.util.Observable, java.lang.Object)
   */
  @Override
  public void update (Observable obs, Object obj) {
    SimulatorResultTranslations tr_sim = s_tr.simulation();

    // La notificación viene del comienzo de la ejecución
    if (m_simulation.isRunning())
      setSimulatingState();
    // La notificación viene de una pausa en la ejecución
    else if (m_simulation.isPaused())
      setPausedContinuedState();
    // La notificación viene de la parada o finalización de la ejecución
    else {
      stopSimulation();
      SimulationResults results = (SimulationResults) obj;

      ArrayList <Environment> envs = m_environments.getEnvironmentList();
      ArrayList <Maze> mazes = new ArrayList <Maze>();
      for (Environment env: envs) {
        if (!mazes.contains(env.getMaze()))
          mazes.add(env.getMaze());
      }

      m_console.writeInfo(tr_sim.title());
      m_console.writeInfo("==================");
      for (int i = 0; i < mazes.size(); i++) {
        Maze maze = mazes.get(i);
        Agent maze_winner = results.getWinner(maze);
        m_console.writeInfo("=== " + tr_sim.maze() + " " + (i + 1) + " (" + maze.getWidth() + "x"
            + maze.getHeight() + ") ===");
        m_console.writeInfo("* " + tr_sim.timeTakenFirst() + ": " + results.timeTakenFirst(maze));
        m_console.writeInfo("* " + tr_sim.timeTakenLast() + ": " + results.timeTakenLast(maze));
        m_console.writeInfo("* " + tr_sim.winner() + ": "
            + (maze_winner != null? maze_winner.getAgentName() : tr_sim.none()));
        m_console.writeInfo("");

        for (int j = 0; j < envs.size(); j++) {
          Environment env = envs.get(j);
          if (env.getMaze() == maze) {
            Agent env_winner = results.getWinner(env);
            m_console.writeInfo("  == " + env.getTitle() + " ==");
            m_console.writeInfo("* " + tr_sim.timeTakenFirst() + ": " + results.timeTakenFirst(env));
            m_console.writeInfo("* " + tr_sim.timeTakenLast() + ": " + results.timeTakenLast(env));
            m_console.writeInfo("* " + tr_sim.winner() + ": "
                + (env_winner != null? env_winner.getAgentName() : tr_sim.none()));
            m_console.writeInfo("");
            m_console.writeInfo("  * " + tr_sim.agentsDetail() + ":");

            for (Map.Entry <Agent, Pair<Integer, Integer>> entry: results.getSteps(env).entrySet()) {
              Agent ag = entry.getKey();
              String finished =
                  maze.containsPoint(new Point(ag.getX(), ag.getY()))? tr_sim.notFinished() : tr_sim
                      .finished();
              m_console.writeInfo("    - " + ag.getAgentName() + ": " +
                                  entry.getValue().first + " " + tr_sim.steps() + " / " +
                                  entry.getValue().second + " " + tr_sim.iterations() +
                                  " [" + finished + "]");
            }
          }
        }
      }
      m_console.writeInfo("==================");
    }
  }

  /*
   * (non-Javadoc)
   *
   * @see es.ull.mazesolver.translations.Translatable#translate()
   */
  @Override
  public void translate () {
    MenuTranslations m_tr = s_tr.menu();
    ButtonTranslations b_tr = s_tr.button();
    Languages m_lang_tr = s_tr.languages();

    m_menu_file.setText(m_tr.file());
    m_menu_env.setText(m_tr.environment());
    m_menu_agent.setText(m_tr.agent());
    m_menu_sim.setText(m_tr.simulation());
    m_menu_config.setText(m_tr.configuration());
    m_menu_language.setText(m_tr.language());
    m_menu_help.setText(m_tr.help());

    m_itm_exit.setText(m_tr.exit());
    m_itm_env_new.setText(m_tr.newEnv() + "...");
    m_itm_env_config.setText(m_tr.configEnv() + "...");
    m_itm_env_save.setText(m_tr.saveMaze() + "...");
    m_itm_env_close.setText(m_tr.closeEnv());
    m_itm_agent_new.setText(m_tr.newAgent() + "...");
    m_itm_agent_open.setText(m_tr.openAgent() + "...");
    m_itm_agent_save.setText(m_tr.saveAgent() + "...");
    m_itm_agent_copy.setText(m_tr.copyAgent());
    m_itm_agent_config.setText(m_tr.configureAgent() + "...");
    m_itm_agent_remove.setText(m_tr.removeAgent());
    m_itm_sim_run.setText(b_tr.run());
    m_itm_sim_step.setText(b_tr.step());
    m_itm_sim_stop.setText(b_tr.stop());
    m_itm_sim_fastrun.setText(b_tr.fastRun() + "...");
    m_itm_mode_sim.setText(m_tr.simulationMode());
    m_itm_mode_edit.setText(m_tr.editMode());
    m_itm_language_spanish.setText(m_lang_tr.spanish());
    m_itm_language_english.setText(m_lang_tr.english());
    m_itm_language_german.setText(m_lang_tr.german());
    m_itm_language_russian.setText(m_lang_tr.russian());
    m_itm_language_french.setText(m_lang_tr.french());
    m_itm_language_italian.setText(m_lang_tr.italian());
    m_itm_language_slovenian.setText(m_lang_tr.slovenian());
    m_itm_language_romanian.setText(m_lang_tr.romanian());
    m_itm_language_greek.setText(m_lang_tr.greek());
    m_itm_userdoc.setText(m_tr.userDocs());
    m_itm_apidoc.setText(m_tr.apiDocs());
    m_itm_bugs.setText(m_tr.knownProblems());
    m_itm_about.setText(m_tr.about() + "...");

    m_run.setText(b_tr.run());
    m_step.setText(b_tr.step());
    m_stop.setText(b_tr.stop());
    m_zoom_lb.setText(b_tr.zoom() + ":");

    String pauseText = b_tr.kontinue();
    if (m_simulation.isRunning() || m_simulation.isStopped())
      pauseText = b_tr.pause();

    m_pause.setText(pauseText);
    m_itm_sim_pause.setText(pauseText);

    m_console.translate();

    if (m_config_panel != null)
      m_config_panel.translate();
  }

  /**
   * Adapta los menús al estado de "Simulación en curso" y la comienza.
   */
  private void startSimulation () {
    //setSimulatingState();
    m_simulation.startSimulation();
  }

  /**
   * Adapta los menús al estado de "Simulación en curso" comienza una simulación
   * rápida.
   * @param steps
   *          Número máximo de pasos que se van a simular.
   */
  private void startFastSimulation (int steps) {
    if (m_simulation.isPaused()) {
      m_simulation.startFastSimulation(steps);
      setPausedContinuedState();
    }
    else {
      setSimulatingState();
      m_simulation.startFastSimulation(steps);
    }
  }

  /**
   * Pausa o reanuda la simulación dependiendo de su estado y mantiene coherente
   * el estado de los menús.
   */
  private void pauseSimulation () {
    if (m_simulation.isPaused())
      m_simulation.startSimulation();
    else
      m_simulation.pauseSimulation();

    setPausedContinuedState();
  }

  /**
   * Para la simulación y vuelve a dejar los menús en su estado inicial.
   */
  private void stopSimulation () {
    setStoppedState();

    m_simulation.stopSimulation();
    for (Environment env: m_environments.getEnvironmentList()) {
      for (int i = 0; i < env.getAgentCount(); i++)
        env.getAgent(i).resetMemory();
    }
  }

  /**
   * Adapta los menús al estado de "Simulación en curso".
   */
  private void setSimulatingState () {
    // Desactivamos los menús que no se pueden utilizar durante la simulación
    m_itm_env_new.setEnabled(false);
    m_itm_env_config.setEnabled(false);

    m_run.setEnabled(false);
    m_step.setEnabled(false);
    m_pause.setEnabled(true);
    m_stop.setEnabled(true);

    m_itm_sim_run.setEnabled(false);
    m_itm_sim_step.setEnabled(false);
    m_itm_sim_pause.setEnabled(true);
    m_itm_sim_stop.setEnabled(true);

    m_itm_mode_edit.setEnabled(false);
  }

  /**
   * Adapta los menús al estado de "Simulación pausada" o "Simulación en curso"
   * dependiendo de si la simulación está pausada o en curso.
   */
  private void setPausedContinuedState () {
    if (m_simulation.isPaused()) {
      m_pause.setText(s_tr.button().kontinue());
      m_step.setEnabled(true);

      m_itm_sim_pause.setText(s_tr.button().kontinue());
      m_itm_sim_step.setEnabled(true);
    }
    else {
      m_pause.setText(s_tr.button().pause());
      m_step.setEnabled(false);

      m_itm_sim_pause.setText(s_tr.button().pause());
      m_itm_sim_step.setEnabled(false);
    }
  }

  /**
   * Adapta los menús al estado de "Simulación parada".
   */
  private void setStoppedState () {
    m_itm_env_new.setEnabled(true);
    m_itm_env_config.setEnabled(true);

    m_pause.setText(s_tr.button().pause());
    m_run.setEnabled(true);
    m_step.setEnabled(true);
    m_pause.setEnabled(false);
    m_stop.setEnabled(false);

    m_itm_sim_pause.setText(s_tr.button().pause());
    m_itm_sim_run.setEnabled(true);
    m_itm_sim_step.setEnabled(true);
    m_itm_sim_pause.setEnabled(false);
    m_itm_sim_stop.setEnabled(false);

    m_itm_mode_edit.setEnabled(true);
  }

  /**
   * Adapta los menús dependiendo del modo de interacción al que se esté
   * cambiando.
   *
   * @param mode
   *          Nuevo modo de interacción.
   */
  private void setInteractionModeState (InteractionMode mode) {
    boolean enable = false;

    switch (mode) {
      case SIMULATION:
        enable = true;
        break;
      case EDITION:
        enable = false;
        break;
    }

    m_run.setEnabled(enable);
    m_step.setEnabled(enable);

    m_menu_sim.setEnabled(enable);
    m_menu_agent.setEnabled(enable);
  }

  private class RunAction implements ActionListener {
    @Override
    public void actionPerformed (ActionEvent e) {
      startSimulation();
    }
  }

  private class StepAction implements ActionListener {
    @Override
    public void actionPerformed (ActionEvent e) {
      m_simulation.stepSimulation();
    }
  }

  private class PauseAction implements ActionListener {
    @Override
    public void actionPerformed (ActionEvent e) {
      pauseSimulation();
    }
  }

  private class StopAction implements ActionListener {
    @Override
    public void actionPerformed (ActionEvent e) {
      stopSimulation();
    }
  }

} public class Maze {
  private ArrayList <ArrayList <MazeCell>> m_maze;
  private Point m_exit;

  /**
   * Crea un laberinto a partir de un algoritmo de generación de laberintos
   * ya inicializado.
   *
   * @param alg
   *          Algoritmo de creación de laberintos ya inicializado
   */
  public Maze (MazeCreationAlgorithm alg) {
    if (alg != null) {
      m_maze = alg.createMaze();
      m_exit = alg.getExit();
    }
    else
      throw new IllegalArgumentException(
          MainWindow.getTranslations().exception().invalidMazeCreationAlgorithm());
  }

  /**
   * Crea un laberinto a partir de su versión serializada en un fichero.
   *
   * @param fileName
   *          Nombre del fichero del que cargar el laberinto.
   * @throws IOException
   *           Cuando no se encuentra el fichero, no se puede abrir para su
   *           lectura o no contiene un laberinto válido.
   */
  public Maze (String fileName) throws IOException {
    loadFile(fileName);
  }

  /**
   * Obtiene la celda situada en una posición concreta del laberinto.
   *
   * @param row
   *          Fila.
   * @param column
   *          Columna.
   * @return Celda en la posición indicada.
   */
  public MazeCell get (int row, int column) {
    return m_maze.get(row).get(column);
  }

  /**
   * Establece el contenido de una celda en el laberinto.
   *
   * @param row
   *          Fila.
   * @param column
   *          Columna.
   * @param cell
   *          Celda que se quiere introducir.
   */
  public void set (int row, int column, MazeCell cell) {
    m_maze.get(row).set(column, cell);
  }

  /**
   * Obtiene el número de columnas (anchura) del laberinto.
   *
   * @return Anchura (en celdas) del laberinto.
   */
  public int getWidth () {
    return m_maze.get(0).size();
  }

  /**
   * Obtiene el número de filas (altura) del laberinto.
   *
   * @return Altura (en celdas) del laberinto.
   */
  public int getHeight () {
    return m_maze.size();
  }

  /**
   * Cambia la salida del laberinto a otro lugar, añadiendo y eliminando las
   * paredes necesarias.
   *
   * @param pos
   *          Índice de la celda en la que se quiere colocar la salida.
   *          Dependiendo de la dirección este valor se referirá a una columna
   *          (si la dirección es arriba o abajo) o a una fila (si la dirección
   *          es izquierda o derecha).
   * @param dir
   *          Borde del laberinto donde colocar la salida.
   */
  public void setExit (int pos, Direction dir) {
    // Comprobamos que los parámetros son válidos
    if (pos < 0 || dir == Direction.NONE)
      return;
    if (dir.isVertical() && pos >= getWidth())
      return;
    if (dir.isHorizontal() && pos >= getHeight())
      return;

    // Tapamos la salida anterior antes de modificar su posición
    if (m_exit.x < 0)
      m_maze.get(m_exit.y).get(0).setWall(Direction.LEFT);
    else if (m_exit.x >= getWidth())
      m_maze.get(m_exit.y).get(getWidth() - 1).setWall(Direction.RIGHT);
    else if (m_exit.y < 0)
      m_maze.get(0).get(m_exit.x).setWall(Direction.UP);
    else if (m_exit.y >= getHeight())
      m_maze.get(getHeight() - 1).get(m_exit.x).setWall(Direction.DOWN);

    // Modificamos la salida del laberinto y abrimos la pared
    switch (dir) {
      case UP:
        m_exit.move(pos, 0);
        break;
      case DOWN:
        m_exit.move(pos, getHeight() - 1);
        break;
      case LEFT:
        m_exit.move(0, pos);
        break;
      case RIGHT:
        m_exit.move(getWidth() - 1, pos);
        break;
      default:
        break;
    }

    m_maze.get(m_exit.y).get(m_exit.x).unsetWall(dir);
    m_exit.setLocation(dir.movePoint(m_exit));
  }

  /**
   * Obtiene una copia del lugar donde está la salida del laberinto.
   *
   * @return La posición donde se encuentra la salida al laberinto.
   */
  public Point getExit () {
    return new Point(m_exit);
  }

  /**
   * Determina si el punto se encuentra dentro del laberinto o no.
   *
   * @param p
   *          Punto a testear.
   * @return Si el punto está dentro del laberinto o no.
   */
  public boolean containsPoint (Point p) {
    return p.x >= 0 && p.y >= 0 && p.x < getWidth() && p.y < getHeight();
  }

  /**
   * Carga una instancia de laberinto de un fichero que contiene una instancia
   * de esta clase serializada.
   *
   * @param fileName
   *          Nombre del fichero del que cargar el laberinto.
   * @throws IOException
   *           Cuando no se encuentra el fichero, no se puede abrir para su
   *           lectura o no contiene un laberinto válido.
   */
  @SuppressWarnings ("unchecked")
  public void loadFile (String fileName) throws IOException {
    try {
      FileInputStream fileIn = new FileInputStream(fileName);
      ObjectInputStream in = new ObjectInputStream(fileIn);
      m_maze = (ArrayList <ArrayList <MazeCell>>) in.readObject();
      m_exit = (Point) in.readObject();
      in.close();
      fileIn.close();
    }
    catch (ClassNotFoundException c) {
      throw new IOException(c);
    }
  }

  /**
   * Guarda la actual instancia de la clase {@link Maze} en un fichero mediante
   * su serialización.
   *
   * @param fileName
   *          Nombre del fichero donde guardar el laberinto.
   * @throws IOException
   *           Cuando el fichero no se puede abrir o no se tienen permisos de
   *           escritura en el mismo.
   */
  public void saveFile (String fileName) throws IOException {
    FileOutputStream fileOut = new FileOutputStream(fileName);
    ObjectOutputStream out = new ObjectOutputStream(fileOut);
    out.writeObject(m_maze);
    out.writeObject(m_exit);
    out.close();
    fileOut.close();
  }

  /**
   * Calcula el número máximo de aristas que se pueden añadir en un laberinto
   * del tamaño dado. Cada arista se refiere a un pasillo abierto entre 2
   * celdas. Se puede ver como el número de paredes que tiene en su interior un
   * laberinto perfecto, sin contar el contorno.
   *
   * @param rows
   *          Número de filas.
   * @param columns
   *          Número de columnas.
   * @return Número de paredes en el interior del laberinto.
   */
  public static int perfectMazeWalls (int rows, int columns) {
    return (rows * columns) - rows - columns + 1;
  }

  /**
   * Calcula el número de aristas que tiene un laberinto perfecto de las
   * dimensiones dadas. Las aristas representan pasillos entre 2 celdas.
   *
   * @param rows
   *          Númnero de filas.
   * @param columns
   *          Número de columnas.
   * @return Número de aristas del laberinto perfecto.
   */
  public static int perfectMazeEdges (int rows, int columns) {
    return (rows * columns) - 1;
  }
} public class MazeCell implements Serializable {
  private static final long serialVersionUID = 4328443829621010840L;

  private short m_cell;

  /**
   * Constructor por defecto. La celda creada está rodeada de muros.
   */
  public MazeCell () {
    m_cell |= Direction.UP.val;
    m_cell |= Direction.DOWN.val;
    m_cell |= Direction.RIGHT.val;
    m_cell |= Direction.LEFT.val;
  }

  /**
   * Cambia el estado de la dirección especificada. Si no había muro, ahora lo
   * hay y viceversa.
   *
   * @param dir
   *          Lado de la celda que se quiere modificar.
   */
  public void toggleWall (Direction dir) {
    if (hasWall(dir))
      unsetWall(dir);
    else
      setWall(dir);
  }

  /**
   * Pone un muro si no lo hay en la dirección especificada.
   *
   * @param dir
   *          Lado de la celda que se quiere modificar.
   */
  public void setWall (Direction dir) {
    m_cell |= dir.val;
  }

  /**
   * Quita el muro si lo hay en la dirección especificada.
   *
   * @param dir
   *          Lado de la celda que se quiere modificar.
   */
  public void unsetWall (Direction dir) {
    m_cell &= ~dir.val;
  }

  /**
   * Elimina todas las paredes de la celda.
   */
  public void removeWalls () {
    m_cell = 0;
  }

  /**
   * Indica si hay un muro en la dirección indicada.
   *
   * @param dir
   *          Lado de la celda que se quiere consultar.
   * @return Si hay una celda en esa dirección o no.
   */
  public boolean hasWall (Direction dir) {
    return (m_cell & dir.val) != 0;
  }

  /**
   * Enumeración de los diferentes estados que puede tener una celda de cara a
   * un agente cualquiera.
   */
  public static enum Vision {
    /**
     * Este estado significa que la celda está vacía y no hay una pared entre
     * las 2 celdas.
     */
    EMPTY,

    /**
     * Este estado significa que hay una pared entre la posición actual del
     * agente y la celda.
     */
    WALL,

    /**
     * Este estado significa que hay un agente sobre esa celda actualmente y no
     * hay nada interponiéndose en medio.
     */
    AGENT,

    /**
     * Este estado significa que la celda referenciada está fuera del laberinto
     * y además no hay una pared en medio.
     */
    OFFLIMITS;
  }

} public class BlackboardManager {
  private HashMap <String, Object> m_blackboards;

  /**
   * Inicializa el gestor de pizarras.
   */
  public BlackboardManager () {
    m_blackboards = new HashMap <String, Object>();
  }

  /**
   * Obtiene la pizarra que hay en el canal indicado.
   *
   * @param channel
   *          Canal del que se quiere obtener la pizarra.
   * @return El objeto pizarra del canal.
   */
  public Object getBlackboard (String channel) {
    return m_blackboards.get(channel);
  }

  /**
   * Añade una nueva pizarra al gestor.
   *
   * @param blackboard
   *          Objeto que representa la nueva pizarra. Las modificaciones
   *          realizadas a este objeto son visibles para todos los agentes que
   *          posean una referencia a la misma.
   * @return El nombre del nuevo canal donde se ha colocado la pizarra.
   */
  public String addBlackboard (Object blackboard) {
    String name = "";
    do {
      name = Long.toUnsignedString(((Double) (Math.random() * Long.MAX_VALUE)).longValue());
    }
    while (m_blackboards.containsKey(name));

    m_blackboards.put(name, blackboard);
    return name;
  }

  /**
   * Intenta añadir la pizarra al canal deseado. Esto será posible sólo si el
   * canal no está ya ocupado.
   *
   * @param blackboard
   *          Objeto que representa la pizarra.
   * @param desired_channel
   *          Canal donde se quiere colocar la pizarra.
   * @return Canal donde finalmente se ha colocado la pizarra.
   */
  public String addBlackboard (Object blackboard, String desired_channel) {
    if (!m_blackboards.containsKey(desired_channel)) {
      m_blackboards.put(desired_channel, blackboard);
      return desired_channel;
    }
    else
      return addBlackboard(blackboard);
  }

  /**
   * Cambia el objeto pizarra asociado a un canal ya creado.
   *
   * @param channel
   *          Canal en el que modificar la pizarra.
   * @param blackboard
   *          Nueva pizarra que colocar en el canal.
   * @return {@code true} si se ha realizado el cambio y {@code false} si el
   *         canal indicado no existía.
   */
  public boolean changeBlackboard (String channel, Object blackboard) {
    if (m_blackboards.containsKey(channel)) {
      m_blackboards.put(channel, blackboard);
      return true;
    }
    return false;
  }

  /**
   * Elimina un canal del gestor de pizarras.
   *
   * @param channel
   *          Canal que eliminar.
   * @return {@code true} si se ha realizado la eliminación y {@code false} si
   *         el canal indicado no existía.
   */
  public boolean removeBlackboard (String channel) {
    if (m_blackboards.containsKey(channel)) {
      m_blackboards.remove(channel);
      return true;
    }
    return false;
  }

  /**
   * Indica si un canal está ocupado.
   *
   * @param channel
   *          Canal que consultar.
   * @return Si el canal consultado existe para este gestor.
   */
  boolean channelUsed (String channel) {
    return m_blackboards.containsKey(channel);
  }
} public enum Direction implements Serializable {
  NONE  ((short) 0x00),
  UP    ((short) 0x01),
  DOWN  ((short) 0x02),
  LEFT  ((short) 0x04),
  RIGHT ((short) 0x08);

  /**
   * Número máximo de direcciones.
   */
  public static int MAX_DIRECTIONS = 5;

  /**
   * Valor asociado a una dirección (campo de bits).
   */
  public short val;

  private static Direction [] values = Direction.values();

  private Direction (short val) {
    this.val = val;
  }

  /**
   * Transforma un short en dirección, comparando sus valores directamente.
   *
   * @param value
   *          Valor (dentro de los valores posibles de dirección).
   * @return Dirección asociada a ese valor.
   */
  public static Direction fromValue (short value) {
    for (Direction i: values)
      if (i.val == value)
        return i;

    return null;
  }

  /**
   * Devuelve la dirección asociada a un índice.
   *
   * @param index
   *          Índice de la dirección. El orden es el siguiente:
   *          <ol start="0">
   *            <li>NONE</li>
   *            <li>UP</li>
   *            <li>DOWN</li>
   *            <li>LEFT</li>
   *            <li>RIGHT</li>
   *          </ol>
   * @return Dirección asociada al índice.
   */
  public static Direction fromIndex (int index) {
    return values[index];
  }

  /**
   * Extrae la dirección asociada al paso entre 2 posiciones contiguas.
   *
   * @param p1
   *          Posición de inicio.
   * @param p2
   *          Posición de destino.
   * @return La dirección que une los 2 puntos contiguos o {@code null} si los
   *         puntos no están contiguos,
   */
  public static Direction fromPoints (Point p1, Point p2) {
    Point diff = new Point(p2.x - p1.x, p2.y - p1.y);
    switch (diff.x) {
      case -1: // izquierda
        return diff.y == 0? Direction.LEFT : null;
      case 1: // derecha
        return diff.y == 0? Direction.RIGHT : null;
      case 0: // misma posición en X
        switch (diff.y) {
          case -1: // arriba
            return Direction.UP;
          case 1: // abajo
            return Direction.DOWN;
          case 0: // misma posición
            return Direction.NONE;
        }
      default:
        return null;
    }
  }

  /**
   * Crea una dirección de forma aleatoria.
   *
   * @return Una dirección aleatoria. No va a ser {@code Direction.NONE}.
   */
  public static Direction random () {
    return values[1 + (int) (Math.random() * 4.0)];
  }

  /**
   * Descompone la dirección en sus componentes x e y, con una magnitud de 1.
   *
   * @return Pareja con la descomposición de la dirección (x, y).
   */
  public Pair <Integer, Integer> decompose () {
    Pair <Integer, Integer> p = new Pair <Integer, Integer>(0, 0);
    switch (this) {
      case NONE:
        break;
      case UP:
        p.second = -1;
        break;
      case DOWN:
        p.second = 1;
        break;
      case LEFT:
        p.first = -1;
        break;
      case RIGHT:
        p.first = 1;
        break;
    }
    return p;
  }

  /**
   * Invierte la posición actual.
   *
   * @return Dirección contraria de la actual.
   */
  public Direction getOpposite () {
    switch (this) {
      case UP:
        return DOWN;
      case DOWN:
        return UP;
      case LEFT:
        return RIGHT;
      case RIGHT:
        return LEFT;
      default:
        return NONE;
    }
  }

  /**
   * Rota la dirección absoluta actual en el sentido indicado.
   *
   * @param rot
   *          Sentido de rotación que aplicar.
   * @return La nueva dirección rotada.
   */
  public Direction rotate (Rotation rot) {
    switch (this) {
      case UP:
        return rot == Rotation.CLOCKWISE? RIGHT : LEFT;
      case DOWN:
        return rot == Rotation.CLOCKWISE? LEFT : RIGHT;
      case LEFT:
        return rot == Rotation.CLOCKWISE? UP : DOWN;
      case RIGHT:
        return rot == Rotation.CLOCKWISE? DOWN : UP;
      default:
        return NONE;
    }
  }

  /**
   * Desplaza un punto en la dirección y lo guarda en un punto nuevo.
   *
   * @param p
   *          Punto que se desea mover en esta dirección.
   * @return Nuevo punto equivalente al indicado desplazado en esta dirección.
   */
  public Point movePoint (final Point p) {
    Pair <Integer, Integer> desp = decompose();
    return new Point(p.x + desp.first, p.y + desp.second);
  }

  /**
   * @return Si la dirección es vertical.
   */
  public boolean isVertical () {
    return this == UP || this == DOWN;
  }

  /**
   * @return Si la dirección es horizontal.
   */
  public boolean isHorizontal () {
    return this == LEFT || this == RIGHT;
  }

} public enum InteractionMode {
  /**
   * Modo simulación. Se puede iniciar, pausar y parar simulaciones.
   */
  SIMULATION,

  /**
   * Modo edición de laberintos. Se puede modificar las paredes de los
   * laberintos.
   */
  EDITION;

} public class MessageManager {
  // Para cada grupo se almacena una lista de mensajes pendientes de transmisión
  // junto al emisor y una lista de receptores, o agentes suscritos al canal.
  private HashMap <String, Pair <ArrayList <Pair <MessageCommunication, Message>>, ArrayList <MessageCommunication>>> m_groups;

  /**
   * Construye una nueva instancia de la clase.
   */
  public MessageManager () {
    m_groups = new HashMap <>();
  }

  /**
   * Suscribe el agente al grupo indicado, de manera que recibe los mensajes
   * enviados a ese grupo.
   *
   * @param agent
   *          Agente que se quiere suscribir.
   * @param group
   *          Grupo al que suscribir el agente.
   * @return {@code true} si el agente se añadió al grupo y {@code false} si no
   *         se pudo añadir el agente al grupo.
   */
  public boolean subscribeGroup (MessageCommunication agent, String group) {
    Pair <ArrayList <Pair <MessageCommunication, Message>>, ArrayList <MessageCommunication>> g =
        m_groups.get(group);
    if (g != null) {
      if (!g.second.contains(agent))
        g.second.add(agent);
      return true;
    }
    return false;
  }

  /**
   * Hace que el agente deje de recibir mensajes del grupo especificado.
   *
   * @param agent
   *          Agente que quiere eliminar su suscripción.
   * @param group
   *          Grupo del cual eliminar la suscripción.
   */
  public void unsubscribeGroup (MessageCommunication agent, String group) {
    Pair <ArrayList <Pair <MessageCommunication, Message>>, ArrayList <MessageCommunication>> g =
        m_groups.get(group);
    if (g != null)
      g.second.remove(agent);
  }

  /**
   * Pone un mensaje a la cola de un grupo para ser enviado posteriormente
   * cuando se llame a {@link #flushMessageQueues}. Que la inserción se realice
   * satisfactoriamente no implica que el mensaje sea enviado.
   *
   * @see #flushMessageQueues
   * @param sender
   *          Agente que envía el mensaje.
   * @param group
   *          Grupo al que se está enviando el mensaje.
   * @param msg
   *          Mensaje que se quiere enviar.
   * @return {@code true} si el mensaje se ha colocado en la cola
   *         satisfactoriamente o {@code false} si no se ha podido.
   */
  public boolean sendMessage (MessageCommunication sender, String group, Message msg) {
    Pair <ArrayList <Pair <MessageCommunication, Message>>, ArrayList <MessageCommunication>> g =
        m_groups.get(group);
    if (g != null) {
      g.first.add(new Pair <>(sender, msg));
      return true;
    }
    return false;
  }

  /**
   * Crea un nuevo grupo y devuelve su nombre.
   *
   * @return Nombre del grupo recién creado.
   */
  public String createGroup () {
    String name = "";
    do {
      name = Long.toUnsignedString(((Double) (Math.random() * Long.MAX_VALUE)).longValue());
    }
    while (m_groups.containsKey(name));

    ArrayList <Pair <MessageCommunication, Message>> msg_queue = new ArrayList <>();
    ArrayList <MessageCommunication> group = new ArrayList <>();
    m_groups.put(name, new Pair <>(msg_queue, group));

    return name;
  }

  /**
   * Indica si un grupo ya ha sido creado.
   *
   * @param group
   *          Grupo del cual se quiere comprobar su existencia.
   * @return Si el grupo existe o no.
   */
  public boolean groupCreated (String group) {
    return m_groups.containsKey(group);
  }

  /**
   * Comprueba si un agente está suscrito a un grupo de mensajes determinado.
   *
   * @param agent
   *          Agente que se quiere comprobar.
   * @param group
   *          Grupo del que se quiere saber si el agente está suscrito.
   * @return Si el agente está suscrito al grupo o no.
   */
  public boolean isSubscribed (MessageCommunication agent, String group) {
    Pair <ArrayList <Pair <MessageCommunication, Message>>, ArrayList <MessageCommunication>> g =
        m_groups.get(group);

    return g != null && g.second.contains(group);
  }

  /**
   * Envía todos los mensajes en las colas a sus destinatarios, pero evitando
   * enviar varias veces el mismo mensaje al mismo destinatario (es posible que
   * un agente envíe el mismo mensaje a varios grupos y que haya algún agente en
   * varios de ellos).
   */
  public void flushMessageQueues () {
    // Creamos una cola de mensajes a ser enviados a cada agente, para detectar
    // las repeticiones de mensajes
    HashMap <MessageCommunication, ArrayList <Pair <MessageCommunication, Message>>> msgs =
        new HashMap <>();

    // Iteramos sobre los grupos
    for (Entry <String, Pair <ArrayList <Pair <MessageCommunication, Message>>, ArrayList <MessageCommunication>>> entry: m_groups
        .entrySet()) {

      // Sólo nos interesan la cola de mensajes y de suscritos al grupo, no el
      // nombre del mismo
      Pair <ArrayList <Pair <MessageCommunication, Message>>, ArrayList <MessageCommunication>> e_val =
          entry.getValue();

      // Recorremos todos los mensajes del grupo y los añadimos a la cola de
      // envío de todos los agentes del grupo
      for (Pair <MessageCommunication, Message> msg: e_val.first)
        for (MessageCommunication recv: e_val.second)
          addMsgToReceiverQueue(msgs, recv, msg);

      // Vaciamos la lista de mensajes, porque ya están a la cola para ser
      // procesados
      e_val.first.clear();
    }

    // Recorremos las listas de mensajes de cada receptor para enviarle sus
    // mensajes pendientes
    for (Entry <MessageCommunication, ArrayList <Pair <MessageCommunication, Message>>> e: msgs
        .entrySet()) {
      MessageCommunication recv = e.getKey();
      for (Pair <MessageCommunication, Message> msg: e.getValue())
        recv.receiveMessage(msg.first, msg.second);
    }
  }

  /**
   * Añade un mensaje a la cola de envío de un agente, evitando añadir mensajes
   * que ya se encuentren en dicha cola.
   *
   * @param msgs
   *          Estructura de datos con la lista de mensajes para cada receptor.
   * @param recv
   *          Receptor del mensaje.
   * @param msg
   *          Mensaje + Emisor del mensaje que se quiere añadir a la cola.
   */
  private void addMsgToReceiverQueue (
      Map <MessageCommunication, ArrayList <Pair <MessageCommunication, Message>>> msgs,
      MessageCommunication recv, Pair <MessageCommunication, Message> msg) {

    ArrayList <Pair <MessageCommunication, Message>> r_msgs = msgs.get(recv);
    if (r_msgs == null) {
      r_msgs = new ArrayList <Pair <MessageCommunication, Message>>();
      msgs.put(recv, r_msgs);
    }

    for (Pair <MessageCommunication, Message> r_msg: r_msgs) {
      // Comparamos las referencias, porque no queremos desechar mensajes
      // iguales, sino repeticiones del mismo mensaje
      if (r_msg.first == msg.first && r_msg.second == msg.second)
        return;
    }

    // Si el mensaje es nuevo, lo añadimos a la lista de mensajes del receptor
    r_msgs.add(msg);
  }
} public class Pair <T1, T2> {
  public T1 first;
  public T2 second;

  /**
   * Construye un par de valores
   *
   * @param f Primer valor del par.
   * @param s Segundo valor del par.
   */
  public Pair (T1 f, T2 s) {
    first = f;
    second = s;
  }

  /*
   * (non-Javadoc)
   *
   * @see java.lang.Object#hashCode()
   */
  @Override
  public int hashCode () {
    return (first != null? first.hashCode() : 0) * (second != null? second.hashCode() : 0);
  }

  /*
   * (non-Javadoc)
   *
   * @see java.lang.Object#equals(java.lang.Object)
   */
  @Override
  public boolean equals (Object obj) {
    if (obj instanceof Pair) {
      Pair <?, ?> o = (Pair <?, ?>) obj;
      try {
        return first.equals(o.first) && second.equals(o.second);
      }
      catch (NullPointerException e) {
        if (first == null && o.first == null) {
          if (second == null && o.second == null)
            return true;
          else if (second != null)
            return second.equals(o.second);
        }
        else if (first != null && first.equals(o.first)) {
          if (second == null && o.second == null)
            return true;
          else if (second != null)
            return second.equals(o.second);
        }
        return false;
      }
    }
    return false;
  }
} public class SimulationResults {
  /**
   * Clase de utilidad para contener toda la información de una simulación en un
   * entorno determinado.
   */
  private class EnvironmentSimulationInfo {
    public Map <Agent, Integer> steps, iterations;
    public long first_elapsed, last_elapsed;
    public Agent winner_agent;

    /**
     * Constructor. Crea una nueva instancia de la clase.
     */
    public EnvironmentSimulationInfo () {
      steps = new HashMap <Agent, Integer>();
      iterations = new HashMap <Agent, Integer>();
      first_elapsed = last_elapsed = -1;
    }
  }

  private Map <Environment, EnvironmentSimulationInfo> m_info;
  private long m_acc_time, m_start_time;

  /**
   * Constructor por defecto. Inicializa los atributos de la clase.
   */
  public SimulationResults () {
    m_info = new HashMap <Environment, EnvironmentSimulationInfo>();
    // Esta variable mide el tiempo que el temporizador ha estado ejecutándose
    // incluso tras hacer pausas.
    m_acc_time = 0;
  }

  /**
   * Elimina toda la información almacenada.
   */
  public void clear () {
    m_info.clear();
    m_acc_time = m_start_time = 0;
  }

  /**
   * Comienza el temporizador para medir el tiempo que tardan los agentes en
   * encontrar la salida al laberinto.
   */
  public void startTimer () {
    m_start_time = System.currentTimeMillis();
  }

  /**
   * Pausa el temporizador. Si se vuelve a iniciar, el tiempo que estuvo antes
   * ejecutándose seguirá presente.
   */
  public void pauseTimer () {
    m_acc_time += System.currentTimeMillis() - m_start_time;
  }

  /**
   * Se indica a las estadísticas que un agente ha salido del laberinto.
   *
   * @param agent
   *          Agente que salió del laberinto.
   */
  public void agentFinished (Agent agent) {
    EnvironmentSimulationInfo info = getInfoFromAgentsEnvironment(agent);

    // Calculamos el tiempo transcurrido para registrar lo que tardó en salir
    long elapsed = m_acc_time + System.currentTimeMillis() - m_start_time;
    if (info.winner_agent == null) {
      info.first_elapsed = elapsed;
      info.winner_agent = agent;
    }
    else
      info.last_elapsed = elapsed;
  }

  /**
   * Indica a las estadísticas que un agente ha realizado un paso.
   *
   * @param agent
   *          Agente que ha realizado el paso.
   */
  public void agentWalked (Agent agent) {
    EnvironmentSimulationInfo info = getInfoFromAgentsEnvironment(agent);
    Integer steps = info.steps.getOrDefault(agent, 0);

    info.steps.put(agent, steps + 1);
  }

  /**
   * Indica a las estadísticas que se ha producido una iteración con el agente
   * dentro del laberinto.
   *
   * @param agent
   *          Agente que ha realizado el paso.
   */
  public void agentIterated (Agent agent) {
    EnvironmentSimulationInfo info = getInfoFromAgentsEnvironment(agent);

    Integer iterations = info.iterations.getOrDefault(agent, 0);
    info.iterations.put(agent, iterations + 1);
  }

  /**
   * Busca el agente que salió antes del laberinto en el entorno, si alguno ha
   * salido.
   *
   * @param env
   *          Entorno en el que buscar al ganador.
   * @return El agente que salió antes del laberinto en el entorno.
   */
  public Agent getWinner (Environment env) {
    EnvironmentSimulationInfo info = m_info.get(env);
    return info != null? info.winner_agent : null;
  }

  /**
   * Busca el agente colocado en el laberinto especificado que salió antes del
   * mismo, esté en el entorno que esté.
   *
   * @param maze
   *          Laberinto del que salió.
   * @return Agente que salió primero del laberinto, si alguno ha salido.
   */
  public Agent getWinner (Maze maze) {
    Agent winner = null;
    int winner_steps = Integer.MAX_VALUE;

    // Buscamos entre todos los entornos donde el laberinto sea el mismo, aquel
    // agente que haya llegado el primero al exterior
    for (Environment env: m_info.keySet()) {
      if (env.getMaze() == maze) {
        EnvironmentSimulationInfo info = m_info.get(env);
        if (info != null) {
          Agent other_winner = info.winner_agent;
          if (other_winner != null) {
            Integer other_steps = info.steps.get(other_winner);
            if (other_steps == null || winner == null || other_steps < winner_steps) {
              winner = other_winner;
              winner_steps = other_steps == null? 0 : other_steps.intValue();
            }
          }
        }
      }
    }

    return winner;
  }

  /**
   * Obtiene el número de pasos e iteraciones que ha realizado cada agente en el
   * entorno en la simulación actual.
   *
   * @param env
   *          Entorno en el que inspeccionar los agentes.
   * @return Número de pasos e iteraciones que ha realizado cada agente en el
   * entorno.
   */
  public Map <Agent, Pair<Integer, Integer>> getSteps (Environment env) {
    // Utilizamos el nº de agentes guardados en lugar del nº actual en el
    // entorno porque se pueden añadir y eliminar agentes en tiempo de ejecución
    EnvironmentSimulationInfo info = m_info.get(env);
    HashMap <Agent, Pair<Integer, Integer>> steps = new HashMap <Agent, Pair<Integer, Integer>>();

    if (info != null) {
      for (int i = 0; i < env.getAgentCount(); ++i) {
        Agent agent = env.getAgent(i);

        Pair <Integer, Integer> agent_info = new Pair <>(
            info.steps.getOrDefault(agent, 0),
            info.iterations.getOrDefault(agent, 0));

        steps.put(agent, agent_info);
      }
    }

    return steps;
  }

  /**
   * Obtiene el tiempo que tardó el primer agente en salir del laberinto.
   *
   * @param env
   *          Entorno en el que evaluar los agentes.
   * @return Tiempo que le llevó salir del laberinto al primero que salió.
   */
  public long timeTakenFirst (Environment env) {
    EnvironmentSimulationInfo info = m_info.get(env);
    return info != null? info.first_elapsed : -1;
  }

  /**
   * Indica la cantidad de tiempo que tardó el agente más rápido en resolver el
   * laberinto, independientemente del entorno en el que se encontrara.
   *
   * @param maze
   *          Laberinto del que salió.
   * @return Tiempo que le llevó salir del laberinto al primero que salió.
   */
  public long timeTakenFirst (Maze maze) {
    long min_time = Long.MAX_VALUE;

    for (Environment env: m_info.keySet()) {
      if (env.getMaze() == maze) {
        long time = timeTakenFirst(env);
        if (time > -1 && time < min_time)
          min_time = time;
      }
    }

    return min_time != Long.MAX_VALUE? min_time : -1;
  }

  /**
   * Obtiene el tiempo que tardó el último agente en salir del entorno.
   *
   * @param env
   *          Entorno en el que evaluar los agentes.
   * @return Tiempo que le llevó salir del laberinto al último que salió.
   */
  public long timeTakenLast (Environment env) {
    EnvironmentSimulationInfo info = m_info.get(env);
    return info != null? info.last_elapsed : -1;
  }

  /**
   * Indica la cantidad de tiempo que tardó el agente más lento en resolver el
   * laberinto, independientemente del entorno en el que se encontrara.
   *
   * @param maze
   *          Laberinto del que salió.
   * @return Tiempo que le llevó salir del laberinto al último que salió.
   */
  public long timeTakenLast (Maze maze) {
    long max_time = Long.MIN_VALUE;

    for (Environment env: m_info.keySet()) {
      if (env.getMaze() == maze) {
        long time = timeTakenLast(env);
        if (time > max_time)
          max_time = time;
      }
    }

    return max_time != Long.MIN_VALUE? max_time : -1;
  }

  /**
   * Busca en m_info la información del entorno asociado al agente indicado. Si
   * no lo encuentra, lo crea.
   *
   * @param agent
   *          Agente de cuyo entorno se quiere extraer la infomación.
   * @return Información de la simulación.
   */
  private EnvironmentSimulationInfo getInfoFromAgentsEnvironment (Agent agent) {
    Environment env = agent.getEnvironment();
    EnvironmentSimulationInfo info = m_info.get(env);

    // Si no hay información almacenada de este agente, lo agregamos a la lista
    if (info == null) {
      info = new EnvironmentSimulationInfo();
      m_info.put(env, info);
    }

    return info;
  }
}
import java.awt.event.MouseMotionListener;
import java.util.ArrayList;
import com.tomtessier.scrollabledesktop.BaseInternalFrame;
import es.ull.mazesolver.agent.Agent;
import es.ull.mazesolver.gui.MainWindow;
import es.ull.mazesolver.maze.Maze;
import es.ull.mazesolver.maze.MazeCell;
import es.ull.mazesolver.util.BlackboardManager;
import es.ull.mazesolver.util.Direction;
import es.ull.mazesolver.util.InteractionMode;
import es.ull.mazesolver.util.MessageManager;
import es.ull.mazesolver.util.Pair;
import es.ull.mazesolver.util.SimulationResults;
import java.awt.Container;
import java.awt.Point;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
/*
 * This file is part of MazeSolver.
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 *
 * Copyright (c) 2014 MazeSolver
 * Sergio M. Afonso Fumero <theSkatrak@gmail.com>
 * Kevin I. Robayna Hernández <kevinirobaynahdez@gmail.com>
 */

/**
 * @file Environment.java
 * @date 25/10/2014
 */
package es.ull.mazesolver.gui.environment;




/**
 * Una instancia de esta clase representa un entorno de ejecución, formado por
 * un laberinto y por un conjunto de agentes.
 */
public class Environment extends BaseInternalFrame {
  private static final long serialVersionUID = 1L;
  private static final int WINDOW_BORDER_WIDTH = 11;
  private static final int WINDOW_BORDER_HEIGHT = 33;
  private static final int WINDOWS_OFFSET = 20;

  private static int s_instance = 0;
  private static Point s_start_pos = new Point();

  private Maze m_maze;
  private ArrayList <Agent> m_agents;
  private int m_selected, m_hovered;

  private BlackboardManager m_blackboard_mgr;
  private MessageManager m_message_mgr;

  private MouseListener m_agent_click = new MouseAdapter() {
    @Override
    public void mousePressed (MouseEvent e) {
      m_selected = getAgentIndexUnderMouse(e.getPoint());
      repaint();
      super.mousePressed(e);
    }
  };

  private MouseMotionListener m_agent_hover_drag = new MouseMotionListener() {
    @Override
    public void mouseMoved (MouseEvent e) {
      m_hovered = getAgentIndexUnderMouse(e.getPoint());
      repaint();
    }

    @Override
    public void mouseDragged (MouseEvent e) {
      Agent ag = getSelectedAgent();
      if (ag != null) {
        Point grid_pos = EnvironmentPanel.screenCoordToGrid(e.getPoint());
        if (m_maze.containsPoint(grid_pos)) {
          ag.setPosition(grid_pos);
          repaint();
        }
      }
    }
  };

  private MouseListener m_wall_click = new MouseAdapter() {
    @Override
    public void mousePressed (MouseEvent e) {
      EnvironmentEditionPanel panel = (EnvironmentEditionPanel) getContentPane();
      Pair<Point, Direction> selected = panel.getWallAt(e.getPoint());

      if (selected != null) {
        Point pos = selected.first;
        Direction dir = selected.second;
        Point adj = dir.movePoint(pos);

        // Si las dos celdas están dentro, se crea/eliminan las dos paredes que
        // las unen
        if (m_maze.containsPoint(adj)) {
          m_maze.get(pos.y, pos.x).toggleWall(dir);
          m_maze.get(adj.y, adj.x).toggleWall(dir.getOpposite());
        }
        // Si sólo una de las dos celdas está dentro hay que cambiar la posición
        // de la salida a ese punto
        else {
          if (dir.isVertical())
            m_maze.setExit(pos.x, dir);
          else // Horizontal
            m_maze.setExit(pos.y, dir);
        }
      }

      repaint();
      super.mousePressed(e);
    }
  };

  /**
   * Constructor para las clases de tipo entorno.
   *
   * @param maze
   *          Laberinto en el que se basa el entorno. Puede ser compartido entre
   *          varios entornos.
   * @param name
   *          Nombre del entorno.
   */
  public Environment (Maze maze, String name) {
    super(name, false, false, false, false);
    ++s_instance;

    setMaze(maze);
    setVisible(true);

    setLocation(s_start_pos);
    s_start_pos.x += WINDOWS_OFFSET;
    s_start_pos.y += WINDOWS_OFFSET;

    m_selected = m_hovered = -1;
    m_agents = new ArrayList <Agent>();

    m_blackboard_mgr = new BlackboardManager();
    m_message_mgr = new MessageManager();

    moveToFront();
  }

  /**
   * Constructor para las clases de tipo entorno.
   *
   * @param maze
   *          Laberinto en el que se basa el entorno. Puede ser compartido entre
   *          varios entornos.
   */
  public Environment (Maze maze) {
    this(maze, "Env " + (s_instance + 1));
  }

  /**
   * Cambia el nombre del entorno.
   *
   * @param name
   *          Nuevo nombre del entorno.
   */
  public void setEnvName (String name) {
    setTitle(name);
  }

  /**
   * @return El nombre del entorno.
   */
  public String getEnvName () {
    return getTitle();
  }

  /**
   * Cambia el modo de interacción con el entorno.
   *
   * @param mode
   *          Nuevo modo de interacción con el entorno.
   */
  public void setInteractionMode (InteractionMode mode) {