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

import java.awt.Component;
import java.io.File;
import java.io.IOException;
import java.io.PrintStream;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Hashtable;

import org.apache.log4j.Logger;
import org.jdom.Element;
import org.contikios.cooja.ContikiError;
import org.contikios.cooja.Cooja;
import org.contikios.cooja.Mote;
import org.contikios.cooja.MoteInterface;
import org.contikios.cooja.MoteInterfaceHandler;
import org.contikios.cooja.MoteType;
import org.contikios.cooja.Simulation;
import org.contikios.cooja.Watchpoint;
import org.contikios.cooja.WatchpointMote;
import org.contikios.cooja.interfaces.IPAddress;
import org.contikios.cooja.mote.memory.MemoryInterface;
import org.contikios.cooja.motes.AbstractEmulatedMote;
import org.contikios.cooja.mspmote.interfaces.Msp802154Radio;
import org.contikios.cooja.mspmote.interfaces.MspSerial;
import org.contikios.cooja.mspmote.plugins.CodeVisualizerSkin;
import org.contikios.cooja.mspmote.plugins.MspBreakpoint;
import org.contikios.cooja.plugins.Visualizer;

import se.sics.mspsim.cli.CommandContext;
import se.sics.mspsim.cli.CommandHandler;
import se.sics.mspsim.cli.LineListener;
import se.sics.mspsim.cli.LineOutputStream;
import se.sics.mspsim.core.EmulationException;
import se.sics.mspsim.core.LogListener;
import se.sics.mspsim.core.Loggable;
import se.sics.mspsim.core.MSP430;
import se.sics.mspsim.core.EmulationLogger.WarningType;
import se.sics.mspsim.platform.GenericNode;
import se.sics.mspsim.ui.ManagedWindow;
import se.sics.mspsim.ui.WindowManager;
import se.sics.mspsim.util.ComponentRegistry;
import se.sics.mspsim.util.ConfigManager;
import se.sics.mspsim.util.DebugInfo;
import se.sics.mspsim.util.ELF;
import se.sics.mspsim.util.MapEntry;
import se.sics.mspsim.util.MapTable;
import se.sics.mspsim.profiler.SimpleProfiler;

import org.contikios.cooja.mspmote.interfaces.MspClock;

/**
 * @author Fredrik Osterlind
 */
public abstract class MspMote extends AbstractEmulatedMote implements Mote, WatchpointMote {
  private static Logger logger = Logger.getLogger(MspMote.class);

  private final static int EXECUTE_DURATION_US = 1; /* We always execute in 1 us steps */

  {
    Visualizer.registerVisualizerSkin(CodeVisualizerSkin.class);
  }

  private CommandHandler commandHandler;
  private MSP430 myCpu = null;
  private MspMoteType myMoteType = null;
  private MspMoteMemory myMemory = null;
  private MoteInterfaceHandler myMoteInterfaceHandler = null;
  public ComponentRegistry registry = null;

  /* Stack monitoring variables */
  private boolean stopNextInstruction = false;

  public GenericNode mspNode = null;

  public MspMote(MspMoteType moteType, Simulation simulation) {
    this.simulation = simulation;
    myMoteType = moteType;

    /* Schedule us immediately */
    requestImmediateWakeup();
  }

  protected void initMote() {
    if (myMoteType != null) {
      initEmulator(myMoteType.getContikiFirmwareFile());
      myMoteInterfaceHandler = createMoteInterfaceHandler();

      /* TODO Create COOJA-specific window manager */
      registry.removeComponent("windowManager");
      registry.registerComponent("windowManager", new WindowManager() {
        public ManagedWindow createWindow(String name) {
          return new ManagedWindow() {
            public void setVisible(boolean b) {
              logger.warn("setVisible() ignored");
            }
            public void setTitle(String string) {
              logger.warn("setTitle() ignored");
            }
            public void setSize(int width, int height) {
              logger.warn("setSize() ignored");
            }
            public void setBounds(int x, int y, int width, int height) {
              logger.warn("setBounds() ignored");
            }
            public void removeAll() {
              logger.warn("removeAll() ignored");
            }
            public void pack() {
              logger.warn("pack() ignored");
            }
            public boolean isVisible() {
              logger.warn("isVisible() return false");
              return false;
            }
            public String getTitle() {
              logger.warn("getTitle() return \"\"");
              return "";
            }
            public void add(Component component) {
              logger.warn("add() ignored");
            }
          };
        }
      });

      try {
        debuggingInfo = ((MspMoteType)getType()).getFirmwareDebugInfo();
      } catch (IOException e) {
        throw (RuntimeException) new RuntimeException("Error: " + e.getMessage()).initCause(e);
      }
    }
  }

