/*
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
 *
 * Copyright 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]"
 *
 * 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.
 *
 * Contributor(s):
 *
 * Portions Copyrighted 2008 Sun Microsystems, Inc.
 */
package org.netbeans.modules.maven.execute;

import org.netbeans.modules.maven.execute.cmd.ExecutionEventObject;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.Reader;
import java.net.URL;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.EnumMap;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.logging.Logger;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.apache.maven.execution.ExecutionEvent;
import org.apache.maven.project.MavenProject;
import org.json.simple.JSONObject;
import org.json.simple.parser.JSONParser;
import org.json.simple.parser.ParseException;
import org.netbeans.api.annotations.common.CheckForNull;
import org.netbeans.api.annotations.common.NonNull;
import org.netbeans.api.annotations.common.NullAllowed;
import org.netbeans.modules.maven.api.execute.RunConfig;
import org.netbeans.api.progress.ProgressHandle;
import org.netbeans.api.project.Project;
import org.netbeans.modules.maven.api.NbMavenProject;
import org.netbeans.modules.maven.api.output.OutputUtils;
import org.netbeans.modules.maven.api.output.OutputVisitor;
import org.netbeans.modules.maven.execute.AbstractMavenExecutor.ResumeFromFinder;
import static org.netbeans.modules.maven.execute.AbstractOutputHandler.PRJ_EXECUTE;
import static org.netbeans.modules.maven.execute.AbstractOutputHandler.SESSION_EXECUTE;
import org.netbeans.modules.maven.execute.cmd.ExecMojo;
import org.netbeans.modules.maven.execute.cmd.ExecProject;
import org.netbeans.modules.maven.execute.cmd.ExecSession;
import org.netbeans.modules.maven.options.MavenSettings;
import org.netbeans.spi.project.ProjectContainerProvider;
import org.netbeans.spi.project.SubprojectProvider;
import org.openide.util.Exceptions;
import org.openide.util.RequestProcessor;
import org.openide.util.RequestProcessor.Task;
import org.openide.windows.IOPosition;
import org.openide.windows.InputOutput;
import org.openide.windows.OutputWriter;

/**
 * handling of output coming from maven commandline builds
 * @author Milos Kleint
 */
public class CommandLineOutputHandler extends AbstractOutputHandler {

    //32 means 16 paralel builds, one for input, one for output. #229904
    private static final RequestProcessor PROCESSOR = new RequestProcessor("Maven ComandLine Output Redirection", Integer.getInteger("maven.concurrent.builds", 16) * 2); //NOI18N
    private static final Logger LOG = Logger.getLogger(CommandLineOutputHandler.class.getName());
    private InputOutput inputOutput;
    /*test*/ static final Pattern DOWNLOAD = Pattern.compile("^(\\d+(/\\d*)? ?(M|K|b|KB|B|\\?)\\s*)+$"); //NOI18N
    private static final Pattern linePattern = Pattern.compile("\\[(DEBUG|INFO|WARNING|ERROR|FATAL)\\] (.*)"); // NOI18N
    public static final Pattern startPatternM2 = Pattern.compile("\\[INFO\\] \\[([\\w]*):([\\w]*)[ ]?.*\\]"); // NOI18N
    public static final Pattern startPatternM3 = Pattern.compile("\\[INFO\\] --- (\\S+):\\S+:(\\S+)(?: [(]\\S+[)])? @ \\S+ ---"); // ExecutionEventLogger.mojoStarted NOI18N
    private static final Pattern mavenSomethingPlugin = Pattern.compile("maven-(.+)-plugin"); // NOI18N
    private static final Pattern somethingMavenPlugin = Pattern.compile("(.+)-maven-plugin"); // NOI18N
    /** @see org.apache.maven.cli.ExecutionEventLogger#logReactorSummary */
    static final Pattern reactorFailure = Pattern.compile("\\[INFO\\] (.+) [.]* FAILURE \\[.+\\]"); // NOI18N
    private static final Pattern stackTraceElement = OutputUtils.linePattern;
    
