/* -*- mode: java; c-basic-offset: 2; indent-tabs-mode: nil -*- */

/*
  Part of the Processing project - http://processing.org

  Copyright (c) 2012-16 The Processing Foundation

  This program is free software; you can redistribute it and/or
  modify it under the terms of the GNU General Public License
  version 2, as published by the Free Software Foundation.

  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, write to the Free Software Foundation,
  Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
*/

package processing.mode.java.debug;

import java.util.List;

import processing.app.Messages;

import com.sun.jdi.AbsentInformationException;
import com.sun.jdi.Location;
import com.sun.jdi.ReferenceType;
import com.sun.jdi.VMDisconnectedException;
import com.sun.jdi.request.BreakpointRequest;


/**
 * Model/Controller of a line breakpoint. Can be set before or while debugging.
 * Adds a highlight using the debuggers view.
 */
public class LineBreakpoint implements ClassLoadListener {
  protected Debugger dbg; // the debugger
  protected LineID line; // the line this breakpoint is set on
  protected BreakpointRequest bpr; // the request on the VM's event request manager
  protected String className;


  /**
   * Create a {@link LineBreakpoint}. If in a debug session, will try to
   * immediately set the breakpoint. If not in a debug session or the
   * corresponding class is not yet loaded the breakpoint will activate on
   * class load.
   *
   * @param line the line id to create the breakpoint on
   * @param dbg the {@link Debugger}
   */
  public LineBreakpoint(LineID line, Debugger dbg) {
    this.line = line;
    line.startTracking(dbg.getEditor().getTab(line.fileName()).getDocument());
    this.dbg = dbg;
    this.className = className();
    set(); // activate the breakpoint (show highlight, attach if debugger is running)
    Messages.log("LBP Created " + toString() + " class: " + this.className);
  }


  /**
   * Create a {@link LineBreakpoint} on a line in the current tab.
   * @param lineIdx the line index of the current tab to create the breakpoint
   */
  // TODO: remove and replace by {@link #LineBreakpoint(LineID line, Debugger dbg)}
  public LineBreakpoint(int lineIdx, Debugger dbg) {
    this(dbg.getEditor().getLineIDInCurrentTab(lineIdx), dbg);
  }


  /**
   * Get the line id this breakpoint is on.
   */
  public LineID lineID() {
    return line;
  }


  /**
   * Test if this breakpoint is on a certain line.
   *
   * @param testLine the line id to test
   * @return true if this breakpoint is on the given line
   */
  public boolean isOnLine(LineID testLine) {
    return line.equals(testLine);
  }


  /**
   * Attach this breakpoint to the VM. Creates and enables a
   * {@link BreakpointRequest}. VM needs to be paused.
   *
   * @param theClass class to attach to
   * @return true on success
   */
  protected boolean attach(ReferenceType theClass) {

    if (theClass == null || className == null ||
        !className.equals(parseTopLevelClassName(theClass.name()))) {
      return false;
    }

    log("trying to attach: " + line.fileName + ":" + line.lineIdx + " to " + theClass.name());

    if (!dbg.isPaused()) {
      log("can't attach breakpoint, debugger not paused");
      return false;
    }

    // find line in java space
    LineID javaLine = dbg.sketchToJavaLine(line);
    if (javaLine == null) {
      log("couldn't find line " + line + " in the java code");
      return false;
    }
    try {
      log("BPs of class: " + theClass + ", line " + (javaLine.lineIdx() + 1));
      List<Location> locations = theClass.locationsOfLine(javaLine.lineIdx() + 1);
      if (locations.isEmpty()) {
        log("no location found for line " + line + " -> " + javaLine);
        return false;
      }
      // use first found location
      bpr = dbg.vm().eventRequestManager().createBreakpointRequest(locations.get(0));
      bpr.enable();
      log("attached breakpoint to " + line + " -> " + javaLine);
      return true;
    } catch (AbsentInformationException ex) {
      Messages.err(null, ex);
    }
    return false;
  }


  protected boolean isAttached() {
    return bpr != null;
  }


  /**
   * Detach this breakpoint from the VM. Deletes the
   * {@link BreakpointRequest}.
   */
  public void detach() {
    if (bpr != null) {
      try {
        dbg.vm().eventRequestManager().deleteEventRequest(bpr);
      } catch (VMDisconnectedException ignore) { }
      bpr = null;
    }
  }


  /**
   * Set this breakpoint. Adds the line highlight. If Debugger is paused
   * also attaches the breakpoint by calling {@link #attach}.
   */
  protected void set() {
    dbg.addClassLoadListener(this); // class may not yet be loaded
    dbg.getEditor().addBreakpointedLine(line);
    if (className != null && dbg.isPaused()) { // debugging right now, try to attach
      for (ReferenceType rt : dbg.getClasses()) {
        // try to attach to all top level or nested classes
        if (attach(rt)) break;
      }
    }
    if (dbg.getEditor().isInCurrentTab(line)) {
      dbg.getEditor().getSketch().setModified(true);
    }
  }


  /**
   * Remove this breakpoint. Clears the highlight and detaches
   * the breakpoint if the debugger is paused.
   */
  public void remove() {
    dbg.removeClassLoadListener(this);
    //System.out.println("removing " + line.lineIdx());
    dbg.getEditor().removeBreakpointedLine(line.lineIdx());
    if (dbg.isPaused()) {
      // immediately remove the breakpoint
      detach();
    }
    line.stopTracking();
    if (dbg.getEditor().isInCurrentTab(line)) {
      dbg.getEditor().getSketch().setModified(true);
    }
  }


  @Override
  public String toString() {
    return line.toString();
  }


  /**
   * Get the name of the class this breakpoint belongs to. Needed for
   * fetching the right location to create a breakpoint request.
   * @return the class name
   */
  protected String className() {
    if (line.fileName().endsWith(".pde")) {
      // pure pde tab
        return line.fileName().substring(0, line.fileName().lastIndexOf(".pde"));
    }

    if (line.fileName().endsWith(".java")) {
      // pure java tab
      return line.fileName().substring(0, line.fileName().lastIndexOf(".java"));
    }
    return null;
  }


  /**
   * Event handler called when a class is loaded in the debugger. Causes the
   * breakpoint to be attached, if its class was loaded.
   *
   * @param theClass the class that was just loaded.
   */
  @Override
  public void classLoaded(ReferenceType theClass) {
    if (!isAttached()) {
      // try to attach
      attach(theClass);
    }
  }


  static public String parseTopLevelClassName(String name) {
    // Get rid of nested class name
    int dollar = name.indexOf('$');
    return (dollar == -1) ? name : name.substring(0, dollar);
  }


  // . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .


  private void log(String msg, Object... args) {
    if (args != null && args.length != 0) {
      Messages.logf(getClass().getName() + " " + msg, args);
    } else {
      Messages.log(getClass().getName() + " " + msg);
    }
  }
}
