package freenet.support;

import static java.util.concurrent.TimeUnit.SECONDS;

import java.io.BufferedOutputStream;
import java.io.Closeable;
import java.io.DataInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.PrintStream;
import java.io.UnsupportedEncodingException;
import java.net.InetAddress;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.ArrayDeque;
import java.util.Calendar;
import java.util.Date;
import java.util.Deque;
import java.util.GregorianCalendar;
import java.util.Locale;
import java.util.StringTokenizer;
import java.util.TimeZone;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.zip.GZIPOutputStream;

import freenet.node.SemiOrderedShutdownHook;
import freenet.node.Version;
import freenet.support.io.FileUtil;

/**
 * Converted the old StandardLogger to Ian's loggerhook interface.
 * 
 * @author oskar
 */
public class FileLoggerHook extends LoggerHook implements Closeable {

	/** Verbosity types */
	public static final int DATE = 1,
		CLASS = 2,
		HASHCODE = 3,
		THREAD = 4,
		PRIORITY = 5,
		MESSAGE = 6,
		UNAME = 7;

	private volatile boolean closed = false;
	private boolean closedFinished = false;

	protected int INTERVAL = Calendar.MINUTE;
	protected int INTERVAL_MULTIPLIER = 5;
	
	private static final String ENCODING = "UTF-8";

        private static volatile boolean logMINOR;
	static {
		Logger.registerLogThresholdCallback(new LogThresholdCallback(){
			@Override
			public void shouldUpdate(){
				logMINOR = Logger.shouldLog(LogLevel.MINOR, this);
			}
		});
	}

	/** Name of the local host (called uname in Unix-like operating systems). */
	private static String uname;
	static {
		uname = "unknown";
	}

	static synchronized void getUName() {
		if(!uname.equals("unknown")) return;
		System.out.println("Getting uname for logging");
		try {
			InetAddress addr = InetAddress.getLocalHost();
			if (addr != null) {
				uname =
					new StringTokenizer(addr.getHostName(), ".").nextToken();
			}
		} catch (Exception e) {
			// Ignored.
		}
	}
	
	private DateFormat df;
	private int[] fmt;
	private String[] str;

	/** Stream to write data to (compressed if rotate is on) */
	protected OutputStream logStream;
	/** Other stream to write data to (may be null) */
	protected OutputStream altLogStream;

	protected final boolean logOverwrite;

	/* Base filename for rotating logs */
	protected String baseFilename = null;
	
	protected File latestFile;
	protected File previousFile;

	/* Whether to redirect stdout */
	protected boolean redirectStdOut = false;
	/* Whether to redirect stderr */
	protected boolean redirectStdErr = false;

	protected final int MAX_LIST_SIZE;
	protected long MAX_LIST_BYTES = 10 * (1 << 20);
	protected long LIST_WRITE_THRESHOLD;

	/**
	 * Something weird happens when the disk gets full, also we don't want to
	 * block So run the actual write on another thread
	 * 
	 * Unfortunately, we can't use ConcurrentBlockingQueue because we need to dump stuff when the queue gets
	 * too big.
	 * 
	 * FIXME PERFORMANCE: Using an ArrayBlockingQueue avoids some unnecessary memory allocations, but it 
	 * means we have to take two locks. 
	 * Seriously consider reverting 88268b99856919df0d42c2787d9ea3674a9f6f0d..e359b4005ef728a159fdee988c483de8ce8f3f6b
	 * to go back to one lock and a LinkedList.
	 */
	protected final ArrayBlockingQueue<byte[]> list;
	protected long listBytes = 0;

	long maxOldLogfilesDiskUsage;
	protected final Deque<OldLogFile> logFiles = new ArrayDeque<OldLogFile>();
	private long oldLogFilesDiskSpaceUsage = 0;

	private static class OldLogFile {
		public OldLogFile(File currentFilename, long startTime, long endTime, long length) {
			this.filename = currentFilename;
			this.start = startTime;
			this.end = endTime;
			this.size = length;
		}
		final File filename;
		final long start; // inclusive
		final long end; // exclusive
		final long size;
	}
	
	public void setMaxListBytes(long len) {
		synchronized(list) {
			MAX_LIST_BYTES = len;
			LIST_WRITE_THRESHOLD = MAX_LIST_BYTES / 4;
		}
	}

