package xtc.lang.blink;

import java.io.IOException;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Set;
import java.util.TreeSet;

/**
 * The Blink break point manager.
 * 
 * @author Byeongcheol Lee
 */
public final class BreakPointManager {

  /** The break point state. */
  public enum BreakPointState {
    ENABLED,
    DISABLED
  }

  /** invalid breakpoint identifier. */
  public static final int INVALID_BREAKPOINT_ID = -1;

  /**
   * The debugger.
   */
  private final Blink dbg;

  /** The Blink user break point list. */
  private final HashMap<Integer, BlinkBreakPoint> breakpoints 
    = new HashMap<Integer, BlinkBreakPoint>();

  /** The deferred Native break point list. */
  private final HashMap<Integer, NativeBreakpoint> deferredNativeBreakpoints 
    = new HashMap<Integer, NativeBreakpoint>();

  /** The next identification sequence number.*/
  private int nextUserBreakPointID = 1;

  /** The saved break point state.*/
  private final HashSet<BlinkBreakPoint> frozenBreakpoints = 
    new HashSet<BlinkBreakPoint>(); 

  /**
   * @param dbg The debugger to work with.
   */
  BreakPointManager(Blink dbg) {
    this.dbg = dbg;
  }

  /**
   * Allocate a new identification number.
   * 
   * @return A new identification number.
   */
  private synchronized int getNextUserBreakPointID() {
    return nextUserBreakPointID++;
  }

  /**
   * Add a native user break point.
   * 
   * @param sourceFile The source file name.
   * @param sourceLine The source line number.
   */
  NativeSourceLineBreakPoint setNativeBreakpoint(String sourceFile, int sourceLine) 
    throws IOException {
    //create a native breakpoint.
    NativeSourceLineBreakPoint nbp;
    boolean deferred;
    if (dbg.IsNativeDebuggerAttached()) {
      dbg.ensureGDBContext();
      int nbpID = dbg.ndb.createBreakpoint(sourceFile, sourceLine);   
      nbp = new NativeSourceLineBreakPoint(nbpID, sourceFile,
          sourceLine);
      deferred = false;
    } else {
      nbp = new NativeSourceLineBreakPoint(sourceFile, sourceLine);
      deferred = true;
      dbg.out("the break point is delayed until the native debugger attached.\n");
    }

    //register
    int id = getNextUserBreakPointID();
    breakpoints.put(id, nbp);
    if (deferred) {
      deferredNativeBreakpoints.put(id, nbp);
    }
    return nbp;
  }

  NativeSymbolBreakpoint setNativeBreakpoint(String symbol) throws IOException {
    NativeSymbolBreakpoint nbp;
    boolean deferred;
    if (dbg.IsNativeDebuggerAttached()) {
      dbg.ensureGDBContext();
      int nbpID = dbg.ndb.createBreakpoint(symbol);
      nbp = new NativeSymbolBreakpoint(nbpID, symbol);
      deferred = false;
    } else {
      nbp = new NativeSymbolBreakpoint(symbol);
      deferred = true;
    }
    int id = getNextUserBreakPointID();
    breakpoints.put(id, nbp);
    if (deferred) {
      deferredNativeBreakpoints.put(id, nbp);
    }
    return nbp;
  }

  /**
   * Add a Java user break point.
   * 
   * @param classFile The class name.
   * @param sourceLine The source line number.
   * @return The break point.
   */
  JavaSourceLineBreakPoint setJavaBreakPoint(String classFile, int sourceLine) {
    int id =getNextUserBreakPointID();
    JavaSourceLineBreakPoint jbp = new JavaSourceLineBreakPoint(classFile, sourceLine);
    breakpoints.put(id, jbp);
    if (!jbp.enable(dbg)) {
      dbg.out("the break point is delayed \n");
    }
    return jbp;
  }

  /**
   * Add a Java user break point.
   * 
   * @param cname The class name.
   * @param mname The method name.
   * @return The breakpoint. 
   */
  JavaEntryBreakpoint setJavaBreakPoint(String cname, String mname) {
    int id = getNextUserBreakPointID();
    JavaEntryBreakpoint jbp = new JavaEntryBreakpoint(cname, mname);
    breakpoints.put(id, jbp);
    if (!jbp.enable(dbg)) {
      dbg.out("the break point is delayed\n");
    }
    return jbp;
  }