  /**
   * Abort execution immediately.
   * May for example be called by a breakpoint handler.
   */
  public void stopNextInstruction() {
    stopNextInstruction = true;
    getCPU().stop();
  }

  protected MoteInterfaceHandler createMoteInterfaceHandler() {
    return new MoteInterfaceHandler(this, getType().getMoteInterfaceClasses());
  }

  /**
   * @return MSP430 CPU
   */
  public MSP430 getCPU() {
    return myCpu;
  }

  public void setCPU(MSP430 cpu) {
    myCpu = cpu;
  }

  @Override
  public MemoryInterface getMemory() {
    return myMemory;
  }

  public void setMemory(MspMoteMemory memory) {
    myMemory = memory;
  }

  /**
   * Prepares CPU, memory and ELF module.
   *
   * @param fileELF ELF file
   * @param cpu MSP430 cpu
   * @throws IOException Preparing mote failed
   */
  protected void prepareMote(File fileELF, GenericNode node) throws IOException {
    this.commandHandler = new CommandHandler(System.out, System.err);
    
    this.mspNode = node;
    
    node.setCommandHandler(commandHandler);

    ConfigManager config = new ConfigManager();
    node.setup(config);

    this.myCpu = node.getCPU();
    this.myCpu.setMonitorExec(true);
    this.myCpu.setTrace(0); /* TODO Enable */
    
    LogListener ll = new LogListener() {
      private Logger mlogger = Logger.getLogger("MSPSim");
      @Override
      public void log(Loggable source, String message) {
        mlogger.debug("" + getID() + ": " + source.getID() + ": " + message);
      }
      
      @Override
      public void logw(Loggable source, WarningType type, String message) throws EmulationException {
        mlogger.warn("" + getID() +": " + "# " + source.getID() + "[" + type + "]: " + message);
      }
    };
    
    this.myCpu.getLogger().addLogListener(ll);

    logger.info("Loading firmware from: " + fileELF.getAbsolutePath());
    Cooja.setProgressMessage("Loading " + fileELF.getName());
    node.loadFirmware(((MspMoteType)getType()).getELF());

    /* Throw exceptions at bad memory access */
    /*myCpu.setThrowIfWarning(true);*/

    /* Create mote address memory */
    MapTable map = ((MspMoteType)getType()).getELF().getMap();
    MapEntry[] allEntries = map.getAllEntries();
    myMemory = new MspMoteMemory(this, allEntries, myCpu);

    myCpu.reset();
  }

  public CommandHandler getCLICommandHandler() {
    return commandHandler;
  }

  /* called when moteID is updated */
  public void idUpdated(int newID) {
  }

  public MoteType getType() {
    return myMoteType;
  }

  public void setType(MoteType type) {
    myMoteType = (MspMoteType) type;
  }

  public MoteInterfaceHandler getInterfaces() {
    return myMoteInterfaceHandler;
  }

  public void setInterfaces(MoteInterfaceHandler moteInterfaceHandler) {
    myMoteInterfaceHandler = moteInterfaceHandler;
  }

  /**
   * Initializes emulator by creating CPU, memory and node object.
   *
   * @param ELFFile ELF file
   * @return True if successful
   */
  protected abstract boolean initEmulator(File ELFFile);

  private boolean booted = false;

  public void simTimeChanged(long diff) {
    /* Compensates for simulation time changes (without simulation execution) */
    lastExecute -= diff;
    nextExecute -= diff;
    scheduleNextWakeup(nextExecute);
  }

  private long lastExecute = -1; /* Last time mote executed */
  private long nextExecute;
  
