package edu.berkeley.nlp.util;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.PrintWriter;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

/**
 * 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.
 * 
 * WARNING: not thread safe.
 */
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;
						rawPrint(", cum. "
								+ new StopWatch(tt.getCurrTimeLong()));
					}
					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
	@Deprecated
	public static void dbg(String format, Object... args) {
		dbg(String.format(format, args));
	}

	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);
	}

	@Deprecated
	public static void error(String format, Object... args) {
		error(String.format(format, args));
	}

	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++;
	}

	@Deprecated
	public static void warning(String format, Object... args) {
		warning(String.format(format, args));
	}

	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.reset();
		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 {
			long elapsed_ms = watch.getCurrTimeLong();
			if (elapsed_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
						/ elapsed_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).
}