  /**
   * Implement "delete [n]" command.
   * 
   * @param id The identifier of break point or watch point.
   */
  void clearBreakpoint(int id) {
    if (!breakpoints.containsKey(id)) {
      dbg.err("not valid break point id -" + id);
      return;
    }

    //check deferred break point.
    if (deferredNativeBreakpoints.containsKey(id)) {
      deferredNativeBreakpoints.remove(id);
    } 

    if (breakpoints.containsKey(id)) {
      BlinkBreakPoint bp = breakpoints.get(id);
      bp.disable(dbg);
      breakpoints.remove(id);
    }
  }

  /**
   * Find a breakpoint using the native breakpoint identifier.
   * @param nativeBreakPointID The identifier.
   * @return The Breakpoint identifier. 
   */
  int findNativeBreakpoint(int nativeBreakPointID) {
    for(final int bpid: breakpoints.keySet()) {
      BlinkBreakPoint bp = breakpoints.get(bpid);
      if (bp instanceof NativeBreakpoint) {
        NativeBreakpoint nbp = (NativeBreakpoint)bp;
        if (nbp.getNativeBreakPointID()== nativeBreakPointID) {
          return bpid; //found
        }
      }
    }
    return INVALID_BREAKPOINT_ID; // not found
  }

  int findJavaBreakpoint(String cname, String mname, int lineNumber) {
    for(final int bpid: breakpoints.keySet()) {
      BlinkBreakPoint bp = breakpoints.get(bpid);
      if (bp instanceof JavaSourceLineBreakPoint) {
        JavaSourceLineBreakPoint jbp = (JavaSourceLineBreakPoint)bp;
        if (jbp.getClassName().equals(cname) 
            && jbp.getLineNumber() == lineNumber) {
          return bpid;
        }
      } else if (bp instanceof JavaEntryBreakpoint) {
        JavaEntryBreakpoint jbp = (JavaEntryBreakpoint)bp;
        if (jbp.matches(cname, mname)) {
          return bpid;
        }
      }
    }
    
    return INVALID_BREAKPOINT_ID; // not found
  }
  
  /**
   * Implement "info break" command.
   */
  void showUserBreakPointList() {
    TreeSet<Integer> breakPointIDList = new TreeSet<Integer>();
    breakPointIDList.addAll(breakpoints.keySet());
    StringBuilder sb = new StringBuilder();
    for (final Integer id : breakPointIDList) {
      BlinkBreakPoint bp = breakpoints.get(id);
      sb.append(id).append("  ");
      sb.append(bp.toString()).append('\n');
    }
    dbg.out(sb.toString());
  }

  /**
   * @return true if there is blink deferred C break point.
   */
  boolean hasDeferredNativeBreakpoint() {
    return deferredNativeBreakpoints.size() > 0;
  }

  /**
   * Handle deferred native break points when the shared library is loaded into
   * the debuggee JVM.
   */
  void handleDeferredNativeBreakPoint() {
    assert dbg.IsNativeDebuggerAttached();
    dbg.ensureGDBContext();
    Set<Integer> resolved = new TreeSet<Integer>();
    try {
      for(final Integer id : deferredNativeBreakpoints.keySet()) {
        NativeBreakpoint bp = deferredNativeBreakpoints.get(id);
        if (bp instanceof NativeSourceLineBreakPoint) {
          NativeSourceLineBreakPoint sbp = (NativeSourceLineBreakPoint)bp;
          int nbpid = dbg.ndb.createBreakpoint(sbp.getSourceFileName(), 
              sbp.getSourceLineNumber());
          sbp.setNativeBreakPointID(nbpid);
          resolved.add(id);
        }
      }
    } catch(IOException e) {
      dbg.err("error during resolving defered native break points.\n"); 
    } finally {
      for(final Integer id : resolved) {
        deferredNativeBreakpoints.remove(id);
      }
    }
  }

  /**
   * Freeze all the active user break points for the expression evaluation.
   */
  void freezeActiveBreakPoints() {
    for(BlinkBreakPoint b: breakpoints.values()) {
      BreakPointState s = b.getState();
      if (s == BreakPointState.ENABLED) {        
        if (b.disable(dbg)) {
          frozenBreakpoints.add(b);
        } else {
          dbg.err("could not freeze :  " + b + "\n");
        }
      }
    }
  }

  /**
   * Restore the user break point state after the expression evaluation.
   */
  void unfreezeAllBreakpoints() {
    HashSet<NativeBreakpoint> gdbBreakPointToWake = new HashSet<NativeBreakpoint>();
    HashSet<JavaBreakpoint> jdbBreakPointToWake = new HashSet<JavaBreakpoint>();

    for(BlinkBreakPoint b: frozenBreakpoints) {
      if (b instanceof NativeBreakpoint) {
        gdbBreakPointToWake.add((NativeBreakpoint)b);
      } else if (b instanceof JavaBreakpoint ) {
        jdbBreakPointToWake.add((JavaBreakpoint)b);
      }
    }
    for(JavaBreakpoint b: jdbBreakPointToWake) {
     b.enable(dbg); 
    }    
    for(NativeBreakpoint b: gdbBreakPointToWake) {
      b.enable(dbg); 
     }
    frozenBreakpoints.clear(); 
  }

