/*
 * Copyright (C) The Apache Software Foundation. All rights reserved.
 *
 * This software is published under the terms of the Apache Software License
 * version 1.1, a copy of which has been included with this distribution in
 * the LICENSE file.
 */
package org.jivesoftware.util.log;

/**
 * The object interacted with by client objects to perform logging.
 *
 * @author <a href="mailto:peter@apache.org">Peter Donald</a>
 */
public class Logger {
    ///Separator character use to separate different categories
    public final static char CATEGORY_SEPARATOR = '.';

    ///The ErrorHandler associated with Logger
    private final ErrorHandler m_errorHandler;

    ///Logger to inherit logtargets and priorities from
    private final Logger m_parent;

    ///the fully qualified name of category
    private final String m_category;

    ///The list of child loggers associated with this logger
    private Logger[] m_children;

    ///The log-targets this logger writes to
    private LogTarget[] m_logTargets;

    ///Indicate that logTargets were set with setLogTargets() rather than inherited
    private boolean m_logTargetsForceSet;

    ///The priority threshold associated with logger
    private Priority m_priority;

    ///Indicate that priority was set with setPriority() rather than inherited
    private boolean m_priorityForceSet;

    /**
     * True means LogEvents will be sent to parents LogTargets
     * aswell as the ones set for this Logger.
     */
    private boolean m_additivity;

    /**
     * Protected constructor for use inside the logging toolkit.
     * You should not be using this constructor directly.
     *
     * @param errorHandler the ErrorHandler logger uses to log errors
     * @param category     the fully qualified name of category
     * @param logTargets   the LogTargets associated with logger
     * @param parent       the parent logger (used for inheriting from)
     */
    Logger(final ErrorHandler errorHandler,
           final String category,
           final LogTarget[] logTargets,
           final Logger parent) {
        m_errorHandler = errorHandler;
        m_category = category;
        m_logTargets = logTargets;
        m_parent = parent;

        if (null == m_logTargets) {
            unsetLogTargets();
        }

        unsetPriority();
    }

    /**
     * Determine if messages of priority DEBUG will be logged.
     *
     * @return true if DEBUG messages will be logged
     */
    public final boolean isDebugEnabled() {
        return m_priority.isLowerOrEqual(Priority.DEBUG);
    }

    /**
     * Log a debug priority event.
     *
     * @param message   the message
     * @param throwable the throwable
     */
    public final void debug(final String message, final Throwable throwable) {
        if (isDebugEnabled()) {
            output(Priority.DEBUG, message, throwable);
        }
    }

    /**
     * Log a debug priority event.
     *
     * @param message the message
     */
    public final void debug(final String message) {
        if (isDebugEnabled()) {
            output(Priority.DEBUG, message, null);
        }
    }

    /**
     * Determine if messages of priority INFO will be logged.
     *
     * @return true if INFO messages will be logged
     */
    public final boolean isInfoEnabled() {
        return m_priority.isLowerOrEqual(Priority.INFO);
    }

    /**
     * Log a info priority event.
     *
     * @param message the message
     */
    public final void info(final String message, final Throwable throwable) {
        if (isInfoEnabled()) {
            output(Priority.INFO, message, throwable);
        }
    }

    /**
     * Log a info priority event.
     *
     * @param message the message
     */
    public final void info(final String message) {
        if (isInfoEnabled()) {
            output(Priority.INFO, message, null);
        }
    }

    /**
     * Determine if messages of priority WARN will be logged.
     *
     * @return true if WARN messages will be logged
     */
    public final boolean isWarnEnabled() {
        return m_priority.isLowerOrEqual(Priority.WARN);
    }

    /**
     * Log a warn priority event.
     *
     * @param message   the message
     * @param throwable the throwable
     */
    public final void warn(final String message, final Throwable throwable) {
        if (isWarnEnabled()) {
            output(Priority.WARN, message, throwable);
        }
    }

    /**
     * Log a warn priority event.
     *
     * @param message the message
     */
    public final void warn(final String message) {
        if (isWarnEnabled()) {
            output(Priority.WARN, message, null);
        }
    }

