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

import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Component;
import java.awt.Point;
import java.awt.Rectangle;
import java.io.File;
import java.util.ArrayList;
import java.util.HashMap;

import javax.swing.JEditorPane;
import javax.swing.JMenuItem;
import javax.swing.JPanel;
import javax.swing.JPopupMenu;
import javax.swing.JScrollPane;
import javax.swing.SwingUtilities;
import javax.swing.event.PopupMenuEvent;
import javax.swing.event.PopupMenuListener;
import javax.swing.text.BadLocationException;
import javax.swing.text.Highlighter;
import javax.swing.text.Highlighter.HighlightPainter;

import jsyntaxpane.DefaultSyntaxKit;
import jsyntaxpane.components.Markers.SimpleMarker;

import org.apache.log4j.Logger;

import org.contikios.cooja.Watchpoint;
import org.contikios.cooja.WatchpointMote;
import org.contikios.cooja.util.JSyntaxAddBreakpoint;
import org.contikios.cooja.util.JSyntaxRemoveBreakpoint;
import org.contikios.cooja.util.StringUtils;

/**
 * Displays source code and allows a user to add and remove breakpoints.
 *
 * @author Fredrik Osterlind
 */
public class CodeUI extends JPanel {
  private static Logger logger = Logger.getLogger(CodeUI.class);

  {
    DefaultSyntaxKit.initKit();
  }

  private JEditorPane codeEditor = null;
  private HashMap<Integer, Integer> codeEditorLines = null;
  protected File displayedFile = null;

  private static final HighlightPainter CURRENT_LINE_MARKER = new SimpleMarker(Color.ORANGE);
  private static final HighlightPainter SELECTED_LINE_MARKER = new SimpleMarker(Color.GREEN);
  private static final HighlightPainter BREAKPOINTS_MARKER = new SimpleMarker(Color.LIGHT_GRAY);
  private final Object currentLineTag;
  private final Object selectedLineTag;
  private final ArrayList<Object> breakpointsLineTags = new ArrayList<Object>();

  private JSyntaxAddBreakpoint actionAddBreakpoint = null;
  private JSyntaxRemoveBreakpoint actionRemoveBreakpoint = null;

  private WatchpointMote mote;

  public CodeUI(WatchpointMote mote) {
    this.mote = mote;

    {
      /* Workaround to configure jsyntaxpane */
      JEditorPane e = new JEditorPane();
      new JScrollPane(e);
      e.setContentType("text/c");
      DefaultSyntaxKit kit = (DefaultSyntaxKit) e.getEditorKit();
      kit.setProperty("Action.addbreakpoint", JSyntaxAddBreakpoint.class.getName());
      kit.setProperty("Action.removebreakpoint", JSyntaxRemoveBreakpoint.class.getName());
      kit.setProperty("PopupMenu", "copy-to-clipboard,-,find,find-next,goto-line,-,addbreakpoint,removebreakpoint");
    }

    setLayout(new BorderLayout());
    codeEditor = new JEditorPane();
    add(new JScrollPane(codeEditor), BorderLayout.CENTER);
    doLayout();

    codeEditorLines = new HashMap<Integer, Integer>();
    codeEditor.setContentType("text/c");
    DefaultSyntaxKit kit = (DefaultSyntaxKit) codeEditor.getEditorKit();
    kit.setProperty("Action.addbreakpoint", JSyntaxAddBreakpoint.class.getName());
    kit.setProperty("Action.removebreakpoint", JSyntaxRemoveBreakpoint.class.getName());
    kit.setProperty("PopupMenu", "copy-to-clipboard,-,find,find-next,goto-line,-,addbreakpoint,removebreakpoint");

    JPopupMenu p = codeEditor.getComponentPopupMenu();
    for (Component c: p.getComponents()) {
      if (c instanceof JMenuItem) {
        if (((JMenuItem) c).getAction() != null &&
            ((JMenuItem) c).getAction() instanceof JSyntaxAddBreakpoint) {
          actionAddBreakpoint = (JSyntaxAddBreakpoint)(((JMenuItem) c).getAction());
          actionAddBreakpoint.setMenuText("Add breakpoint");
        }
        if (((JMenuItem) c).getAction() != null &&
            ((JMenuItem) c).getAction() instanceof JSyntaxRemoveBreakpoint) {
          actionRemoveBreakpoint = (JSyntaxRemoveBreakpoint)(((JMenuItem) c).getAction());
          actionRemoveBreakpoint.setMenuText("Remove breakpoint");
        }
      }
    }

    codeEditor.setText("");
    codeEditorLines.clear();
    codeEditor.setEditable(false);

    Highlighter hl = codeEditor.getHighlighter();
    Object o = null;
    try {
      o = hl.addHighlight(0, 0, CURRENT_LINE_MARKER);
    } catch (BadLocationException e1) {
    }
    currentLineTag = o;

    o = null;
    try {
      o = hl.addHighlight(0, 0, SELECTED_LINE_MARKER);
    } catch (BadLocationException e1) {
    }
    selectedLineTag = o;

    codeEditor.getComponentPopupMenu().addPopupMenuListener(new PopupMenuListener() {
      public void popupMenuWillBecomeVisible(PopupMenuEvent e) {
        /* Disable breakpoint actions */
        actionAddBreakpoint.setEnabled(false);
        actionRemoveBreakpoint.setEnabled(false);

        int line = getCodeEditorMouseLine();
        if (line < 1) {
          return;
        }

        /* Configure breakpoint menu options */
        /* XXX TODO We should ask for the file specified in the firmware, not
         * the actual file on disk. */
        int address =
          CodeUI.this.mote.getExecutableAddressOf(displayedFile, line);
        if (address < 0) {
          return;
        }
        final int start = codeEditorLines.get(line);
        int end = codeEditorLines.get(line+1);
        Highlighter hl = codeEditor.getHighlighter();
        try {
          hl.changeHighlight(selectedLineTag, start, end);
        } catch (BadLocationException e1) {
        }
        boolean hasBreakpoint =
          CodeUI.this.mote.breakpointExists(address);
        if (!hasBreakpoint) {
          actionAddBreakpoint.setEnabled(true);
          actionAddBreakpoint.putValue("WatchpointMote", CodeUI.this.mote);
          actionAddBreakpoint.putValue("WatchpointFile", displayedFile);
          actionAddBreakpoint.putValue("WatchpointLine", new Integer(line));
          actionAddBreakpoint.putValue("WatchpointAddress", new Integer(address));
        } else {
          actionRemoveBreakpoint.setEnabled(true);
          actionRemoveBreakpoint.putValue("WatchpointMote", CodeUI.this.mote);
          actionRemoveBreakpoint.putValue("WatchpointFile", displayedFile);
          actionRemoveBreakpoint.putValue("WatchpointLine", new Integer(line));
          actionRemoveBreakpoint.putValue("WatchpointAddress", new Integer(address));
        }
      }
      public void popupMenuWillBecomeInvisible(PopupMenuEvent e) {
        Highlighter hl = codeEditor.getHighlighter();
        try {
          hl.changeHighlight(selectedLineTag, 0, 0);
        } catch (BadLocationException e1) {
        }
      }
      public void popupMenuCanceled(PopupMenuEvent e) {
      }
    });

    displayNoCode(true);
  }