  /**
   * A base class for the Blink break point.
   */
  static abstract class BlinkBreakPoint {

    /** enable the break point. */
    abstract boolean enable(Blink dbg);

    /** disable the break point. */
    abstract boolean disable(Blink dbg);

    /** query the break point state. */
    abstract BreakPointState getState(); 
  }

  /**
   * A base class for the native break point. 
   */
  static abstract class NativeBreakpoint extends BlinkBreakPoint {
    static final int DEFERRED_BREAKPOINT_ID = -1;
    
    /** The break point state. */
    BreakPointState mdbState = BreakPointState.DISABLED;

    /** The native break point identifier. */
    private int nativeBreakpointID;

    /** Constructor. */
    protected NativeBreakpoint(int nbpID) {
      nativeBreakpointID = nbpID;
    }

    /** Getter/setter method for the nativeBreakPointID. */
    public int getNativeBreakPointID() {
      assert nativeBreakpointID != DEFERRED_BREAKPOINT_ID;
      return nativeBreakpointID;
    }

    public void setNativeBreakPointID(int nativeBreakPointID) {
      assert this.nativeBreakpointID == DEFERRED_BREAKPOINT_ID;
      this.nativeBreakpointID = nativeBreakPointID;
    }

    /** Getter method for break point state. */
    public BreakPointState getState() {
      return mdbState;
    }

    /**
     * Enable the break point.
     * @param dbg The debugger.
     */
    boolean enable(Blink dbg) {
      switch(mdbState) {
      case ENABLED:
        return true;
      case DISABLED:        
        if(!dbg.ensureGDBContext()) {
          return false;
        }
        try {
          // try to set the break point
          dbg.ndb.enableBreakpoint(nativeBreakpointID);
          mdbState = BreakPointState.ENABLED;
        } catch (IOException e) {
          dbg.err("could not set the break point.");
          return false;
        }
        return true;
      default:
        assert false : "not reachable.";
        return false;
      }
    }

    /**
     * Disable this break point in the gdb.
     * 
     * @param dbg The Blink debugger.
     */
    boolean disable(Blink dbg) {      
      switch(mdbState) {
      case DISABLED:
        return true;
      case ENABLED:
        if (!dbg.ensureGDBContext()) {
          dbg.err("counld not disable break point: " + this + "\n");
         return false; 
        }
        try {
          dbg.ndb.disableBreakpoint(nativeBreakpointID);
        } catch (IOException e) {
          dbg.err("cound not resent the break point");
          return false;
        }
        mdbState = BreakPointState.DISABLED;
        return true;
      }
      return false;
    }
  }

  /**
   * A Source level GDB break point.
   */
  static class NativeSourceLineBreakPoint extends NativeBreakpoint {

    /** The source file name.*/
    final String sourceFileName;

    /** The line number in the source file. */
    final int sourceLineNumber;

    /** Constructors. */
    NativeSourceLineBreakPoint(String sname, int lineno) {
      this(DEFERRED_BREAKPOINT_ID, sname, lineno);
    }

    NativeSourceLineBreakPoint(int nbpID, String sname, int lineno) {
      super(nbpID);
      sourceFileName = sname;
      sourceLineNumber = lineno;
    }

    /** Getter method for sourceFileName. */
    String getSourceFileName() {
      return sourceFileName;
    }

    /** Getter method for sourceLineNumber.*/
    int getSourceLineNumber() {
      return sourceLineNumber;
    }

    /**
     * Check the equality.
     * @param o The compared object.
     */
    public boolean equals(Object o) {
      if (o instanceof NativeSourceLineBreakPoint == false)
        return false;
      NativeSourceLineBreakPoint nbp = (NativeSourceLineBreakPoint) o;
      return (this == nbp)
          || (this.sourceFileName.equals(nbp.sourceFileName) && this.sourceLineNumber == nbp.sourceLineNumber);
    }

    /**
     * @return The string representation.
     */
    public String toString() {
      StringBuilder sb = new StringBuilder();
      sb.append("native").append(" ");
      sb.append(sourceFileName).append(":").append(sourceLineNumber);
      return sb.toString();
    }
  }

  static class NativeSymbolBreakpoint extends NativeBreakpoint {
    private final String symbol;

