/*
 * 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.netbeans.core.output2;

import java.awt.Color;
import java.awt.Dialog;
import java.awt.Component;
import java.awt.Container;
import java.awt.Cursor;
import java.awt.EventQueue;
import java.awt.FileDialog;
import java.awt.Frame;
import java.awt.Point;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.KeyEvent;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.io.CharConversionException;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.EnumMap;
import java.util.List;
import java.util.Map;
import java.util.regex.Pattern;
import java.util.regex.PatternSyntaxException;
import javax.swing.AbstractAction;
import javax.swing.Action;
import javax.swing.JCheckBoxMenuItem;
import javax.swing.JComponent;
import javax.swing.JFileChooser;
import javax.swing.JMenu;
import javax.swing.JMenuItem;
import javax.swing.JOptionPane;
import javax.swing.JPopupMenu;
import javax.swing.JSeparator;
import javax.swing.KeyStroke;
import javax.swing.RootPaneContainer;
import javax.swing.SwingUtilities;
import javax.swing.UIDefaults;
import javax.swing.UIManager;
import javax.swing.event.PopupMenuEvent;
import javax.swing.event.PopupMenuListener;
import javax.swing.text.Document;
import javax.swing.text.JTextComponent;
import org.netbeans.api.options.OptionsDisplayer;
import org.netbeans.core.options.keymap.api.KeyStrokeUtils;
import org.netbeans.core.output2.Controller.ControllerOutputEvent;
import org.netbeans.core.output2.ui.AbstractOutputPane;
import org.netbeans.core.output2.ui.AbstractOutputTab;
import org.openide.DialogDisplayer;
import org.openide.NotifyDescriptor;
import org.openide.actions.FindAction;
import org.openide.awt.StatusDisplayer;
import org.openide.util.Exceptions;
import org.openide.util.NbBundle;
import org.openide.windows.IOContainer;
import org.openide.windows.OutputListener;
import org.openide.windows.WindowManager;
import org.openide.xml.XMLUtil;

import static org.netbeans.core.output2.OutputTab.ACTION.*;
import org.netbeans.core.output2.options.OutputOptions;
import org.netbeans.core.output2.ui.OutputKeymapManager;
import org.openide.util.RequestProcessor;
import org.openide.util.WeakListeners;
import org.openide.windows.IOColors;
import org.openide.windows.OutputEvent;


/**
 * A component representing one tab in the output window.
 */
final class OutputTab extends AbstractOutputTab implements IOContainer.CallBacks {
    private static final RequestProcessor RP =
            new RequestProcessor("OutputTab");                          //NOI18N
    private final NbIO io;
    private OutWriter outWriter;
    private PropertyChangeListener optionsListener;
    private volatile boolean actionsLoaded = false;

    OutputTab(NbIO io) {
        this.io = io;
        if (Controller.LOG) Controller.log ("Created an output component for " + io);
        outWriter = ((NbWriter) io.getOut()).out();
        OutputDocument doc = new OutputDocument(outWriter);
        setDocument(doc);
        applyOptions();
        initOptionsListener();
        loadAndInitActions();
    }

    private void applyOptions() {
        Lines lines = getDocumentLines();
        if (lines != null) {
            OutputOptions opts = io.getOptions();
            lines.setDefColor(IOColors.OutputType.OUTPUT,
                    opts.getColorStandard());
            lines.setDefColor(IOColors.OutputType.ERROR,
                    opts.getColorError());
            lines.setDefColor(IOColors.OutputType.INPUT,
                    opts.getColorInput());
            lines.setDefColor(IOColors.OutputType.HYPERLINK,
                    opts.getColorLink());
            lines.setDefColor(IOColors.OutputType.HYPERLINK_IMPORTANT,
                    opts.getColorLinkImportant());
            Color bg = io.getOptions().getColorBackground();
            getOutputPane().getFoldingSideBar().setForeground(
                    opts.getColorStandard());
            setTextViewBackground(getOutputPane().getTextView(), bg);
            getOutputPane().setViewFont(
                    io.getOptions().getFont(getOutputPane().isWrapped()));
        }
    }

    /**
     * Set text view background color correctly. See bug #225829.
     */
    private void setTextViewBackground(JTextComponent textView, Color bg) {
        getOutputPane().getTextView().setBackground(bg);
        getOutputPane().getFoldingSideBar().setBackground(bg);
        if ("Nimbus".equals(UIManager.getLookAndFeel().getName())) { //NOI18N
            UIDefaults defaults = new UIDefaults();
            defaults.put("EditorPane[Enabled].backgroundPainter", bg);  //NOI18N
            textView.putClientProperty("Nimbus.Overrides", defaults);   //NOI18N
            textView.putClientProperty(
                    "Nimbus.Overrides.InheritDefaults", true);          //NOI18N
            textView.setBackground(bg);
        }
    }

    private final TabAction action(ACTION a) {
        return actions.get(a);
    }

    private void installKBActions() {
        for (ACTION a : actionsToInstall) {
            installKeyboardAction(action(a));
        }
    }

    @Override
    public void setDocument (Document doc) {
        if (Controller.LOG) Controller.log ("Set document on " + this + " with " + io);
        assert SwingUtilities.isEventDispatchThread();
        Document old = getDocument();
        hasOutputListeners = false;
        super.setDocument(doc);
        if (old != null && old instanceof OutputDocument) {
            ((OutputDocument) old).dispose();
        }
        applyOptions();
    }

    public void reset() {
        if (origPane != null) {
            setFilter(null, false, false);
        }
        // get new OutWriter
        outWriter = io.out();

        // Workaround for bug 242979.
        Document actualDocument = getDocument();
        if (actualDocument instanceof OutputDocument) {
            OutputDocument od = (OutputDocument) actualDocument;
            if (od.getWriter() == outWriter) {
                return;
            }
        }
        setDocument(new OutputDocument(outWriter));
        applyOptions();
    }