	public void setInterval(String intervalName) throws IntervalParseException {
		StringBuilder sb = new StringBuilder(intervalName.length());
		for(int i=0;i<intervalName.length();i++) {
			char c = intervalName.charAt(i);
			if(!Character.isDigit(c)) break;
			sb.append(c);
		}
		if(sb.length() > 0) {
			String prefix = sb.toString();
			intervalName = intervalName.substring(prefix.length());
			INTERVAL_MULTIPLIER = Integer.parseInt(prefix);
		} else {
			INTERVAL_MULTIPLIER = 1;
		}
		if (intervalName.endsWith("S")) {
			intervalName = intervalName.substring(0, intervalName.length()-1);
		}
		if (intervalName.equalsIgnoreCase("MINUTE"))
			INTERVAL = Calendar.MINUTE;
		else if (intervalName.equalsIgnoreCase("HOUR"))
			INTERVAL = Calendar.HOUR;
		else if (intervalName.equalsIgnoreCase("DAY"))
			INTERVAL = Calendar.DAY_OF_MONTH;
		else if (intervalName.equalsIgnoreCase("WEEK"))
			INTERVAL = Calendar.WEEK_OF_YEAR;
		else if (intervalName.equalsIgnoreCase("MONTH"))
			INTERVAL = Calendar.MONTH;
		else if (intervalName.equalsIgnoreCase("YEAR"))
			INTERVAL = Calendar.YEAR;
		else
			throw new IntervalParseException("invalid interval " + intervalName);
		System.out.println("Set interval to "+INTERVAL+" and multiplier to "+INTERVAL_MULTIPLIER);
	}

	public static class IntervalParseException extends Exception {

		private static final long serialVersionUID = 69847854744673572L;

		public IntervalParseException(String string) {
			super(string);
		}

	}
	
	/**
	 * The extra parameter int digit is to be used for creating a logfile name
	 * when a log exists already with the same date.
	 * @param c
	 * @param digit
	 *			log file name suffix. ignored if this is {@code < 0}
	 * @param compressed
	 * @return
	 */
	protected String getHourLogName(Calendar c, int digit, boolean compressed){
		StringBuilder buf = new StringBuilder(50);
		buf.append(baseFilename).append('-');
		buf.append(Version.buildNumber());
		buf.append('-');
		buf.append(c.get(Calendar.YEAR)).append('-');
		pad2digits(buf, c.get(Calendar.MONTH) + 1);
		buf.append('-');
		pad2digits(buf, c.get(Calendar.DAY_OF_MONTH));
		buf.append('-');
		pad2digits(buf, c.get(Calendar.HOUR_OF_DAY));
		if (INTERVAL == Calendar.MINUTE) {
			buf.append('-');
			pad2digits(buf, c.get(Calendar.MINUTE));
		}
		if (digit > 0) {
			buf.append("-");
			buf.append(digit);
		}
		buf.append(".log");
		if(compressed) buf.append(".gz");
		return buf.toString();
	}

	private StringBuilder pad2digits(StringBuilder buf, int x) {
		String s = Integer.toString(x);
		if (s.length() == 1) {
			buf.append('0');
		}
		buf.append(s);
		return buf;
	}
	
	// Unless we are writing flat out, everything will hit disk within this period.
	private long flushTime = 1000; // Default is 1 second. Will be set by setMaxBacklogNotBusy().

	class WriterThread extends Thread {
		WriterThread() {
			super("Log File Writer Thread");
		}

