/*
 * Log.java - A class for logging events
 * :tabSize=4:indentSize=4:noTabs=false:
 * :folding=explicit:collapseFolds=1:
 *
 * Copyright (C) 1999, 2003 Slava Pestov
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License
 * as published by the Free Software Foundation; either version 2
 * of the License, or any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
 */

package org.gjt.sp.util;

//{{{ Imports
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.io.PrintStream;
import java.io.Writer;

import java.text.DateFormat;

import java.util.*;

import javax.swing.event.ListDataEvent;
import javax.swing.event.ListDataListener;

import javax.swing.ListModel;
import javax.swing.SwingUtilities;

import static java.text.DateFormat.MEDIUM;
//}}}

/**
 * This class provides methods for logging events. In terms of functionality,
 * it is somewhere in between <code>System.out.println()</code> and
 * full-blown logging packages such as log4j.<p>
 *
 * All events are logged to an in-memory buffer and optionally a stream,
 * and those with a high urgency (warnings and errors) are also printed
 * to standard output.<p>
 *
 * Logging of exception tracebacks is supported.<p>
 *
 * This class can also optionally redirect standard output and error
 * to the log, see {@link #init}.
 *
 * @author Slava Pestov
 * @version $Id: Log.java 25123 2020-04-04 22:40:51Z kpouer $
 */
public class Log
{
	//{{{ Constants
	/**
	 * The maximum number of log messages that will be kept in memory.
	 * @since jEdit 2.6pre5
	 */
	public static int MAXLINES = 500;

	/**
	 * Debugging message urgency. Should be used for messages only
	 * useful when debugging a problem.
	 * @since jEdit 2.2pre2
	 */
	public static final int DEBUG = 1;

	/**
	 * Message urgency. Should be used for messages which give more
	 * detail than notices.
	 * @since jEdit 2.2pre2
	 */
	public static final int MESSAGE = 3;

	/**
	 * Notice urgency. Should be used for messages that directly
	 * affect the user.
	 * @since jEdit 2.2pre2
	 */
	public static final int NOTICE = 5;

	/**
	 * Warning urgency. Should be used for messages that warrant
	 * attention.
	 * @since jEdit 2.2pre2
	 */
	public static final int WARNING = 7;

	/**
	 * Error urgency. Should be used for messages that signal a
	 * failure.
	 * @since jEdit 2.2pre2
	 */
	public static final int ERROR = 9;
	//}}}

	//{{{ init() method
	/**
	 * Initializes the log.
	 * @param stdio If true, standard output and error will be
	 * intercepted and sent to the log. The <code>urgency</code>
	 * for these implicit log entries is <code>NOTICE</code>
	 * and <code>ERROR</code> accordingly. Note that in such a situation
	 * <code>System.out.print()</code> calls will not appear on standard
	 * output, if default output level is higher than <code>NOTICE</code>.
	 * @param level Messages with this log level or higher will
	 * be printed to the system console.
	 * @since jEdit 3.2pre4
	 */
	public static void init(boolean stdio, int level)
	{
		if(stdio && System.out == realOut && System.err == realErr)
		{
			System.setOut(createPrintStream(NOTICE,null));
			System.setErr(createPrintStream(ERROR,null));
		}

		Log.level = level;

		// Log some stuff
		log(MESSAGE,Log.class,"When reporting bugs, please"
			+ " include the following information:");
		String[] props = {
			"java.version", "java.vm.version", "java.vm.name",
			"java.runtime.version", "java.runtime.name",
			"java.vendor", "java.compiler", "os.name", "os.version",
			"os.arch", "user.home", "java.home",
			"java.class.path",
			};
		for (String prop : props)
		{
			log(MESSAGE, Log.class, prop + '=' + System.getProperty(prop));
		}
	} //}}}

	//{{{ setLogWriter() method
	/**
	 * Writes all currently logged messages to this stream if there was no
	 * stream set previously, and sets the stream to write future log
	 * messages to.
	 * @param stream The writer
	 * @since jEdit 3.2pre4
	 */
	public static void setLogWriter(Writer stream)
	{
		if(Log.stream == null && stream != null)
		{
			try
			{
				if(wrap)
				{
					for(int i = logLineCount; i < log.length; i++)
					{
						stream.write(log[i]);
						stream.write(lineSep);
					}
				}
				for(int i = 0; i < logLineCount; i++)
				{
					stream.write(log[i]);
					stream.write(lineSep);
				}

				stream.flush();
			}
			catch(Exception e)		// NOPMD
			{
				// do nothing, who cares -- well, PMD will call you on it.
			}
		}

		Log.stream = stream;
	} //}}}