  public void updateBreakpoints() {
    Highlighter hl = codeEditor.getHighlighter();

    for (Object breakpointsLineTag: breakpointsLineTags) {
      hl.removeHighlight(breakpointsLineTag);
    }
    breakpointsLineTags.clear();

    for (Watchpoint w: mote.getBreakpoints()) {
      if (!w.getCodeFile().equals(displayedFile)) {
        continue;
      }

      final int start = codeEditorLines.get(w.getLineNumber());
      int end = codeEditorLines.get(w.getLineNumber()+1);
      try {
        breakpointsLineTags.add(hl.addHighlight(start, end, BREAKPOINTS_MARKER));
      } catch (BadLocationException e1) {
      }
    }
  }

  private int getCodeEditorMouseLine() {
    if (codeEditorLines == null) {
      return -1;
    }
    Point mousePos = codeEditor.getMousePosition();
    if (mousePos == null) {
      return -1;
    }
    int modelPos = codeEditor.viewToModel(mousePos);
    int line = 1;
    while (codeEditorLines.containsKey(line+1)) {
      int next = codeEditorLines.get(line+1);
      if (modelPos < next) {
        return line;
      }
      line++;
    }
    return -1;
  }

  /**
   * Remove any shown source code.
   */
  public void displayNoCode(final boolean markCurrent) {
    SwingUtilities.invokeLater(new Runnable() {
      public void run() {
        displayedFile = null;
        codeEditor.setText("[no source displayed]");
        codeEditor.setEnabled(false);
        codeEditorLines.clear();
        displayLine(-1, markCurrent);
      }
    });
  }

  /**
   * Display given source code and mark given line.
   *
   * @param codeFile Source code file
   * @param lineNr Line numer
   */
  public void displayNewCode(final File codeFile, final int lineNr, final boolean markCurrent) {
    if (!codeFile.equals(displayedFile)) {
      /* Read from disk */
      final String data = StringUtils.loadFromFile(codeFile);
      if (data == null || data.length() == 0) {
        displayNoCode(markCurrent);
        return;
      }
      codeEditor.setEnabled(true);

      String[] lines = data.split("\n");
      logger.info("Opening " + codeFile + " (" + lines.length + " lines)");
      int length = 0;
      codeEditorLines.clear();
      for (int line=1; line-1 < lines.length; line++) {
        codeEditorLines.put(line, length);
        length += lines[line-1].length()+1;
      }
      codeEditor.setText(data.toString());
      displayedFile = codeFile;
      updateBreakpoints();
    }

    SwingUtilities.invokeLater(new Runnable() {
      public void run() {
        displayLine(lineNr, markCurrent);
      }
    });

  }

  /**
   * Mark given line number in shown source code.
   * Should be called from AWT thread.
   *
   * @param lineNumber Line number
   */
  private void displayLine(int lineNumber, boolean markCurrent) {
    try {
      if (markCurrent) {
        /* remove previous highlight */
        Highlighter hl = codeEditor.getHighlighter();
        hl.changeHighlight(currentLineTag, 0, 0);
      }

      if (lineNumber >= 0) {
        final int start = codeEditorLines.get(lineNumber);
        int end = codeEditorLines.get(lineNumber+1);
        if (markCurrent) {
          /* highlight code */
          Highlighter hl = codeEditor.getHighlighter();
          hl.changeHighlight(currentLineTag, start, end);
        }

        /* ensure visible */
        SwingUtilities.invokeLater(new Runnable() {
          public void run() {
            try {
              Rectangle r = codeEditor.modelToView(start);
              if (r != null) {
                codeEditor.scrollRectToVisible(codeEditor.modelToView(start));
              }
            } catch (BadLocationException e) {
            }
          }
        });
      }
    } catch (Exception e) {
      logger.warn("Error when highlighting current line: " + e.getMessage(), e);
    }
  }
}
