/*
 *  Licensed to the Apache Software Foundation (ASF) under one or more
 *  contributor license agreements.  See the NOTICE file distributed with
 *  this work for additional information regarding copyright ownership.
 *  The ASF licenses this file to You under the Apache License, Version 2.0
 *  (the "License"); you may not use this file except in compliance with
 *  the License.  You may obtain a copy of the License at
 *
 *      https://www.apache.org/licenses/LICENSE-2.0
 *
 *  Unless required by applicable law or agreed to in writing, software
 *  distributed under the License is distributed on an "AS IS" BASIS,
 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *  See the License for the specific language governing permissions and
 *  limitations under the License.
 *
 */

package org.apache.tools.ant.listener;

import java.io.PrintStream;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogConfigurationException;
import org.apache.commons.logging.LogFactory;
import org.apache.tools.ant.BuildEvent;
import org.apache.tools.ant.BuildListener;
import org.apache.tools.ant.BuildLogger;
import org.apache.tools.ant.MagicNames;
import org.apache.tools.ant.Project;
import org.apache.tools.ant.Task;
import org.apache.tools.ant.UnknownElement;


/**
 * Jakarta Commons Logging listener.
 * Note: do not use the SimpleLog as your logger implementation as it
 * causes an infinite loop since it writes to System.err, which Ant traps
 * and reroutes to the logger/listener layer.
 *
 * The following names are used for the log:
 *  org.apache.tools.ant.Project.PROJECT_NAME  - for project events
 *  org.apache.tools.ant.Target.TARGET_NAME - for target events
 *  TASK_CLASS_NAME.TARGET_NAME - for events in individual targets.
 *
 * In all target and project names we replace "." and " " with "-".
 *
 * TODO: we should use the advanced context logging features (and expose them
 * in c-l first :-)
 * TODO: this is _very_ inefficient. Switching the out and tracking the logs
 * can be optimized a lot - but may require few more changes to the core.
 *
 * @since Ant 1.5
 */
public class CommonsLoggingListener implements BuildListener, BuildLogger {

    /** Indicates if the listener was initialized. */
    private boolean initialized = false;

    private LogFactory logFactory;

    /**
     * name of the category under which target events are logged
     */
    public static final String TARGET_LOG = MagicNames.ANT_CORE_PACKAGE + ".Target";
    /**
     * name of the category under which project events are logged
     */
    public static final String PROJECT_LOG = MagicNames.ANT_CORE_PACKAGE + ".Project";

    /**
     * Construct the listener and make sure that a LogFactory
     * can be obtained.
     */
    public CommonsLoggingListener() {
    }

    private Log getLog(String cat, String suffix) {
        if (suffix != null) {
            suffix = suffix.replace('.', '-');
            suffix = suffix.replace(' ', '-');
            cat += "." + suffix;
        }
        final PrintStream tmpOut = System.out;
        final PrintStream tmpErr = System.err;
        System.setOut(out);
        System.setErr(err);

        if (!initialized) {
            try {
                logFactory = LogFactory.getFactory();
            } catch (final LogConfigurationException e) {
                e.printStackTrace(System.err);
                return null;
            }
        }

        initialized = true;
        final Log log = logFactory.getInstance(cat);
        System.setOut(tmpOut);
        System.setErr(tmpErr);
        return log;
    }

    /** {@inheritDoc}. */
    @Override
    public void buildStarted(final BuildEvent event) {
        final Log log = getLog(PROJECT_LOG, null);

        if (initialized) {
            realLog(log, "Build started.", Project.MSG_INFO, null);
        }
    }

    /** {@inheritDoc}. */
    @Override
    public void buildFinished(final BuildEvent event) {
        if (initialized) {
            final Log log = getLog(PROJECT_LOG, event.getProject().getName());

            if (event.getException() == null) {
                realLog(log, "Build finished.", Project.MSG_INFO, null);
            } else {
                realLog(log, "Build finished with error.", Project.MSG_ERR,
                        event.getException());
            }
        }
    }

    /**
     * @see BuildListener#targetStarted
     * {@inheritDoc}.
     */
    @Override
    public void targetStarted(final BuildEvent event) {
        if (initialized) {
            final Log log = getLog(TARGET_LOG,
                    event.getTarget().getName());
            // Since task log category includes target, we don't really
            // need this message
            realLog(log, "Start: " + event.getTarget().getName(),
                    Project.MSG_VERBOSE, null);
        }
    }

    /**
     * @see BuildListener#targetFinished
     * {@inheritDoc}.
     */
    @Override
    public void targetFinished(final BuildEvent event) {
        if (initialized) {
            final String targetName = event.getTarget().getName();
            final Log log = getLog(TARGET_LOG,
                    event.getTarget().getName());
            if (event.getException() == null) {
                realLog(log, "Target end: " + targetName, Project.MSG_DEBUG, null);
            } else {
                realLog(log, "Target \"" + targetName
                        + "\" finished with error.", Project.MSG_ERR,
                        event.getException());
            }
        }
    }