		@Override
		@SuppressWarnings("fallthrough")
		public void run() {
			File currentFilename = null;
			byte[] o = null;
			long thisTime;
			long lastTime = -1;
			long startTime;
			long nextHour = -1;
			GregorianCalendar gc = null;
			if (baseFilename != null) {
				latestFile = new File(baseFilename+"-latest.log");
				previousFile = new File(baseFilename+"-previous.log");
				gc = new GregorianCalendar();
				switch (INTERVAL) {
					case Calendar.YEAR :
						gc.set(Calendar.MONTH, 0);
					case Calendar.MONTH :
						gc.set(Calendar.DAY_OF_MONTH, 0);
					case Calendar.WEEK_OF_YEAR :
						if (INTERVAL == Calendar.WEEK_OF_YEAR)
							gc.set(Calendar.DAY_OF_WEEK, 0);
					case Calendar.DAY_OF_MONTH :
						gc.set(Calendar.HOUR, 0);
					case Calendar.HOUR :
						gc.set(Calendar.MINUTE, 0);
					case Calendar.MINUTE :
						gc.set(Calendar.SECOND, 0);
						gc.set(Calendar.MILLISECOND, 0);
				}
				if(INTERVAL_MULTIPLIER > 1) {
					int x = gc.get(INTERVAL);
					gc.set(INTERVAL, (x / INTERVAL_MULTIPLIER) * INTERVAL_MULTIPLIER);
				}
				findOldLogFiles((GregorianCalendar)gc.clone());
				currentFilename = new File(getHourLogName(gc, -1, true));
				synchronized(logFiles) {
					if((!logFiles.isEmpty()) && logFiles.getLast().filename.equals(currentFilename)) {
						logFiles.removeLast();
					}
				}
				logStream = openNewLogFile(currentFilename, true);
				if(latestFile != null) {
					altLogStream = openNewLogFile(latestFile, false);
				}
				System.err.println("Created log files");
				startTime = gc.getTimeInMillis();
		    	if(logMINOR)
		    		Logger.minor(this, "Start time: "+gc+" -> "+startTime);
				lastTime = startTime;
				gc.add(INTERVAL, INTERVAL_MULTIPLIER);
				nextHour = gc.getTimeInMillis();
			}
			long timeWaitingForSync = -1;
			long flush;
			synchronized(this) {
				flush = flushTime;
			}
			while (true) {
				try {
					thisTime = System.currentTimeMillis();
					if (baseFilename != null) {
						if ((thisTime > nextHour) || switchedBaseFilename) {
							currentFilename = rotateLog(currentFilename, lastTime, nextHour, gc);
							
							gc.add(INTERVAL, INTERVAL_MULTIPLIER);
							lastTime = nextHour;
							nextHour = gc.getTimeInMillis();

							if(switchedBaseFilename) {
								synchronized(FileLoggerHook.class) {
									switchedBaseFilename = false;
								}
							}
						}
					}
					boolean died = false;
					boolean timeoutFlush = false;
					synchronized (list) {
						flush = flushTime;
						long maxWait;
						if(timeWaitingForSync == -1)
							maxWait = Long.MAX_VALUE;
						else
							maxWait = timeWaitingForSync + flush;
						o = list.poll();
						while(o == null) {
							if (closed) {
								died = true;
								break;
							}
							try {
								if(thisTime < maxWait) {
									// Wait no more than 500ms since the CloserThread might be waiting for closedFinished.
									list.wait(Math.min(500L, maxWait - thisTime));
									thisTime = System.currentTimeMillis();
									if(listBytes < LIST_WRITE_THRESHOLD) {
										// Don't write at all until the lower bytes threshold is exceeded, or the time threshold is.
										assert((listBytes == 0) == (list.peek() == null));
										if(listBytes != 0 && maxWait == Long.MAX_VALUE)
											maxWait = thisTime + flush;
										if(closed) // If closing, write stuff ASAP.
											o = list.poll();
										else if(maxWait != Long.MAX_VALUE) {
											continue;
										}
									} else {
										// Do NOT use list.poll(timeout) because it uses a separate lock.
										o = list.poll();
									}
								}
							} catch (InterruptedException e) {
								// Ignored.
							}
							if(o == null) {
								if(timeWaitingForSync == -1) {
									timeWaitingForSync = thisTime;
									maxWait = thisTime + flush;
								}
								if(thisTime >= maxWait) {
									timeoutFlush = true;
									timeWaitingForSync = -1; // We have stuff to write, we are no longer waiting.
									break;
								}
							} else break;
						}
						if(o != null) {
							listBytes -= o.length + LINE_OVERHEAD;
						}
					}
					if(timeoutFlush || died) {
						// Flush to disk 
						myWrite(logStream, null);
				        if(altLogStream != null)
				        	myWrite(altLogStream, null);
					}
					if(died) {
						try {
							logStream.close();
						} catch (IOException e) {
							System.err.println("Failed to close log stream: "+e);
						}
						if(altLogStream != null) {
							try {
								altLogStream.close();
							} catch (IOException e) {
								System.err.println("Failed to close compressed log stream: "+e);
							}
						}
						synchronized(list) {
							closedFinished = true;
							list.notifyAll();
						}
						return;
					}
					if(o == null) continue;
					myWrite(logStream,  o);
			        if(altLogStream != null)
			        	myWrite(altLogStream, o);
				} catch (OutOfMemoryError e) {
					System.err.println(e.getClass());
					System.err.println(e.getMessage());
					e.printStackTrace();
				    // FIXME
					//freenet.node.Main.dumpInterestingObjects();
				} catch (Throwable t) {
					System.err.println("FileLoggerHook log writer caught " + t);
					t.printStackTrace(System.err);
				}
			}
		}

		private File rotateLog(File currentFilename, long lastTime, long nextHour, GregorianCalendar gc) {
	        // Switch logs
	        try {
	        	logStream.flush();
	        	if(altLogStream != null) altLogStream.flush();
	        } catch (IOException e) {
	        	System.err.println(
	        		"Flushing on change caught " + e);
	        }
	        try {
	        	logStream.close();
	        } catch (IOException e) {
	        	System.err.println(
	        			"Closing on change caught " + e);
	        }
	        long length = currentFilename.length();
	        OldLogFile olf = new OldLogFile(currentFilename, lastTime, nextHour, length);
	        synchronized(logFiles) {
	        	logFiles.addLast(olf);
	        }
	        oldLogFilesDiskSpaceUsage += length;
	        trimOldLogFiles();
	        // Rotate primary log stream
	        currentFilename = new File(getHourLogName(gc, -1, true));
	        logStream = openNewLogFile(currentFilename, true);
	        if(latestFile != null) {
	        	try {
	        		altLogStream.close();
	        	} catch (IOException e) {
	        		System.err.println(
	        				"Closing alt on change caught " + e);
	        	}
	        	if(previousFile != null && latestFile.exists())
	        		FileUtil.renameTo(latestFile, previousFile);
	        	latestFile.delete();
	        	altLogStream = openNewLogFile(latestFile, false);
	        }
	        return currentFilename;
        }