    public static final Pattern reactorSummaryLine = Pattern.compile("(.+) [.]* (FAILURE|SUCCESS) (\\[.+\\])?"); // NOI18N
    private OutputWriter stdOut;
    private String currentProject;
    private String currentTag;
    Task outTask;
    private Input inp;
    private ProgressHandle handle;
    /** {@link MavenProject#getName} of first project in reactor to fail, if any */
    String firstFailure;
    private final JSONParser parser;
    private ContextImpl contextImpl;
    //the depth is as follows
    //Session -> Project -> [Fork -> ForkedProject] -> Mojo                
    private final ExecutionEventObject.Tree executionTree = new ExecutionEventObject.Tree(null, null);

    private ExecutionEventObject.Tree currentTreeNode = executionTree;
    private boolean inStackTrace = false;
    private boolean addMojoFold = false;
    private boolean addProjectFold = false;
    private URL[] mavencoreurls;

    public CommandLineOutputHandler(InputOutput io, Project proj, ProgressHandle hand, RunConfig config, boolean createVisitorContext) {
        super(proj, hand, config, createVisitorContext ? new OutputVisitor(new ContextImpl()) : new OutputVisitor());
        if (createVisitorContext) {
            contextImpl = (ContextImpl) visitor.getContext();
            assert contextImpl != null;
            contextImpl.setExecutionTree(executionTree);
        }
        this.parser = new JSONParser();
        handle = hand;
        inputOutput = io;
        stdOut = inputOutput.getOut();
//        logger = new Logger();
        initProcessorList(proj, config);
    }
    
    /**
     * 
     * @return null if tree is not being built (maven 2.x)
     */
    public @CheckForNull ExecutionEventObject.Tree getExecutionTree() {
        if (contextImpl != null) {
            return executionTree;
        }
        return null;
    }


    @Override
    protected final void checkSleepiness() {
        if (contextImpl == null) { //only perform for maven 2.x now
            handle.progress(currentProject == null ? "" : currentTag == null ? currentProject : currentProject + " " + currentTag); // NOI18N
        }
        super.checkSleepiness();
    }


    void setStdOut(InputStream inStr) {
        outTask = PROCESSOR.post(new Output(inStr));
    }

    void setStdIn(OutputStream in) {
        inp = new Input(in, inputOutput);
        PROCESSOR.post(inp);
    }

    void waitFor() {
        inp.stopInput();
//        if (inTask != null) {
//            inTask.waitFinished();
//        }
        if (outTask != null) {
            outTask.waitFinished();
        }
    }

    @Override
    protected InputOutput getIO() {
        return this.inputOutput;
    }

    private static final String SEC_MOJO_EXEC = "mojo-execute"; //NOI18N
    private void closeCurrentTag() {
        assert contextImpl == null;
        if (currentTag != null) {
            CommandLineOutputHandler.this.processEnd(getEventId(SEC_MOJO_EXEC, currentTag), stdOut);
            currentTag = null;
        }
    }

    private void mergeClasspath(ExecMojo exec, URL[] coreurls) {
        if (coreurls != null) {
            URL[] urls = exec.getClasspathURLs();
            if (urls == null) {
                exec.setClasspathURLs(coreurls);
            } else {
                List<URL> newones = new ArrayList<URL>();
                newones.addAll(Arrays.asList(urls));
                newones.addAll(Arrays.asList(coreurls));
                exec.setClasspathURLs(newones.toArray(new URL[0]));
            }
        }
    }




    private class Output implements Runnable {
        private static final String INFO_NETBEANS_EXEC_EVENT = "[INFO] NETBEANS-ExecEvent:";

        private final BufferedReader str;
        private boolean skipLF = false;

        public Output(InputStream instream) {
            str = new BufferedReader(new InputStreamReader(instream));
        }

