package fig.basic;

import java.io.*;
import java.lang.reflect.Method;
import java.util.*;

/**
 * The logging output has a tree structure, where each node is a
 * line of output, and the depth of a node is its indent level.
 * A run is the sequence of children of some node.
 * A subset of the lines in the run will get printed.
 */
public class LogInfo {
  public static void track(String format, Object... args) {
    track(String.format(format, args), false);
  }
  public static void track(Object o) {
    track(o, false);
  }
  
  public synchronized static void track_methods(Object o, String[] methodNames) throws Exception {
	  for (String methodName: methodNames) {
		  track_method(o, methodName);
	  }
  }
  /**
   * Invokes a method and wraps it's invocation in a track. The method must be public and 
   * not take any arguments.
   * @param o	Object to call method on
   * @param methodName name of method to invoke
   * @throws Exception
   */
  public synchronized static void track_method(Object o, String methodName, Object...args) throws Exception {
	  Class<? extends Object> c = o.getClass();
      Method[] methods = c.getDeclaredMethods();
      Method targetMethod = null;
      for (Method m: methods) {
      	if (m.getName().equals(methodName)) {
      		targetMethod = m; break;
      	}
      }
      if (targetMethod == null) { 
      	String msg = String.format("Couldn't find method %d in class %s",methodName,c.getName());
      	throw new IllegalArgumentException(msg);        
      }        
		LogInfo.track(methodName);
		targetMethod.invoke(o, args);
		LogInfo.end_track();
  }
  public synchronized static void track_method(Object o, String methodName) throws Exception {
	  track_method(o, methodName, new Object[0]);
  }
  
  
  public synchronized static void track(Object o, boolean printAllLines) {
    track(o, printAllLines, false);
  }
  public synchronized static void track(Object o,
      boolean printAllChildLines, boolean printIfParentPrinted) {
    if(indWithin()) {
      if(printIfParentPrinted && parentPrinted()) thisRun().forcePrint();
      if(thisRun().shouldPrint()) {
        print(o);
        buf.append(" {\n"); // Open the block.

        childRun().init();
        childRun().printAllLines = printAllChildLines;
      }
      else {
        stoppedIndLevel = indLevel;
        maxIndLevel = -maxIndLevel; // Prevent children from outputting.
      }
    }

    indLevel++;
  }

  // Convenient way to end and return a value
  public static <T> T end_track(T x) { end_track(); return x; }

  public synchronized static void end_track() {
    indLevel--;

    if(stoppedIndLevel == indLevel) {
      stoppedIndLevel = -1;
      maxIndLevel = -maxIndLevel; // Restore indent level.
    }

    if(indWithin()) {
      if(thisRun().newLine()) { // Note that we pay for the line only at the end
        // Finish up child level.
        indLevel++;
        int n = thisRun().numOmitted();
        if(n > 0)
          print("... " + n + " lines omitted ...\n");
        indLevel--;
        childRun().finish();

        if(buf.length() > 0) // Nothing was printed, because buf hasn't been emptied.
          buf.delete(0, buf.length()); // Just pretend we didn't open the block.
        else // Something indented was printed.
          print("}"); // Close the block.

        // Print time
        StopWatch ct = childRun().watch;
        if(ct.ms > 1000) {
          rawPrint(" [" + ct);
          if(indLevel > 0) {
            StopWatch tt = thisRun().watch;
            tt.stop();
            rawPrint(", cum. " + tt);
          }
          rawPrint("]");
        }
        rawPrint("\n");
      }
    }
  }

  // Normal printing
  public static void logs(String format, Object... args) {
    logs(String.format(format, args));
  }
  public synchronized static void logs(Object o) {
    if(forcePrint || (indWithin() && thisRun().newLine()))
      printLines(o);
  }
  
  // Always print
  public synchronized static void logsForce(String format, Object...args) {
    printLines(String.format(format, args));
  }
  public synchronized static void logsForce(Object o) {
    thisRun().newLine();
    printLines(o);
  }
  
  // Print if parent printed
  public static void logss(String format, Object... args) {
    logss(String.format(format, args));
  }
  public synchronized static void logss(Object o) {
    if(parentPrinted()) thisRun().forcePrint();
    logs(o);
  }
  private static boolean parentPrinted() {
    // Parent must have been a track, so its run information has not been
    // updated yet.  Therefore, shouldPrint() is valid.
    return indLevel == 0 ||
      (indLevel <= maxIndLevel && parentIndWithin() && parentRun().shouldPrint());
  }

  // Log different types of information
  public static void dbgs(String format, Object... args) {
    dbg(String.format(format, args));
  }
  public static void dbg(Object o) {
    logss("DBG: " + o);
  }
  public static void rants(String format, Object... args) {
    rant(String.format(format, args));
  }
  public static void rant(Object o) {
    logss("RANT: " + o);
  }
  public static void errors(String format, Object... args) {
    error(String.format(format, args));
  }
  public static void error(Object o) {
    if(numErrors < maxPrintErrors)
      print("ERROR: " + o + "\n");
    numErrors++;
  }
  public static void warnings(String format, Object... args) {
    warning(String.format(format, args));
  }
  public static void warning(Object o) {
    print("WARNING: " + o + "\n");
    numWarnings++;
  }

  public static void fails(String format, Object... args) {
    fail(String.format(format, args));
  }
  public static void fail(Object o) {
    throw Exceptions.bad(o);
  }

  // Print random things
  public static void printProgStatus() {
    logs("PROG_STATUS: time = " + watch.stop() + ", memory = " + SysInfoUtils.getUsedMemoryStr());
  }
  public static <T> void printList(String s, String lines) {
    printList(s, Arrays.asList(lines.split("\n")));
  }
  public static <T> void printList(String s, List<T> items) {
    track(s, true);
    for(T x : items) logs(x);
    end_track();
  }