		// Check every minute
		static final int maxSleepTime = 60 * 1000;
		/**
		 * @param b
		 *            the bytes to write, null to flush
		 */
		protected void myWrite(OutputStream os, byte[] b) {
			long sleepTime = 1000;
			while (true) {
				boolean thrown = false;
				try {
					if (b != null)
						os.write(b);
					else
						os.flush();
				} catch (IOException e) {
					System.err.println(
						"Exception writing to log: "
							+ e
							+ ", sleeping "
							+ sleepTime);
					thrown = true;
				}
				if (thrown) {
					try {
						Thread.sleep(sleepTime);
					} catch (InterruptedException e) {
					}
					sleepTime += sleepTime;
					if (sleepTime > maxSleepTime)
						sleepTime = maxSleepTime;
				} else
					return;
			}
		}

		protected OutputStream openNewLogFile(File filename, boolean compress) {
			while (true) {
				long sleepTime = 1000;
				try {
					OutputStream o = new FileOutputStream(filename, !logOverwrite);
					if(compress) {
						// buffer -> gzip -> buffer -> file
						o = new BufferedOutputStream(o, 512*1024); // to file
						o = new GZIPOutputStream(o);
						// gzip block size is 32kB
						o = new BufferedOutputStream(o, 65536); // to gzipper
					} else {
						// buffer -> file
						o = new BufferedOutputStream(o, 512*1024);
					}
					o.write(BOM);
					return o;
				} catch (IOException e) {
					System.err.println(
						"Could not create FOS " + filename + ": " + e);
					System.err.println(
						"Sleeping " + sleepTime / 1000 + " seconds");
					try {
						Thread.sleep(sleepTime);
					} catch (InterruptedException ex) {
					}
					sleepTime += sleepTime;
				}
			}
		}
	}
	
	private static final byte[] BOM;
	
	static {
		try {
			BOM = "\uFEFF".getBytes(ENCODING);
		} catch (UnsupportedEncodingException e) {
			throw new Error(e);
		}
	}

	protected int runningCompressors = 0;
	protected Object runningCompressorsSync = new Object();

	private Date myDate = new Date();

	/**
	 * Create a Logger to append to the given file. If the file does not exist
	 * it will be created.
	 * 
	 * @param filename
	 *            the name of the file to log to.
	 * @param fmt
	 *            log message format string
	 * @param dfmt
	 *            date format string
	 * @param threshold
	 *            Lowest logged priority
	 * @param assumeWorking
	 *            If false, check whether stderr and stdout are writable and if
	 *            not, redirect them to the log file
	 * @exception IOException
	 *                if the file couldn't be opened for append.
	 * @throws IntervalParseException 
	 */
	public FileLoggerHook(
		String filename,
		String fmt,
		String dfmt,
		String logRotateInterval,
		LogLevel threshold,
		boolean assumeWorking,
		boolean logOverwrite,
		long maxOldLogfilesDiskUsage, int maxListSize)
		throws IOException, IntervalParseException {
		this(
			false,
			filename,
			fmt,
			dfmt,
			logRotateInterval,
			threshold,
			assumeWorking,
			logOverwrite,
			maxOldLogfilesDiskUsage,
			maxListSize);
	}
	
	private final Object trimOldLogFilesLock = new Object();
	
	public void trimOldLogFiles() {
		synchronized(trimOldLogFilesLock) {
			while(oldLogFilesDiskSpaceUsage > maxOldLogfilesDiskUsage) {
				OldLogFile olf;
				// TODO: creates a double lock situation, but only here. I think this is okay because the inner lock is only used for trivial things.
				synchronized(logFiles) {
					if(logFiles.isEmpty()) {
						System.err.println("ERROR: INCONSISTENT LOGGER TOTALS: Log file list is empty but still used "+oldLogFilesDiskSpaceUsage+" bytes!");
					}
					olf = logFiles.removeFirst();
				}
				olf.filename.delete();
				oldLogFilesDiskSpaceUsage -= olf.size;
		    	if(logMINOR)
		    		Logger.minor(this, "Deleting "+olf.filename+" - saving "+olf.size+
						" bytes, disk usage now: "+oldLogFilesDiskSpaceUsage+" of "+maxOldLogfilesDiskUsage);
			}
		}
	}