        private String readLine() throws IOException {
            char[] char1 = new char[1];
            boolean isReady = true;
            int count = 0;
            StringBuilder buf = new StringBuilder();
            while (isReady) {
                count = count + 1;
                if (count > 20000) { //#239847 limit the number of bytes/characters read together in one lump, make sure the stringBuilder doesn't grow out of proportions.
                    break; //make it an incomplete line.
                }
                int ret = str.read(char1);
                if (ret != 1) {
                     if (ret == -1 && buf.length() == 0) {
                         return null;
                     }
                    return buf.toString();
                }
                if (skipLF) {
                    skipLF = false;
                    if (char1[0] == '\n') { //NOI18N
                        continue;
                    }
                }
                if (char1[0] == '\n') { //NOI18N
                    return buf.toString();
                }
                if (char1[0] == '\r') { //NOI18N
                    skipLF = true;
                    buf.append(char1[0]);
                    return buf.toString();
                }
                buf.append(char1[0]);
                isReady = str.ready();
                if (!isReady) {
                    synchronized (this) {
                        try {
                            wait(500);
                        } catch (InterruptedException ex) {
                            Exceptions.printStackTrace(ex);
                        } finally {
                            if (!str.ready()) {
                                break;
                            }
                            isReady = true;
                        }
                    }

                }
            }
            return "&^#INCOMPLINE:" + buf.toString(); //NOI18N

        }

        public @Override void run() {
            CommandLineOutputHandler.this.processStart(getEventId(SESSION_EXECUTE, null), stdOut);
            if (contextImpl == null) {
                CommandLineOutputHandler.this.processStart(getEventId(PRJ_EXECUTE,null), stdOut);
            }
            try {

                String line = readLine();
                while (line != null) {
                    if (line.startsWith("&^#INCOMPLINE:")) { //NOI18N
                        stdOut.print(line.substring("&^#INCOMPLINE:".length())); //NOI18N
                        line = readLine();
                        continue;
                    }
                    int execEventIdx = line.indexOf(INFO_NETBEANS_EXEC_EVENT);
                    if (execEventIdx == 0) {
                        processExecEvent(parseExecEvent(line));
//                        stdOut.println(line); //XXX temporary
                        line = readLine();
                        continue;
                    }
                    
                    String nextLine = null;
                    if(execEventIdx > 0) {
                        nextLine = line.substring(execEventIdx);
                        line = line.substring(0, execEventIdx);
                    }                   
                    if (line.startsWith("[INFO] Final Memory:")) { //NOI18N
                        // previous value [INFO] --------------- is too early, the compilation errors don't get processed in this case.
                        //heuristics..
                        if (contextImpl == null) { //only in m2
                            closeCurrentTag();
                        }
                    }
                    
                    String tag = null;
                    if (contextImpl == null) {
                        Matcher match = startPatternM3.matcher(line);
                        if (match.matches()) {
                            String mojoArtifact = match.group(1);
                            mojoArtifact = goalPrefixFromArtifactId(mojoArtifact);
                            tag = mojoArtifact + ':' + match.group(2);
                        } else {
                            match = startPatternM2.matcher(line);
                            if (match.matches()) {
                                tag = match.group(1) + ':' + match.group(2);
                            }
                        }
                    }
                    if (tag != null) { //only in m2
                        closeCurrentTag();
                        currentTag = tag;
                        CommandLineOutputHandler.this.processStart(getEventId(SEC_MOJO_EXEC, tag), stdOut);
                        checkSleepiness();
                    }
                    
                    boolean isDownloadProgress = false;
                    if(line.length() > 0 && line.charAt(line.length() - 1) == '\r') {
                        // issue #252514
                        if (DOWNLOAD.matcher(line).matches()) {
                            isDownloadProgress = true;
                        } else {
                            line = line.substring(0, line.length() - 1);
                        }
                    }
                    if(!isDownloadProgress) {
                        Matcher match = linePattern.matcher(line);
                        if (match.matches()) {
                            String levelS = match.group(1);
                            Level level = Level.valueOf(levelS);
                            String text = match.group(2);
                            updateFoldForException(text);
                            processLine(MavenSettings.getDefault().isShowLoggingLevel() ? line : text, stdOut, level);
                            if (level == Level.INFO && contextImpl == null) { //only perform for maven 2.x now
                                checkProgress(text);
                            }
                        } else {
                            // oh well..
                            updateFoldForException(line);
                            processLine(line, stdOut, Level.INFO);
                        }
                    }
                    if (contextImpl == null && firstFailure == null) {
                        Matcher match = reactorFailure.matcher(line);
                        if (match.matches()) {
                            firstFailure = match.group(1);
                        }
                    }
                    //these two are a bit shaky and depend on output details that might not be available in the future.
                    //however there's no other way to have the proper line marked as beginning of a section (as the event comes first)
                    //without this, the last line of previous output would be marked as beginning of the fold.
                    if (addMojoFold && line.startsWith("[INFO] ---")) {     //NOI18N
                        currentTreeNode.startFold(inputOutput);
                        addMojoFold = false;
                    }
                    if (addProjectFold && line.startsWith("[INFO] Building")) {
                        currentTreeNode.startFold(inputOutput);
                        addProjectFold = false;
                    }
                    line = nextLine != null ? nextLine : readLine();
                }
            } catch (IOException ex) {
                java.util.logging.Logger.getLogger(CommandLineOutputHandler.class.getName()).log(java.util.logging.Level.FINE, null, ex);
            } finally {
                if (contextImpl == null) {
                    CommandLineOutputHandler.this.processEnd(getEventId(PRJ_EXECUTE, null), stdOut);
                } else {
                    completeTreeAtEnd();
                }
                CommandLineOutputHandler.this.processEnd(getEventId(SESSION_EXECUTE, null), stdOut);
                try {
                    str.close();
                } catch (IOException ex) {
                    ex.printStackTrace();
                }
            }
        }

