/*******************************************************************************
 * Copyright (c) 1997, 2018 by ProSyst Software GmbH
 * http://www.prosyst.com
 *
 * This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License 2.0
 * which accompanies this distribution, and is available at
 * https://www.eclipse.org/legal/epl-2.0/
 *
 * SPDX-License-Identifier: EPL-2.0
 *
 * Contributors:
 *    ProSyst Software GmbH - initial API and implementation
 *******************************************************************************/
package org.eclipse.equinox.internal.util.ref;

import java.io.*;
import java.util.Vector;
import org.eclipse.equinox.internal.util.UtilActivator;
import org.eclipse.equinox.internal.util.event.Queue;
import org.eclipse.equinox.internal.util.hash.HashIntObjNS;
import org.eclipse.equinox.internal.util.security.PrivilegedRunner;
import org.eclipse.equinox.internal.util.security.SecurityUtil;
import org.osgi.framework.BundleContext;
import org.osgi.framework.ServiceReference;
import org.osgi.service.log.LogService;
import org.osgi.util.tracker.ServiceTracker;
import org.osgi.util.tracker.ServiceTrackerCustomizer;

/**
 * Log class is responsible for forwarding bundles's messages to the LogService
 * or console output.
 * 
 * @author Pavlin Dobrev
 * @version 1.0
 */

public class Log implements LogInterface, ServiceTrackerCustomizer<LogService, LogService>, PrivilegedRunner.PrivilegedDispatcher {

	/**
	 * Flag, pointing if printingOnConsole is allowed
	 * 
	 * @deprecated since osgilib verion 1.3.9 use set/get PrintOnConsole
	 */
	@Deprecated
	public boolean printOnConsole = false;

	/**
	 * Flag, pointing whether printing on console should be done if log service
	 * is not available
	 */
	public boolean autoPrintOnConsole = false;

	/**
	 * Flag, pointing if logging debuging info is allowed
	 * 
	 * @deprecated since osgilib verion 1.3.9 use set/get Debug
	 */
	@Deprecated
	public boolean debug = false;

	/**
	 * Flag, pointing whether printing on console should be done for errors
	 * (exceptions) and warnings
	 */
	private boolean logErrorLevel = false;

	private ServiceTracker<LogService, LogService> logTracker;

	protected static final SecurityUtil securityUtil = new SecurityUtil();

	private boolean isClosed = false;

	private long bundleId;
	/** BundleContext to get LogService and service owner of Log object BundleId */
	protected BundleContext bc;
	private static Vector<Log> logs = new Vector<>();
	private static Log listener;

	public Log(BundleContext bc) {
		this(bc, true);
	}

	/**
	 * Constructs a log object, used for logging information in the LogService.
	 * If the LogService is unavailble, the information is printed on the
	 * server's console.
	 * 
	 * @param bc
	 *            BundleContext, necessary to get LogService, in case it is
	 *            started.
	 */
	public Log(BundleContext bc, boolean initDebug) {
		if (initDebug) {
			debug = securityUtil.getBooleanProperty("equinox.util.ref.log.debug");
			logErrorLevel = securityUtil.getBooleanProperty("equinox.log.errorlevel");
			autoPrintOnConsole = securityUtil.getBooleanProperty("equinox.util.ref.log.autoPrintOnConsole");
			printOnConsole = securityUtil.getBooleanProperty("equinox.util.ref.log.printOnConsole");
		}
		if (bc != null) {
			this.bc = bc;
			bundleId = bc.getBundle().getBundleId();

			if (UtilActivator.startup && UtilActivator.points != null)
				UtilActivator.points[0] = System.currentTimeMillis();

			initSysServices();

			if (UtilActivator.startup && UtilActivator.points != null)
				UtilActivator.points[1] = System.currentTimeMillis();
			synchronized (logs) {
				if (listener == null)
					initListener();
				logs.addElement(this);
			}

			if (UtilActivator.startup && UtilActivator.points != null)
				UtilActivator.points[2] = System.currentTimeMillis();
		} else
			printOnConsole = true;
	}