  private long executed = 0;
  private long skipped = 0;
  
  public void execute(long time) {
    execute(time, EXECUTE_DURATION_US);
  }

  public void execute(long t, int duration) {
    MspClock clock = ((MspClock) (myMoteInterfaceHandler.getClock()));
    double deviation = clock.getDeviation();
    long drift = clock.getDrift();

    /* Wait until mote boots */
    if (!booted && clock.getTime() < 0) {
      scheduleNextWakeup(t - clock.getTime());
      return;
    }
    booted = true;

    if (stopNextInstruction) {
      stopNextInstruction = false;
      scheduleNextWakeup(t);
      throw new RuntimeException("MSPSim requested simulation stop");
    }

    if (lastExecute < 0) {
      /* Always execute one microsecond the first time */
      lastExecute = t;
    }
    if (t < lastExecute) {
      throw new RuntimeException("Bad event ordering: " + lastExecute + " < " + t);
    }

    if (((1-deviation) * executed) > skipped) {
      lastExecute = lastExecute + duration; // (t+duration) - (t-lastExecute);
      nextExecute = t+duration;
      skipped += duration;
      scheduleNextWakeup(nextExecute);
    }
    
    /* Execute MSPSim-based mote */
    /* TODO Try-catch overhead */
    try {
      nextExecute = myCpu.stepMicros(Math.max(0, t-lastExecute), duration) + t + duration;
      lastExecute = t;
    } catch (EmulationException e) {
      String trace = e.getMessage() + "\n\n" + getStackTrace();
      throw (ContikiError)
      new ContikiError(trace).initCause(e);
    }

    /* Schedule wakeup */
    if (nextExecute < t) {
      throw new RuntimeException(t + ": MSPSim requested early wakeup: " + nextExecute);
    }

    /*logger.debug(t + ": Schedule next wakeup at " + nextExecute);*/
    executed += duration; 
    scheduleNextWakeup(nextExecute);

    if (stopNextInstruction) {
      stopNextInstruction = false;
      throw new RuntimeException("MSPSim requested simulation stop");
    }

    /* XXX TODO Reimplement stack monitoring using MSPSim internals */
    /*if (monitorStackUsage) {
      int newStack = cpu.reg[MSP430.SP];
      if (newStack < stackPointerLow && newStack > 0) {
        stackPointerLow = cpu.reg[MSP430.SP];

        // Check if stack is writing in memory
        if (stackPointerLow < heapStartAddress) {
          stackOverflowObservable.signalStackOverflow();
          stopNextInstruction = true;
          getSimulation().stopSimulation();
        }
      }
    }*/
  }

  public String getStackTrace() {
    return executeCLICommand("stacktrace");
  }

  public int executeCLICommand(String cmd, CommandContext context) {
    return commandHandler.executeCommand(cmd, context);
  }

  public String executeCLICommand(String cmd) {
    final StringBuilder sb = new StringBuilder();
    LineListener ll = new LineListener() {
      public void lineRead(String line) {
        sb.append(line).append("\n");
      }
    };
    PrintStream po = new PrintStream(new LineOutputStream(ll));
    CommandContext c = new CommandContext(commandHandler, null, "", new String[0], 1, null);
    c.out = po;
    c.err = po;

    if (0 != executeCLICommand(cmd, c)) {
      sb.append("\nWarning: command failed");
    }

    return sb.toString();
  }

  public int getCPUFrequency() {
    return myCpu.getDCOFrequency();
  }

  public int getID() {
    return getInterfaces().getMoteID().getMoteID();
  }