        private ExecutionEventObject parseExecEvent(String line) {
            String jsonContent = line.substring(INFO_NETBEANS_EXEC_EVENT.length());
            try {
                Object o = parser.parse(jsonContent);
//                System.out.println("o=" + o);
                if (o instanceof JSONObject) {
                    JSONObject json = (JSONObject) o;
                    return ExecutionEventObject.create(json);
                }
            } catch (ParseException ex) {
                Exceptions.printStackTrace(ex);
//                System.out.println("exc=" + ex);
            }
            return null;
        }

 

        /**
         * Check whether the line is start of a stacktrace, inside a stacktrace,
         * or is another text, and update folds accordingly.
         */
        private void updateFoldForException(String line) {
            if (stackTraceElement.matcher(line).find()) {
                inStackTrace = true;
                if (!currentTreeNode.hasInnerOutputFold()) {
                    currentTreeNode.startInnerOutputFold(inputOutput);
                }
            } else  if (inStackTrace) {
                currentTreeNode.finishInnerOutputFold();
                inStackTrace = false;
            }
        }

        
    }
    
        private void completeTreeAtEnd() {
            //#229877 at the end of the build, verify that the tree is complete.
            for (ExecutionEventObject.Tree nd : executionTree.getChildrenNodes()) {
                if (nd.getEndEvent() == null) {
                    ExecutionEventObject innerEnd = createEndForStart(nd.getStartEvent());
                    trimTree(innerEnd);
                }
            }
        }
       