	/** Initialize oldLogFiles */
	public void findOldLogFiles(GregorianCalendar gc) {
		gc = (GregorianCalendar) gc.clone();
		File currentFilename = new File(getHourLogName(gc, -1, true));
		System.out.println("Finding old log files. New log file is "+currentFilename);
		File numericSameDateFilename;
		int slashIndex = baseFilename.lastIndexOf(File.separatorChar);
		File dir;
		String prefix;
		if(slashIndex == -1) {
			dir = new File(System.getProperty("user.dir"));
			prefix = baseFilename.toLowerCase();
		} else {
			dir = new File(baseFilename.substring(0, slashIndex));
			prefix = baseFilename.substring(slashIndex+1).toLowerCase();
		}
		File[] files = dir.listFiles();
		if(files == null) return;
		java.util.Arrays.sort(files);
		long lastStartTime = -1;
		File oldFile = null;
        if(latestFile.exists())
        	FileUtil.renameTo(latestFile, previousFile);

		for(File f: files) {
			String name = f.getName();
			if(name.toLowerCase().startsWith(prefix)) {
				if(name.equals(previousFile.getName()) || name.equals(latestFile.getName())) {
					continue;
				}
				if(!name.endsWith(".log.gz")) {
					if(logMINOR) Logger.minor(this, "Does not end in .log.gz: "+name);
					f.delete();
					continue;
				} else {
					name = name.substring(0, name.length()-".log.gz".length());
				}
				name = name.substring(prefix.length());
				if((name.length() == 0) || (name.charAt(0) != '-')) {
					if(logMINOR) Logger.minor(this, "Deleting unrecognized: "+name+" ("+f.getPath()+ ')');
					f.delete();
					continue;
				} else
					name = name.substring(1);
				String[] tokens = name.split("-");
				int[] nums = new int[tokens.length];
				for(int j=0;j<tokens.length;j++) {
					try {
						nums[j] = Integer.parseInt(tokens[j]);
					} catch (NumberFormatException e) {
						Logger.normal(this, "Could not parse: "+tokens[j]+" into number from "+name);
						// Broken
						f.delete();
						continue;
					}
				}
				if(nums.length > 1)
					gc.set(Calendar.YEAR, nums[1]);
				if(nums.length > 2)
					gc.set(Calendar.MONTH, nums[2]-1);
				if(nums.length > 3)
					gc.set(Calendar.DAY_OF_MONTH, nums[3]);
				if(nums.length > 4)
					gc.set(Calendar.HOUR_OF_DAY, nums[4]);
				if(nums.length > 5)
					gc.set(Calendar.MINUTE, nums[5]);
				gc.set(Calendar.SECOND, 0);
				gc.set(Calendar.MILLISECOND, 0);
				long startTime = gc.getTimeInMillis();
				if(oldFile != null) {
					long l = oldFile.length();
					OldLogFile olf = new OldLogFile(oldFile, lastStartTime, startTime, l);
					synchronized(logFiles) {
						logFiles.addLast(olf);
					}
					synchronized(trimOldLogFilesLock) {
						oldLogFilesDiskSpaceUsage += l;
					}
				}
				lastStartTime = startTime;
				oldFile = f;
			} else {
				// Nothing to do with us
				Logger.normal(this, "Unknown file: "+name+" in the log directory");
			}
		}
		//If a compressed log file already exists for a given date,
		//add a number to the end of the file that already exists
		if(currentFilename != null && currentFilename.exists()) {
			System.out.println("Old log file exists for this time period: "+currentFilename);
			for(int a = 1;; a++){
				numericSameDateFilename = new File(getHourLogName(gc, a, true));
				if(numericSameDateFilename == null || !numericSameDateFilename.exists()) {
					if(numericSameDateFilename != null) {
						System.out.println("Renaming to: "+numericSameDateFilename);
						FileUtil.renameTo(currentFilename, numericSameDateFilename);
					}
					break;
				}
			}
		}
		if(oldFile != null) {
			long l = oldFile.length();
			OldLogFile olf = new OldLogFile(oldFile, lastStartTime, System.currentTimeMillis(), l);
			synchronized(logFiles) {
				logFiles.addLast(olf);
			}
			synchronized(trimOldLogFilesLock) {
				oldLogFilesDiskSpaceUsage += l;
			}
		}
		trimOldLogFiles();
	}

	public FileLoggerHook(
			String filename,
			String fmt,
			String dfmt,
			String threshold,
			String logRotateInterval,
			boolean assumeWorking,
			boolean logOverwrite,
			long maxOldLogFilesDiskUsage,
			int maxListSize)
			throws IOException, InvalidThresholdException, IntervalParseException {
			this(filename,
				fmt,
				dfmt,
				logRotateInterval,
				LogLevel.valueOf(threshold.toUpperCase()),
				assumeWorking,
				logOverwrite,
				maxOldLogFilesDiskUsage,
				maxListSize);
		}

	private void checkStdStreams() {
		// Redirect System.err and System.out to the Logger Printstream
		// if they don't exist (like when running under javaw)
		System.out.print(" \b");
		if (System.out.checkError()) {
			redirectStdOut = true;
		}
		System.err.print(" \b");
		if (System.err.checkError()) {
			redirectStdErr = true;
		}
	}

