/*
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
 *
 * Copyright 1997-2010 Oracle and/or its affiliates. All rights reserved.
 *
 * Oracle and Java are registered trademarks of Oracle and/or its affiliates.
 * Other names may be trademarks of their respective owners.
 *
 * The contents of this file are subject to the terms of either the GNU
 * General Public License Version 2 only ("GPL") or the Common
 * Development and Distribution License("CDDL") (collectively, the
 * "License"). You may not use this file except in compliance with the
 * License. You can obtain a copy of the License at
 * http://www.netbeans.org/cddl-gplv2.html
 * or nbbuild/licenses/CDDL-GPL-2-CP. See the License for the
 * specific language governing permissions and limitations under the
 * License.  When distributing the software, include this License Header
 * Notice in each file and include the License file at
 * nbbuild/licenses/CDDL-GPL-2-CP.  Oracle designates this
 * particular file as subject to the "Classpath" exception as provided
 * by Oracle in the GPL Version 2 section of the License file that
 * accompanied this code. If applicable, add the following below the
 * License Header, with the fields enclosed by brackets [] replaced by
 * your own identifying information:
 * "Portions Copyrighted [year] [name of copyright owner]"
 *
 * Contributor(s):
 *
 * The Original Software is NetBeans. The Initial Developer of the Original
 * Software is Sun Microsystems, Inc. Portions Copyright 1997-2006 Sun
 * Microsystems, Inc. All Rights Reserved.
 *
 * If you wish your version of this file to be governed by only the CDDL
 * or only the GPL Version 2, indicate your decision by adding
 * "[Contributor] elects to include this software in this distribution
 * under the [CDDL or GPL Version 2] license." If you do not indicate a
 * single choice of license, a recipient has the option to distribute
 * your version of this file under either the CDDL, the GPL Version 2 or
 * to extend the choice of license to its licensees as provided above.
 * However, if you add GPL Version 2 code and therefore, elected the GPL
 * Version 2 license, then the option applies only if the new code is
 * made subject to such option by the copyright holder.
 */

package org.apache.tools.ant.module.bridge.impl;

import java.io.File;
import java.io.IOException;
import java.lang.reflect.Method;
import java.net.URL;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.apache.tools.ant.BuildEvent;
import org.apache.tools.ant.BuildException;
import org.apache.tools.ant.BuildListener;
import org.apache.tools.ant.Location;
import org.apache.tools.ant.Project;
import org.apache.tools.ant.RuntimeConfigurable;
import org.apache.tools.ant.Target;
import org.apache.tools.ant.Task;
import org.apache.tools.ant.module.bridge.AntBridge;
import org.apache.tools.ant.module.run.Hyperlink;
import org.apache.tools.ant.module.run.LoggerTrampoline;
import org.apache.tools.ant.module.run.StandardLogger;
import org.apache.tools.ant.module.spi.AntEvent;
import org.apache.tools.ant.module.spi.AntLogger;
import org.apache.tools.ant.module.spi.AntSession;
import org.apache.tools.ant.module.spi.TaskStructure;
import org.netbeans.api.annotations.common.NonNull;
import org.netbeans.api.progress.ProgressHandle;
import org.netbeans.modules.project.indexingbridge.IndexingBridge;
import org.openide.awt.StatusDisplayer;
import org.openide.util.Lookup;
import org.openide.util.NbBundle;
import org.openide.util.NbCollections;
import org.openide.util.Parameters;
import org.openide.util.RequestProcessor;
import org.openide.util.WeakSet;
import org.openide.windows.InputOutput;
import org.openide.windows.OutputListener;
import org.openide.windows.OutputWriter;

/**
 * NetBeans-sensitive build logger.
 * Just delegates all events to the registered SPI loggers
 * through an abstraction layer.
 * Synchronization: all callbacks are synchronized, both to protect access to logger
 * caches, and to prevent AntBridge.suspend/resumeDelegation from being called with
 * dynamic overlaps.
 * @author Jesse Glick
 */
final class NbBuildLogger implements BuildListener, LoggerTrampoline.AntSessionImpl {
    
    private static final Logger LOG = Logger.getLogger(NbBuildLogger.class.getName());
    
    final AntSession thisSession;
    
    private final File origScript;
    private String[] targets = null;
    final OutputWriter out;
    final OutputWriter err;
    final InputOutput io;
    private final int verbosity;
    private final Map<String,String> properties;
    private final Set<? extends String> concealedProperties;
    private final String displayName;
    private final Runnable interestingOutputCallback;
    private final ProgressHandle handle;
    private boolean insideRunTask = false; // #95201
    private IndexingBridge.Lock protectedMode; // #211005
    private final Object protectedModeLock = new Object();
    private final RequestProcessor.Task sleepTask = new RequestProcessor(NbBuildLogger.class.getName(), 1, false, false).create(new Runnable() {
        public @Override void run() {
            handle.suspend(insideRunTask ? NbBundle.getMessage(NbBuildLogger.class, "MSG_sleep_running") : "");
            exitProtectedMode();
        }
    });
    private static final int SLEEP_DELAY = 5000;
    
    private final Map<AntLogger,Object> customData = new HashMap<AntLogger,Object>();
    
    private List<AntLogger> interestedLoggers = null;
    private Map<File/*|null*/,Collection<AntLogger>> interestedLoggersByScript = new HashMap<File,Collection<AntLogger>>();
    private Map<String/*|null*/,Collection<AntLogger>> interestedLoggersByTarget = new HashMap<String,Collection<AntLogger>>();
    private Map<String/*|null*/,Collection<AntLogger>> interestedLoggersByTask = new HashMap<String,Collection<AntLogger>>();
    private Map<Integer,Collection<AntLogger>> interestedLoggersByLevel = new HashMap<Integer,Collection<AntLogger>>();
    
    private final Set<Project> projectsWithProperties = Collections.synchronizedSet(new WeakSet<Project>());
    
    private final Set<Throwable> consumedExceptions = new WeakSet<Throwable>();
    