	//{{{ get/setBeepOnOutput method
	/**
	 * @since jEdit 5.1pre1
	 */
	public static boolean getBeepOnOutput()
	{
		return beepOnOutput;
	}

	/**
	 * When <code>beepOnOutput</code> is set, every output going to standard
	 * error is signaled by a standard beep. This is intended for debugging
	 * purposes, to allow for immediate problem detection.
	 * @since jEdit 5.1pre1
	 */
	public static void setBeepOnOutput(boolean beepOnOutput)
	{
		Log.beepOnOutput = beepOnOutput;
	} //}}}
	
	public static void setMaxLines(int newMax)
	{
		if (newMax == MAXLINES)
			return;
		
		// find last non-null entry in log array
		int lineCount = 0;
		for (int i = 0; i < log.length; i++) 
		{
			if (log[i] == null)
				break;
			++lineCount;
		}
		
		// copy entries from log to newLog
		String[] newLog = new String[newMax];
		if (newMax > lineCount)
		{
			System.arraycopy(log, 0, newLog, 0, lineCount);
		}
		else
		{
			// lineCount > newMax
			System.arraycopy(log, lineCount - newMax, newLog, 0, newMax);
			logLineCount = newMax;
		}
		
		MAXLINES = newMax;
		log = newLog;
		listModel.update(lineCount, true);
	}
	
	public static int getMaxLinex()
	{
		return MAXLINES;	
	}

	//{{{ flushStream() method
	/**
	 * Flushes the log stream.
	 * @since jEdit 2.6pre5
	 */
	public static void flushStream()
	{
		if(stream != null)
		{
			try
			{
				stream.flush();
			}
			catch(IOException io)
			{
				io.printStackTrace(realErr);
			}
		}
	} //}}}

	//{{{ closeStream() method
	/**
	 * Closes the log stream. Should be done before your program exits.
	 * @since jEdit 2.6pre5
	 */
	public static void closeStream()
	{
		if(stream != null)
		{
			try
			{
				stream.close();
				stream = null;
			}
			catch(IOException io)
			{
				io.printStackTrace(realErr);
			}
		}
	} //}}}

	//{{{ getLogListModel() method
	/**
	 * Returns the list model for viewing the log contents.
	 * @since jEdit 4.2pre1
	 */
	public static ListModel<String> getLogListModel()
	{
		return listModel;
	} //}}}

	//{{{ log() method
	/**
	 * Logs an exception with a message.
	 *
	 * If an exception is the cause of a call to {@link #log}, then
	 * the exception should be explicitly provided so that it can
	 * be presented to the (debugging) user in a useful manner
	 * (not just the exception message, but also the exception stack trace)
	 *
	 * @since jEdit 4.3pre5
	 */
	public static void log(int urgency, Object source, Object message,
		Throwable exception)
	{
		// We can do nicer here, but this is a start...
		log(urgency,source,message);
		log(urgency,source,exception);
	} //}}}

	//{{{ log() method
	/**
	 * Logs a message. This method is thread-safe.<p>
	 *
	 * The following code sends a typical debugging message to the activity
	 * log:
	 * <pre>Log.log(Log.DEBUG,this,"counter = " + counter);</pre>
	 * The corresponding activity log entry might read as follows:
	 * <pre>[debug] JavaParser: counter = 15</pre>
	 *
	 * @param urgency The urgency; can be one of
	 * <code>Log.DEBUG</code>, <code>Log.MESSAGE</code>,
	 * <code>Log.NOTICE</code>, <code>Log.WARNING</code>, or
	 * <code>Log.ERROR</code>.
	 * @param source The source of the message, either an object or a
	 * class instance. When writing log messages from macros, set
	 * this parameter to <code>BeanShell.class</code> to make macro
	 * errors easier to spot in the activity log.
	 * @param message The message. This can either be a string or
	 * an exception
	 *
	 * @since jEdit 2.2pre2
	 */
	public static void log(int urgency, Object source, Object message)
	{
		String _source;
		if(source == null)
		{
			_source = Thread.currentThread().getName();
			if(_source == null)
			{
				_source = Thread.currentThread().getClass().getName();
			}
		}
		else if(source instanceof Class)
			_source = ((Class<?>)source).getName();
		else
			_source = source.getClass().getName();
		int index = _source.lastIndexOf('.');
		if(index != -1)
			_source = _source.substring(index+1);

		if(message instanceof Throwable)
		{
			_logException(urgency,source,(Throwable)message);
		}
		else
		{
			String _message = String.valueOf(message);
			// If multiple threads log stuff, we don't want
			// the output to get mixed up
			synchronized(LOCK)
			{
				StringTokenizer st = new StringTokenizer(
					_message,"\r\n");
				int lineCount = 0;
				boolean oldWrap = wrap;
				while(st.hasMoreTokens())
				{
					lineCount++;
					_log(urgency,_source,st.nextToken()
						.replace('\t',' '));
				}
				listModel.update(lineCount,oldWrap);
			}
		}
	} //}}}

