package xtc.lang.blink;

import java.util.LinkedList;
import java.util.List;
import java.util.ListIterator;
import java.io.IOException;

import xtc.lang.blink.CallStack.NativeCallFrame.NativeFrameType;

/**
 * The Java/C mixed call stack representation.
 * 
 * @author Byeongcheol Lee
 */
public final class CallStack {

  /** The language of the stack frame. */
  static enum FrameLanguage {JAVA, C, JEANNIE};

  /**
   * Extract a mixed language call stack.
   * 
   * @param dbg The Blink debugger.
   * @param remapper The symbol remapper.
   * @return The call stack.
   */
  public static CallStack extractCallStack(Blink dbg, SymbolMapper remapper)
      throws IOException {
    FrameLanguage lang;
    List<JavaCallFrame> jFrames;
    List<NativeCallFrame> nFrames;
    switch (dbg.getDebugControlStatus()) {
    case JDB:
      lang = FrameLanguage.JAVA;
      // get java frames
      jFrames = dbg.jdb.getFrames();
      // get C frames
      dbg.j2c();
      nFrames = dbg.ndb.getFrames(); 
      dbg.jret();
      //remove naive frames due to the j2c transition. 
      nFrames.remove(0);
      nFrames.remove(0);
      break;
    case GDB:
      lang = FrameLanguage.C;
      // get C frames
      nFrames = dbg.ndb.getFrames();
      // get Java frames.
      dbg.c2j();
      jFrames = dbg.jdb.getFrames();
      dbg.jret();
      break;
    default:
      assert false : "should not be reachable.";
      return null;
    }
    
    if (dbg.options.getVerboseLevel() >=1) {
      dbg.out("building mixed frames\n");
      dbg.out(toString(jFrames, nFrames));
    }

    // build mixed frames.
    List<MicroCallFrame> interleaved = interleaveFrames(lang, jFrames, nFrames);
    List<MicroCallFrame> simplified = simplifyFrames(interleaved);
    List<ICallFrame> mixed = processJeannieFrames(simplified, remapper);

    CallStack callStack = new CallStack(mixed);
    return callStack;
  }

  /**
   * Interleave call frames beginning with the current language.
   *
   * @param lang The top frame language.
   * @param jframes The Java frames.
   * @param nframes The native frames.
   * @return The interleaved frames.
   */
  private static List<MicroCallFrame> interleaveFrames(FrameLanguage lang,
      List<JavaCallFrame> jframes, List<NativeCallFrame> nframes) {
    LinkedList<MicroCallFrame> interleaved = new LinkedList<MicroCallFrame>();
    ListIterator<JavaCallFrame> jframeIt = jframes.listIterator();
    ListIterator<NativeCallFrame> nframeIt = nframes.listIterator();
    NativeCallFrame nframe = nframeIt.hasNext() ? nframeIt.next() : null;
    JavaCallFrame jframe = jframeIt.hasNext() ? jframeIt.next() : null;
    while(nframe != null || jframe != null) {
      switch(lang) {
      case C:
        if (nframe != null) {
          interleaved.addLast(nframe);
          if (nframe.getType() == NativeFrameType.J2C_PROXY) {
            lang = FrameLanguage.JAVA;
            assert jframe != null && jframe.isJavaNativeMethod();
          }
          nframe = nframeIt.hasNext() ? nframeIt.next() : null;
        } else {
          lang = FrameLanguage.JAVA;
        }
        break;
      case JAVA:
        if (jframe != null) {
          interleaved.addLast(jframe);
          if (jframeIt.hasNext()) {
            jframe = jframeIt.next();
            if (jframe.isJavaNativeMethod()) {
              lang = FrameLanguage.C;
              assert nframe != null;
              assert nframe.getType() == NativeFrameType.C2J_PROXY;
            }
          } else {
            jframe = null;
          }
        } else {
          lang = FrameLanguage.C;
        }
        break;
      }
    }
    return interleaved;
  }

  /**
   * Simplify the input call frames by eliminating agent frames and redundant
   * frames.
   * 
   * @param interleaved The interleaved frames.
   * @return The simplified frames.
   */
  private static List<MicroCallFrame> simplifyFrames(
      List<MicroCallFrame> interleaved) {
    LinkedList<MicroCallFrame> simplified = new LinkedList<MicroCallFrame>();
    for(final MicroCallFrame frame: interleaved) {
      if (frame instanceof JavaCallFrame) {
        JavaCallFrame jframe = (JavaCallFrame)frame;
        if (!jframe.isJavaNativeMethod()) {
          simplified.add(jframe);
        }
      } else if (frame instanceof NativeCallFrame) {
        NativeCallFrame nframe = (NativeCallFrame)frame;
        NativeFrameType type = nframe.getType();
        switch(type) {
        case NORMAL:
          simplified.add(nframe);
          break;
        case J2C_PROXY:
        case C2J_PROXY:
          break;
        default:
          assert false : "not reachable";
          break;
        }
      } else {
        assert false : "not reachable";
      }
    }
    return simplified;
  }