    /** whether this process should be halted at the next safe point */
    private boolean stop = false;
    /** whether this process is thought to be still running */
    private boolean running = true;
    private static final ThreadLocal<Boolean> insideToString = new ThreadLocal<Boolean>() {
        protected @Override Boolean initialValue() {
            return false;
        }
    };
    
    /**
     * Map from master build scripts to maps from imported target names to imported locations.
     * Hack for lack of Target.getLocation() in Ant 1.6.2 and earlier.
     * Unused if targetGetLocation is not null.
     */
    private final Map<String,Map<String,String>> knownImportedTargets = Collections.synchronizedMap(new HashMap<String,Map<String,String>>());
    /**
     * Main script known to be being parsed at the moment.
     * Unused if targetGetLocation is not null.
     */
    private String currentlyParsedMainScript = null;
    /**
     * Imported script known to be being parsed at the moment.
     * Unused if targetGetLocation is not null.
     */
    private String currentlyParsedImportedScript = null;
    /**
     * Last task which was known to be running. Heuristic. Cf. #49464.
     */
    private Task lastTask = null;
    private synchronized Task getLastTask() {
        return lastTask;
    }
    private synchronized void setLastTask(Task lastTask) {
        this.lastTask = lastTask;
    }
    
    @SuppressWarnings("LeakingThisInConstructor")
    NbBuildLogger(File origScript, OutputWriter out, OutputWriter err, int verbosity, String displayName, Map<String,String> properties,
            Set<? extends String> concealedProperties, Runnable interestingOutputCallback, ProgressHandle handle, InputOutput io) {
        thisSession = LoggerTrampoline.ANT_SESSION_CREATOR.makeAntSession(this);
        this.origScript = origScript;
        this.out = out;
        this.err = err;
        this.io = io;
        this.verbosity = verbosity;
        this.properties = properties;
        this.concealedProperties = concealedProperties;
        this.displayName = displayName;
        this.interestingOutputCallback = interestingOutputCallback;
        this.handle = handle;
        LOG.log(Level.FINE, "---- Initializing build of {0} \"{1}\" at verbosity {2} ----", new Object[] {origScript, displayName, verbosity});
        enterProtectedMode(isCompileOnSave(properties));
    }

    //where
    private static boolean isCompileOnSave(final Map<String,String> properties) {
        return "true".equals(properties.get("nb.wait.for.caches")); //NOI18N
    }

    private void enterProtectedMode(final boolean waitForScan) {
        synchronized (protectedModeLock) {
            if (protectedMode == null) {
                protectedMode = IndexingBridge.getDefault().protectedMode(waitForScan);
            }
        }
    }
    private void exitProtectedMode() {
        synchronized (protectedModeLock) {
            if (protectedMode != null) {
                protectedMode.release();
                protectedMode = null;
            }
        }
    }
    
    /** Try to stop running at the next safe point. */
    public void stop() {
        stop = true;
    }
    
    /** Stop the build now if requested. Also restarts sleep timer. */
    private void checkForStop() {
        if (stop) {
            StatusDisplayer.getDefault().setStatusText(NbBundle.getMessage(NbBuildLogger.class, "MSG_stopped", displayName));
            throw new ThreadDeath();
        }
        if (running) {
            handle.switchToIndeterminate();
            enterProtectedMode(false);
            sleepTask.schedule(SLEEP_DELAY);
        }
    }

    /**
     * Notify this process that it has been shut down.
     * Refuse any further queries on AntEvent etc.
     * @see "#71266"
     */
    public void shutdown() {
        running = false;
        out.close();
        err.close();
        handle.finish();
        sleepTask.cancel();
        exitProtectedMode();
    }

    private void verifyRunning() {
        if (!running && !insideToString.get()) {
            throw new ThreadDeath(); // AntSession/AntEvent/TaskStructure method called after completion of Ant process
        }
    }
    
    /**
     * Compute a list of loggers to use for this session.
     * Do not do it in the constructor since the actual targets will not have been
     * set and some loggers may care about the targets. However if buildInitializationFailed
     * is called before then, initialize them anyway.
     */
    private synchronized void initInterestedLoggers() {
        if (interestedLoggers == null) {
            interestedLoggers = new ArrayList<AntLogger>();
            for (AntLogger l : Lookup.getDefault().lookupAll(AntLogger.class)) {
                if (l.interestedInSession(thisSession)) {
                    interestedLoggers.add(l);
                }
            }
            LOG.log(Level.FINEST, "getInterestedLoggers: loggers={0}", interestedLoggers);
        }
    }

    private synchronized Collection<AntLogger> getInterestedLoggers() {
        initInterestedLoggers();
        return new ArrayList<AntLogger>(interestedLoggers);
    }

    @SuppressWarnings({"unchecked", "rawtypes"}) // could use List<Collection<AntLogger>> but too slow?
    private final Collection<AntLogger>[] interestedLoggersByVariousCriteria = new Collection[4];
    private static final Comparator<Collection<AntLogger>> INTERESTED_LOGGERS_SORTER = new Comparator<Collection<AntLogger>>() {
        public @Override int compare(Collection<AntLogger> c1, Collection<AntLogger> c2) {
            int x = c1.size() - c2.size(); // reverse sort by size
            if (x != 0) {
                return x;
            } else {
                return System.identityHashCode(c1) - System.identityHashCode(c2);
            }
        }
    };
    /**
     * Get those loggers interested in a given event.
     */
    private Collection<AntLogger> getInterestedLoggersByEvent(AntEvent e) {
        File scriptLocation = e.getScriptLocation();
        String targetName = e.getTargetName();
        String taskName = e.getTaskName();
        int logLevel = e.getLogLevel();
        synchronized (this) { // #132945: <parallel> can deadlock if you block on event info here
            initInterestedLoggers();
            // Start with the smallest one and go down.
            interestedLoggersByVariousCriteria[0] = getInterestedLoggersByScript(scriptLocation);
            interestedLoggersByVariousCriteria[1] = getInterestedLoggersByTarget(targetName);
            interestedLoggersByVariousCriteria[2] = getInterestedLoggersByTask(taskName);
            interestedLoggersByVariousCriteria[3] = getInterestedLoggersByLevel(logLevel);
            Arrays.sort(interestedLoggersByVariousCriteria, INTERESTED_LOGGERS_SORTER);
            LOG.log(Level.FINEST, "getInterestedLoggersByVariousCriteria: event={0} loggers={1}", new Object[] {e, Arrays.asList(interestedLoggersByVariousCriteria)});
            // XXX could probably be even a bit more efficient by iterating on the fly...
            // and by skipping the sorting which is probably overkill for a small number of a loggers (or hardcode the sort)
            List<AntLogger> loggers = new LinkedList<AntLogger>(interestedLoggersByVariousCriteria[0]);
            for (int i = 1; i < 4; i++) {
                loggers.retainAll(interestedLoggersByVariousCriteria[i]);
            }
            LOG.log(Level.FINEST, "getInterestedLoggersByEvent: event={0} loggers={1}", new Object[] {e, loggers});
            return loggers;
        }
    }
    