	public FileLoggerHook(
		OutputStream os,
		String fmt,
		String dfmt,
		LogLevel threshold) throws IntervalParseException {
		this(os, fmt, dfmt, threshold, true);
		logStream = os;
	}
	
	public FileLoggerHook(
			OutputStream os,
			String fmt,
			String dfmt,
			String threshold) throws InvalidThresholdException, IntervalParseException {
			this(os, fmt, dfmt, LogLevel.valueOf(threshold.toUpperCase()), true);
			logStream = os;
		}

	/**
	 * Create a Logger to send log output to the given PrintStream.
	 * 
	 * @param stream
	 *            the PrintStream to send log output to.
	 * @param fmt
	 *            log message format string
	 * @param dfmt
	 *            date format string
	 * @param threshold
	 *            Lowest logged priority
	 * @throws IntervalParseException 
	 */
	public FileLoggerHook(
		OutputStream stream,
		String fmt,
		String dfmt,
		LogLevel threshold,
		boolean overwrite) throws IntervalParseException {
		this(fmt, dfmt, threshold, "HOUR", overwrite, -1, 10000);
		logStream = stream;
	}

	public void start() {
		if(redirectStdOut) {
			try {
				System.setOut(new PrintStream(new OutputStreamLogger(LogLevel.NORMAL, "Stdout: ", ENCODING), false, ENCODING));
				if(redirectStdErr)
					System.setErr(new PrintStream(new OutputStreamLogger(LogLevel.ERROR, "Stderr: ", ENCODING), false, ENCODING));
			} catch (UnsupportedEncodingException e) {
				throw new Error(e);
			}
		}
		WriterThread wt = new WriterThread();
		wt.setDaemon(true);
		CloserThread ct = new CloserThread();
		SemiOrderedShutdownHook.get().addLateJob(ct);
		wt.start();
	}
	
	public FileLoggerHook(
		boolean rotate,
		String baseFilename,
		String fmt,
		String dfmt,
		String logRotateInterval,
		LogLevel threshold,
		boolean assumeWorking,
		boolean logOverwrite,
		long maxOldLogfilesDiskUsage, int maxListSize)
		throws IOException, IntervalParseException {
		this(fmt, dfmt, threshold, logRotateInterval, logOverwrite, maxOldLogfilesDiskUsage, maxListSize);
		//System.err.println("Creating FileLoggerHook with threshold
		// "+threshold);
		if (!assumeWorking)
			checkStdStreams();
		if (rotate) {
			this.baseFilename = baseFilename;
		} else {
			logStream = new BufferedOutputStream(new FileOutputStream(baseFilename, !logOverwrite), 65536);
		}
	}
	
	public FileLoggerHook(
			boolean rotate,
			String baseFilename,
			String fmt,
			String dfmt,
			String threshold,
			String logRotateInterval,
			boolean assumeWorking,
			boolean logOverwrite,
			long maxOldLogFilesDiskUsage, int maxListSize) throws IOException, InvalidThresholdException, IntervalParseException{
		this(rotate,baseFilename,fmt,dfmt,logRotateInterval,LogLevel.valueOf(threshold.toUpperCase()),assumeWorking,logOverwrite,maxOldLogFilesDiskUsage,maxListSize);
	}

	private FileLoggerHook(String fmt, String dfmt, LogLevel threshold, String logRotateInterval, boolean overwrite, long maxOldLogfilesDiskUsage, int maxListSize) throws IntervalParseException {
		super(threshold);
		this.maxOldLogfilesDiskUsage = maxOldLogfilesDiskUsage;
		this.logOverwrite = overwrite;
		setInterval(logRotateInterval);
		
		MAX_LIST_SIZE = maxListSize;
		list = new ArrayBlockingQueue<byte[]>(MAX_LIST_SIZE);
		
		setDateFormat(dfmt);
		setLogFormat(fmt);
	}

	private void setLogFormat(String fmt) {
		if ((fmt == null) || (fmt.length() == 0))
			fmt = "d:c:h:t:p:m";
		char[] f = fmt.toCharArray();

		ArrayList<Integer> fmtVec = new ArrayList<Integer>();
		ArrayList<String> strVec = new ArrayList<String>();

		StringBuilder sb = new StringBuilder();

		boolean comment = false;
		for (char fi: f) {
			int type = numberOf(fi);
			if(type == UNAME)
				getUName();
			if (!comment && (type != 0)) {
				if (sb.length() > 0) {
					strVec.add(sb.toString());
					fmtVec.add(0);
					sb = new StringBuilder();
				}
				fmtVec.add(type);
			} else if (fi == '\\') {
				comment = true;
			} else {
				comment = false;
				sb.append(fi);
			}
		}
		if (sb.length() > 0) {
			strVec.add(sb.toString());
			fmtVec.add(0);
		}

		this.fmt = new int[fmtVec.size()];
		int size = fmtVec.size();
		for (int i = 0; i < size; ++i)
			this.fmt[i] = fmtVec.get(i);

		this.str = new String[strVec.size()];
		str = strVec.toArray(str);
	}