    NativeSymbolBreakpoint(String symbo) {
      this(DEFERRED_BREAKPOINT_ID, symbo);
    }
    NativeSymbolBreakpoint(int nbpID, String symbo) {
      super(nbpID);
      this.symbol = symbo;
    }

    public final String getSymbo() {
      return symbol;
    }
  }
  
  /**
   * A base class for the JDB break point.
   */
  static abstract class JavaBreakpoint extends BlinkBreakPoint {

    /** The break point state. */
    BreakPointState mdbState = BreakPointState.DISABLED;


    /** Getter method for the state. */
    BreakPointState getState() {
      return mdbState;
    }
  }

  /**
   * A source level JDB break point (e.g. Main:12 ).
   */
  static class JavaSourceLineBreakPoint extends JavaBreakpoint {

    /** The source file name. */
    private final String className;

    /** The line number in the source file. */
    private final int lineNumber;

    /**
     * @param sname The source file name.
     * @param lineno The line number.
     */
    JavaSourceLineBreakPoint(String sname, int lineno) {
      className = sname;
      lineNumber = lineno;
    }

    /** Getter method for class name. */
    String getClassName() {return className;}

    /** Getter method for line number. */
    int getLineNumber() {return lineNumber;}

    /**
     * Check the equality.
     * 
     * @param o The compared object.
     */
    public boolean equals(Object o) {
      if (o instanceof NativeSourceLineBreakPoint == false)
        return false;
      JavaSourceLineBreakPoint nbp = (JavaSourceLineBreakPoint) o;
      return (this == nbp)
          || (this.className.equals(nbp.className) && this.lineNumber == nbp.lineNumber);
    }

    /**
     * @return The string representation.
     */
    public String toString() {
      StringBuilder sb = new StringBuilder();
      sb.append(" ").append("java").append(" ");
      sb.append(className).append(":").append(lineNumber);
      return sb.toString();
    }

    /**
     * Request the jdb to set my break point
     * 
     * @param dbg The debugger.
     */
    boolean enable(Blink dbg) {
      switch(mdbState) {
      case DISABLED:
        if(!dbg.ensureJDBContext()) {
          return false;
        }
        try {
          // try to set the break point
          dbg.jdb.setBreakPoint(className, lineNumber);
        } catch (IOException e) {
          dbg.err("could not set the break point.");
          return false;
        }
        mdbState = BreakPointState.ENABLED;
        return true;
      case ENABLED:
        return true;
      }
      return false;
    }

    /**
     * Delete this break.
     * 
     * @param dbg The Blink debugger.
     */
    boolean disable(Blink dbg) {
      switch(mdbState) {
      case DISABLED:
        return true;
      case ENABLED:
        try {
          if (!dbg.ensureJDBContext()) {
            return false;
          }
          dbg.jdb.clearBreakPoint(className, lineNumber);
        } catch (IOException e) {
          dbg.err("cound not resent the break point");
          return false;
        }
        mdbState = BreakPointState.DISABLED;
        return true;
      }
      return false;
    }
  }

  /**
   * A JDB method name break point. (e.g. Foo.bar() ).
   */
  static class JavaEntryBreakpoint extends JavaBreakpoint {
    private final String cname;
    private final String mname;

    /**
     * @param classAndmethod The class name and method.
     */
    public JavaEntryBreakpoint(final String cname, final String mname) {
      super();
      this.cname = cname;
      this.mname = mname;
    }

    /** Getters. */
    public String getCname() {return cname;}
    public String getMname() {return mname;}

    public boolean matches(String cname, String mname) {
      return this.cname.equals(cname) && this.mname.equals(mname);
    }
    /**
     * Enable the break point.
     * @param dbg The debugger.
     */
    boolean enable(Blink dbg) {
      switch(mdbState) {
      case DISABLED:
        if(!dbg.ensureJDBContext()) {
          return false;
        }
        try {
          // try to set the break point
          dbg.jdb.setBreakPoint(cname + "." + mname);
        } catch (IOException e) {
          dbg.err("could not set the break point.");
          return false;
        }
        mdbState = BreakPointState.ENABLED;
        return true;
      case ENABLED:
        return true;
      }
      return false;
    }

    /**
     * Disable the break point.
     * @param dbg The Blink debugger.
     */
    boolean disable(Blink dbg) {
      switch(mdbState) {
      case DISABLED:
        return true;
      case ENABLED:
        try {
          if (!dbg.ensureJDBContext()) {
            return false;
          }
          dbg.jdb.clearBreakPoint(cname + "." + "mname");
        } catch (IOException e) {
          dbg.err("cound not resent the break point");
          return false;
        }
        mdbState = BreakPointState.DISABLED;
        return true;
      }
      return false;
    }
  }
}
