package org.ovirt.engine.core.utils.log;

import org.apache.commons.lang.exception.ExceptionUtils;
import org.ovirt.engine.core.utils.log.Logged.LogLevel;

/**
 * Utilities for logging commands using the {@link Logged} annotation for configuring the logging behavior of a class.<br>
 * Usage example:
 *
 * <pre>
 * public void execute() {
 *     String logId = LoggedUtils.createLogId();
 *     <b>LoggedUtils.logEntry(log, logId, this);</b>
 *
 *     try {
 *         Object returnValue = executeCommand();
 *         <b>LoggedUtils.logReturn(log, logId, this, returnValue);</b>
 *         return returnValue;
 *     } catch (Exception e) {
 *         <b>LoggedUtils.logError(log, logId, this, e);</b>
 *         throw e;
 *     }
 * }
 * </pre>
 */
public class LoggedUtils {

    /* --- Constants --- */

    /**
     * Log format for entry log message.
     */
    protected static final String ENTRY_LOG = "START, {0}, log id: {1}";

    /**
     * Log format for return log message with no return value.
     */
    protected static final String EXIT_LOG_VOID = "FINISH, {0}, log id: {1}";

    /**
     * Log format for return log message with a return value.
     */
    protected static final String EXIT_LOG_RETURN_VALUE = "FINISH, {0}, return: {1}, log id: {2}";

    /**
     * Log format for exception log message with exception message.
     */
    protected static final String ERROR_LOG = "ERROR, {0}, exception: {1}, log id: {2}";

    /* --- Public Methods --- */

    /**
     * Get the object (reference) id for an object. This should be a rather unique value, since it's based on the
     * object's hash code.
     *
     * @param obj
     *            The object to get the id for (can be <code>null</code>).
     * @return The id, in HEX representation (for <code>null</code> it's 0).
     * @see System#identityHashCode(Object)
     */
    public static String getObjectId(Object obj) {
        return Integer.toHexString(System.identityHashCode(obj));
    }

    /**
     * Log entry into a command to the given log, using the specification in the {@link Logged} annotation on the
     * object's. If there's no {@link Logged} annotation present, or if the log level isn't sufficient to log, then
     * nothing happens.
     *
     * @param log
     *            The log to log to.
     * @param id
     *            ID of the message, for finding the return message.
     * @param obj
     *            The object to log for.
     */
    public static void logEntry(Log log, String id, Object obj) {
        Logged logged = getAnnotation(obj);
        if (logged != null && isLogLevelOn(log, logged.executionLevel())) {
            log(log, logged.executionLevel(), ENTRY_LOG, determineMessage(log, logged, obj), id);
        }
    }

    /**
     * Log return of a command to the given log, using the specification in the {@link Logged} annotation on the
     * object's. If there's no {@link Logged} annotation present, or if the log level isn't sufficient to log, then
     * nothing happens.
     *
     * @param log
     *            The log to log to.
     * @param id
     *            ID of the message, for finding the entry message.
     * @param obj
     *            The object to log for.
     */
    public static void logReturn(Log log, String id, Object obj, Object returnValue) {
        Logged logged = getAnnotation(obj);
        if (logged != null) {
            LogLevel logLevel = logged.executionLevel();
            if (isLogLevelOn(log, logLevel)) {
                if (returnValue == null
                        || !isLogLevelOn(log, LogLevel.getMinimalLevel(logLevel, logged.returnLevel()))) {
                    log(log, logLevel, EXIT_LOG_VOID, obj.getClass().getSimpleName(), id);
                } else {
                    log(log, logLevel, EXIT_LOG_RETURN_VALUE, obj.getClass().getSimpleName(), returnValue, id);
                }
            }
        }
    }

    /**
     * Log error of a command to the given log, using the specification in the {@link Logged} annotation on the
     * object's. If there's no {@link Logged} annotation present, or if the log level isn't sufficient to log, then
     * nothing happens.
     *
     * @param log
     *            The log to log to.
     * @param id
     *            ID of the message, for finding the entry message.
     * @param obj
     *            The object to log for.
     */
    public static void logError(Log log, String id, Object obj, Throwable t) {
        Logged logged = getAnnotation(obj);
        if (logged != null && isLogLevelOn(log, logged.errorLevel())) {
            log(log, logged.errorLevel(), ERROR_LOG, determineMessage(log, logged, obj),
                    ExceptionUtils.getMessage(t), id, t);
        }
    }

    /* --- Helper Methods --- */

    /**
     * Log a message to the log, taking into account the log level (checking if it's active, and using it in the log).
     * The message may contain parameters as in the {@code MessageFormat} specification, and they will undergo
     * transformation only if the log level is sufficient for the given log.
     *
     * @param log
     *            The log to log to.
     * @param logLevel
     *            The log level (may not be null).
     * @param message
     *            The message to log.
     * @param parameters
     *            Optional parameters for the message.
     * @see MessageFormat#format(Object[], StringBuffer, java.text.FieldPosition)
     */
    protected static void log(Log log, Logged.LogLevel logLevel, String message, Object... parameters) {
        try {
            if (isLogLevelOn(log, logLevel)) {
                switch (logLevel) {
                case FATAL:
                    log.fatalFormat(message, parameters);
                    break;
                case ERROR:
                    log.errorFormat(message, parameters);
                    break;
                case WARN:
                    log.warnFormat(message, parameters);
                    break;
                case INFO:
                    log.infoFormat(message, parameters);
                    break;
                case DEBUG:
                    log.debugFormat(message, parameters);
                    break;
                case TRACE:
                    log.traceFormat(message, parameters);
                    break;
                case OFF:
                default:
                    break;
                }
            }
        } catch (Throwable th) {
            try {
                log.error("Cannot perform logging", th);
            } catch (Throwable th1) {
                // Cannot really do any logging - better not try again, we have a serious logging problem
            }
        }
    }

    /**
     * Return the annotation present on the object's class. Since the annotation is inherited, it can be places on a
     * superclass and retrieved from there.
     *
     * @param obj
     *            The object to get the annotation for.
     * @return The {@link Logged} annotation from the class, or <code>null</code> if there isn't any.
     */
    protected static Logged getAnnotation(Object obj) {
        return (obj == null ? null : obj.getClass().getAnnotation(Logged.class));
    }

    /**
     * Determine if the given log level is active for the given log.
     *
     * @param log
     *            The log to check.
     * @param logLevel
     *            The log level to check if active.
     * @return Whether the log level is active on the log or not.
     */
    protected static boolean isLogLevelOn(Log log, Logged.LogLevel logLevel) {
        switch (logLevel) {
        case FATAL:
            return log.isFatalEnabled();
        case ERROR:
            return log.isErrorEnabled();
        case WARN:
            return log.isWarnEnabled();
        case INFO:
            return log.isInfoEnabled();
        case DEBUG:
            return log.isDebugEnabled();
        case TRACE:
            return log.isTraceEnabled();
        case OFF:
        default:
            return false;
        }
    }

    /**
     * Determine the message to print, according to whether the parameters should be expanded or not.
     *
     * @param log
     *            The log is needed to check if the parameters should be printed according to their level.
     * @param logged
     *            The logging definition, for determining whether to expand the parameters or not.
     * @param obj
     *            The object for which to get the message.
     *
     * @return The message for logging.
     */
    protected static Object determineMessage(Log log, Logged logged, Object obj) {
        if (logged != null
                && !isLogLevelOn(log, LogLevel.getMinimalLevel(logged.parametersLevel(), logged.executionLevel()))) {
            return obj.getClass().getName();
        }

        return obj;
    }
}