        private void processExecEvent(ExecutionEventObject obj) {
            if (obj == null) {
                return;
            }
            checkProgress(obj);
            
            if (ExecutionEvent.Type.SessionStarted.equals(obj.type)) {
                if (currentTreeNode != executionTree) {
                    //we are not at a real start, something restarted the build..
                    completeTreeAtEnd();
                }
                mavencoreurls = ((ExecSession)obj).getMnvcoreurls();
            }
            if (ExecutionEvent.Type.MojoStarted.equals(obj.type)) {
                growTree(obj);
                addMojoFold = true;
                ExecMojo exec = (ExecMojo) obj;
                String tag = goalPrefixFromArtifactId(exec.plugin.artifactId) + ":" + exec.goal;
                ExecutionEventObject.Tree prjNode = currentTreeNode.findParentNodeOfType(ExecutionEvent.Type.ProjectStarted);
                assert prjNode != null;
                ExecProject p = (ExecProject) prjNode.getStartEvent();
                handle.progress(p.gav.artifactId + " " + tag);
                CommandLineOutputHandler.this.processStart(getEventId(SEC_MOJO_EXEC, tag), stdOut);
            }
            if (ExecutionEvent.Type.MojoSucceeded.equals(obj.type)) {
                if (MavenSettings.getDefault().isCollapseSuccessFolds()) {
                    currentTreeNode.collapseFold();
                }
                currentTreeNode.finishFold();
                ExecMojo exec = (ExecMojo) obj;
                mergeClasspath(exec, mavencoreurls);
                trimTree(exec);
                String tag = goalPrefixFromArtifactId(exec.plugin.artifactId) + ":" + exec.goal;
                CommandLineOutputHandler.this.processEnd(getEventId(SEC_MOJO_EXEC, tag), stdOut);
            }
            else if (ExecutionEvent.Type.MojoFailed.equals(obj.type)) {
                currentTreeNode.finishFold();
                ExecMojo exec = (ExecMojo) obj;
                mergeClasspath(exec, mavencoreurls);
                trimTree(exec);
                String tag = goalPrefixFromArtifactId(exec.plugin.artifactId) + ":" + exec.goal;
                CommandLineOutputHandler.this.processFail(getEventId(SEC_MOJO_EXEC, tag), stdOut);
            }
            else if (ExecutionEvent.Type.ProjectStarted.equals(obj.type)) {
                growTree(obj);
                addProjectFold = true;
                if (contextImpl != null) {
                    ExecProject pr = (ExecProject)obj;
                    Project project = pr.findProject();
                    contextImpl.setCurrentProject(project);
                    CommandLineOutputHandler.this.processStart(getEventId(PRJ_EXECUTE, null), stdOut);                    
                }
            }
            else if (ExecutionEvent.Type.ProjectSkipped.equals(obj.type)) {
                //growTree(obj);
                //trimTree(obj);
                //GlobalOutputProcessor currently depens on skipped projects not being added to tree.
            }
            else if (ExecutionEvent.Type.ProjectSucceeded.equals(obj.type)) {
                if (MavenSettings.getDefault().isCollapseSuccessFolds()) {
                    currentTreeNode.collapseFold();
                }
                currentTreeNode.finishFold();
                trimTree(obj);
                CommandLineOutputHandler.this.processEnd(getEventId(PRJ_EXECUTE, null), stdOut);                    
            }
            else if (ExecutionEvent.Type.ProjectFailed.equals(obj.type)) {
                currentTreeNode.finishFold();
                trimTree(obj);
                CommandLineOutputHandler.this.processEnd(getEventId(PRJ_EXECUTE, null), stdOut);                    
            } else if (ExecutionEvent.Type.ForkStarted.equals(obj.type)) {
                growTree(obj);
            } else if (ExecutionEvent.Type.ForkedProjectStarted.equals(obj.type)) {
                growTree(obj);
            } else if (ExecutionEvent.Type.ForkFailed.equals(obj.type) || ExecutionEvent.Type.ForkSucceeded.equals(obj.type)) {
                trimTree(obj);
            } else if (ExecutionEvent.Type.ForkedProjectFailed.equals(obj.type) || ExecutionEvent.Type.ForkedProjectSucceeded.equals(obj.type)) {
                trimTree(obj);
            } else if (!MavenSettings.getDefault().isAlwaysShowOutput() && ExecutionEvent.Type.SessionEnded.equals(obj.type)) {
                for (ExecutionEventObject.Tree node : executionTree.getChildrenNodes()) {
                    if (node.getEndEvent() != null && ExecutionEvent.Type.ProjectFailed.equals(node.getEndEvent().type)) {
                        getIO().select();
                        break;
                    }
                }
            }
        }

        private String goalPrefixFromArtifactId(String mojoArtifact) {
            // XXX M3 reports artifactId of mojo whereas M2 reports goalPrefix; do not want to force every OutputProcessor to handle both
            // XXX consider searching index on ArtifactInfo.PLUGIN_PREFIX instead
            Matcher match2 = mavenSomethingPlugin.matcher(mojoArtifact);
            if (match2.matches()) {
                mojoArtifact = match2.group(1);
            } else {
                match2 = somethingMavenPlugin.matcher(mojoArtifact);
                if (match2.matches()) {
                    mojoArtifact = match2.group(1);
                }
            }
            return mojoArtifact;
        }
        
