/*
 * Copyright (c) 2012, 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.
 */

import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Component;
import java.awt.Toolkit;
import java.awt.datatransfer.Clipboard;
import java.awt.datatransfer.StringSelection;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.MouseEvent;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Observable;
import java.util.Observer;

import javax.swing.AbstractAction;
import javax.swing.Action;
import javax.swing.Box;
import javax.swing.JButton;
import javax.swing.JScrollPane;
import javax.swing.JTable;
import javax.swing.Timer;
import javax.swing.table.AbstractTableModel;
import javax.swing.table.DefaultTableCellRenderer;

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

import se.sics.cooja.ClassDescription;
import se.sics.cooja.GUI;
import se.sics.cooja.Mote;
import se.sics.cooja.PluginType;
import se.sics.cooja.SimEventCentral.MoteCountListener;
import se.sics.cooja.Simulation;
import se.sics.cooja.VisPlugin;
import se.sics.cooja.interfaces.Radio;

/**
 * Tracks radio events to sum up transmission, reception, and radio on times.
 * This plugin can be run without visualization, i.e. from a Contiki test.
 *
 * @author Fredrik Osterlind, Adam Dunkels
 */
@ClassDescription("Mote radio duty cycle")
@PluginType(PluginType.SIM_PLUGIN)
public class PowerTracker extends VisPlugin {
  private static Logger logger = Logger.getLogger(PowerTracker.class);

  private static final int POWERTRACKER_UPDATE_INTERVAL = 100; /* ms */

  private static final int COLUMN_MOTE = 0;
  private static final int COLUMN_RADIOON = 1;
  private static final int COLUMN_RADIOTX = 2;
  private static final int COLUMN_RADIORX = 3;

  private Simulation simulation;
  private MoteCountListener moteCountListener;
  private ArrayList<MoteTracker> moteTrackers = new ArrayList<MoteTracker>();

  private JTable table;
  private int tableMaxRadioOnIndex = -1;