  //////////////////////////////////////////////////////////// 

  // Options
  @Option(gloss="Maximum indent level.")
    public static int maxIndLevel = 10;
  @Option(gloss="Maximum number of milliseconds between consecutive lines of output.")
    public static int msPerLine = 1000;
  @Option(gloss="File to write log.")
    public static String file = "";
  @Option(gloss="Whether to output to the console.", name="stdout")
    public static boolean writeToStdout = true;
  @Option(gloss="Dummy placeholder for a comment")
    static public String note = "";
  @Option(gloss="Force printing from logs*")
  	static public boolean forcePrint;
  @Option(gloss="Maximum number of errors (via error()) to print")
  	static public int maxPrintErrors = 10000;

  static { updateStdStreams(); }
  public static void updateStdStreams() {
    try {
      stdin  = CharEncUtils.getReader(System.in);
      stdout = CharEncUtils.getWriter(System.out);
      stderr = CharEncUtils.getWriter(System.err);
    } catch(IOException e) {
      throw new RuntimeException(e);
    }
  }

  public static void init() {
    // Write to file, stdout?
    if(!file.equals("")) {
      fout = IOUtils.openOutHard(file);
    }
    if(writeToStdout) out = stdout;
  }

  private static LogRun parentRun() { return runs.get(indLevel-1); }
  private static LogRun thisRun()   { return runs.get(indLevel); }
  private static LogRun childRun()  { return runs.get(indLevel+1); }

  // If we were to print a new line, should we print?
  private static boolean indWithin()       { return indLevel   <= maxIndLevel; }
  private static boolean parentIndWithin() { return indLevel-1 <= maxIndLevel; }

  // Internal: don't use these functions directly

  private static void rawPrint(Object o) {
    if(out != null) { out.print(o); out.flush(); }
    if(fout != null) { fout.print(o); fout.flush(); }
  }

  // Print with indent; flush the buffer as necessary
  private static void print(Object o) {
    rawPrint(buf);
    buf.delete(0, buf.length());
    for(int i = 0; i < indLevel; i++) rawPrint("  ");
    rawPrint(o);
  }
  // If there are new lines, put indents before them
  private static void printLines(Object o) {
    if(o == null) o = "null";
    String s = StrUtils.toString(o);
    if(s.indexOf('\n') == -1)
      print(s+"\n");
    else
      for(String t : StrUtils.split(s, "\n")) print(t+"\n");
  }

  public static StopWatch getWatch() { return watch; }
  public static int getNumErrors() { return numErrors; }
  public static int getNumWarnings() { return numWarnings; }

  public static BufferedReader stdin;
  public static PrintWriter stdout, stderr;

  // Private state.
  static PrintWriter out, fout;
  static int indLevel;           // Current indent level.
  static int stoppedIndLevel;    // At what level did we stop printing
  static StringBuilder buf;      // The buffer to be flushed out the next time _logs is called.
  static ArrayList<LogRun> runs; // Indent level -> state
  static StopWatch watch;        // StopWatch that starts at the beginning of the program
  static int numErrors;          // Number of errors made
  static int numWarnings;        // Number of warnings

  // Default setup
  static {
    buf = new StringBuilder();
    indLevel = 0;
    stoppedIndLevel = -1;

    runs = new ArrayList<LogRun>(128);
    for(int i = 0; i < 128; i++)
      runs.add(new LogRun());
    watch = new StopWatch();
    watch.start();

    out = stdout;
  }
}

/**
 * A run is a sequence of lines of text, some of which are printed.
 * Stores the state associated with a run.
 */
class LogRun {
  public LogRun() {
    watch = new StopWatch();
    init();
  }
  void init() {
    numLines = 0;
    numLinesPrinted = 0;
    nextLineToPrint = 0;
    printAllLines = false;
    watch.start();
  }
  void finish() {
    // Make it clear that this run is not printed.
    // Otherwise, logss might think its
    // parent was printed when it really wasn't.
    nextLineToPrint = -1;
    watch.stop();
  }

  void forcePrint() { forcePrint = true; }
  boolean shouldPrint() { return forcePrint || nextLineToPrint == numLines; }
  int numOmitted() { return numLines - numLinesPrinted; }

  /**
   * Decide whether to print the next line.  If yes,
   * then you must print it.
   * @return Whether the next line should be printed.
   */
  boolean newLine() {
    boolean p = shouldPrint();
    numLines++;
    if(!p) return false; // Assume forcePrint == false
    
    // Ok, we're going to print this line.
    numLinesPrinted++;

    // Decide next line to print.
    int msPerLine = LogInfo.msPerLine;
    if(numLines <= 2  || // Print first few lines anyway.
       msPerLine == 0 || // Print everything.
       printAllLines  || // Print every line in this run (by fiat).
       forcePrint)       // Force-printed things shouldn't affect timing.
      nextLineToPrint++;
    else {
      watch.stop();
      if(watch.ms == 0) { // No time has elapsed.  
        // This usually applies in the beginning of a run when we have
        // no idea how long things are going to take
        nextLineToPrint *= 2; // Exponentially increase time between lines.
      }
      else // Try to maintain the number of lines per second.
        nextLineToPrint += (int)Math.max((double)numLines * msPerLine / watch.ms, 1);
    }
    forcePrint = false;

    return true;
  }

  int numLines;           // Number of lines that we've gone through so far in this run.
  int numLinesPrinted;    // Number of lines actually printed.
  int nextLineToPrint;    // Next line to be printed (lines are 0-based).
  StopWatch watch;        // Keeps track of time spent on this run.
  boolean printAllLines;  // Whether or not to force the printing of each line.
  boolean forcePrint;     // Whether to print out the next item (is reset afterwards).
}