    /**
     * Determine if messages of priority ERROR will be logged.
     *
     * @return true if ERROR messages will be logged
     */
    public final boolean isErrorEnabled() {
        return m_priority.isLowerOrEqual(Priority.ERROR);
    }

    /**
     * Log a error priority event.
     *
     * @param message   the message
     * @param throwable the throwable
     */
    public final void error(final String message, final Throwable throwable) {
        if (isErrorEnabled()) {
            output(Priority.ERROR, message, throwable);
        }
    }

    /**
     * Log a error priority event.
     *
     * @param message the message
     */
    public final void error(final String message) {
        if (isErrorEnabled()) {
            output(Priority.ERROR, message, null);
        }
    }

    /**
     * Determine if messages of priority FATAL_ERROR will be logged.
     *
     * @return true if FATAL_ERROR messages will be logged
     */
    public final boolean isFatalErrorEnabled() {
        return m_priority.isLowerOrEqual(Priority.FATAL_ERROR);
    }

    /**
     * Log a fatalError priority event.
     *
     * @param message   the message
     * @param throwable the throwable
     */
    public final void fatalError(final String message, final Throwable throwable) {
        if (isFatalErrorEnabled()) {
            output(Priority.FATAL_ERROR, message, throwable);
        }
    }

    /**
     * Log a fatalError priority event.
     *
     * @param message the message
     */
    public final void fatalError(final String message) {
        if (isFatalErrorEnabled()) {
            output(Priority.FATAL_ERROR, message, null);
        }
    }

    /**
     * Make this logger additive, which means send all log events to parent
     * loggers LogTargets regardless of whether or not the
     * LogTargets have been overidden.
     * <p/>
     * This is derived from Log4js notion of Additivity.
     *
     * @param additivity true to make logger additive, false otherwise
     */
    public final void setAdditivity(final boolean additivity) {
        m_additivity = additivity;
    }

    /**
     * Determine if messages of priority  will be logged.
     *
     * @return true if messages will be logged
     */
    public final boolean isPriorityEnabled(final Priority priority) {
        return m_priority.isLowerOrEqual(priority);
    }

    /**
     * Log a event at specific priority with a certain message and throwable.
     *
     * @param message   the message
     * @param priority  the priority
     * @param throwable the throwable
     */
    public final void log(final Priority priority,
                          final String message,
                          final Throwable throwable) {
        if (m_priority.isLowerOrEqual(priority)) {
            output(priority, message, throwable);
        }
    }

    /**
     * Log a event at specific priority with a certain message.
     *
     * @param message  the message
     * @param priority the priority
     */
    public final void log(final Priority priority, final String message) {
        log(priority, message, null);
    }

    /**
     * Set the priority for this logger.
     *
     * @param priority the priority
     */
    public synchronized void setPriority(final Priority priority) {
        m_priority = priority;
        m_priorityForceSet = true;
        resetChildPriorities(false);
    }

    /**
     * Unset the priority of Logger.
     * (Thus it will use it's parent's priority or DEBUG if no parent.
     */
    public synchronized void unsetPriority() {
        unsetPriority(false);
    }

    /**
     * Unset the priority of Logger.
     * (Thus it will use it's parent's priority or DEBUG if no parent.
     * If recursive is true unset priorities of all child loggers.
     *
     * @param recursive true to unset priority of all child loggers
     */
    public synchronized void unsetPriority(final boolean recursive) {
        if (null != m_parent)
            m_priority = m_parent.m_priority;
        else
            m_priority = Priority.DEBUG;

        m_priorityForceSet = false;
        resetChildPriorities(recursive);
    }

    /**
     * Set the log targets for this logger.
     *
     * @param logTargets the Log Targets
     */
    public synchronized void setLogTargets(final LogTarget[] logTargets) {
        m_logTargets = logTargets;
        setupErrorHandlers();
        m_logTargetsForceSet = true;
        resetChildLogTargets(false);
    }