    public OutputDocument getDocument() {
        Document d = getOutputPane().getDocument();
        if (d instanceof OutputDocument) {
            return (OutputDocument) d;
        }
        return null;
    }

    protected AbstractOutputPane createOutputPane() {
        return new OutputPane(this);
    }

    public void inputSent(String txt) {
        if (Controller.LOG) Controller.log("Input sent on OutputTab: " + txt);
        getOutputPane().lockScroll();
        NbIO.IOReader in = io.in();
        if (in != null) {
            if (Controller.LOG) Controller.log("Sending input to " + in);
            in.pushText(txt + "\n");
            outWriter.print(txt, null, false, null, null, OutputKind.IN, true);
        }
    }

    protected void inputEof() {
        if (Controller.LOG) Controller.log ("Input EOF");
        NbIO.IOReader in = io.in();
        if (in != null) {
            in.eof();
        }
    }

    public void hasSelectionChanged(boolean val) {
        if (isShowing() && actionsLoaded) {
            actions.get(ACTION.COPY).setEnabled(val);
            actions.get(ACTION.SELECT_ALL).setEnabled(!getOutputPane().isAllSelected());
        }
    }

    public NbIO getIO() {
        return io;
    }
    
    void requestActive() {
        io.getIOContainer().requestActive();
    }

    public void lineClicked(int line, int pos) {
        OutWriter out = getOut();
        int range[] = new int[2];
        OutputListener l = out.getLines().getListener(pos, range);
        if (l != null) {
            int size = out.getLines().getCharCount();
            assert range[1] < size : "Size: " + size + " range: " + range[0] + " " + range[1];
            ControllerOutputEvent oe = new ControllerOutputEvent(io, out, line);
            l.outputLineAction(oe);
            //Select the text on click if it is still visible
            if (getOutputPane().getLength() >= range[1]) { // #179768
                getOutputPane().sendCaretToPos(range[0], range[1], true);
            }
        }
    }

    void enterPressed(int caretMark, int caretDot) {
        int start, end;
        if (caretMark < caretDot) {
            start = caretMark;
            end = caretDot;
        } else {
            start = caretDot;
            end = caretMark;
        }
        if (getOut().getLines().isListener(start, end)) {
            int[] range = new int[2];
            OutputListener listener = getOut().getLines().nearestListener(
                    start, false, range);
            if (listener != null && range[0] <= start && range[1] >= end) {
                int line = getOut().getLines().getLineAt(start);
                OutputEvent oe = new ControllerOutputEvent(io, line);
                listener.outputLineAction(oe);
            }
        }
    }

    @Override
    public String toString() {
        return "OutputTab@" + System.identityHashCode(this) + " for " + io;
    }

    private boolean hasOutputListeners = false;

    public void documentChanged(OutputPane pane) {
        if (filtOut != null && pane == origPane) {
            filtOut.readFrom(outWriter);
        }
        boolean hadOutputListeners = hasOutputListeners;
        hasOutputListeners = getOut() != null && (getOut().getLines().firstListenerLine() >= 0 || getOut().getLines().firstImportantListenerLine() >= 0);
        if (hasOutputListeners != hadOutputListeners) {
            hasOutputListenersChanged(hasOutputListeners);
        }

        IOContainer ioContainer = io.getIOContainer();
        if (io.isFocusTaken()) {
	    // The following two lines pulled up from select per bug#185209
	    ioContainer.open();
	    // ioContainer.requestVisible();

            ioContainer.select(this);
            ioContainer.requestVisible();
        }
        Controller.getDefault().updateName(this);
        if (this == ioContainer.getSelected() && ioContainer.isActivated()) {
            updateActions();
        }
    }

    /**
     * Called when the output stream has been closed, to navigate to the
     * first line which shows an error (if any).
     */
    private void navigateToFirstErrorLine() {
        OutWriter out = getOut();
        if (out != null) {
            int line = out.getLines().firstImportantListenerLine();
            if (Controller.LOG) {
                Controller.log("NAV TO FIRST LISTENER LINE: " + line);
            }
            if (line >= 0) {
                getOutputPane().sendCaretToLine(line, false);
            }
        }
    }

     void hasOutputListenersChanged(boolean hasOutputListeners) {
        if (hasOutputListeners && getOutputPane().isScrollLocked()) {
            navigateToFirstErrorLine();
        }
    }

    public void activated() {
        updateActions();
    }

    public void closed() {
        io.setClosed(true);
        io.setStreamClosed(true);
        Controller.getDefault().removeTab(io);
        NbWriter w = io.writer();
        if (w != null && w.isClosed()) {
            //Will dispose the document
            setDocument(null);
            io.dispose();
        } else if (w != null) {
            //Something is still writing to the stream, but we're getting rid of the tab.  Don't dispose
            //the writer, just kill the tab's document
            if (getOut() != null) {
                getOut().setDisposeOnClose(true);
            }
            getDocument().disposeQuietly();
            NbIOProvider.dispose(io);
        }
    }

    public void deactivated() {
    }

    public void selected() {
    }

    /**
     * Determine if the new caret position is close enough that the scrollbar should be re-locked
     * to the end of the document.
     *
     * @param dot The caret position
     * @return if it should be locked
     */
    public boolean shouldRelock(int dot) {
        OutWriter w = getOut();
        if (w != null && !w.isClosed()) {
            int dist = Math.abs(w.getLines().getCharCount() - dot);
            return dist < 100;
        }
        return false;
    }

    /**
     * Flag used to block navigating the editor to the first error line when
     * selecting the error line in the output window after a build (or maybe
     * it should navigate the editor there?  Could be somewhat rude...)
     */
    boolean ignoreCaretChanges = false;