    private synchronized Collection<AntLogger> getInterestedLoggersByScript(File script) {
        initInterestedLoggers();
        Collection<AntLogger> c = interestedLoggersByScript.get(script);
        if (c == null) {
            c = new LinkedHashSet<AntLogger>(interestedLoggers.size());
            interestedLoggersByScript.put(script, c);
            for (AntLogger l : interestedLoggers) {
                if (l.interestedInAllScripts(thisSession) || (script != null && l.interestedInScript(script, thisSession))) {
                    c.add(l);
                }
            }
            LOG.log(Level.FINEST, "getInterestedLoggersByScript: script={0} loggers={1}", new Object[] {script, c});
        }
        return c;
    }
    
    private synchronized Collection<AntLogger> getInterestedLoggersByTarget(String target) {
        Collection<AntLogger> c = interestedLoggersByTarget.get(target);
        if (c == null) {
            c = new LinkedHashSet<AntLogger>(interestedLoggers.size());
            interestedLoggersByTarget.put(target, c);
            for (AntLogger l : interestedLoggers) {
                String[] interestingTargets = l.interestedInTargets(thisSession);
                if (interestingTargets == AntLogger.ALL_TARGETS ||
                        (target != null && Arrays.asList(interestingTargets).contains(target)) ||
                        (target == null && interestingTargets == AntLogger.NO_TARGETS)) {
                    c.add(l);
                }
            }
            LOG.log(Level.FINEST, "getInterestedLoggersByTarget: target={0} loggers={1}", new Object[] {target, c});
        }
        return c;
    }
    
    private synchronized Collection<AntLogger> getInterestedLoggersByTask(String task) {
        Collection<AntLogger> c = interestedLoggersByTask.get(task);
        if (c == null) {
            c = new LinkedHashSet<AntLogger>(interestedLoggers.size());
            interestedLoggersByTask.put(task, c);
            for (AntLogger l : interestedLoggers) {
                String[] tasks = l.interestedInTasks(thisSession);
                if (tasks == AntLogger.ALL_TASKS ||
                        (task != null && Arrays.asList(tasks).contains(task)) ||
                        (task == null && tasks == AntLogger.NO_TASKS)) {
                    c.add(l);
                }
            }
            LOG.log(Level.FINEST, "getInterestedLoggersByTask: task={0} loggers={1}", new Object[] {task, c});
        }
        return c;
    }
    
    private synchronized Collection<AntLogger> getInterestedLoggersByLevel(int level) {
        Collection<AntLogger> c = interestedLoggersByLevel.get(level);
        if (c == null) {
            c = new LinkedHashSet<AntLogger>(interestedLoggers.size());
            interestedLoggersByLevel.put(level, c);
            for (AntLogger l : interestedLoggers) {
                if (level == -1) {
                    c.add(l);
                } else {
                    int[] levels = l.interestedInLogLevels(thisSession);
                    for (int _level : levels)  {
                        if (_level == level) {
                            c.add(l);
                            break;
                        }
                    }
                }
            }
            LOG.log(Level.FINEST, "getInterestedLoggersByLevel: level={0} loggers={1}", new Object[] {level, c});
        }
        return c;
    }
    
    synchronized void setActualTargets(String[] targets) {
        this.targets = targets;
    }
    
    void buildInitializationFailed(BuildException be) {
        AntEvent ev = LoggerTrampoline.ANT_EVENT_CREATOR.makeAntEvent(new Event(be));
        LOG.log(Level.FINE, "buildInitializationFailed: {0}", ev);
        for (AntLogger l : getInterestedLoggersByScript(null)) {
            l.buildInitializationFailed(ev);
        }
        interestingOutputCallback.run();
    }
    
    public @Override void buildStarted(BuildEvent ev) {
        AntBridge.suspendDelegation();
        try {
            checkForStop();
            AntEvent e = LoggerTrampoline.ANT_EVENT_CREATOR.makeAntEvent(new Event(ev, false));
            LOG.log(Level.FINE, "buildStarted: {0}", e);
            for (AntLogger l : getInterestedLoggers()) {
                try {
                    l.buildStarted(e);
                } catch (RuntimeException x) {
                    LOG.log(Level.WARNING, null, x);
                }
            }
        } finally {
            AntBridge.resumeDelegation();
        }
    }
    
    public @Override void buildFinished(BuildEvent ev) {
        AntBridge.suspendDelegation();
        try {
            // #82160: do not call checkForStop() here
            stop = false; // do not throw ThreadDeath on messageLogged from BridgeImpl cleanup code
            setLastTask(null);
            AntEvent e = LoggerTrampoline.ANT_EVENT_CREATOR.makeAntEvent(new Event(ev, false));
            LOG.log(Level.FINE, "buildFinished: {0}", e);
            if (e.getException() != null) {
                LOG.log(Level.FINE, null, e.getException());
            }
            for (AntLogger l : getInterestedLoggers()) {
                try {
                    l.buildFinished(e);
                } catch (RuntimeException x) {
                    LOG.log(Level.WARNING, null, x);
                } catch (Error x) {
                    LOG.log(Level.WARNING, null, x);
                }
            }
            if (e.getException() != null) {
                interestingOutputCallback.run();
            }
        } finally {
            AntBridge.resumeDelegation();
        }
    }
    