	private void initListener() {
		try {
			securityUtil.doPrivileged(this, OPEN_TYPE, null);
		} catch (IllegalStateException ise) {
			/* must be rethrown */
			throw ise;
		} catch (Throwable ignore) {
			ignore.printStackTrace();
		}
	}

	void initListener0() {
		synchronized (logs) {
			initListenerNS();
		}
	}

	void initListenerNS() {
		logTracker = new ServiceTracker<>(bc, org.osgi.service.log.LogService.class, this);
		logTracker.open();
		listener = this;
	}

	/**
	 * Logs error messages. If <code>printOnConsole</code> is true, or if the
	 * <code>LogService</code> is unavailable, log info is printed on console.
	 * 
	 * @param str
	 *            Message description of the error.
	 * @param ex
	 *            Throwable object, containing the stack trace; may be null.
	 */
	@Override
	public void error(String str, Throwable ex) {
		if (isClosed)
			return;

		boolean logResult = logMessage(LogService.LOG_ERROR, str, ex);
		if (printOnConsole || (!logResult && autoPrintOnConsole) || logErrorLevel) {
			dumpOnConsole("ERROR ", str, bundleId, ex);
		}
	}

	/**
	 * Logs error messages. If <code>printOnConsole</code> is true, or if the
	 * <code>LogService</code> is unavailable, log info is printed on console.
	 * 
	 * @param moduleID
	 * @param msgID
	 * @param msg
	 *            Message description of the error.
	 * @param t
	 *            Throwable object, containing the stack trace; may be null.
	 * @param synch
	 *            Weather tracer to work synchronious or not
	 */
	public void error(int moduleID, int msgID, String msg, Throwable t, boolean synch) {
		if (isClosed)
			return;

		boolean logResult = true;
		if (msg != null || t != null) {
			logResult = logMessage(LogService.LOG_ERROR, msg, t);
		}
		if (printOnConsole || (!logResult && autoPrintOnConsole) || logErrorLevel) {
			dumpOnConsole(buildDebugString(moduleID, msgID, msg, "ERROR " + bundleId + " "), t);
		}
	}

	/**
	 * Logs warning messages. If <code>printOnConsole</code> is true, or if
	 * the <code>LogService</code> is unavailable, log info is printed on
	 * console.
	 * 
	 * @param str
	 *            Message description of the error.
	 * @param ex
	 *            Throwable object, containing the stack trace; may be null.
	 */
	@Override
	public void warning(String str, Throwable ex) {
		if (isClosed)
			return;

		boolean logResult = logMessage(LogService.LOG_WARNING, str, ex);
		if (printOnConsole || (!logResult && autoPrintOnConsole) || logErrorLevel) {
			dumpOnConsole("WARNING ", str, bundleId, ex);
		}
	}

	/**
	 * Logs warning messages. If <code>printOnConsole</code> is true, or if
	 * the <code>LogService</code> is unavailable, log info is printed on
	 * console.
	 * 
	 * @param moduleID
	 * @param msgID
	 * @param msg
	 *            Message description of the error.
	 * @param t
	 *            Throwable object, containing the stack trace; may be null.
	 * @param synch
	 *            Weather tracer to work synchronious or not
	 */
	public void warning(int moduleID, int msgID, String msg, Throwable t, boolean synch) {
		if (isClosed)
			return;

		boolean logResult = true;
		if (msg != null || t != null) {
			logResult = logMessage(LogService.LOG_WARNING, msg, t);
		}
		if (printOnConsole || (!logResult && autoPrintOnConsole) || logErrorLevel) {
			dumpOnConsole(buildDebugString(moduleID, msgID, msg, "WARNING " + bundleId + " "), t);
		}
	}