    private void growTree(ExecutionEventObject obj) {
        ExecutionEventObject.Tree tn = new ExecutionEventObject.Tree(obj, currentTreeNode);
        //fork events come before the mojo events, we want them as childs, to know what form belongs to which mojo.
        if (tn.getStartEvent().type.equals(ExecutionEvent.Type.MojoStarted) && !currentTreeNode.getChildrenNodes().isEmpty()) {
            //check if the previous fork should be added to this event
            ExecutionEventObject.Tree lastSibling = currentTreeNode.getChildrenNodes().get(currentTreeNode.getChildrenNodes().size() - 1 );
            while (lastSibling != null && lastSibling.getEndEvent() != null && (ExecutionEvent.Type.ForkFailed.equals(lastSibling.getEndEvent().type) || ExecutionEvent.Type.ForkSucceeded.equals(lastSibling.getEndEvent().type))) {
                currentTreeNode.getChildrenNodes().remove(lastSibling);
                tn.getChildrenNodes().add(0, lastSibling);
                lastSibling.reassingParent(tn);
                lastSibling = currentTreeNode.getChildrenNodes().isEmpty() ? null : currentTreeNode.getChildrenNodes().get(currentTreeNode.getChildrenNodes().size() - 1 );
            }
        }
        currentTreeNode.getChildrenNodes().add(tn);
        currentTreeNode = tn;
        currentTreeNode.setStartOffset(IOPosition.currentPosition(inputOutput));
    }

    private void trimTree(ExecutionEventObject obj) {
        ExecutionEventObject start = currentTreeNode.getStartEvent();
        while (!matchingEvents(obj.type, start.type)) { //#229877
            ExecutionEventObject innerEnd = createEndForStart(start);
            processExecEvent(innerEnd);
            start = currentTreeNode.getStartEvent();
            //potentially never ending+ recursive loop, how to intercept?
        }
        currentTreeNode.setEndOffset(IOPosition.currentPosition(inputOutput));
        currentTreeNode.setEndEvent(obj);
        currentTreeNode = currentTreeNode.getParentNode();
    }

    private boolean matchingEvents(ExecutionEvent.Type typeEnd, ExecutionEvent.Type typeStart) {
        ExecutionEvent.Type match = END_TO_START_Mappings.get(typeEnd);
        assert match != null : "unknown event type:" + typeEnd;
        return typeStart.equals(match);
    }
    
    // an artificial way of creating event objects for events that didn't come the natural way of parsing the output.
    // typically happens when user stops the build or some other cases described in issue 229877
    private ExecutionEventObject createEndForStart(ExecutionEventObject start) {
        ExecutionEventObject toRet;
        if (start instanceof ExecMojo) {
            ExecMojo startEx = (ExecMojo) start;
            toRet = new ExecMojo(startEx.goal, startEx.plugin, startEx.phase, startEx.executionId, ExecutionEvent.Type.MojoFailed);
        } else if (start instanceof ExecProject) {
            ExecProject startPrj = (ExecProject) start;
            toRet = new ExecProject(startPrj.gav, startPrj.currentProjectLocation, ExecutionEvent.Type.ProjectFailed);
        } else if (start instanceof ExecSession) {
            ExecSession ss = (ExecSession) start;
            toRet = new ExecSession(ss.projectCount, ExecutionEvent.Type.SessionEnded);
        } else {
            ExecutionEvent.Type endType;
            if (start.type.equals(ExecutionEvent.Type.ForkStarted)) {
                endType = ExecutionEvent.Type.ForkFailed;
            } else if (start.type.equals(ExecutionEvent.Type.ForkedProjectStarted)) {
                endType = ExecutionEvent.Type.ForkedProjectFailed;
            } else {
                throw new RuntimeException("unknown event type: " + start.type);
            }
            toRet = new ExecutionEventObject(endType);
        }
        return toRet;
    }