    /**
     * Unset the logtargets for this logger.
     * This logger (and thus all child loggers who don't specify logtargets) will
     * inherit from the parents LogTargets.
     */
    public synchronized void unsetLogTargets() {
        unsetLogTargets(false);
    }

    /**
     * Unset the logtargets for this logger and all child loggers if recursive is set.
     * The loggers unset (and all child loggers who don't specify logtargets) will
     * inherit from the parents LogTargets.
     */
    public synchronized void unsetLogTargets(final boolean recursive) {
        if (null != m_parent)
            m_logTargets = m_parent.safeGetLogTargets();
        else
            m_logTargets = null;

        m_logTargetsForceSet = false;
        resetChildLogTargets(recursive);
    }

    /**
     * Get all the child Loggers of current logger.
     *
     * @return the child loggers
     */
    public synchronized Logger[] getChildren() {
        if (null == m_children) return new Logger[0];

        final Logger[] children = new Logger[m_children.length];

        for (int i = 0; i < children.length; i++) {
            children[i] = m_children[i];
        }

        return children;
    }

    /**
     * Create a new child logger.
     * The category of child logger is [current-category].subcategory
     *
     * @param subCategory the subcategory of this logger
     * @return the new logger
     * @throws IllegalArgumentException if subCategory has an empty element name
     */
    public synchronized Logger getChildLogger(final String subCategory)
            throws IllegalArgumentException {
        final int end = subCategory.indexOf(CATEGORY_SEPARATOR);

        String nextCategory = null;
        String remainder = null;

        if (-1 == end)
            nextCategory = subCategory;
        else {
            if (end == 0) {
                throw new IllegalArgumentException("Logger categories MUST not have empty elements");
            }

            nextCategory = subCategory.substring(0, end);
            remainder = subCategory.substring(end + 1);
        }

        //Get FQN for category
        String category = null;
        if (m_category.equals(""))
            category = nextCategory;
        else {
            category = m_category + CATEGORY_SEPARATOR + nextCategory;
        }

        //Check existing children to see if they
        //contain next Logger for step in category
        if (null != m_children) {
            for (int i = 0; i < m_children.length; i++) {
                if (m_children[i].m_category.equals(category)) {
                    if (null == remainder)
                        return m_children[i];
                    else
                        return m_children[i].getChildLogger(remainder);
                }
            }
        }

        //Create new logger
        final Logger child = new Logger(m_errorHandler, category, null, this);

        //Add new logger to child list
        if (null == m_children) {
            m_children = new Logger[]{child};
        }
        else {
            final Logger[] children = new Logger[m_children.length + 1];
            System.arraycopy(m_children, 0, children, 0, m_children.length);
            children[m_children.length] = child;
            m_children = children;
        }

        if (null == remainder)
            return child;
        else
            return child.getChildLogger(remainder);
    }

    /**
     * Retrieve priority associated with Logger.
     *
     * @return the loggers priority
     * @deprecated This method violates Inversion of Control principle.
     *             It will downgraded to protected access in a future
     *             release. When user needs to check priority it is advised
     *             that they use the is[Priority]Enabled() functions.
     */
    public final Priority getPriority() {
        return m_priority;
    }

    /**
     * Retrieve category associated with logger.
     *
     * @return the Category
     * @deprecated This method violates Inversion of Control principle.
     *             If you are relying on its presence then there may be
     *             something wrong with the design of your system
     */
    public final String getCategory() {
        return m_category;
    }

    /**
     * Get a copy of log targets for this logger.
     *
     * @return the child loggers
     * @deprecated This method is deprecated and will be removed in Future version.
     *             Previously it allowed unsafe access to logtargets which permitted
     *             masqurade attacks. It currently returns a zero sized array.
     */
    public LogTarget[] getLogTargets() {
        return new LogTarget[0];
    }

