/*
 * 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.
 *
 */

package org.contikios.cooja.plugins;

import java.awt.Component;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.util.Collection;
import java.util.Observable;
import java.util.Observer;
import java.util.Vector;

import javax.swing.AbstractAction;
import javax.swing.Action;
import javax.swing.BorderFactory;
import javax.swing.BoxLayout;
import javax.swing.JButton;
import javax.swing.JCheckBox;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.SwingUtilities;

import org.apache.log4j.Logger;
import org.jdom.Element;

import org.contikios.cooja.ClassDescription;
import org.contikios.cooja.Cooja;
import org.contikios.cooja.Mote;
import org.contikios.cooja.MoteInterface;
import org.contikios.cooja.MoteType;
import org.contikios.cooja.PluginType;
import org.contikios.cooja.Simulation;
import org.contikios.cooja.VisPlugin;
import org.contikios.cooja.contikimote.ContikiMoteType;
import org.contikios.cooja.interfaces.Button;
import org.contikios.cooja.interfaces.LED;
import org.contikios.cooja.interfaces.Log;
import org.contikios.cooja.interfaces.PIR;
import org.contikios.cooja.interfaces.Position;
import org.contikios.cooja.interfaces.Radio;

/**
 * Allows a user to observe several different parts of the simulator, stopping a
 * simulation whenever an object changes.
 *
 * @author Fredrik Osterlind
 */
@ClassDescription("Breakpoints")
@PluginType(PluginType.SIM_PLUGIN)
public class EventListener extends VisPlugin {
  private static final long serialVersionUID = 1L;

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

  private Simulation mySimulation;

  private Vector<EventObserver> allObservers = new Vector<EventObserver>();

  private EventListener myPlugin;

  private JLabel messageLabel = null;

  private JButton actionButton = null;

  private JPanel interfacePanel = null;

  private JPanel generalPanel = null;

  protected abstract class EventObserver implements Observer {
    protected Observable myObservation = null;

    protected EventListener myParent = null;

    public EventObserver(EventListener parent, Observable objectToObserve) {
      myParent = parent;
      myObservation = objectToObserve;
      objectToObserve.addObserver(this);
    }

    /**
     * Stop observing object (for cleaning up).
     */
    public void detachFromObject() {
      myObservation.deleteObserver(this);
    }

    /**
     * @return Object being observed.
     */
    public Observable getObservable() {
      return myObservation;
    }
  };

  protected class InterfaceEventObserver extends EventObserver {
    private Mote myMote = null;

    public InterfaceEventObserver(EventListener parent, Mote mote,
        Observable objectToObserve) {
      super(parent, objectToObserve);
      myMote = mote;
    }

    public void update(Observable obs, Object obj) {
      final MoteInterface moteInterface = (MoteInterface) obs;
      int moteID = myMote.getID();

      myParent.actOnChange("'" + Cooja.getDescriptionOf(moteInterface.getClass())
          + "'" + " of mote '" + (moteID > 0 ? Integer.toString(moteID) : "?")
          + "'" + " changed at time "
          + myParent.mySimulation.getSimulationTime(), new AbstractAction(
          "View interface visualizer") {
        public void actionPerformed(ActionEvent e) {
          MoteInterfaceViewer plugin =
            (MoteInterfaceViewer) mySimulation.getCooja().tryStartPlugin(
                MoteInterfaceViewer.class, mySimulation.getCooja(), mySimulation, myMote);
          plugin.setSelectedInterface(Cooja.getDescriptionOf(moteInterface.getClass()));
        }
      });
    }
  }

  protected class GeneralEventObserver extends EventObserver {
    public GeneralEventObserver(EventListener parent, Observable objectToObserve) {
      super(parent, objectToObserve);
    }

    public void update(Observable obs, Object obj) {
      myParent.actOnChange("'" + Cooja.getDescriptionOf(obs.getClass()) + "'"
          + " changed at time " + myParent.mySimulation.getSimulationTime(),
          null);
    }
  }