  public PowerTracker(final Simulation simulation, final GUI gui) {
    super("PowerTracker", gui, false);
    this.simulation = simulation;

    /* Automatically add/delete motes */
    simulation.getEventCentral().addMoteCountListener(moteCountListener = new MoteCountListener() {
      public void moteWasAdded(Mote mote) {
        addMote(mote);
        table.invalidate();
        table.repaint();
      }
      public void moteWasRemoved(Mote mote) {
        removeMote(mote);
        table.invalidate();
        table.repaint();
      }
    });
    for (Mote m: simulation.getMotes()) {
      addMote(m);
    }

    if (!GUI.isVisualized()) {
      return;
    }

    AbstractTableModel model = new AbstractTableModel() {
      public int getRowCount() {
        return moteTrackers.size()+1;
      }
      public int getColumnCount() {
        return 4;
      }
      public String getColumnName(int col) {
        if (col == COLUMN_MOTE) {
          return "Mote";
        }
        if (col == COLUMN_RADIOON) {
          return "Radio on (%)";
        }
        if (col == COLUMN_RADIOTX) {
          return "Radio TX (%)";
        }
        if (col == COLUMN_RADIORX) {
          return "Radio RX (%)";
        }
        return null;
      }
      public Object getValueAt(int rowIndex, int col) {
        if (rowIndex < 0 || rowIndex >= moteTrackers.size()+1) {
          return null;
        }

        if (rowIndex == moteTrackers.size()) {
          /* Average */
          long radioOn = 0;
          long radioTx = 0;
          long radioRx = 0;
          long duration = 0;
          for (MoteTracker mt: moteTrackers) {
            radioOn += mt.radioOn;
            radioTx += mt.radioTx;
            radioRx += mt.radioRx;

            duration += mt.duration;
          }

          if (col == COLUMN_MOTE) {
            return "AVERAGE";
          }
          if (col == COLUMN_RADIOON) {
            return String.format("%2.2f%%", 100.0*radioOn/duration);
          }
          if (col == COLUMN_RADIOTX) {
            return String.format("%2.2f%%", 100.0*radioTx/duration);
          }
          if (col == COLUMN_RADIORX) {
            return String.format("%2.2f%%", 100.0*radioRx/duration);
          }
          return null;
        }

        MoteTracker rule = moteTrackers.get(rowIndex);
        if (col == COLUMN_MOTE) {
          return rule.mote.toString();
        }
        if (col == COLUMN_RADIOON) {
          return String.format("%2.2f%%", 100.0*rule.getRadioOnRatio());
        }
        if (col == COLUMN_RADIOTX) {
          return String.format("%2.2f%%", 100.0*rule.getRadioTxRatio());
        }
        if (col == COLUMN_RADIORX) {
          return String.format("%2.2f%%", 100.0*rule.getRadioRxRatio());
        }
        return null;
      }
    };
    table = new JTable(model) {
      public String getToolTipText(MouseEvent e) {
        java.awt.Point p = e.getPoint();
        int rowIndex = table.rowAtPoint(p);
        if (rowIndex < 0 || rowIndex >= moteTrackers.size()) {
          return null;
        }
        MoteTracker mt = moteTrackers.get(rowIndex);
        return "<html><pre>" + mt.toString() + "</html>";
      }
    };
    table.setDefaultRenderer(Object.class, new DefaultTableCellRenderer() {
      public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int column) {
        Component c = super.getTableCellRendererComponent(table, value, isSelected, hasFocus, row, column);
        if (row == tableMaxRadioOnIndex) {
          setBackground(isSelected ? table.getSelectionBackground():Color.RED);
          setForeground(isSelected ? table.getSelectionForeground():Color.WHITE);
        } else {
          setBackground(isSelected ? table.getSelectionBackground():table.getBackground());
          setForeground(isSelected ? table.getSelectionForeground():table.getForeground());
        }
        return c;
      }
    });

    Box control = Box.createHorizontalBox();
    control.add(Box.createHorizontalGlue());
    control.add(new JButton(printAction));
    control.add(new JButton(resetAction));

    this.getContentPane().add(BorderLayout.CENTER, new JScrollPane(table));
    this.getContentPane().add(BorderLayout.SOUTH, control);
    setSize(400, 400);