  public boolean setConfigXML(Simulation simulation, Collection<Element> configXML, boolean visAvailable) {
    setSimulation(simulation);
    if (myMoteInterfaceHandler == null) {
      myMoteInterfaceHandler = createMoteInterfaceHandler();
    }

    try {
      debuggingInfo = ((MspMoteType)getType()).getFirmwareDebugInfo();
    } catch (IOException e) {
      throw (RuntimeException) new RuntimeException("Error: " + e.getMessage()).initCause(e);
    }

    for (Element element: configXML) {
      String name = element.getName();

      if (name.equals("motetype_identifier")) {
        /* Ignored: handled by simulation */
      } else if ("breakpoints".equals(element.getName())) {
        setWatchpointConfigXML(element.getChildren(), visAvailable);
      } else if (name.equals("interface_config")) {
        String intfClass = element.getText().trim();

        /* Backwards compatibility: se.sics -> org.contikios */
        if (intfClass.startsWith("se.sics")) {
          intfClass = intfClass.replaceFirst("se\\.sics", "org.contikios");
        }

        if (intfClass.equals("org.contikios.cooja.mspmote.interfaces.MspIPAddress")) {
        	intfClass = IPAddress.class.getName();
        }
        if (intfClass.equals("org.contikios.cooja.mspmote.interfaces.ESBLog")) {
        	intfClass = MspSerial.class.getName();
        }
        if (intfClass.equals("org.contikios.cooja.mspmote.interfaces.SkyByteRadio")) {
        	intfClass = Msp802154Radio.class.getName();
        }
        if (intfClass.equals("org.contikios.cooja.mspmote.interfaces.SkySerial")) {
        	intfClass = MspSerial.class.getName();
        }

        Class<? extends MoteInterface> moteInterfaceClass = simulation.getCooja().tryLoadClass(
              this, MoteInterface.class, intfClass);

        if (moteInterfaceClass == null) {
          logger.fatal("Could not load mote interface class: " + intfClass);
          return false;
        }

        MoteInterface moteInterface = getInterfaces().getInterfaceOfType(moteInterfaceClass);
        if (moteInterface == null) {
            logger.fatal("Could not find mote interface of class: " + moteInterfaceClass);
            return false;
        }
        moteInterface.setConfigXML(element.getChildren(), visAvailable);
      }
    }

    /* Schedule us immediately */
    requestImmediateWakeup();
    return true;
  }

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

    /* Breakpoints */
    element = new Element("breakpoints");
    element.addContent(getWatchpointConfigXML());
    config.add(element);

    // Mote interfaces
    for (MoteInterface moteInterface: getInterfaces().getInterfaces()) {
      element = new Element("interface_config");
      element.setText(moteInterface.getClass().getName());

      Collection<Element> interfaceXML = moteInterface.getConfigXML();
      if (interfaceXML != null) {
        element.addContent(interfaceXML);
        config.add(element);
      }
    }