    /**
     * Called when the text caret has changed position - will call OutputListener.outputLineSelected if
     * there is a listener for that position.
     *
     * @param pos The line the caret is in
     */
    int lastCaretListenerRange[];
    void caretPosChanged(int pos) {
        if (!ignoreCaretChanges) {
            if (lastCaretListenerRange != null && pos >= lastCaretListenerRange[0] && pos < lastCaretListenerRange[1]) {
                return;
            }
            OutWriter out = getOut();
            if (out != null) {
                int[] range = new int[2];
                OutputListener l = out.getLines().getListener(pos, range);
                if (l != null) {
                    ControllerOutputEvent oe = new ControllerOutputEvent(io, out.getLines().getLineAt(pos));
                    l.outputLineSelected(oe);
                    lastCaretListenerRange = range;
                } else {
                    lastCaretListenerRange = null;
                }
            }
        }
    }

    private void sendCaretToError(boolean backward) {
        OutWriter out = getOut();
        if (out != null) {
            AbstractOutputPane op = getOutputPane();
            int selStart = op.getSelectionStart();
            int selEnd = op.getSelectionEnd();
            int pos = op.getCaretPos();

            // check if link is selected
            if (selStart != selEnd && pos == selStart && out.getLines().isListener(selStart, selEnd)) {
                pos = backward ? selStart - 1 : selEnd + 1;
            }
            int[] lpos = new int[2];
            OutputListener l = out.getLines().nearestListener(pos, backward, lpos);
            if (l != null) {
                op.sendCaretToPos(lpos[0], lpos[1], true);
                if (!io.getIOContainer().isActivated()) {
                    ControllerOutputEvent ce = new ControllerOutputEvent(io,  out.getLines().getLineAt(lpos[0]));
                    l.outputLineAction(ce);
                }
            }
        }
    }

    /**
     * Searching from current position
     * @param reversed true for reverse search
     * @return true if found
     */
    private boolean find(boolean reversed) {
        OutWriter out = getOut();
        if (out != null) {
            String lastPattern = FindDialogPanel.result();
            if (lastPattern == null) {
                return false;
            }
            int pos = reversed ? getOutputPane().getSelectionStart() : getOutputPane().getCaretPos();
            if (pos > getOutputPane().getLength() || pos < 0) {
                pos = 0;
            }
            boolean regExp = FindDialogPanel.regExp();
            boolean matchCase = FindDialogPanel.matchCase();
            int[] sel = reversed ? out.getLines().rfind(pos, lastPattern, regExp, matchCase)
                    : out.getLines().find(pos, lastPattern, regExp, matchCase);
            String appendMsg = null;
            if (sel == null) {
                sel = reversed ? out.getLines().rfind(out.getLines().getCharCount(), lastPattern, regExp, matchCase)
                        : out.getLines().find(0, lastPattern, regExp, matchCase);
                if (sel != null) {
                    appendMsg = NbBundle.getMessage(OutputTab.class, reversed ? "MSG_SearchFromEnd" : "MSG_SearchFromBeg");
                }
            }
            String msg;
            if (sel != null) {
                getOutputPane().unlockScroll();
                int line = out.getLines().getLineAt(sel[0]);
                ensureLineVisible(out, line);
                getOutputPane().setSelection(sel[0], sel[1]);
                int col = sel[0] - out.getLines().getLineStart(line);
                msg = NbBundle.getMessage(OutputTab.class, "MSG_Found", lastPattern, line + 1, col + 1);
                if (appendMsg != null) {
                    msg = msg + "; " + appendMsg;
                }
            } else {
                msg = NbBundle.getMessage(OutputTab.class, "MSG_NotFound", lastPattern);
            }
            StatusDisplayer.getDefault().setStatusText(msg);
            return sel != null;
        }
        return false;
    }

    /**
     * Ensure that a line is visible (not inside a collapsed fold). If a change
     * in the lines object is needed, fire immediately.
     */
    private void ensureLineVisible(OutWriter out, int line) {
        if (!out.getLines().isVisible(line)) {
            out.getLines().showFoldsForLine(line);
            if (out.getLines() instanceof ActionListener) {
                ((ActionListener) out.getLines()).actionPerformed(null);
            }
        }
    }

    /**
     * Holds the last written to directory for the save as file chooser.
     */
    private static String lastDir = null;

    /**
     * Invokes a file dialog and if a file is chosen, saves the output to that file.
     */
    void saveAs() {
        OutWriter out = getOut();
        if (out == null) {
            return;
        }
        File f = showFileChooser(this);
        if (f != null) {
            try {
                synchronized (out) {
                    out.getLines().saveAs(f.getPath());
                }
            } catch (IOException ioe) {
                NotifyDescriptor notifyDesc = new NotifyDescriptor(
                        NbBundle.getMessage(OutputTab.class, "MSG_SaveAsFailed", f.getPath()),
                        NbBundle.getMessage(OutputTab.class, "LBL_SaveAsFailedTitle"),
                        NotifyDescriptor.DEFAULT_OPTION,
                        NotifyDescriptor.ERROR_MESSAGE,
                        new Object[]{NotifyDescriptor.OK_OPTION},
                        NotifyDescriptor.OK_OPTION);

                DialogDisplayer.getDefault().notify(notifyDesc);
            }
        }
    }