	/**
	 * Logs info messages. If <code>printOnConsole</code> is true, or if the
	 * <code>LogService</code> is unavailable, message is printed on console.
	 * 
	 * @param str
	 *            Message to be logged.
	 */
	@Override
	public void info(String str) {
		if (isClosed)
			return;

		boolean logResult = logMessage(LogService.LOG_INFO, str, null);
		if (printOnConsole || (!logResult && autoPrintOnConsole)) {
			dumpOnConsole("INFO ", str, bundleId, null);
		}
	}

	/**
	 * Logs info messages. If <code>printOnConsole</code> is true, or if the
	 * <code>LogService</code> is unavailable, log info is printed on console.
	 * 
	 * @param moduleID
	 * @param msgID
	 * @param msg
	 *            Message description of the error.
	 * @param synch
	 *            Weather tracer to work synchronious or not
	 */
	public void info(int moduleID, int msgID, String msg, boolean synch) {
		if (isClosed)
			return;

		boolean logResult = true;
		if (msg != null) {
			logResult = logMessage(LogService.LOG_INFO, msg, null);
		}
		if (printOnConsole || (!logResult && autoPrintOnConsole)) {
			dumpOnConsole(buildDebugString(moduleID, msgID, msg, "INFO " + bundleId + " "), null);
		}
	}

	/**
	 * Logs debug information if <code>debug</code> flag is true. If
	 * LogService is unaccessible or printOnConsole flag is true, log info is
	 * printed on console.
	 * 
	 * @param str
	 *            Message description.
	 * @param ex
	 *            Throwable object, containing the stack trace; may be null.
	 */
	@Override
	public void debug(String str, Throwable ex) {
		if (!debug || isClosed)
			return;

		boolean logResult = logMessage(LogService.LOG_DEBUG, str, ex);
		if (printOnConsole || (!logResult && autoPrintOnConsole)) {
			dumpOnConsole("DEBUG ", str, bundleId, ex);
		}
	}

	/**
	 * Logs debug messages. If <code>printOnConsole</code> is true, or if the
	 * <code>LogService</code> is unavailable, log info is printed on console.
	 * 
	 * @param moduleID
	 * @param msgID
	 * @param msg
	 *            Message description of the error.
	 * @param t
	 *            Throwable object, containing the stack trace; may be null.
	 * @param synch
	 *            Indicates whether tracer should log the message synchronously
	 *            or not
	 */
	public void debug(int moduleID, int msgID, String msg, Throwable t, boolean synch) {
		debug(moduleID, msgID, msg, t, synch, false, false, true);
	}

	/**
	 * Logs debug messages. If <code>printOnConsole</code> is true, or if the
	 * <code>LogService</code> is unavailable, log info is printed on console.
	 * 
	 * @param moduleID
	 * @param msgID
	 * @param msg
	 *            Message description of the error.
	 * @param t
	 *            Throwable object, containing the stack trace; may be null.
	 * @param synch
	 *            Indicates whether tracer should log the message synchronously
	 *            or not
	 * @param measurement
	 *            Indicates whether the message is a measurement or not
	 */
	public void debug(int moduleID, int msgID, String msg, Throwable t, boolean synch, boolean measurement) {
		debug(moduleID, msgID, msg, t, synch, measurement, false, true);
	}

	/**
	 * Logs debug messages. If <code>printOnConsole</code> is true, or if the
	 * <code>LogService</code> is unavailable, log info is printed on console.
	 * 
	 * @param moduleID
	 * @param msgID
	 * @param msg
	 *            Message description of the error.
	 * @param t
	 *            Throwable object, containing the stack trace; may be null.
	 * @param synch
	 *            Indicates whether tracer should log the message synchronously
	 *            or not
	 * @param measurement
	 *            Indicates whether the message is a measurement or not
	 * @param display
	 *            Indicates whether the message should be displayed in native
	 *            GUI
	 */
	public void debug(int moduleID, int msgID, String msg, Throwable t, boolean synch, boolean measurement, boolean display) {
		debug(moduleID, msgID, msg, t, synch, measurement, display, true);
	}