  /**
   * Condense a sequence Java and native frames into a single Jeannie frame.
   * Condense as much as possible, and return mixed Java, native and Jeannie
   * frames.
   * 
   * @param frames The interleaved frame.
   * @param remapper The symbol remapper.
   * @return The mixed frames.
   */
  private static List<ICallFrame> processJeannieFrames(
      List<MicroCallFrame> frames, SymbolMapper remapper) {
    List<ICallFrame> mixedFrames = new LinkedList<ICallFrame>();
    LinkedList<MicroCallFrame> framesForJeannie = null;
    String jeannieMethodName = null;
    for(final MicroCallFrame frame :frames) {
      String sname = frame.getSymbolName();
      String srcName = frame.getSourceFile();
      SymbolMapper.MethodRemapEntry e = remapper.lookupMethodRemap(sname, srcName);
      boolean isJeannieMicroFrame = (e != null);
      if (isJeannieMicroFrame) {
        if (framesForJeannie == null) {
          // The start of the Jeannie frame.
          jeannieMethodName = e.getSourceLanguageName(); 
          framesForJeannie = new LinkedList<MicroCallFrame>();
        }
        // The middle or the end of Jeannie frame.
        framesForJeannie.add(frame);
      } else {
         //Flush any pending Jeannie frame.        
        if (framesForJeannie != null) {
          mixedFrames.add(new JeannieCallFrame(jeannieMethodName, framesForJeannie));
          framesForJeannie = null;
        }
        // add a normal Java/C frame.
        mixedFrames.add(frame);
      }
    }
    // Flush any pending Jeannie frame.
    if (framesForJeannie != null) {
      mixedFrames.add(new JeannieCallFrame(jeannieMethodName, framesForJeannie));
      framesForJeannie = null;
    }
    return mixedFrames;
  }

  /**
   * Create a user readable representation of java and native frames for
   * debugging purpose.
   * 
   * @param jframes The Java frames.
   * @param nframes The native frames.
   * @return The user readable string representation.
   */
  private static String toString(List<JavaCallFrame> jframes, 
      List<NativeCallFrame> nframes) {
    StringBuffer buf = new StringBuffer();
    buf.append("Dumping java and native frames\n");
    buf.append("Java frames: \n");
    for(JavaCallFrame f : jframes) {
      buf.append(f.toString()).append('\n');
    }
    buf.append("Native frames: \n");
    for(NativeCallFrame f : nframes) {
      buf.append(f.toString()).append('\n');
    }
    return buf.toString();
  }

  /** a list of Java, C, and Jeannie call frames. */
  private final List<ICallFrame> mixedFrames;

  /**
   * The constructor.
   *
   * @param mixedFrames The mixed Java/C frame.
   * @param topFrameLanguage The language of the top frame.
   * @param javaFrames The list of Java call frames.
   * @param nativeFrames The list of Native call frames.
   */
  private CallStack(List<ICallFrame> mixedFrames) {
    this.mixedFrames= mixedFrames;
  }

  /**
   * Get mixed stack frame by specifying its id.
   *  
   * @param id the frame id.
   */
  public ICallFrame getCallFrame(int id) {
    assert id >= 0 && id < size();
    return mixedFrames.get(id);
  }

  /**
   * Try to get Java frame at a frame id. If the frame at the id is Java frame,
   * just return this frame. If the frame is a Jeannie macro frame, try obtaining
   * a Java frame within the Jeannie macro frame. Otherwise, just return null.
   *
   * @param id The frame identifier.
   * @return The Java frame or null. 
   */
  public NativeCallFrame getNativeCallFrame(int id) {
    ICallFrame f = getCallFrame(id);
    if (f instanceof NativeCallFrame) {
      return (NativeCallFrame)f;
    } else if (f instanceof JeannieCallFrame) {
      JeannieCallFrame jeannieFrame = (JeannieCallFrame)f;
      return jeannieFrame.getTopNativeFrame();
    } else {
      assert f instanceof JavaCallFrame;
      return null;
    }
  }