    /**
     * Shows a file dialog and an overwrite dialog if the file exists, returning
     * null if the user chooses not to overwrite.  Will use an AWT FileDialog for
     * Aqua, per Apple UI guidelines.
     *
     * @param owner A parent component for the dialog - the top level ancestor will
     *        actually be used so positioning is correct
     * @return A file to write to
     */
    private static File showFileChooser(JComponent owner) {
        File f = null;
        String dlgTtl = NbBundle.getMessage(Controller.class, "TITLE_SAVE_DLG"); //NOI18N

        boolean isAqua = "Aqua".equals(UIManager.getLookAndFeel().getID()); //NOI18N

        if (isAqua) {
            //Apple UI guidelines recommend against ever using JFileChooser
            Container frameOrDialog = owner.getTopLevelAncestor();
            FileDialog fd;
            if (frameOrDialog instanceof Frame) {
                fd = new FileDialog((Frame) frameOrDialog, dlgTtl, FileDialog.SAVE);
            } else {
                fd = new FileDialog((Dialog) frameOrDialog, dlgTtl, FileDialog.SAVE);
            }
            if (lastDir != null && new File(lastDir).exists()) {
                fd.setDirectory(lastDir);
            }
            fd.setModal(true);
            fd.setVisible(true);
            if (fd.getFile() != null && fd.getDirectory() != null) {
                String s = fd.getDirectory() + fd.getFile();
                f = new File(s);
                if (f.exists() && f.isDirectory()) {
                    f = null;
                }
            }
        } else {
            JFileChooser jfc = new JFileChooser();
            if (lastDir != null && new File(lastDir).exists()) {
                File dir = new File(lastDir);
                if (dir.exists()) {
                    jfc.setCurrentDirectory(dir);
                }
            }
            jfc.setName(dlgTtl);
            jfc.setDialogTitle(dlgTtl);

            if (jfc.showSaveDialog(owner.getTopLevelAncestor()) == JFileChooser.APPROVE_OPTION) {
                f = jfc.getSelectedFile();
            }
        }

        if (f != null && f.exists() && !isAqua) { //Aqua's file dialog takes care of this
            String msg = NbBundle.getMessage(Controller.class,
                    "FMT_FILE_EXISTS", new Object[]{f.getName()}); //NOI18N
            String title = NbBundle.getMessage(Controller.class,
                    "TITLE_FILE_EXISTS"); //NOI18N
            if (JOptionPane.showConfirmDialog(owner.getTopLevelAncestor(), msg, title,
                    JOptionPane.OK_CANCEL_OPTION) != JOptionPane.OK_OPTION) {
                f = null;
            }
        }
        if (f != null) {
            lastDir = f.getParent();
        }
        return f;
    }

    /**
     * Called when a line is clicked - if an output listener is listening on that
     * line, it will be sent <code>outputLineAction</code>.
     */
    private void openHyperlink() {
        OutWriter out = getOut();
        if (out != null) {
            int pos = getOutputPane().getCaretPos();
            int[] range = new int[2];
            OutputListener l = out.getLines().getListener(pos, range);
            if (l != null) {
                ignoreCaretChanges = true;
                getOutputPane().sendCaretToPos(range[0], range[1], true);
                ignoreCaretChanges = false;
                ControllerOutputEvent coe = new ControllerOutputEvent(io, out.getLines().getLineAt(pos));
                l.outputLineAction(coe);
            }
        }
    }