	//{{{ Private members

	//{{{ Instance variables
	private static final Object LOCK;
	private static String[] log;
	private static int logLineCount;
	private static boolean wrap;
	private static int level;
	private static Writer stream;
	private static final String lineSep;
	private static final PrintStream realOut;
	private static final PrintStream realErr;
	private static final LogListModel listModel;
	private static final DateFormat timeFormat;
	private static final int MAX_THROWABLES = 10;
	public static final List<Throwable> throwables;
	// initialized externally through setBeepOnOutput method
	private static boolean beepOnOutput = false;
	// to prevent too much beeping we remember last beep time
	private static long lastBeepTime = 0;
	//}}}

	//{{{ Class initializer
	static
	{
		LOCK = new Object();
		level = WARNING;

		realOut = System.out;
		realErr = System.err;

		log = new String[MAXLINES];
		lineSep = System.getProperty("line.separator");
		listModel = new LogListModel();
		
		timeFormat = DateFormat.getTimeInstance(MEDIUM);
		throwables = Collections.synchronizedList(new ArrayList<Throwable>(MAX_THROWABLES));
	} //}}}

	//{{{ createPrintStream() method
	private static PrintStream createPrintStream(final int urgency,
		final Object source)
	{
		return new LogPrintStream(urgency, source);
	} //}}}

	//{{{ _logException() method
	private static void _logException(final int urgency,
		final Object source,
		final Throwable message)
	{
		PrintStream out = createPrintStream(urgency,source);
		if (urgency >= level)
		{
			synchronized (throwables)
			{
				if (throwables.size() == MAX_THROWABLES)
				{
					throwables.remove(0);
				}
				throwables.add(message);
			}
		}
		synchronized(LOCK)
		{
			message.printStackTrace(out);
		}
	} //}}}

	//{{{ _log() method
	private static void _log(int urgency, String source, String message)
	{
		String fullMessage = timeFormat.format(new Date()) + " ["+Thread.currentThread().getName()+"] [" + urgencyToString(urgency) + "] " + source
			+ ": " + message;

		try
		{
			log[logLineCount] = fullMessage;
			if(++logLineCount >= log.length)
			{
				wrap = true;
				logLineCount = 0;
			}

			if(stream != null)
			{
				stream.write(fullMessage);
				stream.write(lineSep);
			}
		}
		catch(Exception e)
		{
			e.printStackTrace(realErr);
		}

		if(urgency >= level)
		{
			if(urgency == ERROR)
				realErr.println(fullMessage);
			else
				realOut.println(fullMessage);

			if (beepOnOutput)
			{
				long time = System.currentTimeMillis();
				
				if (time - lastBeepTime > 1000)
				{
					javax.swing.UIManager.getLookAndFeel().provideErrorFeedback(null); 
					lastBeepTime = System.currentTimeMillis();
				}
			}
		}
	} //}}}

	//{{{ urgencyToString() method
	private static String urgencyToString(int urgency)
	{
		switch(urgency)
		{
		case DEBUG:
			return "debug";
		case MESSAGE:
			return "message";
		case NOTICE:
			return "notice";
		case WARNING:
			return "warning";
		case ERROR:
			return "error";
		}

		throw new IllegalArgumentException("Invalid urgency: " + urgency);
	} //}}}

	//}}}

	//{{{ LogListModel class
	static class LogListModel implements ListModel<String>
	{
		final List<ListDataListener> listeners = new ArrayList<>();