    public @Override void targetStarted(BuildEvent ev) {
        AntBridge.suspendDelegation();
        try {
            checkForStop();
            setLastTask(null);
            AntEvent e = LoggerTrampoline.ANT_EVENT_CREATOR.makeAntEvent(new Event(ev, false));
            LOG.log(Level.FINE, "targetStarted: {0}", e);
            for (AntLogger l : getInterestedLoggersByEvent(e)) {
                try {
                    l.targetStarted(e);
                } catch (RuntimeException x) {
                    LOG.log(Level.WARNING, null, x);
                }
            }
            // Update progress handle label so user can see what is being run.
            Project p = ev.getProject();
            String projectName = null;
            if (p != null) {
                projectName = p.getName();
            }
            String targetName = e.getTargetName();
            if (targetName != null) {
                String message;
                if (projectName != null) {
                    message = NbBundle.getMessage(NbBuildLogger.class, "MSG_progress_target", projectName, targetName);
                } else {
                    message = targetName;
                }
                /*
                if (message.equals(displayName)) {
                    // Redundant in this case.
                    message = "";
                }
                 */
                handle.progress(message);
            }
        } finally {
            AntBridge.resumeDelegation();
        }
    }
    
    public @Override void targetFinished(BuildEvent ev) {
        AntBridge.suspendDelegation();
        try {
            checkForStop();
            setLastTask(null);
            AntEvent e = LoggerTrampoline.ANT_EVENT_CREATOR.makeAntEvent(new Event(ev, false));
            LOG.log(Level.FINE, "targetFinished: {0}", e);
            for (AntLogger l : getInterestedLoggersByEvent(e)) {
                try {
                    l.targetFinished(e);
                } catch (RuntimeException x) {
                    LOG.log(Level.WARNING, null, x);
                }
            }
        } finally {
            AntBridge.resumeDelegation();
        }
    }
    
    public @Override void taskStarted(BuildEvent ev) {
        AntBridge.suspendDelegation();
        try {
            checkForStop();
            setLastTask(ev.getTask());
            AntEvent e = LoggerTrampoline.ANT_EVENT_CREATOR.makeAntEvent(new Event(ev, false));
            LOG.log(Level.FINE, "taskStarted: {0}", e);
            for (AntLogger l : getInterestedLoggersByEvent(e)) {
                try {
                    l.taskStarted(e);
                } catch (RuntimeException x) {
                    LOG.log(Level.WARNING, null, x);
                }
            }
            if ("input".equals(e.getTaskName())) { // #81139; NOI18N
                TaskStructure s = e.getTaskStructure();
                if (s != null) {
                    String def = s.getAttribute("defaultvalue"); // NOI18N
                    if (def != null) {
                        NbInputHandler.setDefaultValue(e.evaluate(def));
                    }
                }
            }
            if (isRunTask(e)) {
                insideRunTask = true;
            }
        } finally {
            AntBridge.resumeDelegation();
        }
    }

    private boolean isRunTask(AntEvent event) { // #95201
        String taskName = event.getTaskName();
        return "java".equals(taskName) || "exec".equals(taskName); // NOI18N
    }

    public @Override void taskFinished(BuildEvent ev) {
        AntBridge.suspendDelegation();
        try {
            checkForStop();
            setLastTask(null);
            AntEvent e = LoggerTrampoline.ANT_EVENT_CREATOR.makeAntEvent(new Event(ev, false));
            LOG.log(Level.FINE, "taskFinished: {0}", e);
            for (AntLogger l : getInterestedLoggersByEvent(e)) {
                try {
                    l.taskFinished(e);
                } catch (RuntimeException x) {
                    LOG.log(Level.WARNING, null, x);
                }
            }
            NbInputHandler.setDefaultValue(null);
            if (isRunTask(e)) {
                insideRunTask = false;
            }
        } finally {
            AntBridge.resumeDelegation();
        }
    }
    
    /**
     * Pattern matching an Ant message logged when it is parsing a build script.
     * Hack for lack of Target.getLocation() in Ant 1.6.2 and earlier.
     * Captured groups:
     * <ol>
     * <li>absolute path of build script
     * </ol>
     */
    private static final Pattern PARSING_BUILDFILE_MESSAGE =
        Pattern.compile("parsing buildfile (.+) with URI = (?:.+)"); // NOI18N
    
    /**
     * Pattern matching an Ant message logged when it is importing a build script.
     * Hack for lack of Target.getLocation() in Ant 1.6.2 and earlier.
     * Captured groups:
     * <ol>
     * <li>absolute path of build script which is doing the importing
     * </ol>
     */
    private static final Pattern IMPORTING_FILE_MESSAGE =
        Pattern.compile("Importing file (?:.+) from (.+)"); // NOI18N
    
    /**
     * Pattern matching an Ant message logged when it has encountered a target in some build script.
     * Hack for lack of Target.getLocation() in Ant 1.6.2 and earlier.
     * Captured groups:
     * <ol>
     * <li>target name
     * </ol>
     */
    private static final Pattern PARSED_TARGET_MESSAGE =
        Pattern.compile(" \\+Target: (.+)"); // NOI18N
    