	private void setDateFormat(String dfmt) {
		if ((dfmt != null) && (dfmt.length() != 0)) {
			try {
				df = new SimpleDateFormat(dfmt);
			} catch (RuntimeException e) {
				df = DateFormat.getDateTimeInstance();
			}
		} else
			df = DateFormat.getDateTimeInstance();

		df.setTimeZone(TimeZone.getTimeZone("UTC"));
	}

	@Override
	public void log(Object o, Class<?> c, String msg, Throwable e, LogLevel priority) {
		if (!instanceShouldLog(priority, c))
			return;

		if (closed)
			return;
		
		StringBuilder sb = new StringBuilder( e == null ? 512 : 1024 );
		int sctr = 0;

		for (int f: fmt) {
			switch (f) {
				case 0 :
					sb.append(str[sctr++]);
					break;
				case DATE :
					long now = System.currentTimeMillis();
					synchronized (this) {
						myDate.setTime(now);
						sb.append(df.format(myDate));
					}
					break;
				case CLASS :
					sb.append(c == null ? "<none>" : c.getName());
					break;
				case HASHCODE :
					sb.append(
						o == null
							? "<none>"
							: Integer.toHexString(o.hashCode()));
					break;
				case THREAD :
					sb.append(Thread.currentThread().getName());
					break;
				case PRIORITY :
					sb.append(priority.name());
					break;
				case MESSAGE :
					sb.append(msg);
					break;
				case UNAME :
					sb.append(uname);
					break;
			}
		}
		sb.append('\n');

		// Write stacktrace if available
		for(int j=0;j<20 && e != null;j++) {
			sb.append(e.toString());
			
			StackTraceElement[] trace = e.getStackTrace();
			
			if(trace == null)
				sb.append("(null)\n");
			else if(trace.length == 0)
				sb.append("(no stack trace)\n");
			else {
				sb.append('\n');
				for(StackTraceElement elt: trace) {
					sb.append("\tat ");
					sb.append(elt.toString());
					sb.append('\n');
				}
			}
			
			Throwable cause = e.getCause();
			if(cause != e) e = cause;
			else break;
		}

		try {
			logString(sb.toString().getBytes(ENCODING));
		} catch (UnsupportedEncodingException e1) {
			throw new Error(e1);
		}
	}

	/** Memory allocation overhead (estimated through experimentation with bsh) */
	private static final int LINE_OVERHEAD = 60;
	
	public void logString(byte[] b) throws UnsupportedEncodingException {
		synchronized (list) {
			int sz = list.size();
			if(!list.offer(b)) {
				byte[] ss = list.poll();
				if(ss != null) listBytes -= ss.length + LINE_OVERHEAD;
				ss = list.poll();
				if(ss != null) listBytes -= ss.length + LINE_OVERHEAD;
				String err =
					"GRRR: ERROR: Logging too fast, chopped "
						+ 2
						+ " entries, "
						+ listBytes
						+ " bytes in memory\n";
				byte[] buf = err.getBytes(ENCODING);
				if(list.offer(buf))
					listBytes += (buf.length + LINE_OVERHEAD);
				if(list.offer(b))
					listBytes += (b.length + LINE_OVERHEAD);
			} else
				listBytes += (b.length + LINE_OVERHEAD);
			int x = 0;
			if (listBytes > MAX_LIST_BYTES) {
				while ((list.size() > (MAX_LIST_SIZE * 0.9F))
					|| (listBytes > (MAX_LIST_BYTES * 0.9F))) {
					byte[] ss;
					ss = list.poll();
					listBytes -= (ss.length + LINE_OVERHEAD);
					x++;
				}
				String err =
					"GRRR: ERROR: Logging too fast, chopped "
						+ x
						+ " entries, "
						+ listBytes
						+ " bytes in memory\n";
				byte[] buf = err.getBytes(ENCODING);
				if(!list.offer(buf)) {
					byte[] ss = list.poll();
					if(ss != null) listBytes -= ss.length + LINE_OVERHEAD;
					if(list.offer(buf))
						listBytes += (buf.length + LINE_OVERHEAD);
				} else
					listBytes += (buf.length + LINE_OVERHEAD);
			}
			if (sz == 0)
				list.notifyAll();
		}
	}

	public long listBytes() {
		synchronized (list) {
			return listBytes;
		}
	}

	public static int numberOf(char c) {
		switch (c) {
			case 'd' :
				return DATE;
			case 'c' :
				return CLASS;
			case 'h' :
				return HASHCODE;
			case 't' :
				return THREAD;
			case 'p' :
				return PRIORITY;
			case 'm' :
				return MESSAGE;
			case 'u' :
				return UNAME;
			default :
				return 0;
		}
	}