    /**
     * @see BuildListener#taskStarted
     * {@inheritDoc}.
     */
    @Override
    public void taskStarted(final BuildEvent event) {
        if (initialized) {
            final Task task = event.getTask();
            Object real = task;
            if (task instanceof UnknownElement) {
                final Object realObj = ((UnknownElement) task).getTask();
                if (realObj != null) {
                    real = realObj;
                }
            }
            final Log log = getLog(real.getClass().getName(), null);
            if (log.isTraceEnabled()) {
                realLog(log, "Task \"" + task.getTaskName() + "\" started ",
                        Project.MSG_VERBOSE, null);
            }
        }
    }

    /**
     * @see BuildListener#taskFinished
     * {@inheritDoc}.
     */
    @Override
    public void taskFinished(final BuildEvent event) {
        if (initialized) {
            final Task task = event.getTask();
            Object real = task;
            if (task instanceof UnknownElement) {
                final Object realObj = ((UnknownElement) task).getTask();
                if (realObj != null) {
                    real = realObj;
                }
            }
            final Log log = getLog(real.getClass().getName(), null);
            if (event.getException() == null) {
                if (log.isTraceEnabled()) {
                    realLog(log, "Task \"" + task.getTaskName() + "\" finished.",
                            Project.MSG_VERBOSE, null);
                }
            } else {
                realLog(log, "Task \"" + task.getTaskName()
                        + "\" finished with error.", Project.MSG_ERR,
                        event.getException());
            }
        }
    }


    /**
     * @see BuildListener#messageLogged
     * {@inheritDoc}.
     */
    @Override
    public void messageLogged(final BuildEvent event) {
        if (initialized) {
            Object categoryObject = event.getTask();
            String categoryString;
            String categoryDetail = null;

            if (categoryObject == null) {
                categoryObject = event.getTarget();
                if (categoryObject == null) {
                    categoryString = PROJECT_LOG;
                    categoryDetail = event.getProject().getName();
                } else {
                    categoryString = TARGET_LOG;
                    categoryDetail = event.getTarget().getName();
                }
            } else {
                // It's a task - append the target
                if (event.getTarget() != null) {
                    categoryString = categoryObject.getClass().getName();
                    categoryDetail = event.getTarget().getName();
                } else {
                    categoryString = categoryObject.getClass().getName();
                }

            }

            final Log log = getLog(categoryString, categoryDetail);
            final int priority = event.getPriority();
            final String message = event.getMessage();
            realLog(log, message, priority, null);
        }
    }

    private void realLog(final Log log, final String message, final int priority, final Throwable t) {
        final PrintStream tmpOut = System.out;
        final PrintStream tmpErr = System.err;
        System.setOut(out);
        System.setErr(err);
        switch (priority) {
            case Project.MSG_ERR:
                if (t == null) {
                    log.error(message);
                } else {
                    log.error(message, t);
                }
                break;
            case Project.MSG_WARN:
                if (t == null) {
                    log.warn(message);
                } else {
                    log.warn(message, t);
                }
                break;
            case Project.MSG_INFO:
                if (t == null) {
                    log.info(message);
                } else {
                    log.info(message, t);
                }
                break;
            case Project.MSG_VERBOSE:
            case Project.MSG_DEBUG:
                log.debug(message);
                break;
            default:
                log.error(message);
                break;
        }
        System.setOut(tmpOut);
        System.setErr(tmpErr);
    }

    // CheckStyle:VisibilityModifier OFF - bc
    PrintStream out = System.out;
    PrintStream err = System.err;
    // CheckStyle:VisibilityModifier ON

    /**
     * Set the the output level.
     * This is not used, the logger config is used instead.
     * @param level ignored
     */
    @Override
    public void setMessageOutputLevel(final int level) {
        // Use the logger config
    }

    @Override
    public int getMessageOutputLevel() {
        return Project.MSG_INFO;
    }

    /**
     * Set the output print stream.
     * @param output the output stream
     */
    @Override
    public void setOutputPrintStream(final PrintStream output) {
        this.out = output;
    }

    /**
     * Set emacs mode.
     * This is ignored.
     * @param emacsMode ignored
     */
    @Override
    public void setEmacsMode(final boolean emacsMode) {
        // Doesn't make sense for c-l. Use the logger config
    }

    /**
     * Set the error print stream.
     * @param err the error stream
     */
    @Override
    public void setErrorPrintStream(final PrintStream err) {
        this.err = err;
    }

}