    public @Override void messageLogged(BuildEvent ev) {
        if (!running) { // #145722
            return;
        }
        AntBridge.suspendDelegation();
        try {
            checkForStop();
            AntEvent e = LoggerTrampoline.ANT_EVENT_CREATOR.makeAntEvent(new Event(ev, true));
            LOG.log(Level.FINER, "messageLogged: {0}", e);
            for (AntLogger l : getInterestedLoggersByEvent(e)) {
                try {
                    l.messageLogged(e);
                } catch (RuntimeException x) {
                    LOG.log(Level.WARNING, null, x);
                }
            }
            // Let the hacks begin!
            String msg = ev.getMessage();
            if (msg.contains("ant.PropertyHelper") || /* #71816 */ msg.contains("ant.projectHelper")) { // NOI18N
                // Only after this has been defined can we get any properties.
                // Even trying earlier will give a recursion error since this pseudoprop
                // is set lazily, which produces a new message logged event.
                projectsWithProperties.add(ev.getProject());
            }
            if (targetGetLocation == null) {
                // Try to figure out which imported targets belong to which actual scripts.
                // XXX consider keeping a singleton Matcher for each pattern and reusing it
                // or just doing string comparisons
                Matcher matcher;
                if ((matcher = PARSING_BUILDFILE_MESSAGE.matcher(msg)).matches()) {
                    if (currentlyParsedMainScript != null) {
                        currentlyParsedImportedScript = matcher.group(1);
                    }
                    LOG.log(Level.FINE, "Got PARSING_BUILDFILE_MESSAGE: {0}", currentlyParsedImportedScript);
                    setLastTask(null);
                } else if ((matcher = IMPORTING_FILE_MESSAGE.matcher(msg)).matches()) {
                    currentlyParsedMainScript = matcher.group(1);
                    currentlyParsedImportedScript = null;
                    LOG.log(Level.FINE, "Got IMPORTING_FILE_MESSAGE: {0}", currentlyParsedMainScript);
                    setLastTask(null);
                } else if ((matcher = PARSED_TARGET_MESSAGE.matcher(msg)).matches()) {
                    if (currentlyParsedMainScript != null && currentlyParsedImportedScript != null) {
                        Map<String,String> targetLocations = knownImportedTargets.get(currentlyParsedMainScript);
                        if (targetLocations == null) {
                            targetLocations = new HashMap<String,String>();
                            knownImportedTargets.put(currentlyParsedMainScript, targetLocations);
                        }
                        targetLocations.put(matcher.group(1), currentlyParsedImportedScript);
                    }
                    LOG.log(Level.FINE, "Got PARSED_TARGET_MESSAGE: {0}", matcher.group(1));
                    setLastTask(null);
                }
            }
        } finally {
            AntBridge.resumeDelegation();
        }
    }
    
    public @Override File getOriginatingScript() {
        verifyRunning();
        return origScript;
    }
    
    public @Override String[] getOriginatingTargets() {
        verifyRunning();
        return targets != null ? targets : new String[0];
    }
    
    public @Override synchronized Object getCustomData(AntLogger logger) {
        verifyRunning();
        return customData.get(logger);
    }
    
    public @Override synchronized void putCustomData(AntLogger logger, Object data) {
        verifyRunning();
        customData.put(logger, data);
    }
    
    public @Override void println(String message, boolean error, OutputListener listener) {
        verifyRunning();
        LOG.log(Level.FINEST, "println: error={0} listener={1} message={2}", new Object[] {error, listener, message});
        OutputWriter ow = error ? err : out;
        try {
            if (listener != null) {
                // Loggers wishing for more control can use getIO and do it themselves.
                boolean important = StandardLogger.isImportant(message);
                ow.println(message, listener, important);
                interestingOutputCallback.run();
            } else {
                ow.println(message);
            }
        } catch (IOException e) {
            LOG.log(Level.WARNING, null, e);
        }
    }
    
    public @Override void deliverMessageLogged(AntEvent originalEvent, String message, int level) {
        verifyRunning();
        if (originalEvent == null) {
            throw new IllegalArgumentException("Must pass an original event to deliverMessageLogged"); // NOI18N
        }
        if (message == null) {
            throw new IllegalArgumentException("Must pass a message to deliverMessageLogged"); // NOI18N
        }
        if (level < AntEvent.LOG_ERR || level > AntEvent.LOG_DEBUG) {
            throw new IllegalArgumentException("Unknown log level for reposted log event: " + level); // NOI18N
        }
        LOG.log(Level.FINEST, "deliverMessageLogged: level={0} message={1}", new Object[] {level, message});
        AntEvent newEvent = LoggerTrampoline.ANT_EVENT_CREATOR.makeAntEvent(new RepostedEvent(originalEvent, message, level));
        for (AntLogger l : getInterestedLoggersByEvent(newEvent)) {
            try {
                l.messageLogged(newEvent);
            } catch (RuntimeException x) {
                LOG.log(Level.WARNING, null, x);
            }
        }
    }
    
    public @Override synchronized void consumeException(Throwable t) throws IllegalStateException {
        verifyRunning();
        if (isExceptionConsumed(t)) {
            throw new IllegalStateException("Already consumed " + t); // NOI18N
        }
        consumedExceptions.add(t);
    }
    
    public @Override synchronized boolean isExceptionConsumed(Throwable t) {
        verifyRunning();
        if (consumedExceptions.contains(t)) {
            return true;
        }
        // Check for nested exceptions too.
        Throwable nested = t.getCause();
        if (nested != null && isExceptionConsumed(nested)) {
            // cache that
            consumedExceptions.add(t);
            return true;
        }
        return false;
    }
    
    public @Override int getVerbosity() {
        verifyRunning();
        return verbosity;
    }

    @Override public Map<String, String> getProperties() {
        return Collections.unmodifiableMap(properties);
    }

    @Override
    public boolean isConcealed(@NonNull final String propertyName) {
        Parameters.notNull("propertyName", propertyName);   //NOI18N
        return concealedProperties.contains(propertyName);
    }
    
    String getDisplayNameNoLock() {
        return displayName;
    }
    
    public @Override String getDisplayName() {
        verifyRunning();
        return displayName;
    }

    public @Override OutputListener createStandardHyperlink(URL file, String message, int line1, int column1, int line2, int column2) {
        verifyRunning();
        return new Hyperlink(file, message, line1, column1, line2, column2);
    }

    public @Override InputOutput getIO() {
        return io;
    }
    