	/**
	 * Logs debug messages. If <code>printOnConsole</code> is true, or if the
	 * <code>LogService</code> is unavailable, log info is printed on console.
	 * 
	 * @param moduleID
	 * @param msgID
	 * @param msg
	 *            Message description of the error.
	 * @param t
	 *            Throwable object, containing the stack trace; may be null.
	 * @param synch
	 *            Indicates whether tracer should log the message synchronously
	 *            or not
	 * @param measurement
	 *            Indicates whether the message is a measurement or not
	 * @param display
	 *            Indicates whether the message should be displayed in native
	 *            GUI
	 * @param logInFile
	 *            Indicates whether the message should be logged into the log
	 *            file or not. Used for measurements' logs.
	 */
	public void debug(int moduleID, int msgID, String msg, Throwable t, boolean synch, boolean measurement, boolean display, boolean logInFile) {
		if (!debug && !measurement || isClosed)
			return;

		String message = msg;
		if (measurement) {
			message = buildDebugString(moduleID, msgID, msg, "DEBUG " + bundleId + " ");
		}

		boolean logResult = logInFile ? true : (message == null && t == null);
		if (logInFile && (message != null || t != null)) {
			logResult = logMessage(LogService.LOG_DEBUG, message, t);
		}

		/* Checks are added for different framework implementations. */
		if (printOnConsole || (!logResult && autoPrintOnConsole)) {
			message = buildDebugString(moduleID, msgID, msg, "DEBUG " + bundleId + " ");
			dumpOnConsole(message, t);
		}
	}

	private void initSysServices() {
		if (security) {
			try {
				securityUtil.doPrivileged(this, GET_SYS_SERVICES_TYPE, null);
			} catch (Throwable ignore) {
				ignore.printStackTrace();
			}
		}
	}

	LogService getService0() throws IllegalArgumentException {
		synchronized (logs) {
			ServiceReference<LogService> logRef = listener.logTracker.getServiceReference();
			LogService ls = null;
			if (logRef != null) {
				ls = bc.getService(logRef);
			}
			return ls;
		}
	}

	private LogService getService() throws IllegalArgumentException {
		if (bc == null)
			return null; // standalone hack
		try {
			return (LogService) securityUtil.doPrivileged(this, GET_SERVICE_TYPE, null);
		} catch (IllegalArgumentException e) {
			throw e;
		} catch (Exception e) {
			/* this will not happen */
			e.printStackTrace();
			throw new IllegalArgumentException(e.toString());
		}
	}

	private boolean logMessage(int messageType, String messageText, Throwable t) {
		LogService ls = null;

		try {
			ls = getService();
		} catch (IllegalStateException ise) { // invalid bundle context
			synchronized (logs) {
				close0();
			}
			return false;
		}
		boolean result = true;
		if (ls == null) {
			result = false;
			testClose(); // test the listener
		} else {
			try {
				ls.log(messageType, messageText, t);
			} catch (IllegalStateException ise) { // the log service instance
				// is not valid
				result = false;
				testClose(); // test the listener
			}
		}
		return result;
	}

	private void dumpOnConsole(String prefix, String msg, long bundleId, Throwable t) {
		System.out.println(prefix + bundleId + " " + msg);
		if (t != null) {
			t.printStackTrace();
		}
	}

	/**
	 * Prints a message to the console or a log dispatcher
	 * 
	 * @param msg -
	 *            the message to print, which contains the prefix and the
	 *            bundleID in itself
	 * @param t
	 *            throwable object, which stack trace should be printed
	 */
	private void dumpOnConsole(String msg, Throwable t) {
		System.out.println(msg);
		if (t != null) {
			t.printStackTrace();
		}
	}

	private void testClose() {
		synchronized (logs) {
			if (listener != null)
				try {
					listener.bc.getBundle();
					return;
				} catch (IllegalStateException ise) {
					listener.close0();
				}
		}
	}

	/**
	 * Releases the Log's resources: ungets LogService, removes the
	 * ServiceListener from the framework and nulls references. After invocation
	 * of this method, this Log object can be used no longer.
	 */
	@Override
	public void close() {
		if (bc != null) {
			synchronized (logs) {
				close0();
			}
		}
	}