    /**
     * Internal method to do actual outputting.
     *
     * @param priority  the priority
     * @param message   the message
     * @param throwable the throwable
     */
    private final void output(final Priority priority,
                              final String message,
                              final Throwable throwable) {
        final LogEvent event = new LogEvent();
        event.setCategory(m_category);
//        event.setContextStack( ContextStack.getCurrentContext( false ) );
        event.setContextMap(ContextMap.getCurrentContext(false));

        if (null != message) {
            event.setMessage(message);
        }
        else {
            event.setMessage("");
        }

        event.setThrowable(throwable);
        event.setPriority(priority);

        //this next line can kill performance. It may be wise to
        //disable it sometimes and use a more granular approach
        event.setTime(System.currentTimeMillis());

        output(event);
    }

    private final void output(final LogEvent event) {
        //cache a copy of targets for thread safety
        //It is now possible for another thread
        //to replace m_logTargets
        final LogTarget[] targets = m_logTargets;

        if (null == targets) {
            final String message = "LogTarget is null for category '" + m_category + "'";
            m_errorHandler.error(message, null, event);
        }
        else if (!m_additivity) {
            fireEvent(event, targets);
        }
        else {
            //If log targets were not inherited, additivity is true
            //then fire an event to local targets
            if (m_logTargetsForceSet) {
                fireEvent(event, targets);
            }

            //if we have a parent Logger then send log event to parent
            if (null != m_parent) {
                m_parent.output(event);
            }
        }
    }

    private final void fireEvent(final LogEvent event, final LogTarget[] targets) {
        for (int i = 0; i < targets.length; i++) {
            //No need to clone array as addition of a log-target
            //will result in changin whole array
            targets[i].processEvent(event);
        }
    }

    /**
     * Update priority of children if any.
     */
    private synchronized void resetChildPriorities(final boolean recursive) {
        if (null == m_children) return;

        final Logger[] children = m_children;

        for (int i = 0; i < children.length; i++) {
            children[i].resetPriority(recursive);
        }
    }

    /**
     * Update priority of this Logger.
     * If this loggers priority was manually set then ignore
     * otherwise get parents priority and update all children's priority.
     */
    private synchronized void resetPriority(final boolean recursive) {
        if (recursive) {
            m_priorityForceSet = false;
        }
        else if (m_priorityForceSet) {
            return;
        }

        m_priority = m_parent.m_priority;
        resetChildPriorities(recursive);
    }

    /**
     * Retrieve logtarget array contained in logger.
     * This method is provided so that child Loggers can access a
     * copy of  parents LogTargets.
     *
     * @return the array of LogTargets
     */
    private synchronized LogTarget[] safeGetLogTargets() {
        if (null == m_logTargets) {
            if (null == m_parent)
                return new LogTarget[0];
            else
                return m_parent.safeGetLogTargets();
        }
        else {
            final LogTarget[] logTargets = new LogTarget[m_logTargets.length];

            for (int i = 0; i < logTargets.length; i++) {
                logTargets[i] = m_logTargets[i];
            }

            return logTargets;
        }
    }

    /**
     * Update logTargets of children if any.
     */
    private synchronized void resetChildLogTargets(final boolean recursive) {
        if (null == m_children) return;

        for (int i = 0; i < m_children.length; i++) {
            m_children[i].resetLogTargets(recursive);
        }
    }

    /**
     * Set ErrorHandlers of LogTargets if necessary.
     */
    private synchronized void setupErrorHandlers() {
        if (null == m_logTargets) return;

        for (int i = 0; i < m_logTargets.length; i++) {
            final LogTarget target = m_logTargets[i];
            if (target instanceof ErrorAware) {
                ((ErrorAware)target).setErrorHandler(m_errorHandler);
            }
        }
    }

    /**
     * Update logTarget of this Logger.
     * If this loggers logTarget was manually set then ignore
     * otherwise get parents logTarget and update all children's logTarget.
     */
    private synchronized void resetLogTargets(final boolean recursive) {
        if (recursive) {
            m_logTargetsForceSet = false;
        }
        else if (m_logTargetsForceSet) {
            return;
        }

        m_logTargets = m_parent.safeGetLogTargets();
        resetChildLogTargets(recursive);
    }
}