    return config;
  }

  public String getExecutionDetails() {
    return executeCLICommand("stacktrace");
  }

  public String getPCString() {
    int pc = myCpu.getPC();
    ELF elf = myCpu.getRegistry().getComponent(ELF.class);
    DebugInfo di = elf.getDebugInfo(pc);

    /* Following code examples from MSPsim, DebugCommands.java */
    if (di == null) {
      di = elf.getDebugInfo(pc + 1);
    }
    if (di == null) {
      /* Return PC value */
      SimpleProfiler sp = (SimpleProfiler)myCpu.getProfiler();
      try {
        MapEntry mapEntry = sp.getCallMapEntry(0);
        if (mapEntry != null) {
          String file = mapEntry.getFile();
          if (file != null) {
            if (file.indexOf('/') >= 0) {
              file = file.substring(file.lastIndexOf('/')+1);
            }
          }
          String name = mapEntry.getName();
          return file + ":?:" + name;
        }
        return String.format("*%02x", pc);
      } catch (Exception e) {
        return null;
      }
    }

    int lineNo = di.getLine();
    String file = di.getFile();
    file = file==null?"?":file;
    if (file.contains("/")) {
      /* strip path */
      file = file.substring(file.lastIndexOf('/')+1, file.length());
    }

    String function = di.getFunction();
    function = function==null?"":function;
    if (function.contains(":")) {
      /* strip arguments */
      function = function.substring(0, function.lastIndexOf(':'));
    }
    if (function.equals("* not available")) {
      function = "?";
    }
    return file + ":" + lineNo + ":" + function;

    /*return executeCLICommand("line " + myCpu.getPC());*/
  }


  /* WatchpointMote */
  private ArrayList<WatchpointListener> watchpointListeners = new ArrayList<WatchpointListener>();
  private ArrayList<MspBreakpoint> watchpoints = new ArrayList<MspBreakpoint>();
  private Hashtable<File, Hashtable<Integer, Integer>> debuggingInfo = null;

  public void addWatchpointListener(WatchpointListener listener) {
    watchpointListeners.add(listener);
  }
  public void removeWatchpointListener(WatchpointListener listener) {
    watchpointListeners.remove(listener);
  }
  public WatchpointListener[] getWatchpointListeners() {
    return watchpointListeners.toArray(new WatchpointListener[0]);
  }

  public Watchpoint addBreakpoint(File codeFile, int lineNr, int address) {
    MspBreakpoint bp = new MspBreakpoint(this, address, codeFile, new Integer(lineNr));
    watchpoints.add(bp);

    for (WatchpointListener listener: watchpointListeners) {
      listener.watchpointsChanged();
    }
    return bp;
  }
  public void removeBreakpoint(Watchpoint watchpoint) {
    ((MspBreakpoint)watchpoint).unregisterBreakpoint();
    watchpoints.remove(watchpoint);

    for (WatchpointListener listener: watchpointListeners) {
      listener.watchpointsChanged();
    }
  }
  public Watchpoint[] getBreakpoints() {
    return watchpoints.toArray(new Watchpoint[0]);
  }

  public boolean breakpointExists(int address) {
    if (address < 0) {
      return false;
    }
    for (Watchpoint watchpoint: watchpoints) {
      if (watchpoint.getExecutableAddress() == address) {
        return true;
      }
    }
    return false;
  }
  public boolean breakpointExists(File file, int lineNr) {
    for (Watchpoint watchpoint: watchpoints) {
      if (watchpoint.getCodeFile() == null) {
        continue;
      }
      if (watchpoint.getCodeFile().compareTo(file) != 0) {
        continue;
      }
      if (watchpoint.getLineNumber() != lineNr) {
        continue;
      }
      return true;
    }
    return false;
  }

  public int getExecutableAddressOf(File file, int lineNr) {
    if (file == null || lineNr < 0 || debuggingInfo == null) {
      return -1;
    }

    /* Match file */
    Hashtable<Integer, Integer> lineTable = debuggingInfo.get(file);
    if (lineTable == null) {
      for (File f: debuggingInfo.keySet()) {
        if (f != null && f.getName().equals(file.getName())) {
          lineTable = debuggingInfo.get(f);
          break;
        }
      }
    }
    if (lineTable == null) {
      return -1;
    }

    /* Match line number */
    Integer address = lineTable.get(lineNr);
    if (address != null) {
      for (Integer l: lineTable.keySet()) {
        if (l != null && l.intValue() == lineNr) {
          /* Found line address */
          return lineTable.get(l);
        }
      }
    }

    return -1;
  }

  private long lastBreakpointCycles = -1;
  public void signalBreakpointTrigger(MspBreakpoint b) {
    if (lastBreakpointCycles == myCpu.cycles) {
      return;
    }

    lastBreakpointCycles = myCpu.cycles;
    if (b.stopsSimulation() && getSimulation().isRunning()) {
      /* Stop simulation immediately */
      stopNextInstruction();
    }

    /* Notify listeners */
    WatchpointListener[] listeners = getWatchpointListeners();
    for (WatchpointListener listener: listeners) {
      listener.watchpointTriggered(b);
    }
  }

  public Collection<Element> getWatchpointConfigXML() {
    ArrayList<Element> config = new ArrayList<Element>();
    Element element;

    for (MspBreakpoint breakpoint: watchpoints) {
      element = new Element("breakpoint");
      element.addContent(breakpoint.getConfigXML());
      config.add(element);
    }

    return config;
  }
  public boolean setWatchpointConfigXML(Collection<Element> configXML, boolean visAvailable) {
    for (Element element : configXML) {
      if (element.getName().equals("breakpoint")) {
        MspBreakpoint breakpoint = new MspBreakpoint(this);
        if (!breakpoint.setConfigXML(element.getChildren(), visAvailable)) {
          logger.warn("Could not restore breakpoint: " + breakpoint);
        } else {
          watchpoints.add(breakpoint);
        }
      }
    }
    return true;
  }
}