	private boolean close0() {
		logs.removeElement(this);
		isClosed = true;
		if (listener == this) {
			try {
				logTracker.close();
				logTracker = null;
			} catch (IllegalStateException ise) {
			}
			Log ls = null;
			while (logs.size() > 0) {
				ls = logs.elementAt(0);
				try {
					ls.initListener();
					break;
				} catch (IllegalStateException ise) {
					logs.removeElementAt(0);
					ls = null;
				}
			}
			listener = ls;
		}
		return (listener != null);
	}

	/**
	 * enable/disable print on console
	 * 
	 * @param value
	 *            boolean if true enables print on console else disables it
	 */
	@Override
	public void setPrintOnConsole(boolean value) {
		printOnConsole = value;
	}

	/**
	 * enable/disable loging of debug info
	 * 
	 * @param value
	 *            boolean if true enables loging of debug info else disables it
	 */
	@Override
	public void setDebug(boolean value) {
		debug = value;
	}

	/**
	 * Gets the flag, which enables logging debug messages.
	 * 
	 * @return true if debugging is enabled
	 */
	@Override
	public boolean getDebug() {
		return debug;
	}

	/**
	 * Gets the flag, which enables printing log messages on the console.
	 * 
	 * @return true if printingon console is enabled
	 */
	@Override
	public boolean getPrintOnConsole() {
		return printOnConsole;
	}

	@Override
	public LogService addingService(ServiceReference<LogService> reference) {
		return bc.getService(reference);
	}

	@Override
	public void modifiedService(ServiceReference<LogService> reference, LogService service) {
	}

	@Override
	public void removedService(ServiceReference<LogService> reference, LogService service) {
	}

	private HashIntObjNS map = null;
	private HashIntObjNS starts = null;

	public void setMaps(HashIntObjNS map0, HashIntObjNS starts0) {
		map = map0;
		starts = starts0;
	}

	private String getModuleName(int moduleID) {
		return (String) map.get(-moduleID);
	}

	private String getMsgValue(int msgID) {
		String res = (String) map.get(msgID);
		if (res == null && starts != null) {
			try {
				int startID = ((Integer) starts.get(msgID)).intValue();
				res = "END OF " + (String) map.get(startID);
			} catch (Exception e) {
			}
		}
		return res;
	}

	private static final char[] chars = {']', ' ', ':', ' '};

	private String buildDebugString(int moduleID, int msgID, String message, String prefix) {

		if (map == null) {
			return prefix + " [" + moduleID + "] " + msgID + " : " + (message == null ? "" : message);
		}

		StringBuilder sBuf = new StringBuilder(prefix).append("[");

		String module = getModuleName(moduleID);
		sBuf.append(module != null ? module : String.valueOf(moduleID));

		sBuf.append(chars, 0, 2);

		if (msgID != 0) {
			// map msgID to String
			String msg = getMsgValue(msgID);
			sBuf.append(msg != null ? msg : String.valueOf(msgID));
		}

		if (message != null)
			sBuf.append(chars, 2, 2).append(message);

		return sBuf.toString();
	}

	// public String toString() {
	// return bc.toString();
	// }

	private static boolean security = false;

	public static boolean security() {
		if (UtilActivator.bc != null) {
			try {
				Object ssecurity = UtilActivator.bc.getProperty("equinox.security");
				security = ssecurity != null && !"none".equals(ssecurity);
			} catch (Throwable t) {
				// no security implementation.
			}
		} else {
			try {
				Object ssecurity = System.getProperty("equinox.security");
				security = ssecurity != null && !"none".equals(ssecurity);
			} catch (Throwable t) {
				// no security implementation.
			}
		}
		return security;
	}

	String baseName;
	boolean synch;
	FileOutputStream fos = null;
	static String logsdir = null;

	static Queue queue;
	static boolean running = false;
	static Saver saver;
	// boolean date = false;
	long lastTime;