	@Override
	public void close() {
		closed = true;
	}

	class CloserThread extends Thread {
		@Override
		public void run() {
			synchronized(list) {
				closed = true;
				long deadline = System.currentTimeMillis() + SECONDS.toMillis(10);
				while(!closedFinished) {
					int wait = (int) (deadline - System.currentTimeMillis());
					if(wait <= 0) return;
					try {
						list.wait(wait);
					} catch (InterruptedException e) {
						// Ok.
					}
				}
				System.out.println("Completed writing logs to disk.");
			}
		}
	}

	/**
	 * Print a human- and script- readable list of available log files.
	 * @throws IOException 
	 */
	public void listAvailableLogs(OutputStreamWriter writer) throws IOException {
		OldLogFile[] oldLogFiles;
		synchronized(logFiles) {
			oldLogFiles = logFiles.toArray(new OldLogFile[logFiles.size()]);
		}
		DateFormat tempDF = DateFormat.getDateTimeInstance(DateFormat.SHORT, DateFormat.SHORT, Locale.ENGLISH);
		tempDF.setTimeZone(TimeZone.getTimeZone("GMT"));
		for(OldLogFile olf: oldLogFiles) {
			writer.write(olf.filename.getName()+" : "+tempDF.format(new Date(olf.start))+" to "+tempDF.format(new Date(olf.end))+ " - "+olf.size+" bytes\n");
		}
	}

	public void sendLogByContainedDate(long time, OutputStream os) throws IOException {
		OldLogFile toReturn = null;
		synchronized(logFiles) {
			for(OldLogFile olf : logFiles) {
		    	if(logMINOR)
		    		Logger.minor(this, "Checking "+time+" against "+olf.filename+" : start="+olf.start+", end="+olf.end);
				if((time >= olf.start) && (time < olf.end)) {
					toReturn = olf;
					if(logMINOR) Logger.minor(this, "Found "+olf);
					break;
				}
			}
			if(toReturn == null)
				return; // couldn't find it
		}
		FileInputStream fis = new FileInputStream(toReturn.filename);
		DataInputStream dis = new DataInputStream(fis);
		long written = 0;
		long size = toReturn.size;
		byte[] buf = new byte[4096];
		while(written < size) {
			int toRead = (int) Math.min(buf.length, (size - written));
			try {
				dis.readFully(buf, 0, toRead);
			} catch (IOException e) {
				Logger.error(this, "Could not read bytes "+written+" to "+(written + toRead)+" from file "+toReturn.filename+" which is supposed to be "+size+" bytes ("+toReturn.filename.length()+ ')');
				return;
			}
			os.write(buf, 0, toRead);
			written += toRead;
		}
		dis.close();
		fis.close();
	}

	/** Set the maximum size of old (gzipped) log files to keep.
	 * Will start to prune old files immediately, but this will likely not be completed
	 * by the time the function returns as it is run off-thread.
	 */
	public void setMaxOldLogsSize(long val) {
		synchronized(trimOldLogFilesLock) {
			maxOldLogfilesDiskUsage = val;
		}
		Runnable r = new Runnable() {
			@Override
			public void run() {
				trimOldLogFiles();
			}
		};
		Thread t = new Thread(r, "Shrink logs");
		t.setDaemon(true);
		t.start();
	}

	private boolean switchedBaseFilename;
	
	public void switchBaseFilename(String filename) {
		synchronized(this) {
			this.baseFilename = filename;
			switchedBaseFilename = true;
		}
	}

	public void waitForSwitch() {
		long now = System.currentTimeMillis();
		synchronized(this) {
			if(!switchedBaseFilename) return;
			long startTime = now;
			long endTime = startTime + 10000;
			while(((now = System.currentTimeMillis()) < endTime) && !switchedBaseFilename) {
				try {
					wait(Math.max(1, endTime-now));
				} catch (InterruptedException e) {
					// Ignore
				}
			}
		}
	}

	public void deleteAllOldLogFiles() {
		synchronized(trimOldLogFilesLock) {
			while(true) {
				OldLogFile olf;
				synchronized(logFiles) {
					if(logFiles.isEmpty()) return;
					olf = logFiles.removeFirst();
				}
				olf.filename.delete();
				oldLogFilesDiskSpaceUsage -= olf.size;
				if(logMINOR)
					Logger.minor(this, "Deleting "+olf.filename+" - saving "+olf.size+
							" bytes, disk usage now: "+oldLogFilesDiskSpaceUsage+" of "+maxOldLogfilesDiskUsage);
			}
		}
	}

	/**
	 * This is used by the lost-lock deadlock detector so MUST NOT TAKE A LOCK ever!
	 */
	public boolean hasRedirectedStdOutErrNoLock() {
		return redirectStdOut || redirectStdErr;
	}

	public synchronized void setMaxBacklogNotBusy(long val) {
		flushTime = val;
	}
}