    private static final Map<ExecutionEvent.Type, ExecutionEvent.Type> END_TO_START_Mappings;
    static {
        END_TO_START_Mappings = new EnumMap<ExecutionEvent.Type, ExecutionEvent.Type>(ExecutionEvent.Type.class);
        END_TO_START_Mappings.put(ExecutionEvent.Type.ForkFailed, ExecutionEvent.Type.ForkStarted);
        END_TO_START_Mappings.put(ExecutionEvent.Type.ForkSucceeded, ExecutionEvent.Type.ForkStarted);
        END_TO_START_Mappings.put(ExecutionEvent.Type.ForkedProjectFailed, ExecutionEvent.Type.ForkedProjectStarted);
        END_TO_START_Mappings.put(ExecutionEvent.Type.ForkedProjectSucceeded, ExecutionEvent.Type.ForkedProjectStarted);
        END_TO_START_Mappings.put(ExecutionEvent.Type.MojoFailed, ExecutionEvent.Type.MojoStarted);
        END_TO_START_Mappings.put(ExecutionEvent.Type.MojoSucceeded, ExecutionEvent.Type.MojoStarted);
        END_TO_START_Mappings.put(ExecutionEvent.Type.ProjectFailed, ExecutionEvent.Type.ProjectStarted);
        END_TO_START_Mappings.put(ExecutionEvent.Type.ProjectSucceeded, ExecutionEvent.Type.ProjectStarted);
        END_TO_START_Mappings.put(ExecutionEvent.Type.SessionEnded, ExecutionEvent.Type.SessionEnded);
    }


    ResumeFromFinder createResumeFromFinder() {
        if (contextImpl == null) {
            if (firstFailure != null) {
                return new FindByName(firstFailure);
            }
            return null;
        }
        for (ExecutionEventObject.Tree prj : executionTree.getChildrenNodes()) {
            if (prj.getEndEvent() != null && ExecutionEvent.Type.ProjectFailed.equals(prj.getEndEvent().type)) {
                    //our first failure
                return new FindByEvents( (ExecProject) prj.getStartEvent());
            }
        }
        return null;
    }
    
    
    //old school instance relying on output only.. currently only used in maven 2.x
    private static class FindByName implements ResumeFromFinder {

        private final @NonNull String firstFailure;

        /**
         * @param firstFailure {@link MavenProject#getName}
         */
        private FindByName(@NonNull String firstFailure) {
            this.firstFailure = firstFailure;
        }

        @Override public @CheckForNull NbMavenProject find(@NonNull Project root) {
            // XXX EventSpy (#194090) would make this more reliable and efficient
            for (Project module : root.getLookup().lookup(ProjectContainerProvider.class).getContainedProjects().getProjects()) {
                if (Thread.interrupted()) {
                    break;
                }
                NbMavenProject nbmp = module.getLookup().lookup(NbMavenProject.class);
                if (nbmp == null) {
                    continue;
                }
                MavenProject mp = nbmp.getMavenProject();
                if (firstFailure.equals(mp.getName())) {
                    return nbmp;
                }
            }
            return null;
        }

    }    
    
    private static class FindByEvents implements ResumeFromFinder {
        private final ExecProject execProject;

        private FindByEvents(ExecProject execProject) {
            this.execProject = execProject;
        }
        

        @Override
        public @CheckForNull NbMavenProject find(@NonNull Project root) {
            //we don't need the parameter, we have the exec tree
            Project project = execProject.findProject();
            if (project != null) {
                return  project.getLookup().lookup(NbMavenProject.class);
            }
            return null;
        }
        
    }
    

    static class Input implements Runnable {

        private InputOutput inputOutput;
        private OutputStream str;
        private boolean stopIn = false;

        public Input(OutputStream out, InputOutput inputOutput) {
            str = out;
            this.inputOutput = inputOutput;
        }

        public void stopInput() {
            stopIn = true;
            try {
                inputOutput.getIn().close();
            } catch (IOException ex) {
                Exceptions.printStackTrace(ex);
            }
        }

        public @Override void run() {
            Reader in = inputOutput.getIn();
            try {
                while (true) {
                    int read = in.read();
                    if (read != -1) {
                        str.write(read);
                        str.flush();
                    } else {
                        str.close();
                        return;
                    }
                    if (stopIn) {
                        return;
                    }
                }

            } catch (IOException ex) {
                ex.printStackTrace();
            } finally {
                try {
                    str.close();
                } catch (IOException ex) {
                    ex.printStackTrace();
                }
            }
        }
    }
    