  /**
   * @param simulationToControl
   *          Simulation to control
   */
  public EventListener(Simulation simulationToControl, Cooja gui) {
    super("Event Listener", gui);

    mySimulation = simulationToControl;
    myPlugin = this;

    /* Create selectable interfaces list (only supports Contiki mote types) */
    Vector<Class<? extends MoteInterface>> allInterfaces = new Vector<Class<? extends MoteInterface>>();
    Vector<Class<? extends MoteInterface>> allInterfacesDups = new Vector<Class<? extends MoteInterface>>();

    // Add standard interfaces
    allInterfacesDups.add(Button.class);
    allInterfacesDups.add(LED.class);
    allInterfacesDups.add(Log.class);
    allInterfacesDups.add(PIR.class);
    allInterfacesDups.add(Position.class);
    allInterfacesDups.add(Radio.class);

    for (MoteType moteType : simulationToControl.getMoteTypes()) {
      if (moteType instanceof ContikiMoteType) {
        Class<? extends MoteInterface>[] arr = ((ContikiMoteType) moteType).getMoteInterfaceClasses();
        for (Class<? extends MoteInterface> intf : arr) {
          allInterfacesDups.add(intf);
        }
      }
    }
    for (Class<? extends MoteInterface> moteTypeClass : allInterfacesDups) {
      if (!allInterfaces.contains(moteTypeClass)) {
        allInterfaces.add(moteTypeClass);
      }
    }

    interfacePanel = new JPanel();
    interfacePanel.setLayout(new BoxLayout(interfacePanel, BoxLayout.Y_AXIS));
    interfacePanel.setBorder(BorderFactory.createEmptyBorder(10, 5, 10, 5));
    for (Class<? extends MoteInterface> interfaceClass : allInterfaces) {
      JCheckBox checkBox = new JCheckBox(Cooja.getDescriptionOf(interfaceClass),
          false);
      checkBox.setToolTipText(interfaceClass.getName());
      checkBox.putClientProperty("interface_class", interfaceClass);
      checkBox.addActionListener(interfaceCheckBoxListener);

      interfacePanel.add(checkBox);
    }
    if (allInterfaces.isEmpty()) {
      interfacePanel.add(new JLabel("No used interface classes detected"));
    }

    // Create general selectable list
    generalPanel = new JPanel();
    generalPanel.setLayout(new BoxLayout(generalPanel, BoxLayout.Y_AXIS));
    generalPanel.setBorder(BorderFactory.createEmptyBorder(10, 5, 10, 5));

    JCheckBox simCheckBox = new JCheckBox("Simulation event", false);
    simCheckBox.putClientProperty("observable", mySimulation);
    simCheckBox.addActionListener(generalCheckBoxListener);
    generalPanel.add(simCheckBox);

    JCheckBox radioMediumCheckBox = new JCheckBox("Radio medium event", false);
    radioMediumCheckBox.putClientProperty("observable", mySimulation
        .getRadioMedium().getRadioTransmissionObservable());
    radioMediumCheckBox.addActionListener(generalCheckBoxListener);
    generalPanel.add(radioMediumCheckBox);

    // Add components
    JPanel mainPanel = new JPanel();
    mainPanel.setLayout(new BoxLayout(mainPanel, BoxLayout.Y_AXIS));
    setContentPane(mainPanel);

    mainPanel.add(new JLabel("Break on general changes:"));
    mainPanel.add(generalPanel);

    mainPanel.add(new JLabel("Break on mote interface changes:"));
    mainPanel.add(new JScrollPane(interfacePanel));

    messageLabel = new JLabel("[no change detected yet]");
    actionButton = new JButton("[no action available]");
    actionButton.setEnabled(false);
    mainPanel.add(new JLabel("Last message:"));
    mainPanel.add(messageLabel);
    mainPanel.add(actionButton);

    pack();

    try {
      setSelected(true);
    } catch (java.beans.PropertyVetoException e) {
      // Could not select
    }

  }

  private void actOnChange(final String message, final Action action) {
    if (!mySimulation.isRunning()) {
      return;
    }

    mySimulation.stopSimulation();

    SwingUtilities.invokeLater(new Runnable() {
      public void run() {
        messageLabel.setText(message);
        actionButton.setAction(action);
        actionButton.setVisible(action != null);
      }
    });
  }

