/*
 * SPDX-License-Identifier: LGPL-2.1-only
 *
 * Copyright (C) 2013 David Goulet <dgoulet@efficios.com>
 */

package org.lttng.ust.agent;

import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.logging.Handler;
import java.util.logging.Logger;

/**
 * The central agent managing the JUL and Log4j handlers.
 *
 * @author David Goulet
 * @deprecated Applications are now expected to manage their Logger and Handler
 *             objects.
 */
@Deprecated
public class LTTngAgent {

	private static LTTngAgent instance = null;

	/**
	 * Public getter to acquire a reference to this singleton object.
	 *
	 * @return The agent instance
	 */
	public static synchronized LTTngAgent getLTTngAgent() {
		if (instance == null) {
			instance = new LTTngAgent();
		}
		return instance;
	}

	/**
	 * Dispose the agent. Applications should call this once they are done
	 * logging. This dispose function is non-static for backwards
	 * compatibility purposes.
	 */
	@SuppressWarnings("static-method")
	public void dispose() {
		synchronized (LTTngAgent.class) {
			if (instance != null) {
				instance.disposeInstance();
				instance = null;
			}
		}
		return;
	}

	private ILttngHandler julHandler = null;
	private ILttngHandler log4jAppender = null;

	/**
	 * Private constructor. This is a singleton and a reference should be
	 * acquired using {@link #getLTTngAgent()}.
	 */
	private LTTngAgent() {
		initJulHandler();
		initLog4jAppender();
	}

	/**
	 * "Destructor" method.
	 */
	private void disposeInstance() {
		disposeJulHandler();
		disposeLog4jAppender();
	}

	/**
	 * Create a LTTng-JUL handler, and attach it to the JUL root logger.
	 */
	private void initJulHandler() {
		try {
			Class<?> julHandlerClass = Class.forName("org.lttng.ust.agent.jul.LttngLogHandler");
			/*
			 * It is safer to use Constructor.newInstance() rather than
			 * Class.newInstance(), because it will catch the exceptions thrown
			 * by the constructor below (which happens if the Java library is
			 * present, but the matching JNI one is not).
			 */
			Constructor<?> julHandlerCtor = julHandlerClass.getConstructor();
			julHandler = (ILttngHandler) julHandlerCtor.newInstance();

			/* Attach the handler to the root JUL logger */
			Logger.getLogger("").addHandler((Handler) julHandler);

			/*
			 * If any of the following exceptions happen, it means we could not
			 * find or initialize LTTng JUL classes. We will not setup LTTng JUL
			 * tracing in this case.
			 */
		} catch (SecurityException e) {
		} catch (IllegalAccessException e) {
		} catch (IllegalArgumentException e) {
		} catch (ClassNotFoundException e) {
		} catch (NoSuchMethodException e) {
		} catch (InstantiationException e) {
		} catch (InvocationTargetException e) {
		}
	}

	/**
	 * Create a LTTng-logj4 appender, and attach it to the log4j root logger.
	 */
	private void initLog4jAppender() {
		/*
		 * Since Log4j is a 3rd party library, we first need to check if we can
		 * load any of its classes.
		 */
		if (!testLog4jClasses()) {
			return;
		}

		try {
			Class<?> log4jAppenderClass = Class.forName("org.lttng.ust.agent.log4j.LttngLogAppender");
			Constructor<?> log4jAppendCtor = log4jAppenderClass.getConstructor();
			log4jAppender = (ILttngHandler) log4jAppendCtor.newInstance();

			/*
			 * If any of the following exceptions happen, it means we could not
			 * find or initialize LTTng log4j classes. We will not setup LTTng
			 * log4j tracing in this case.
			 */
		} catch (SecurityException e) {
			return;
		} catch (ClassNotFoundException e) {
			return;
		} catch (NoSuchMethodException e) {
			return;
		} catch (IllegalArgumentException e) {
			return;
		} catch (InstantiationException e) {
			return;
		} catch (IllegalAccessException e) {
			return;
		} catch (InvocationTargetException e) {
			return;
		}

		/*
		 * Attach the appender to the root Log4j logger. Slightly more tricky
		 * here, as log4j.Logger is not in the base Java library, and we do not
		 * want the "common" package to depend on log4j. So we have to obtain it
		 * through reflection too.
		 */
		try {
			Class<?> loggerClass = Class.forName("org.apache.log4j.Logger");
			Class<?> appenderClass = Class.forName("org.apache.log4j.Appender");

			Method getRootLoggerMethod = loggerClass.getMethod("getRootLogger", (Class<?>[]) null);
			Method addAppenderMethod = loggerClass.getMethod("addAppender", appenderClass);

			Object rootLogger = getRootLoggerMethod.invoke(null, (Object[]) null);
			addAppenderMethod.invoke(rootLogger, log4jAppender);

			/*
			 * We have checked for the log4j library version previously, none of
			 * the following exceptions should happen.
			 */
		} catch (SecurityException e) {
			throw new IllegalStateException(e);
		} catch (ClassNotFoundException e) {
			throw new IllegalStateException(e);
		} catch (NoSuchMethodException e) {
			throw new IllegalStateException(e);
		} catch (IllegalArgumentException e) {
			throw new IllegalStateException(e);
		} catch (IllegalAccessException e) {
			throw new IllegalStateException(e);
		} catch (InvocationTargetException e) {
			throw new IllegalStateException(e);
		}
	}