    // Accessors for stuff which is specific to particular versions of Ant.
    private static final Method targetGetLocation; // 1.6.2+
    private static final Method locationGetFileName; // 1.6+
    private static final Method locationGetLineNumber; // 1.6+
    private static final Method runtimeConfigurableGetAttributeMap; // 1.6+
    private static final Method runtimeConfigurableGetChildren; // 1.6+
    private static final Method runtimeConfigurableGetText; // 1.6+
    static {
        Method _targetGetLocation = null;
        try {
            _targetGetLocation = Target.class.getMethod("getLocation"); // NOI18N
            if (AntBridge.getInterface().getAntVersion().indexOf("1.6.2") != -1) { // NOI18N
                // Unfortunately in 1.6.2 the method exists but it doesn't work (Ant #28599):
                _targetGetLocation = null;
            }
        } catch (NoSuchMethodException e) {
            // OK
        } catch (Exception e) {
            LOG.log(Level.WARNING, null, e);
        }
        targetGetLocation = _targetGetLocation;
        Method _locationGetFileName = null;
        try {
            _locationGetFileName = Location.class.getMethod("getFileName"); // NOI18N
        } catch (NoSuchMethodException e) {
            // OK
        } catch (Exception e) {
            LOG.log(Level.WARNING, null, e);
        }
        locationGetFileName = _locationGetFileName;
        Method _locationGetLineNumber = null;
        try {
            _locationGetLineNumber = Location.class.getMethod("getLineNumber"); // NOI18N
        } catch (NoSuchMethodException e) {
            // OK
        } catch (Exception e) {
            LOG.log(Level.WARNING, null, e);
        }
        locationGetLineNumber = _locationGetLineNumber;
        Method _runtimeConfigurableGetAttributeMap = null;
        try {
            _runtimeConfigurableGetAttributeMap = RuntimeConfigurable.class.getMethod("getAttributeMap"); // NOI18N
        } catch (NoSuchMethodException e) {
            // OK
        } catch (Exception e) {
            LOG.log(Level.WARNING, null, e);
        }
        runtimeConfigurableGetAttributeMap = _runtimeConfigurableGetAttributeMap;
        Method _runtimeConfigurableGetChildren = null;
        try {
            _runtimeConfigurableGetChildren = RuntimeConfigurable.class.getMethod("getChildren"); // NOI18N
        } catch (NoSuchMethodException e) {
            // OK
        } catch (Exception e) {
            LOG.log(Level.WARNING, null, e);
        }
        runtimeConfigurableGetChildren = _runtimeConfigurableGetChildren;
        Method _runtimeConfigurableGetText = null;
        try {
            _runtimeConfigurableGetText = RuntimeConfigurable.class.getMethod("getText"); // NOI18N
        } catch (NoSuchMethodException e) {
            // OK
        } catch (Exception e) {
            LOG.log(Level.WARNING, null, e);
        }
        runtimeConfigurableGetText = _runtimeConfigurableGetText;
    }

    /**
     * Try to find the location of an Ant target.
     * @param project if not null, the main project from which this target might have been imported
     */
    private Location getLocationOfTarget(Target target, Project project) {
        if (targetGetLocation != null) {
            try {
                return (Location) targetGetLocation.invoke(target);
            } catch (Exception e) {
                LOG.log(Level.WARNING, null, e);
            }
        }
        // For Ant 1.6.2 and earlier, hope we got the right info from the hacks above.
        LOG.log(Level.FINEST, "knownImportedTargets: {0}", knownImportedTargets);
        if (project != null) {
            String file = project.getProperty("ant.file"); // NOI18N
            if (file != null) {
                Map<String,String> targetLocations = knownImportedTargets.get(file);
                if (targetLocations != null) {
                    String importedFile = targetLocations.get(target.getName());
                    if (importedFile != null) {
                        // Have no line number, note.
                        return new Location(importedFile);
                    }
                }
            }
        }
        // Dunno.
        return null;
    }
    
    private static String getFileNameOfLocation(Location loc) {
        if (locationGetFileName != null) {
            try {
                return (String) locationGetFileName.invoke(loc);
            } catch (Exception e) {
                LOG.log(Level.WARNING, null, e);
            }
        }
        // OK, using Ant 1.5.x.
        String locs = loc.toString();
        // Format: "$file:$line: " or "$file: " or ""
        int x = locs.indexOf(':');
        if (x != -1) {
            return locs.substring(0, x);
        } else {
            return null;
        }
    }
    
    private static int getLineNumberOfLocation(Location loc) {
        if (locationGetLineNumber != null) {
            try {
                return (Integer) locationGetLineNumber.invoke(loc);
            } catch (Exception e) {
                LOG.log(Level.WARNING, null, e);
            }
        }
        // OK, using Ant 1.5.x.
        String locs = loc.toString();
        // Format: "$file:$line: " or "$file: " or ""
        int x = locs.indexOf(':');
        if (x != -1) {
            int x2 = locs.indexOf(':', x + 1);
            if (x2 != -1) {
                String line = locs.substring(x + 1, x2);
                try {
                    return Integer.parseInt(line);
                } catch (NumberFormatException e) {
                    // ignore?
                }
            }
        }
        return 0;
    }
    
    private static Map<String,String> getAttributeMapOfRuntimeConfigurable(RuntimeConfigurable rc) {
        Map<String, String> m = new HashMap<String, String>();
        if (runtimeConfigurableGetAttributeMap != null) {
            try {
                for (Map.Entry<?,?> entry : ((Map<?,?>) runtimeConfigurableGetAttributeMap.invoke(rc)).entrySet()) {
                    m.put(((String) entry.getKey()).toLowerCase(Locale.ENGLISH), (String) entry.getValue());
                }
            } catch (Exception e) {
                LOG.log(Level.WARNING, null, e);
            }
        }
        return m;
    }

    @SuppressWarnings("unchecked")
    private static Enumeration<RuntimeConfigurable> getChildrenOfRuntimeConfigurable(RuntimeConfigurable rc) {
        if (runtimeConfigurableGetChildren != null) {
            try {
                return (Enumeration<RuntimeConfigurable>) runtimeConfigurableGetChildren.invoke(rc);
            } catch (Exception e) {
                LOG.log(Level.WARNING, null, e);
            }
        }
        return Collections.enumeration(Collections.<RuntimeConfigurable>emptySet());
    }
    