  private ActionListener interfaceCheckBoxListener = new ActionListener() {
    public void actionPerformed(ActionEvent e) {
      Class<? extends MoteInterface> interfaceClass = (Class<? extends MoteInterface>) ((JCheckBox) e
          .getSource()).getClientProperty("interface_class");
      boolean shouldObserve = ((JCheckBox) e.getSource()).isSelected();

      if (!shouldObserve) {
        // Remove existing observers
        for (EventObserver obs : allObservers.toArray(new EventObserver[0])) {
          Class<? extends Observable> objClass = obs.getObservable().getClass();
          if (objClass == interfaceClass ||
              interfaceClass.isAssignableFrom(objClass)) {
            obs.detachFromObject();
            allObservers.remove(obs);
          }
        }
      } else {
        // Register new observers
        for (int i = 0; i < mySimulation.getMotesCount(); i++) {
          MoteInterface moteInterface = mySimulation.getMote(i).getInterfaces()
              .getInterfaceOfType(interfaceClass);
          if (moteInterface != null) {
            allObservers.add(new InterfaceEventObserver(myPlugin, mySimulation
                .getMote(i), moteInterface));
          }
        }
      }
    }
  };

  private ActionListener generalCheckBoxListener = new ActionListener() {
    public void actionPerformed(ActionEvent e) {
      Observable observable = (Observable) ((JCheckBox) e.getSource())
          .getClientProperty("observable");
      boolean shouldObserve = ((JCheckBox) e.getSource()).isSelected();

      if (!shouldObserve) {
        // Remove existing observers
        for (EventObserver obs : allObservers.toArray(new EventObserver[0])) {
          if (obs.getObservable() == observable) {
            obs.detachFromObject();
            allObservers.remove(obs);
          }
        }
      } else {
        // Register new observers
        allObservers.add(new GeneralEventObserver(myPlugin, observable));
      }
    }
  };

  public void closePlugin() {
    // Remove all existing observers
    for (EventObserver obs : allObservers) {
      obs.detachFromObject();
    }
  }

  public Collection<Element> getConfigXML() {
    Vector<Element> config = new Vector<Element>();

    Element element;

    /* Save general observers */
    for (Component comp: generalPanel.getComponents()) {
      if (comp instanceof JCheckBox) {
        JCheckBox checkBox = (JCheckBox) comp;
        if (checkBox.isSelected()) {
          element = new Element("general");
          element.setText(checkBox.getText());
          config.add(element);
        }
      }
    }

    /* Save interface observers */
    for (Component comp: interfacePanel.getComponents()) {
      if (comp instanceof JCheckBox) {
        JCheckBox checkBox = (JCheckBox) comp;
        if (checkBox.isSelected()) {
          element = new Element("interface");
          element.setText(checkBox.getText());
          config.add(element);
        }
      }
    }

    return config;
  }

  public boolean setConfigXML(Collection<Element> configXML, boolean visAvailable) {

    /* Load general observers */
    for (Element element : configXML) {
      if (element.getName().equals("general")) {
        for (Component comp: generalPanel.getComponents()) {
          if (comp instanceof JCheckBox) {
            JCheckBox checkBox = (JCheckBox) comp;
            if (checkBox.getText().equals(element.getText())) {
              checkBox.setSelected(true);
              generalCheckBoxListener.actionPerformed(new ActionEvent(checkBox, ActionEvent.ACTION_PERFORMED, ""));
            }
          }
        }
      }

      /* Load interface observers */
      else if (element.getName().equals("interface")) {
        for (Component comp: interfacePanel.getComponents()) {
          if (comp instanceof JCheckBox) {
            JCheckBox checkBox = (JCheckBox) comp;
            if (checkBox.getText().equals(element.getText())) {
              checkBox.setSelected(true);
              interfaceCheckBoxListener.actionPerformed(new ActionEvent(checkBox, ActionEvent.ACTION_PERFORMED, ""));
            }
          }
        }
      }

      else {
        return false;
      }
    }

    return true;
  }

}