  /**
   * Try to get Native frame at a frame id. If the frame at the id is native
   * frame, just return this frame. If the frame is a Jeannie macro frame, try
   * obtaining a native frame within the Jeannie macro frame. Otherwise, just
   * return null.
   * 
   * @param id The frame identifier.
   * @return The native frame or null.
   */
  public JavaCallFrame getJavaCallFrame(int id) {
    ICallFrame frame = getCallFrame(id);
    if (frame instanceof JavaCallFrame) {
      return (JavaCallFrame)frame;
    } else if (frame instanceof JeannieCallFrame) {
      JeannieCallFrame jeannieFrame = (JeannieCallFrame)frame;
      return jeannieFrame.getTopJavaFrame();
    } else {
      assert frame instanceof NativeCallFrame;
      return null;
    }
  }

  /**
   * Get the number of the mixed frames.
   * 
   * @return The size of the mixed frame. 
   */
  public int size() {
   return mixedFrames.size(); 
  }

  /**
   * Get the readable representation of this mixed frame.
   * 
   * @return The string representation.
   */
  public String toString() {
    StringBuffer sb = new StringBuffer();
    sb.append("Mixed frames\n");
    int i = 0;
    for(final ICallFrame f : mixedFrames) {
      sb.append("  [" + (i++) + "]" + f + "\n");
    }
    return sb.toString();
  }

  /**
   * The unified call frame interface for mixed langauage.
   */
  static interface ICallFrame {

    /** Getter for the line number. */
    public int getLineNumber();

    /** Getter for the source file. */
    public String getSourceFile();

    /** Getter for the top micro frame. */
    public MicroCallFrame getTopMicroFrame();

    /** Get the langauge. */
    public FrameLanguage getLanguage(); 
  }

  /**
   * A base class for the mixed language frame. 
   */
  static abstract class MicroCallFrame implements ICallFrame {

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

    /** The line number.*/
    private final int lineNumber;

    /** The constructor. */
    protected MicroCallFrame(final String sourceFile, final int lineNumber) {
      this.sourceFile = sourceFile;
      this.lineNumber = lineNumber;
    }

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

    /** Getter method for the source file. */
    public final String getSourceFile() {
      return sourceFile;
    }

    /** Getter method for the symbol name. */
    public abstract String getSymbolName();
  }

  /**
   * A Java call frame representation. 
   */
  static class JavaCallFrame extends MicroCallFrame {

    /** The jdb indentifier. */
    private final int frameID;

    /** The class name. */
    private final String className;

    /** The method name. */
    private final String methodName;

    /** Wheather or not Java native method. */
    private final boolean isJavaNativeMethod;

    /**
     * The constructor. 
     * 
     * @param jdbIdentifier The jdb identifier.
     * @param sourceFile The source file name.
     * @param lineNumber The line number.
     * @param isJavaNativeMethod The Java native method flag.
     * @param className The class name.
     * @param methodName The method name.
     */
    protected JavaCallFrame(int jdbIdentifier, String sourceFile,
        int lineNumber, boolean isJavaNativeMethod, String className,
        String methodName) {
      super(sourceFile, lineNumber);
      /** The identifier that the gdb can recognize.*/
      this.frameID = jdbIdentifier;
      this.className = className;
      this.methodName = methodName;
      this.isJavaNativeMethod = isJavaNativeMethod;
    }

    /** Getters. */
    public int getJdbIdentifier() {
      return frameID;
    }
    public final String getClassName() {
      return className;
    }
    public final String getMethodName() {
      return methodName;
    }
    public FrameLanguage getLanguage() {
      return FrameLanguage.JAVA;
    }
    public String getSymbolName() {
      return className+ "."+methodName;
    }
    public MicroCallFrame getTopMicroFrame() {
      return this;
    }
    public final boolean isJavaNativeMethod() {
      return isJavaNativeMethod;
    }

    /**
     * Get readable representation.
     * 
     * @return The representation.
     */
    public String toString() {
      StringBuffer sb = new StringBuffer();
      sb.append(className);
      sb.append('.');
      sb.append(methodName);
      if (getSourceFile() != null && getLineNumber() >= 1) {
        sb.append("  (" + getSourceFile() + ":" + getLineNumber() + ")");
      } else {
        sb.append("  (native method)");
      }
      sb.append(" Java");
      return sb.toString(); 
    }
  }

  /**
   * The Native call frame representation. 
   */
  static class NativeCallFrame extends MicroCallFrame {

    public enum NativeFrameType {NORMAL, J2C_PROXY, C2J_PROXY};

    /** The native debugger specific frame identifier.*/
    private final int frameID;

    /** The function name.*/
    private final String functionName;

    /** The frame type. */
    private final NativeFrameType type;
    