    repaintTimer.start();
  }

  private Action resetAction = new AbstractAction("Reset") {
    public void actionPerformed(ActionEvent e) {
      Runnable r = new Runnable() {
        public void run() {
          reset();
        }
      };
      if (simulation.isRunning()) {
        simulation.invokeSimulationThread(r);
      } else {
        r.run();
      }
    }
  };

  private Action printAction = new AbstractAction("Print to console/Copy to clipboard") {
    public void actionPerformed(ActionEvent e) {
      String output = radioStatistics(true, true, false);
      logger.info("PowerTracker output:\n\n" + output);

      Clipboard clipboard = Toolkit.getDefaultToolkit().getSystemClipboard();
      StringSelection stringSelection = new StringSelection(output);
      clipboard.setContents(stringSelection, null);
    }
  };

  public String radioStatistics() {
    return radioStatistics(true, true, false);
  }

  public String radioStatistics(boolean radioHW, boolean radioRXTX, boolean onlyAverage) {
    StringBuilder sb = new StringBuilder();

    /* Average */
    long radioOn = 0;
    long radioTx = 0;
    long radioRx = 0;
    long radioInterfered = 0;
    long duration = 0;
    for (MoteTracker mt: moteTrackers) {
      radioOn += mt.radioOn;
      radioTx += mt.radioTx;
      radioRx += mt.radioRx;
      radioInterfered += mt.radioInterfered;

      duration += mt.duration;
    }
    if (radioHW) {
      sb.append(String.format("AVG" + " ON " + (radioOn + " us ") + "%2.2f %%", 100.0*radioOn/duration) + "\n");
    }
    if (radioRXTX) {
      sb.append(String.format("AVG" + " TX " + (radioTx + " us ") + "%2.2f %%", 100.0*radioTx/duration) + "\n");
      sb.append(String.format("AVG" + " RX " + (radioRx + " us ") + "%2.2f %%", 100.0*radioRx/duration) + "\n");
      sb.append(String.format("AVG" + " INT " + (radioInterfered + " us ") + "%2.2f %%", 100.0*radioInterfered/duration) + "\n");
    }

    if (onlyAverage) {
      return sb.toString();
    }

    for (MoteTracker mt: moteTrackers) {
      sb.append(mt.toString(radioHW, radioRXTX));
    }
    return sb.toString();
  }

  private static class MoteTracker implements Observer {
    /* last radio state */
    private boolean radioWasOn;
    private RadioState lastRadioState;
    private long lastUpdateTime;

    /* accumulating radio state durations */
    long duration = 0;
    long radioOn = 0;
    long radioTx = 0;
    long radioRx = 0;
    long radioInterfered = 0;

    private Simulation simulation;
    private Mote mote;
    private Radio radio;

    public MoteTracker(Mote mote) {
      this.simulation = mote.getSimulation();
      this.mote = mote;
      this.radio = mote.getInterfaces().getRadio();

      radioWasOn = radio.isRadioOn();
      if (radio.isTransmitting()) {
        lastRadioState = RadioState.TRANSMITTING;
      } else if (radio.isReceiving()) {
        lastRadioState = RadioState.RECEIVING;
      } else if (radio.isInterfered()){
        lastRadioState = RadioState.INTERFERED;
      } else {
        lastRadioState = RadioState.IDLE;
      }
      lastUpdateTime = simulation.getSimulationTime();

      radio.addObserver(this);
    }

    public void update(Observable o, Object arg) {
      update();
    }
    public void update() {
      long now = simulation.getSimulationTime();

      accumulateDuration(now - lastUpdateTime);

      /* Radio on/off */
      if (radioWasOn) {
        accumulateRadioOn(now - lastUpdateTime);
      }

      /* Radio tx/rx */
      if (lastRadioState == RadioState.TRANSMITTING) {
        accumulateRadioTx(now - lastUpdateTime);
      } else if (lastRadioState == RadioState.RECEIVING) {
        accumulateRadioRx(now - lastUpdateTime);
      } else if (lastRadioState == RadioState.INTERFERED) {
        accumulateRadioIntefered(now - lastUpdateTime);
      }

      /* Await next radio event */
      if (radio.isTransmitting()) {
        lastRadioState = RadioState.TRANSMITTING;
      } else if (!radio.isRadioOn()) {
        lastRadioState = RadioState.IDLE;
      } else if (radio.isInterfered()) {
        lastRadioState = RadioState.INTERFERED;
      } else if (radio.isReceiving()) {
        lastRadioState = RadioState.RECEIVING;
      } else {
        lastRadioState = RadioState.IDLE;
      }
      radioWasOn = radio.isRadioOn();
      lastUpdateTime = now;
    }

    protected void accumulateDuration(long t) {
      duration += t;
    }
    protected void accumulateRadioOn(long t) {
      radioOn += t;
    }
    protected void accumulateRadioTx(long t) {
      radioTx += t;
    }
    protected void accumulateRadioRx(long t) {
      radioRx += t;
    }
    protected void accumulateRadioIntefered(long t) {
      radioInterfered += t;
    }

    protected double getRadioOnRatio() {
      return 1.0*radioOn/duration;
    }

    protected double getRadioTxRatio() {
      return 1.0*radioTx/duration;
    }

    protected double getRadioInterferedRatio() {
      return 1.0*radioInterfered/duration;
    }

    protected double getRadioRxRatio() {
      return 1.0*radioRx/duration;
    }

    public Mote getMote() {
      return mote;
    }

    public void dispose() {
      radio.deleteObserver(this);
      radio = null;
      mote = null;
    }

    public String toString() {
      return toString(true, true);
    }
    public String toString(boolean radioHW, boolean radioRXTX) {
      StringBuilder sb = new StringBuilder();
      String moteString = mote.toString().replace(' ', '_');

      sb.append(moteString + " MONITORED " + duration + " us\n");
      if (radioHW) {
        sb.append(String.format(moteString + " ON " + (radioOn + " us ") + "%2.2f %%", 100.0*getRadioOnRatio()) + "\n");
      }
      if (radioRXTX) {
        sb.append(String.format(moteString + " TX " + (radioTx + " us ") + "%2.2f %%", 100.0*getRadioTxRatio()) + "\n");
        sb.append(String.format(moteString + " RX " + (radioRx + " us ") + "%2.2f %%", 100.0*getRadioRxRatio()) + "\n");
        sb.append(String.format(moteString + " INT " + (radioInterfered + " us ") + "%2.2f %%", 100.0*getRadioInterferedRatio()) + "\n");
      }
      return sb.toString();
    }
  }

  private MoteTracker createMoteTracker(Mote mote) {
    final Radio moteRadio = mote.getInterfaces().getRadio();
    if (moteRadio == null) {
      return null;
    }

    /* Radio observer */
    MoteTracker tracker = new MoteTracker(mote);
    tracker.update(null, null);
    return tracker;
  }

  public void reset() {
    while (moteTrackers.size() > 0) {
      removeMote(moteTrackers.get(0).mote);
    }
    for (Mote m: simulation.getMotes()) {
      addMote(m);
    }
  }

  private void addMote(Mote mote) {
    if (mote == null) {
      return;
    }

    MoteTracker t = createMoteTracker(mote);
    if (t != null) {
      moteTrackers.add(t);
    }

    setTitle("PowerTracker: " + moteTrackers.size() + " motes");
  }

  private void removeMote(Mote mote) {
    /* Remove mote tracker(s) */
    MoteTracker[] trackers = moteTrackers.toArray(new MoteTracker[0]);
    for (MoteTracker t: trackers) {
      if (t.getMote() == mote) {
        t.dispose();
        moteTrackers.remove(t);
      }
    }

    setTitle("PowerTracker: " + moteTrackers.size() + " motes");
  }

  public void closePlugin() {
    /* Remove repaint timer */
    repaintTimer.stop();

    simulation.getEventCentral().removeMoteCountListener(moteCountListener);

    /* Remove mote trackers */
    for (Mote m: simulation.getMotes()) {
      removeMote(m);
    }
    if (!moteTrackers.isEmpty()) {
      logger.fatal("Mote observers not cleaned up correctly");
      for (MoteTracker t: moteTrackers.toArray(new MoteTracker[0])) {
        t.dispose();
      }
    }
  }

  public enum RadioState {
    IDLE, RECEIVING, TRANSMITTING, INTERFERED
  }

  private Timer repaintTimer = new Timer(POWERTRACKER_UPDATE_INTERVAL, new ActionListener() {
    public void actionPerformed(ActionEvent e) {
      /* Identify max radio on */
      double maxRadioOn = 0;
      int maxRadioOnIndex = -1;
      for (int i=0; i < moteTrackers.size(); i++) {
        MoteTracker mt = moteTrackers.get(i);
        if (mt.getRadioOnRatio() > maxRadioOn) {
          maxRadioOn = mt.getRadioOnRatio();
          maxRadioOnIndex = i;
        }
      }
      if (maxRadioOnIndex >= 0) {
        tableMaxRadioOnIndex = maxRadioOnIndex;
      }

      table.repaint();
    }
  });

  public Collection<Element> getConfigXML() {
    return null;
  }
  public boolean setConfigXML(Collection<Element> configXML, boolean visAvailable) {
    return true;
  }

}