    private static String getTextOfRuntimeConfigurable(RuntimeConfigurable rc) {
        if (runtimeConfigurableGetText != null) {
            try {
                return ((StringBuffer) runtimeConfigurableGetText.invoke(rc)).toString();
            } catch (Exception e) {
                LOG.log(Level.WARNING, null, e);
            }
        }
        return "";
    }
    
    /**
     * Standard event implementation, delegating to the Ant BuildEvent and Project.
     */
    private final class Event implements LoggerTrampoline.AntEventImpl {
        
        private boolean consumed = false;
        private final BuildEvent e;
        private final Throwable exception;
        private final int level;
        private File scriptLocation;
        
        /**
         * Create a new regular event.
         * @param e the Ant build event
         * @param msgLogged true for logged events
         */
        public Event(BuildEvent e, boolean msgLogged) {
            this.e = e;
            exception = e.getException();
            if (msgLogged) {
                level = e.getPriority();
            } else {
                level = -1;
            }
        }
        
        /**
         * Create a new event for buildInitializationFailed.
         * @param exception the problem
         */
        public Event(Throwable exception) {
            e = null;
            this.exception = exception;
            level = -1;
        }
        
        public @Override AntSession getSession() {
            verifyRunning();
            return thisSession;
        }

        public @Override void consume() throws IllegalStateException {
            verifyRunning();
            if (consumed) {
                throw new IllegalStateException("Event already consumed"); // NOI18N
            }
            consumed = true;
        }

        public @Override boolean isConsumed() {
            verifyRunning();
            return consumed;
        }

        public @Override File getScriptLocation() {
            verifyRunning();
            if (scriptLocation != null) {
                return scriptLocation;
            }
            if (e == null) {
                return null;
            }
            Task task = e.getTask();
            if (task != null) {
                Location l = task.getLocation();
                if (l != null) {
                    String file = getFileNameOfLocation(l);
                    if (file != null) {
                        return scriptLocation = new File(file);
                    }
                }
            }
            Target target = e.getTarget();
            Project project = getProjectIfPropertiesDefined();
            if (target != null) {
                Location l = getLocationOfTarget(target, project);
                if (l != null) {
                    String file = getFileNameOfLocation(l);
                    if (file != null) {
                        return scriptLocation = new File(file);
                    }
                }
            }
            // #49464: guess at task.
            Task lastTask = getLastTask();
            if (lastTask != null) {
                Location l = lastTask.getLocation();
                if (l != null) {
                    String file = getFileNameOfLocation(l);
                    if (file != null) {
                        return scriptLocation = new File(file);
                    }
                }
            }
            // #104103: lastTask is more likely to be accurate.
            // Consider a call to Project.log from within a task run in an imported script.
            if (project != null) {
                String file = project.getProperty("ant.file"); // NOI18N
                if (file != null) {
                    return scriptLocation = new File(file);
                }
            }
            // #57153 suggests using SubBuildListener, but is it really necessary?
            return null;
        }
        
        private Project getProjectIfPropertiesDefined() {
            Project project = e.getProject();
            if (project != null && projectsWithProperties.contains(project)) {
                return project;
            } else {
                return null;
            }
        }

        public @Override int getLine() {
            verifyRunning();
            if (e == null) {
                return -1;
            }
            Task task = e.getTask();
            if (task != null) {
                Location l = task.getLocation();
                if (l != null) {
                    int line = getLineNumberOfLocation(l);
                    if (line > 0) {
                        return line;
                    }
                }
            }
            Target target = e.getTarget();
            if (target != null) {
                Location l = getLocationOfTarget(target, getProjectIfPropertiesDefined());
                if (l != null) {
                    int line = getLineNumberOfLocation(l);
                    if (line > 0) {
                        return line;
                    }
                }
            }
            // #49464: guess at task.
            Task lastTask = getLastTask();
            if (lastTask != null) {
                Location l = lastTask.getLocation();
                if (l != null) {
                    int line = getLineNumberOfLocation(l);
                    if (line > 0) {
                        return line;
                    }
                }
            }
            return -1;
        }

        public @Override String getTargetName() {
            verifyRunning();
            if (e == null) {
                return null;
            }
            Target target = e.getTarget();
            if (target != null) {
                String name = target.getName();
                if (name != null && name.length() > 0) {
                    return name;
                }
            }
            // #49464: guess at task.
            Task lastTask = getLastTask();
            if (lastTask != null) {
                target = lastTask.getOwningTarget();
                if (target != null) {
                    String name = target.getName();
                    if (name != null && name.length() > 0) {
                        return name;
                    }
                }
            }
            return null;
        }

        public @Override String getTaskName() {
            verifyRunning();
            if (e == null) {
                return null;
            }
            Task task = e.getTask();
            if (task != null) {
                return task.getRuntimeConfigurableWrapper().getElementTag();
            }
            // #49464: guess at task.
            Task lastTask = getLastTask();
            if (lastTask != null) {
                return lastTask.getRuntimeConfigurableWrapper().getElementTag();
            }
            return null;
        }

        public @Override TaskStructure getTaskStructure() {
            verifyRunning();
            Task task = e.getTask();
            if (task != null) {
                return LoggerTrampoline.TASK_STRUCTURE_CREATOR.makeTaskStructure(new TaskStructureImpl(task.getRuntimeConfigurableWrapper()));
            }
            // #49464: guess at task.
            Task lastTask = getLastTask();
            if (lastTask != null) {
                return LoggerTrampoline.TASK_STRUCTURE_CREATOR.makeTaskStructure(new TaskStructureImpl(lastTask.getRuntimeConfigurableWrapper()));
            }
            return null;
        }

        public @Override String getMessage() {
            verifyRunning();
            if (e == null) {
                return null;
            }
            return e.getMessage();
        }

        public @Override int getLogLevel() {
            verifyRunning();
            return level;
        }