    /** The constructor. */
    protected NativeCallFrame(int frameID, String functionName,
        String sourceFile, int lineNumber, NativeFrameType type) {
      super(sourceFile, lineNumber);
      this.frameID = frameID;
      this.functionName = functionName;
      this.type = type;
    }

    /** Getter method for the gdb identifier. */
    public int getFrameID() {
      return frameID;
    }

    /** Getter method for gdb function symbol name.*/
    public final String getFunctionName() {
      return functionName;
    }

    /** Getter method for the language. */
    public FrameLanguage getLanguage() {
      return FrameLanguage.C;
    }

    /** Getter method for top micro frame. */
    public MicroCallFrame getTopMicroFrame() {
      return this;
    }

    /** Geter method for tye symbol name. */
    public String getSymbolName() {
      return functionName;
    }

    public final NativeFrameType getType() {return type;}
    
    /**
     * @return The string representation.
     */
    public String toString() {
      StringBuffer sb = new StringBuffer();
      sb.append(functionName);
      if (getSourceFile() != null && getLineNumber() >= 1) {
        sb.append("  (" + getSourceFile() + ":" + getLineNumber() + ")");
      }
      sb.append(" C");
      return sb.toString(); 
    }
  }

  /**
   * The Jeannie call frame representation. A list of nonempty micro Java/C
   * frames makes a single macro Jeannie call frame. The first Java/C frame in
   * this list is close to the top of stack frame, and this frame is
   * representive of the current program counter. Therefore, a number of the
   * ICallFrame interface method calls are delegated to the same method of the
   * first element.
   */
  static class JeannieCallFrame implements ICallFrame {

    /** The user visible name of this Jeannie frame. */
    final String name;

    /** The list of Java/Native frames. */
    final MicroCallFrame[] frames;

    /**
     * @param name The name of this Jeannie frame.
     * @param frameList The list of Java/Native frames.
     */
    private JeannieCallFrame(String name, List<MicroCallFrame> frameList) {
      this.name = name;
      this.frames = frameList.toArray(new MicroCallFrame[0]); 
      assert frames != null && frames.length > 0;
    }

    /** Getter method for the line number. */
    public int getLineNumber() {
      return frames[0].getLineNumber();
    }

    /** Getter method for the source file. */
    public String getSourceFile() {
      return frames[0].getSourceFile();
    }

    /** Getter method for the language transition flag. */
    public boolean isTransition() {
      return false;
    }

    /** Getter method for the language. */
    public FrameLanguage getLanguage() {
      return FrameLanguage.JEANNIE;
    }

    /** Getter method for the top micro frame. */
    public MicroCallFrame getTopMicroFrame() {
      return frames[0];
    }

    /** Getter method for the micro frame. */
    public MicroCallFrame getMicroFrame(int i) {
      assert i >=0 && i < frames.length;
      return frames[i];
    }

    /** Gettter method for the number of micro frames. */
    public int getNumberofMicroFrames() {
      return frames.length;
    }

    /** Getter method for the top Java frame. */
    public JavaCallFrame getTopJavaFrame() {
      for(int i = 0;i <frames.length;i++) {
        MicroCallFrame f = frames[i];
        if (f instanceof JavaCallFrame) {
          return (JavaCallFrame)f;
        }
      }
      return null;
    }

    /** Getter method for the top native frame.*/
    public NativeCallFrame getTopNativeFrame() {
      for(int i = 0;i <frames.length;i++) {
        MicroCallFrame f = frames[i];
        if (f instanceof NativeCallFrame) {
          return (NativeCallFrame)f;
        }
      }
      return null;
    }

    /**
     * Obtain a string representation.
     * 
     * @return The string representation.
     */
    public String toString() {
      StringBuffer sb = new StringBuffer();
      sb.append(name);
      String sourceFile = getSourceFile();
      int lineNumber = getLineNumber();
      if (sourceFile != null && lineNumber >= 1) {
        sb.append("  (" + sourceFile + ":" + lineNumber + ")");
      }
      sb.append(" Jeannie");
      return sb.toString(); 
    }
  }

  /** 
   * A Local variable in the frame.
   */
  static class LocalVariable {

    /** The name of the local variable. */
    private final String name;

    /** The value of the local variable. */
    private final String value;

    /**
     * Constructor.
     * 
     * @param name The name.
     * @param value The value.
     */
    public LocalVariable(String name, String value) {
      this.name = name;
      this.value = value;
    }

    /** Getter method for the name. */
    public String getName() {return name;}

    /** Getter method for the value. */
    public String getValue() {return value;}
  }
}