    /**
     * Post the output window's popup menu
     *
     * @param p The point clicked
     * @param src The source of the click event
     */
    @NbBundle.Messages({"STATUS_Initializing=Output Window is Initializing"})
    void postPopupMenu(Point p, Component src) {
        if (!actionsLoaded) {
            StatusDisplayer.getDefault().setStatusText(
                    Bundle.STATUS_Initializing());
            return;
        }
        JPopupMenu popup = new JPopupMenu();
        Action[] a = getToolbarActions();
        if (a.length > 0) {
            boolean added = false;
            for (int i = 0; i < a.length; i++) {
                if (a[i].getValue(Action.NAME) != null) {
                    // add the proxy that doesn't show icons #67451
                    popup.add(new ProxyAction(a[i]));
                    added = true;
                }
            }
            if (added) {
                popup.addSeparator();
            }
        }

        List<TabAction> activeActions = new ArrayList<TabAction>(popupItems.length);
        for (int i = 0; i < popupItems.length; i++) {
            if (popupItems[i] == null) {
                popup.addSeparator();
            } else {
                TabAction ta = action(popupItems[i]);
                if (popupItems[i] == ACTION.WRAP) {
                    JCheckBoxMenuItem item = new JCheckBoxMenuItem(ta);
                    item.setSelected(getOutputPane().isWrapped());
                    activeActions.add(ta);
                    popup.add(item);
                } else if (popupItems[i] == ACTION.FILTER) {
                    JCheckBoxMenuItem item = new JCheckBoxMenuItem(ta);
                    item.setSelected(origPane != null);
                    activeActions.add(ta);
                    popup.add(item);
                } else {
                    if ((popupItems[i] == ACTION.CLOSE
                            && !io.getIOContainer().isCloseable(this))) {
                        continue;
                    }
                    JMenuItem item = popup.add(ta);
                    activeActions.add(ta);
                    if (popupItems[i] == ACTION.FIND) {
                        item.setMnemonic(KeyEvent.VK_F);
                    }
                }
            }
        }
        addFoldingActionsToPopUpMenu(popup, activeActions);
        // hack to remove the esc keybinding when doing popup..
        KeyStroke esc = KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0);
        JComponent c = getOutputPane().getTextView();
        Object escHandle = c.getInputMap().get(esc);
        c.getInputMap().remove(esc);
        getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT).remove(esc);

        popup.addPopupMenuListener(new PMListener(activeActions, escHandle));
        popup.show(src, p.x, p.y);

    }

    private void addFoldingActionsToPopUpMenu(JPopupMenu menu,
            List<TabAction> activeActions) {
        JMenu submenu = new JMenu(NbBundle.getMessage(
                OutputTab.class, "LBL_OutputFolds"));                   //NOI18N
        for (ACTION a : popUpFoldItems) {
            if (a == null) {
                submenu.addSeparator();
            } else {
                TabAction ta = action(a);
                activeActions.add(ta);
                submenu.add(new JMenuItem(ta));
            }
        }
        menu.addSeparator();
        menu.add(submenu);
    }

    void updateActions() {
        if (!actionsLoaded) {
            return;
        }
        OutputPane pane = (OutputPane) getOutputPane();
        int len = pane.getLength();
        boolean enable = len > 0;
        OutWriter out = getOut();
        action(SAVEAS).setEnabled(enable);
        action(SELECT_ALL).setEnabled(enable);
        action(COPY).setEnabled(pane.hasSelection());
        boolean hasErrors = out == null ? false : out.getLines().hasListeners();
        action(NEXT_ERROR).setEnabled(hasErrors);
        action(PREV_ERROR).setEnabled(hasErrors);
    }

    FilteredOutput filtOut;
    AbstractOutputPane origPane;

    private void setFilter(String pattern, boolean regExp, boolean matchCase) {
        if (pattern == null) {
            assert origPane != null;
            setOutputPane(origPane);
            origPane = null;
            filtOut.dispose();
            filtOut = null;
        } else {
            assert origPane == null;
            origPane = getOutputPane();
            filtOut = new FilteredOutput(pattern, regExp, matchCase);
            setOutputPane(filtOut.getPane());
            try {
                waitCursor(true);
                filtOut.readFrom(outWriter);
                installKBActions();
            } finally {
                waitCursor(false);
            }
        }
        validate();
        getOutputPane().repaint();
        requestFocus();
    }

    private void waitCursor(boolean enable) {
        RootPaneContainer root = ((RootPaneContainer) getTopLevelAncestor());
        Cursor cursor = Cursor.getPredefinedCursor(enable ? Cursor.WAIT_CURSOR : Cursor.DEFAULT_CURSOR);
        root.getGlassPane().setCursor(cursor);
        root.getGlassPane().setVisible(enable);
    }

    OutWriter getOut() {
        return origPane != null ? filtOut.getWriter() : outWriter;
    }

    private void disableHtmlName() {
        Controller.getDefault().removeFromUpdater(this);
        String escaped;
        try {
            escaped = XMLUtil.toAttributeValue(io.getName() + " ");
        } catch (CharConversionException e) {
            escaped = io.getName() + " ";
        }
        //#88204 apostophes are escaped in xm but not html
        io.getIOContainer().setTitle(this, escaped.replace("&apos;", "'"));
    }

    private void initOptionsListener() {
        optionsListener = new PropertyChangeListener() {
            @Override
            public void propertyChange(PropertyChangeEvent evt) {
                Lines lines = getDocumentLines();
                if (lines != null) {
                    String pn = evt.getPropertyName();
                    OutputOptions opts = io.getOptions();
                    updateOptionsProperty(pn, lines, opts);
                    OutputTab.this.repaint();
                }
            }
        };
        this.io.getOptions().addPropertyChangeListener(
                WeakListeners.propertyChange(optionsListener, io.getOptions()));
    }

    private void updateOptionsProperty(String pn, Lines lines,
            OutputOptions opts) {

        if (OutputOptions.PROP_COLOR_STANDARD.equals(pn)) {
            lines.setDefColor(IOColors.OutputType.OUTPUT,
                    opts.getColorStandard());
            getOutputPane().getFoldingSideBar().setForeground(
                    opts.getColorStandard());
        } else if (OutputOptions.PROP_COLOR_ERROR.equals(pn)) {
            lines.setDefColor(IOColors.OutputType.ERROR,
                    opts.getColorError());
        } else if (OutputOptions.PROP_COLOR_INPUT.equals(pn)) {
            lines.setDefColor(IOColors.OutputType.INPUT,
                    opts.getColorInput());
        } else if (OutputOptions.PROP_COLOR_LINK.equals(pn)) {
            lines.setDefColor(IOColors.OutputType.HYPERLINK,
                    opts.getColorLink());
        } else if (OutputOptions.PROP_COLOR_LINK_IMPORTANT.equals(pn)) {
            lines.setDefColor(IOColors.OutputType.HYPERLINK_IMPORTANT,
                    opts.getColorLinkImportant());
        } else if (OutputOptions.PROP_COLOR_BACKGROUND.equals(pn)) {
            Color bg = opts.getColorBackground();
            setTextViewBackground(getOutputPane().getTextView(), bg);
        } else if (OutputOptions.PROP_FONT.equals(pn)) {
            if (!getOutputPane().isWrapped()) {
                getOutputPane().setViewFont(opts.getFont());
            }
        } else if (OutputOptions.PROP_FONT_SIZE_WRAP.equals(pn)) {
            if (getOutputPane().isWrapped()) {
                getOutputPane().setViewFont(opts.getFontForWrappedMode());
            }
        }
    }

    private void loadAndInitActions() {
        RP.post(new Runnable() {
            @Override
            public void run() {
                createActions();
                EventQueue.invokeLater(new Runnable() {
                    @Override
                    public void run() {
                        installKBActions();
                        getActionMap().put("jumpPrev", //NOI18N
                                action(PREV_ERROR));
                        getActionMap().put("jumpNext", //NOI18N
                                action(NEXT_ERROR));
                        getActionMap().put(FindAction.class.getName(),
                                action(FIND));
                        getActionMap().put(
                                javax.swing.text.DefaultEditorKit.copyAction,
                                action(COPY));
                        actionsLoaded = true;
                        updateActions();
                        setInputVisible(isInputVisible()); // update action
                    }
                });
            }
        });
    }

    static enum ACTION { COPY, WRAP, SAVEAS, CLOSE, NEXT_ERROR, PREV_ERROR,
                         SELECT_ALL, FIND, FIND_NEXT, NAVTOLINE, POSTMENU,
                         FIND_PREVIOUS, CLEAR, NEXTTAB, PREVTAB, LARGER_FONT,
                         SMALLER_FONT, SETTINGS, FILTER, PASTE, COLLAPSE_FOLD,
                         EXPAND_FOLD, COLLAPSE_ALL, EXPAND_ALL, COLLAPSE_TREE,
                         EXPAND_TREE}

    private static final ACTION[] popupItems = new ACTION[] {
        COPY, PASTE, null, FIND, FIND_NEXT, FIND_PREVIOUS, FILTER, null,
        WRAP, LARGER_FONT, SMALLER_FONT, SETTINGS, null,
        SAVEAS, CLEAR, CLOSE,
    };

    private static final ACTION[] popUpFoldItems = new ACTION[]{
        COLLAPSE_FOLD, EXPAND_FOLD, null,
        COLLAPSE_ALL, EXPAND_ALL, null,
        COLLAPSE_TREE, EXPAND_TREE
    };

    private static final ACTION[] actionsToInstall = new ACTION[] {
            COPY, SELECT_ALL, FIND, FIND_NEXT, FIND_PREVIOUS, WRAP, LARGER_FONT,
            SMALLER_FONT, SAVEAS, CLOSE, COPY, NAVTOLINE, POSTMENU, CLEAR, FILTER,
            COLLAPSE_FOLD, EXPAND_FOLD, COLLAPSE_ALL, EXPAND_ALL, COLLAPSE_TREE,
            EXPAND_TREE
    };

    private final Map<ACTION, TabAction> actions = new EnumMap<ACTION, TabAction>(ACTION.class);;

    private void createActions() {
        KeyStrokeUtils.refreshActionCache();
        for (ACTION a : ACTION.values()) {
            TabAction action;
            switch(a) {
                case COPY:
                case PASTE:
                case WRAP:
                case SAVEAS:
                case CLOSE:
                case NEXT_ERROR:
                case PREV_ERROR:
                case SELECT_ALL:
                case FIND:
                case FIND_NEXT:
                case FIND_PREVIOUS:
                case FILTER:
                case LARGER_FONT:
                case SMALLER_FONT:
                case SETTINGS:
                case CLEAR:
                case COLLAPSE_FOLD:
                case EXPAND_FOLD:
                case COLLAPSE_ALL:
                case EXPAND_ALL:
                case COLLAPSE_TREE:
                case EXPAND_TREE:
                    action = new TabAction(a, "ACTION_"+a.name());
                    break;
                case NAVTOLINE:
                    action = new TabAction(a, "navToLine", //NOI18N
                                KeyStroke.getKeyStroke(KeyEvent.VK_ENTER, 0));
                    break;
                case POSTMENU:
                    action = new TabAction(a, "postMenu", //NOI18N
                                KeyStroke.getKeyStroke(KeyEvent.VK_F10, KeyEvent.SHIFT_DOWN_MASK));
                    break;
                case NEXTTAB:
                    action = new TabAction(a, "NextViewAction", //NOI18N
                                (KeyStroke) null);
                    break;
                case PREVTAB:
                    action = new TabAction(a, "PreviousViewAction", //NOI18N
                                (KeyStroke) null);
                    break;
                default:
                    throw new IllegalStateException("Unhandled action "+a);
            }
            actions.put(a, action);
        }
    }

    /**
     * A stateless action which will find the owning OutputTab's controller and call
     * actionPerformed with its ID as an argument.
     */
    class TabAction extends AbstractAction {

        private ACTION action;

        /**
         * Create a ControllerAction with the specified action ID (constants defined in Controller),
         * using the specified bundle key.  Expects the following contents in the bundle:
         * <ul>
         * <li>A name for the action matching the passed key</li>
         * <li>An accelerator for the action matching [key].accel</li>
         * </ul>
         * @param id An action ID
         * @param bundleKey A key for the bundle associated with the Controller class
         * @see org.openide.util.Utilities#stringToKey
         */
        TabAction(ACTION action, String bundleKey) {
            if (bundleKey != null) {
                String name = NbBundle.getMessage(OutputTab.class, bundleKey);
                List<KeyStroke[]> accels = getAcceleratorsFor(action);
                this.action = action;
                putValue(NAME, name);
                if (accels != null && accels.size() > 0) {
                    List<KeyStroke> l = new ArrayList<KeyStroke>(accels.size());
                    for (KeyStroke[] ks : accels) {
                        if (ks.length == 1) { // ignore multi-key accelerators
                            l.add(ks[0]);
                        }
                    }
                    if (l.size() > 0) {
                        putValue(ACCELERATORS_KEY,
                                l.toArray(new KeyStroke[l.size()]));
                        putValue(ACCELERATOR_KEY, l.get(0));
                    }
                }
            }
        }

        /**
         * Create a ControllerAction with the specified ID, name and keystroke.  Actions created
         * using this constructor will not be added to the popup menu of the component.
         *
         * @param id The ID
         * @param name A programmatic name for the item
         * @param stroke An accelerator keystroke
         */
        TabAction(ACTION action, String name, KeyStroke stroke) {
            this.action = action;
            putValue(NAME, name);
            putValue(ACCELERATOR_KEY, stroke);
        }

        void clearListeners() {
            PropertyChangeListener[] l = changeSupport.getPropertyChangeListeners();
            for (int i = 0; i < l.length; i++) {
                removePropertyChangeListener(l[i]);
            }
        }

        /**
         * Get a keyboard accelerator from the resource bundle, with special handling
         * for the mac keyboard layout.
         *
         * @param action Action to get accelerator for.
         * @return A keystroke
         */
        private List<KeyStroke[]> getAcceleratorsFor(ACTION action) {
            switch (action) {
                case COPY:
                    return KeyStrokeUtils.getKeyStrokesForAction(
                            "copy-to-clipboard", null);                 //NOI18N
                case PASTE:
                    return KeyStrokeUtils.getKeyStrokesForAction(
                            "paste-from-clipboard", null);              //NOI18N
                case SAVEAS:
                    return KeyStrokeUtils.getKeyStrokesForAction(
                            OutputKeymapManager.SAVE_AS_ACTION_ID, null);
                case CLOSE:
                    return KeyStrokeUtils.getKeyStrokesForAction(
                            OutputKeymapManager.CLOSE_ACTION_ID, null);
                case NEXT_ERROR:
                    return KeyStrokeUtils.getKeyStrokesForAction(
                            "next-error", null);                        //NOI18N
                case PREV_ERROR:
                    return KeyStrokeUtils.getKeyStrokesForAction(
                            "previous-error", null);                    //NOI18N
                case SELECT_ALL:
                    return KeyStrokeUtils.getKeyStrokesForAction(
                            "select-all", null);                        //NOI18N
                case FIND:
                    return KeyStrokeUtils.getKeyStrokesForAction(
                            "org.openide.actions.FindAction", null);    //NOI18N
                case FIND_NEXT:
                    return KeyStrokeUtils.getKeyStrokesForAction(
                            "find-next", null);                         //NOI18N
                case FIND_PREVIOUS:
                    return KeyStrokeUtils.getKeyStrokesForAction(
                            "find-previous", null);                     //NOI18N
                case FILTER:
                    return KeyStrokeUtils.getKeyStrokesForAction(
                            OutputKeymapManager.FILTER_ACTION_ID, null);
                case LARGER_FONT:
                    return KeyStrokeUtils.getKeyStrokesForAction(
                            OutputKeymapManager.LARGER_FONT_ACTION_ID, null);
                case SMALLER_FONT:
                    return KeyStrokeUtils.getKeyStrokesForAction(
                            OutputKeymapManager.SMALLER_FONT_ACTION_ID, null);
                case SETTINGS:
                    return KeyStrokeUtils.getKeyStrokesForAction(
                            OutputKeymapManager.OUTPUT_SETTINGS_ACTION_ID, null);
                case CLEAR:
                    return KeyStrokeUtils.getKeyStrokesForAction(
                            OutputKeymapManager.CLEAR_ACTION_ID, null);
                case WRAP:
                    return KeyStrokeUtils.getKeyStrokesForAction(
                            OutputKeymapManager.WRAP_ACTION_ID, null);
                case COLLAPSE_FOLD:
                    return KeyStrokeUtils.getKeyStrokesForAction(
                            "collapse-fold", null);                     //NOI18N
                case EXPAND_FOLD:
                    return KeyStrokeUtils.getKeyStrokesForAction(
                            "expand-fold", null);                       //NOI18N
                case COLLAPSE_ALL:
                    return KeyStrokeUtils.getKeyStrokesForAction(
                            "collapse-all-folds", null);                //NOI18N
                case EXPAND_ALL:
                    return KeyStrokeUtils.getKeyStrokesForAction(
                            "expand-all-folds", null);                  //NOI18N
                case COLLAPSE_TREE:
                    return KeyStrokeUtils.getKeyStrokesForAction(
                            "collapse-fold-tree", null);                //NOI18N
                case EXPAND_TREE:
                    return KeyStrokeUtils.getKeyStrokesForAction(
                            "expand-fold-tree", null);                  //NOI18N
                default:
                    return null;
            }
        }

        public ACTION getAction() {
            return action;
        }

        @Override
        public boolean isEnabled() {
            if (getIO().isClosed()) {
                // Cached action for an already closed tab.
                // Try to find the appropriate action for a new active tab...
                JComponent selected = getIO().getIOContainer().getSelected();
                if (OutputTab.this != selected && selected instanceof OutputTab) {
                    OutputTab tab = (OutputTab) selected;
                    Action a = tab.action(action);
                    if (a != null) {
                        return a.isEnabled();
                    }
                }
                return false;
            }
            return super.isEnabled();
        }

        public void actionPerformed(ActionEvent e) {
            if (getIO().isClosed()) {
                // Cached action for an already closed tab.
                // Try to find the appropriate action for a new active tab...
                JComponent selected = getIO().getIOContainer().getSelected();
                if (OutputTab.this != selected && selected instanceof OutputTab) {
                    OutputTab tab = (OutputTab) selected;
                    Action a = tab.action(action);
                    if (a != null) {
                        a.actionPerformed(e);
                    }
                }
                return ;
            }
            switch (getAction()) {
                case COPY:
                    getOutputPane().copy();
                    break;
                case PASTE:
                    getOutputPane().paste();
                    break;
                case WRAP:
                    boolean wrapped = getOutputPane().isWrapped();
                    getOutputPane().setWrapped(!wrapped);
                    break;
                case SAVEAS:
                    saveAs();
                    break;
                case CLOSE:
                    io.getIOContainer().remove(OutputTab.this);
                    break;
                case NEXT_ERROR:
                    sendCaretToError(false);
                    break;
                case PREV_ERROR:
                    sendCaretToError(true);
                    break;
                case SELECT_ALL:
                    getOutputPane().selectAll();
                    break;
                case FIND:
                     {
                        String pattern = getFindDlgResult(getOutputPane().getSelectedText(), "LBL_Find_Title", "LBL_Find_What", "BTN_Find"); //NOI18N
                        if (pattern != null && find(false)) {
                            Action findNext = action(FIND_NEXT);
                            Action findPrev = action(FIND_PREVIOUS);
                            if (findNext != null) {
                                findNext.setEnabled(true);
                            }
                            if (findPrev != null) {
                                findPrev.setEnabled(true);
                            }
                            requestFocus();
                        }
                    }
                    break;
                case FIND_NEXT:
                    find(false);
                    break;
                case FIND_PREVIOUS:
                    find(true);
                    break;
                case NAVTOLINE:
                    openHyperlink();
                    break;
                case POSTMENU:
                    postPopupMenu(new Point(0, 0), OutputTab.this);
                    break;
                case CLEAR:
                    NbWriter writer = io.writer();
                    if (writer != null) {
                        try {
                            boolean vis = isInputVisible();
                            boolean closed = io.isStreamClosed();
                            writer.reset();
                            setInputVisible(vis);
                            io.setStreamClosed(closed);
                        } catch (IOException ioe) {
                            Exceptions.printStackTrace(ioe);
                        }
                    }
                    break;
                case SMALLER_FONT:
                    Controller.getDefault().changeFontSizeBy(-1, getOutputPane().isWrapped());
                    break;
                case LARGER_FONT:
                    Controller.getDefault().changeFontSizeBy(1, getOutputPane().isWrapped());
                    break;
                case SETTINGS:
                    OptionsDisplayer.getDefault().open(
                            "Advanced/OutputSettings");                 //NOI18N
                    break;
                case FILTER:
                    if (origPane != null) {
                        setFilter(null, false, false);
                    } else {
                        String pattern = getFindDlgResult(getOutputPane().getSelectedText(),
                                "LBL_Filter_Title", "LBL_Filter_What", "BTN_Filter"); //NOI18N
                        if (pattern != null) {
                            setFilter(pattern, FindDialogPanel.regExp(), FindDialogPanel.matchCase());
                        }
                    }
                    break;
                case COLLAPSE_FOLD:
                    getOutputPane().collapseFold();
                    break;
                case EXPAND_FOLD:
                    getOutputPane().expandFold();
                    break;
                case COLLAPSE_ALL:
                    getOutputPane().collapseAllFolds();
                    break;
                case EXPAND_ALL:
                    getOutputPane().expandAllFolds();
                    break;
                case COLLAPSE_TREE:
                    getOutputPane().collapseFoldTree();
                    break;
                case EXPAND_TREE:
                    getOutputPane().expandFoldTree();
                    break;
                default:
                    assert false;
            }
        }
    }

    @Override
    public void setInputVisible(boolean val) {
        super.setInputVisible(val);
        Action pasteAction = action(PASTE);
        if (pasteAction != null) {
            pasteAction.setEnabled(val);
        }
    }

    private boolean validRegExp(String pattern) {
        try {
            Pattern.compile(pattern);
            return true;
        } catch (PatternSyntaxException ex) {
            JOptionPane.showMessageDialog(getTopLevelAncestor(), 
                    NbBundle.getMessage(OutputTab.class, "FMT_Invalid_RegExp", pattern),
                    NbBundle.getMessage(OutputTab.class, "LBL_Invalid_RegExp"), JOptionPane.ERROR_MESSAGE);
            return false;
        }
    }

    private String getFindDlgResult(String selection, String title, String label, String button) {
        String pattern = FindDialogPanel.getResult(selection, title, label, button); //NOI18N
        while (pattern != null && FindDialogPanel.regExp()) {
            if (validRegExp(pattern)) {
                break;
            }
            pattern = FindDialogPanel.getResult(pattern, title, label, button); //NOI18N
        }
        return pattern;
    }

    private static class ProxyAction implements Action {

        private Action orig;

        ProxyAction(Action original) {
            orig = original;
        }

        public Object getValue(String key) {
            if (Action.SMALL_ICON.equals(key)) {
                return null;
            }
            return orig.getValue(key);
        }

        public void putValue(String key, Object value) {
            orig.putValue(key, value);
        }

        public void setEnabled(boolean b) {
            orig.setEnabled(b);
        }

        public boolean isEnabled() {
            return orig.isEnabled();
        }

        public void addPropertyChangeListener(PropertyChangeListener listener) {
            orig.addPropertyChangeListener(listener);
        }

        public void removePropertyChangeListener(PropertyChangeListener listener) {
            orig.removePropertyChangeListener(listener);
        }

        public void actionPerformed(ActionEvent e) {
            orig.actionPerformed(e);
        }
    }

    /**
     * #47166 - a disposed tab which has had its popup menu shown remains
     * referenced through PopupItems->JSeparator->PopupMenu->Invoker->OutputPane->OutputTab
     */
    private class PMListener implements PopupMenuListener {

        private List<TabAction> popupItems;
        private Object handle;

        PMListener(List<TabAction> popupItems, Object escHandle) {
            this.popupItems = popupItems;
            handle = escHandle;
        }

        public void popupMenuWillBecomeInvisible(PopupMenuEvent e) {
            JPopupMenu popup = (JPopupMenu) e.getSource();
            popup.removeAll();
            popup.setInvoker(null);
            // hack
            KeyStroke esc = KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0);
            JComponent c = getOutputPane().getTextView();
            c.getInputMap().put(esc, handle);
            getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT).put(esc, handle);

            //hack end
            popup.removePopupMenuListener(this);
            for (TabAction action : popupItems) {
                action.clearListeners();
            }
        }

        public void popupMenuCanceled(PopupMenuEvent e) {
            popupMenuWillBecomeInvisible(e);
        }

        public void popupMenuWillBecomeVisible(PopupMenuEvent e) {
            //do nothing
        }
    }

    private class FilteredOutput {
        String pattern;
        OutWriter out;
        OutputPane pane;
        OutputDocument doc;
        int readCount;
        Pattern compPattern;
        boolean regExp;
        boolean matchCase;

        public FilteredOutput(String pattern, boolean regExp, boolean matchCase) {
            this.pattern = (regExp || matchCase) ? pattern : pattern.toLowerCase();
            this.regExp = regExp;
            this.matchCase = matchCase;
            out = new OutWriter();
            pane = new OutputPane(OutputTab.this);
            doc = new OutputDocument(out);
            pane.setDocument(doc);
        }

        boolean passFilter(String str) {
            if (regExp) {
                if (compPattern == null) {
                    compPattern = matchCase ? Pattern.compile(pattern) : Pattern.compile(pattern, Pattern.CASE_INSENSITIVE);
                }
                return compPattern.matcher(str).find();
            } else {
                return matchCase ? str.contains(pattern) : str.toLowerCase().contains(pattern);
            }
        }

        OutputPane getPane() {
            return pane;
        }

        OutWriter getWriter() {
            return out;
        }

        synchronized void readFrom(OutWriter orig) {
            AbstractLines lines = (AbstractLines) orig.getLines();
            // Last line is not guaranteed to be finished, see #219839.
            int lineCount = lines.getLineCount() - (orig.isClosed() ? 0 : 1);
            while (readCount < lineCount) {
                try {
                    int line = readCount++;
                    String str = lines.getLine(line);
                    if (!passFilter(str)) {
                        continue;
                    }
                    LineInfo info = lines.getExistingLineInfo(line);
                    out.print(str, info, lines.isImportantLine(line));
                } catch (IOException ex) {
                    Exceptions.printStackTrace(ex);
                }
            }
        }

        void dispose() {
            out.dispose();
        }
    }

    private Lines getDocumentLines() {
        OutputDocument doc = getDocument();
        return doc == null ? null : doc.getLines();
    }
}