        public @Override Throwable getException() {
            verifyRunning();
            return exception;
        }

        public @Override String getProperty(String name) {
            verifyRunning();
            Project project = getProjectIfPropertiesDefined();
            if (project != null) {
                String v = project.getProperty(name);
                if (v != null) {
                    return v;
                } else {
                    Object o = project.getReference(name);
                    if (o != null) {
                        return o.toString();
                    } else {
                        return null;
                    }
                }
            } else {
                return null;
            }
        }

        public @Override Set<String> getPropertyNames() {
            verifyRunning();
            Project project = getProjectIfPropertiesDefined();
            if (project != null) {
                Set<String> s = new HashSet<String>();
                s.addAll(NbCollections.checkedSetByFilter(project.getProperties().keySet(), String.class, true));
                s.addAll(NbCollections.checkedSetByFilter(project.getReferences().keySet(), String.class, true));
                return s;
            } else {
                return Collections.emptySet();
            }
        }

        public @Override String evaluate(String text) {
            verifyRunning();
            Project project = getProjectIfPropertiesDefined();
            if (project != null) {
                return project.replaceProperties(text);
            } else {
                return text;
            }
        }
        
        @Override
        public String toString() {
            assert !insideToString.get();
            insideToString.set(true);
            try {
            StringBuilder b = new StringBuilder("Event"); // NOI18N
            String s = getTargetName();
            if (s != null) {
                b.append(";targ=").append(s); // NOI18N
            }
            s = getTaskName();
            if (s != null) {
                b.append(";task=").append(s); // NOI18N
            }
            if (exception != null) {
                b.append(";exc=").append(exception); // NOI18N
            }
            if (level != -1) {
                b.append(";lvl=").append(LEVEL_NAMES[level]); // NOI18N
            }
            if (consumed) {
                b.append(";consumed"); // NOI18N
            }
            s = getMessage();
            if (s != null) {
                b.append(";msg=").append(s); // NOI18N
            }
            File f = getScriptLocation();
            if (f != null) {
                b.append(";scrLoc=").append(f); // NOI18N
            }
            return b.toString();
            } finally {
                insideToString.set(false);
            }
        }
        
    }
    private static final String[] LEVEL_NAMES = {"ERR", "WARN", "INFO", "VERBOSE", "DEBUG"}; // NOI18N
    
    /**
     * Reposted event delegating to an original one except for message and level.
     * @see #deliverMessageLogged
     */
    private final class RepostedEvent implements LoggerTrampoline.AntEventImpl {
        
        private final AntEvent originalEvent;
        private final String message;
        private final int level;
        private boolean consumed = false;
        
        public RepostedEvent(AntEvent originalEvent, String message, int level) {
            this.originalEvent = originalEvent;
            this.message = message;
            this.level = level;
        }
        
        public @Override void consume() throws IllegalStateException {
            verifyRunning();
            if (consumed) {
                throw new IllegalStateException("Event already consumed"); // NOI18N
            }
            consumed = true;
        }

        public @Override boolean isConsumed() {
            verifyRunning();
            return consumed;
        }
        
        public @Override AntSession getSession() {
            return originalEvent.getSession();
        }
        
        public @Override File getScriptLocation() {
            return originalEvent.getScriptLocation();
        }
        
        public @Override int getLine() {
            return originalEvent.getLine();
        }
        
        public @Override String getTargetName() {
            return originalEvent.getTargetName();
        }
        
        public @Override String getTaskName() {
            return originalEvent.getTaskName();
        }
        
        public @Override TaskStructure getTaskStructure() {
            return originalEvent.getTaskStructure();
        }
        
        public @Override String getMessage() {
            verifyRunning();
            return message;
        }
        
        public @Override int getLogLevel() {
            verifyRunning();
            return level;
        }
        
        public @Override Throwable getException() {
            verifyRunning();
            return null;
        }
        
        public @Override String getProperty(String name) {
            return originalEvent.getProperty(name);
        }
        
        public @Override Set<String> getPropertyNames() {
            return originalEvent.getPropertyNames();
        }
        
        public @Override String evaluate(String text) {
            return originalEvent.evaluate(text);
        }
        
        @Override
        public String toString() {
            return "RepostedEvent[consumed=" + consumed + ",level=" + level + ",message=" + message + /*",orig=" + originalEvent +*/ "]"; // NOI18N
        }
        
    }
    
    /**
     * Implementation of TaskStructure based on an Ant Task.
     * @see Event#getTaskStructure
     */
    private final class TaskStructureImpl implements LoggerTrampoline.TaskStructureImpl {
        
        private final RuntimeConfigurable rc;
        
        public TaskStructureImpl(RuntimeConfigurable rc) {
            this.rc = rc;
        }
        
        public @Override String getName() {
            verifyRunning();
            String name = rc.getElementTag();
            if (name != null) {
                return name;
            } else {
                // What does this mean?
                return "";
            }
        }
        
        public @Override String getAttribute(String name) {
            verifyRunning();
            return getAttributeMapOfRuntimeConfigurable(rc).get(name.toLowerCase(Locale.ENGLISH));
        }
        
        public @Override Set<String> getAttributeNames() {
            verifyRunning();
            return getAttributeMapOfRuntimeConfigurable(rc).keySet();
        }
        
        public @Override String getText() {
            verifyRunning();
            String s = getTextOfRuntimeConfigurable(rc);
            if (s.length() > 0) {
                // XXX is it appropriate to trim() this? probably not
                return s;
            } else {
                return null;
            }
        }
        
        public @Override TaskStructure[] getChildren() {
            verifyRunning();
            List<TaskStructure> structures = new ArrayList<TaskStructure>();
            for (RuntimeConfigurable subrc : NbCollections.iterable(getChildrenOfRuntimeConfigurable(rc))) {
                structures.add(LoggerTrampoline.TASK_STRUCTURE_CREATOR.makeTaskStructure(new TaskStructureImpl(subrc)));
            }
            return structures.toArray(new TaskStructure[structures.size()]);
        }
        
    }

}