		//{{{ fireIntervalAdded() method
		private void fireIntervalAdded(int index1, int index2)
		{
			for (ListDataListener listener : listeners)
			{
				listener.intervalAdded(new ListDataEvent(this,
					ListDataEvent.INTERVAL_ADDED, index1, index2));
			}
		} //}}}

		//{{{ fireIntervalRemoved() method
		private void fireIntervalRemoved(int index1, int index2)
		{
			for (ListDataListener listener : listeners)
			{
				listener.intervalRemoved(new ListDataEvent(
					this, ListDataEvent.INTERVAL_REMOVED, index1, index2));
			}
		} //}}}

		//{{{ addListDataListener() method
		@Override
		public void addListDataListener(ListDataListener listener)
		{
			listeners.add(listener);
		} //}}}

		//{{{ removeListDataListener() method
		@Override
		public void removeListDataListener(ListDataListener listener)
		{
			listeners.remove(listener);
		} //}}}

		//{{{ getElementAt() method
		@Override
		public String getElementAt(int index)
		{
			if(wrap)
			{
				if(index < MAXLINES - logLineCount)
					return log[index + logLineCount];
				else
					return log[index - MAXLINES + logLineCount];
			}
			else
				return log[index];
		} //}}}

		//{{{ getSize() method
		@Override
		public int getSize()
		{
			if(wrap)
				return MAXLINES;
			else
				return logLineCount;
		} //}}}

		//{{{ update() method
		void update(final int lineCount, final boolean oldWrap)
		{
			if(lineCount == 0 || listeners.isEmpty())
				return;

			SwingUtilities.invokeLater(() ->
			{
				if(wrap)
				{
					if(oldWrap)
						fireIntervalRemoved(0,lineCount - 1);
					else
					{
						fireIntervalRemoved(0,
							logLineCount);
					}
					fireIntervalAdded(
						MAXLINES - lineCount + 1,
						MAXLINES);
				}
				else
				{
					fireIntervalAdded(
						logLineCount - lineCount + 1,
						logLineCount);
				}
			});
		} //}}}
	} //}}}

	//{{{ LogPrintStream class
	/**
	 * A print stream that uses the "Log" class to output the messages,
	 * and has special treatment for the printf() function. Using this
	 * stream has one caveat: printing messages that don't have a line
	 * break at the end will have one added automatically...
	 */
	private static class LogPrintStream extends PrintStream {

		private final ByteArrayOutputStream buffer;
		private final OutputStream orig;

		//{{{ LogPrintStream constructor
		LogPrintStream(int urgency, Object source)
		{
			super(new LogOutputStream(urgency, source));
			buffer = new ByteArrayOutputStream();
			orig = out;
		} //}}}

		//{{{ printf() method
		/**
		 * This is a hack to allow "printf" to not print weird
		 * stuff to the output. Since "printf" doesn't seem to
		 * print the whole message in one shot, our output
		 * stream above would break a line of log into several
		 * lines; so we buffer the result of the printf call and
		 * print the whole thing in one shot. A similar hack
		 * would be needed for the "other" printf method, but
		 * I'll settle for the common case only.
		 */
		@Override
		public PrintStream printf(String format, Object... args)
		{
			synchronized (orig)
			{
				buffer.reset();
				out = buffer;
				super.printf(format, args);

				try
				{
					byte[] data = buffer.toByteArray();
					orig.write(data, 0, data.length);
					out = orig;
				}
				catch (IOException ioe)		// NOPMD
				{
					// don't do anything?
				}
				finally
				{
					buffer.reset();
				}
			}
			return this;
		} //}}}
	} //}}}

	//{{{ LogOutputStream class
	private static class LogOutputStream extends OutputStream
	{
		private final int 	urgency;
		private final Object 	source;

		//{{{ LogOutputStream constructor
		LogOutputStream(int urgency, Object source)
		{
			this.urgency 	= urgency;
			this.source 	= source;
		} //}}}

		//{{{ write() method
		@Override
		public synchronized void write(int b)
		{
			byte[] barray = { (byte)b };
			write(barray,0,1);
		} //}}}

		//{{{ write() method
		@Override
		public synchronized void write(byte[] b, int off, int len)
		{
			String str = new String(b,off,len);
			log(urgency,source,str);
		} //}}}
	} //}}}
}