    private void checkProgress(ExecutionEventObject eeo) {
        if (ExecutionEvent.Type.ProjectDiscoveryStarted.equals(eeo.type)) {
            handle.switchToIndeterminate();
        } else if (ExecutionEvent.Type.SessionStarted.equals(eeo.type)) {
            reactorSize = ((ExecSession)eeo).projectCount + 1;
            projectCount = 0;
            handle.switchToDeterminate(reactorSize);
        } else if (ExecutionEvent.Type.ProjectStarted.equals(eeo.type)) {
            handle.progress(((ExecProject)eeo).gav.artifactId, Math.min(++projectCount, reactorSize));
        } else if (ExecutionEvent.Type.ProjectSkipped.equals(eeo.type)) {
            handle.progress(((ExecProject)eeo).gav.artifactId + " " + ((ExecProject)eeo).gav.version, Math.min(++projectCount, reactorSize));
        } else if (ExecutionEvent.Type.SessionEnded.equals(eeo.type)) {
            
        }
    }

    /**
     * #192200: try to indicate progress esp. in a reactor build.
     * @see org.apache.maven.cli.ExecutionEventLogger
     */
    //only done for maven 2.x now.
    private void checkProgress(String text) {
        switch (state) {
        case INITIAL:
            if (text.equals("Reactor Build Order:")) { // NOI18N
                state = ProgressState.GOT_REACTOR_BUILD_ORDER;
            }
            break;
        case GOT_REACTOR_BUILD_ORDER:
            if (text.trim().isEmpty()) {
                state = ProgressState.GETTING_REACTOR_PROJECTS;
            } else {
                state = ProgressState.INITIAL; // ???
            }
            break;
        case GETTING_REACTOR_PROJECTS:
            if (text.trim().isEmpty()) {
                state = ProgressState.NORMAL;
                reactorSize++; // so we do not show 100% completion while building last project
                handle.switchToDeterminate(reactorSize);
                LOG.log(java.util.logging.Level.FINE, "reactor size: {0}", reactorSize);
            } else {
                reactorSize++;
            }
            break;
        case NORMAL:
            if (forkCount == 0 && text.matches("-+")) { // NOI18N
                state = ProgressState.GOT_DASHES;
            } else if (text.startsWith(">>> ")) { // NOI18N
                forkCount++;
                LOG.log(java.util.logging.Level.FINE, "fork count up to {0}", forkCount);
            } else if (forkCount > 0 && text.startsWith("<<< ")) { // NOI18N
                forkCount--;
                LOG.log(java.util.logging.Level.FINE, "fork count down to {0}", forkCount);
            }
            break;
        case GOT_DASHES:
            if (text.startsWith("Building ") && !text.startsWith("Building in ") || text.startsWith("Skipping ")) { // NOI18N
                currentProject = text.substring(9);
                closeCurrentTag();
                handle.progress(currentProject, Math.min(++projectCount, reactorSize));
                LOG.log(java.util.logging.Level.FINE, "got project #{0}: {1}", new Object[] {projectCount, currentProject});
            }
            state = ProgressState.NORMAL;
            break;
        default:
            assert false : state;
        }
    }
    enum ProgressState {
        INITIAL,
        GOT_REACTOR_BUILD_ORDER,
        GETTING_REACTOR_PROJECTS,
        NORMAL,
        GOT_DASHES,
    }
    private ProgressState state = ProgressState.INITIAL;
    private int forkCount;
    private int reactorSize;
    private int projectCount;
    
   public static class ContextImpl implements OutputVisitor.Context {

        private Project currentProject;
        private ExecutionEventObject.Tree executionTree;

        ContextImpl() {
        }

        @Override
        public @CheckForNull Project getCurrentProject() {
            return currentProject;
        }
        
        public void setCurrentProject(@NullAllowed Project currentProject) {
            this.currentProject = currentProject;
        }

        private void setExecutionTree(ExecutionEventObject.Tree executionTree) {
            this.executionTree = executionTree;
        }

        public ExecutionEventObject.Tree getExecutionTree() {
            return executionTree;
        }
        
    }    

}