	/**
	 * Check if log4j >= 1.2.15 library is present.
	 */
	private static boolean testLog4jClasses() {
		Class<?> loggingEventClass;

		try {
			loggingEventClass = Class.forName("org.apache.log4j.spi.LoggingEvent");
		} catch (ClassNotFoundException e) {
			/*
			 * Log4j classes not found, no need to create the relevant objects
			 */
			return false;
		}

		/*
		 * Detect capabilities of the log4j library. We only support log4j >=
		 * 1.2.15. The getTimeStamp() method was introduced in log4j 1.2.15, so
		 * verify that it is available.
		 *
		 * We can't rely on the getPackage().getImplementationVersion() call
		 * that would retrieves information from the manifest file found in the
		 * JAR since the manifest file shipped from upstream is known to be
		 * broken in several versions of the library.
		 *
		 * More info: https://issues.apache.org/bugzilla/show_bug.cgi?id=44370
		 */
		try {
			loggingEventClass.getDeclaredMethod("getTimeStamp");
		} catch (NoSuchMethodException e) {
			System.err.println(
					"Warning: The loaded log4j library is too old. Log4j tracing with LTTng will be disabled.");
			return false;
		} catch (SecurityException e) {
			return false;
		}

		return true;
	}

	/**
	 * Detach the JUL handler from its logger and close it.
	 */
	private void disposeJulHandler() {
		if (julHandler == null) {
			/* The JUL handler was not activated, we have nothing to do */
			return;
		}
		Logger.getLogger("").removeHandler((Handler) julHandler);
		julHandler.close();
		julHandler = null;
	}

	/**
	 * Detach the log4j appender from its logger and close it.
	 */
	private void disposeLog4jAppender() {
		if (log4jAppender == null) {
			/* The log4j appender was not active, we have nothing to do */
			return;
		}

		/*
		 * Detach the appender from the log4j root logger. Again, we have to do
		 * this via reflection.
		 */
		try {
			Class<?> loggerClass = Class.forName("org.apache.log4j.Logger");
			Class<?> appenderClass = Class.forName("org.apache.log4j.Appender");

			Method getRootLoggerMethod = loggerClass.getMethod("getRootLogger", (Class<?>[]) null);
			Method removeAppenderMethod = loggerClass.getMethod("removeAppender", appenderClass);

			Object rootLogger = getRootLoggerMethod.invoke(null, (Object[]) null);
			removeAppenderMethod.invoke(rootLogger, log4jAppender);

			/*
			 * We were able to attach the appender previously, we should not
			 * have problems here either!
			 */
		} catch (SecurityException e) {
			throw new IllegalStateException(e);
		} catch (ClassNotFoundException e) {
			throw new IllegalStateException(e);
		} catch (NoSuchMethodException e) {
			throw new IllegalStateException(e);
		} catch (IllegalArgumentException e) {
			throw new IllegalStateException(e);
		} catch (IllegalAccessException e) {
			throw new IllegalStateException(e);
		} catch (InvocationTargetException e) {
			throw new IllegalStateException(e);
		}

		/* Close the appender */
		log4jAppender.close();
		log4jAppender = null;
	}

}