	// byte[] bdate;

	public Log(String baseName, boolean synch, BundleContext bc) {
		this(bc);
		this.baseName = baseName;
		this.synch = synch;
		if (!synch)
			synchronized (Log.class) {
				if (queue == null) {
					queue = new Queue(20);
					saver = new Saver();
				}
			}
	}

	public void trace(byte[] bytes) {
		if (!synch)
			synchronized (queue) {
				queue.put(this);
				queue.put(bytes);
				if (!running) {
					running = true;
					UtilActivator.thMan.execute(saver, "File Log Thread");
				}
			}
		else
			trace(bytes, 0, bytes.length);
	}

	public void trace(byte[] bytes, int off, int len) {
		if (logsdir == null)
			synchronized (Log.class) {
				if (logsdir == null) {
					try {
						logsdir = UtilActivator.bc.getProperty("equinox.logsDir");
						if (logsdir == null)
							logsdir = "./logs";
						File logs = new File(logsdir);
						logs.mkdirs();
					} catch (Exception exc) {
						exc.printStackTrace();
					}
				}
			}
		if (fos == null)
			synchronized (this) {
				if (fos == null) {
					StringBuilder fname = new StringBuilder(logsdir.length() + baseName.length() + 1);
					fname.append(logsdir).append(File.separatorChar).append(baseName);
					try {
						fos = new FileOutputStream(fname.toString(), true);
					} catch (IOException ioExc) {
						ioExc.printStackTrace();
					}
				}
			}
		try {
			fos.write(bytes, off, len);
			fos.write(10);
		} catch (IOException ioExc) {
			ioExc.printStackTrace();
		}
	}

	@Override
	public void finalize() {
		if (fos != null)
			try {
				fos.close();
			} catch (IOException ioExc) {
				ioExc.printStackTrace();
			}
	}

	/**
	 * Checks the auto print on console flag
	 * 
	 * @return true, if autoPrintOnConsole is enabled
	 */
	public boolean isAutoPrintOnConsole() {
		return autoPrintOnConsole;
	}

	/**
	 * Enables/disables auto printing on console. This flag points whether
	 * printing on console should be done if log service is not available
	 * 
	 * @param autoPrintOnConsole
	 *            if true enables auto print on console else disables it.
	 */
	public void setAutoPrintOnConsole(boolean autoPrintOnConsole) {
		this.autoPrintOnConsole = autoPrintOnConsole;
	}

	/**
	 * Returns whether printing on console should be done for errors
	 * (exceptions) and warnings
	 * 
	 * @return Returns the error log level flag - if true the error and warnings
	 *         will be print on console
	 */
	public boolean isLogErrorLevel() {
		return logErrorLevel;
	}

	private static final byte OPEN_TYPE = 0;
	private static final byte GET_SERVICE_TYPE = 1;
	private static final byte GET_SYS_SERVICES_TYPE = 2;

	/**
	 * @see org.eclipse.equinox.internal.util.security.PrivilegedRunner.PrivilegedDispatcher#dispatchPrivileged(int,
	 *      java.lang.Object, java.lang.Object, java.lang.Object,
	 *      java.lang.Object)
	 */
	@Override
	public Object dispatchPrivileged(int type, Object arg1, Object arg2, Object arg3, Object arg4) throws Exception {
		switch (type) {
			case OPEN_TYPE :
				initListener0();
				break;
			case GET_SERVICE_TYPE :
				return getService0();
			case GET_SYS_SERVICES_TYPE :
				break;
		}
		return null;
	}

}// Log class

class Saver implements Runnable {

	@Override
	public void run() {
		byte[] bytes = null;
		Log log = null;
		while (true) {
			synchronized (Log.queue) {
				log = (Log) Log.queue.get();
				if (log == null) {
					Log.running = false;
					bytes = null;
					return;
				}
				bytes = (byte[]) Log.queue.get();
			}
			log.trace(bytes, 0, bytes.length);
		}
	}
}
