/*
 * The contents of this file are subject to the terms of the Common Development
 * and Distribution License (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.html
 * or http://www.netbeans.org/cddl.txt.
 * 
 * When distributing Covered Code, include this CDDL Header Notice in each file
 * and include the License file at http://www.netbeans.org/cddl.txt.
 * If applicable, add the following below the CDDL Header, with the fields
 * enclosed by brackets [] replaced by your own identifying information:
 * "Portions Copyrighted [year] [name of copyright owner]"
 * 
 * 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.
 */

package org.netbeans.core.output;

import java.util.WeakHashMap;
import java.util.HashSet;
import java.io.PipedWriter;

import java.awt.*;
import java.awt.event.*;
import java.awt.datatransfer.*;
import java.beans.PropertyChangeListener;
import java.beans.PropertyChangeEvent;
import java.util.Map;
import java.util.Set;
import javax.swing.AbstractAction;

import javax.swing.JPopupMenu;
import javax.swing.JMenuItem;
import javax.swing.JTabbedPane;
import javax.swing.KeyStroke;
import javax.swing.SwingUtilities;

import org.openide.ErrorManager;
import org.openide.windows.*;
import org.openide.util.NbBundle;
import org.openide.util.Mutex;
import org.openide.util.Utilities;
import org.openide.util.WeakSet;
import org.openide.util.datatransfer.*;
import org.openide.nodes.Node;

import org.netbeans.lib.terminalemulator.*;

/**
 * TopComponent for output tabs. It handles output in new winsts implementation.
 *
 * @author  Marek Slama
 *
 */

public class OutputView extends TopComponent implements PropertyChangeListener, ActionListener {


    private static final boolean DEBUG = false;
    
    public static final String ICON_RESOURCE =
        "/org/netbeans/core/resources/frames/output.gif"; // NOI18N
    
    /** If true, the error output is separated from the normal output */

    /** Mapping of string:OutputTabInner */
    static final Map ioCache = new WeakHashMap(7);

    /** Singleton instances of the standard output tabs */
    private static OutputTabInner standard;
    
    private static final long serialVersionUID = 3276523892250080205L;
    
    private static Factory factory;
    
    /** Singleton instance */
    private static OutputView DEFAULT;
    
    private JTabbedPane tabbedPane;
    
    /** Set of opened components in OutputView */
    private Set openedComps = new HashSet(10);
    
    /** Set of closed components in OutputView */
    private Set closedComps = new WeakSet(10);
    
    private String baseName;
    
    /**
     * State machine (State _enum_ really) for recognizing java exception
     * patterns.
     * See comment above appendText() for general idea.
     */
    private static class State {
	private final String name;
	private State(String name) { 
	    this.name = name;
	}
	public String toString() { return name; }

	public static final State init = new State("init");
	public static final State collect = new State("collect");
	public static final State pass = new State("pass");
    };

    private static void debug(String s) {
        if (!DEBUG) return;
        s = "OutputView:"+s+"\r\n"; // NOI18N
        try {
            String dir = outputSettings().getDirectory().getAbsolutePath();
            java.io.FileOutputStream fos = new java.io.FileOutputStream (dir + "/debug.log", true); // NOI18N
            fos.write(s.getBytes());
            fos.close();
        } catch (java.io.IOException e) {
            ErrorManager.getDefault().notify(ErrorManager.INFORMATIONAL, e);
        }
    }
    
    private OutputView () {
        synchronized (OutputView.class) {
            setActivatedNodes (null);
            
            setIcon(Utilities.loadImage("org/netbeans/core/resources/outputSettings.gif")); // NOI18N
        
            TopComponent.getRegistry().addPropertyChangeListener(
                org.openide.util.WeakListener.propertyChange(this, TopComponent.getRegistry()));
            
            // set accessiblle description
            getAccessibleContext ().setAccessibleName (
                NbBundle.getBundle (OutputView.class).getString ("ACSN_OutputWindow"));
            getAccessibleContext ().setAccessibleDescription (
                NbBundle.getBundle (OutputView.class).getString ("ACSD_OutputWindow"));
            setBorder(null);
            setLayout(new BorderLayout());
        }        
    }
    
    private OutputView (String name) {
        this();
        setName(name);
        baseName = name;
    }
    
    public void addNotify() {
        super.addNotify();
        TabHandlePopupListener.install();
    }
    
    /** Return preferred ID */
    protected String preferredID () {
        return "output"; //NOI18N
    }
    
    public void removeNotify() {
        TabHandlePopupListener.uninstall();
        super.removeNotify();
    }
    
    public void requestActive () {
        requestActive(true);
    }
    
    public void requestActive(boolean focus) {
        //Intentionally does not change active component if there is no
        //inner term to focus - this will leave winsys in an inconsistent
        //state - the active tc will be the output window but keyboard focus
        //will be in the editor
        if (focus) {
            Component c = getSelectedComponent();
            if (c != null) {
                if (c instanceof OutputTabInner) {
                    boolean focused = c.requestFocusInWindow();
                    if (focused) {
                        super.requestActive();
                    }
                }
            }
        }
    }
    
    /* Singleton accessor. As OutputView is persistent singleton this
     * accessor makes sure that OutputView is deserialized by window system.
     * Uses known unique TopComponent ID "output" to get OutputView instance
     * from window system. "output" is name of settings file defined in module layer.
     */
    public static synchronized OutputView findDefault () {
        if (DEFAULT == null) {
            TopComponent tc = WindowManager.getDefault().findTopComponent("output"); // NOI18N
            if (tc != null) {
                if (tc instanceof OutputView) {
                    DEFAULT = (OutputView) tc;
                } else {
                    //Incorrect settings file?
                    IllegalStateException exc = new IllegalStateException
                    ("Incorrect settings file. Unexpected class returned." // NOI18N
                    + " Expected:" + OutputView.class.getName() // NOI18N
                    + " Returned:" + tc.getClass().getName()); // NOI18N
                    ErrorManager.getDefault().notify(ErrorManager.INFORMATIONAL, exc);
                    //Fallback to accessor reserved for window system.
                    OutputView.getDefault();
                }
            } else {
                //OutputView cannot be deserialized
                //Fallback to accessor reserved for window system.
                OutputView.getDefault();
            }
        }
        return DEFAULT;
    }
    
    /* Singleton accessor reserved for window system ONLY. Used by window system to create
     * OutputView instance from settings file when method is given. Use <code>findDefault</code>
     * to get correctly deserialized instance of OutputView. */
    public static synchronized OutputView getDefault () {
        if (DEFAULT == null) {
            DEFAULT = new OutputView(NbBundle.getBundle(OutputView.class).getString("CTL_OutputWindow_OutputTab"));
        }
        return DEFAULT;
    }
    
    /* Accessor reserved for window system, to discard the default instance on project switch. */
    public static synchronized void discardDefault() {
        if (DEFAULT != null) {
            final OutputView last = DEFAULT;
            DEFAULT = null;
            Mutex.EVENT.writeAccess(new Runnable() {
                public void run() {
                    last.clear();
                }
            });
        }
    }
    
    
    void clear() {
        discardAllTabs();
        closedComps.clear();
        close();
    }
    
    /** Overriden to explicitely set persistence type of OutputView
     * to PERSISTENCE_ALWAYS */
    public int getPersistenceType() {
        return TopComponent.PERSISTENCE_ALWAYS;
    }
    
    public static InputOutput getIO(String name, boolean newIO) {
        //System.out.println("++ OutputView.getIO ENTER");
        //Thread.dumpStack();
        InputOutput inpo = getFactory().getIO(name, newIO);
        /*System.out.println("++ OutputView.getIO RETURN"
        + " c:" + ((TopComponent) inpo).getName()
        + " [" + inpo.getClass().getName() + "]");*/
        return inpo;
    }

    public static OutputWriter getStdOut() {
        //System.out.println("++ OutputView.getStdOut");
        return getFactory().getStdOut ();
    }
    
    /** Reads OutputView.
    */
//    public void readExternal (ObjectInput oi)
//    throws IOException, ClassNotFoundException {
//        super.readExternal (oi);
//    }
    
    /** Writes OutputView. */
//    public void writeExternal (ObjectOutput oo) throws IOException {
//        super.writeExternal(oo);
//    }
    
    /** Resolve to singleton instance */
    public Object readResolve() throws java.io.ObjectStreamException {
        return OutputView.getDefault();
    }
    
    
    private JTabbedPane getTabbedPane() {
        if(tabbedPane == null) {
            tabbedPane = new JTabbedPane(JTabbedPane.TOP);
            tabbedPane.getInputMap(WHEN_ANCESTOR_OF_FOCUSED_COMPONENT).put(
                KeyStroke.getKeyStroke(KeyEvent.VK_F4, KeyEvent.CTRL_DOWN_MASK), 
                "discard"); //NOI18N
            tabbedPane.getActionMap().put("discard", new DiscardAction());
        }
        return tabbedPane;
    }
    
    private class DiscardAction extends AbstractAction {
        public void actionPerformed (ActionEvent ae) {
            discardTab();
        }
    }    
    
    // Methods to manipulate with tabs inside OutputView.
    /* Select component inside tab. */
    void requestVisible (final Component c) {
        /*System.out.println("OutputView.requestVisible ENTER"
        + " c:" + c.getName()
        + " th:" + Thread.currentThread().getName());
        Thread.dumpStack();*/
        Mutex.EVENT.readAccess(new Runnable() {
            public void run() {
                // The output TopComponent has to be visible too.
                OutputView.this.requestVisible();
                
                /*System.out.println("OutputView.requestVisible ENTER DELAYED"
                + " c:" + c.getName()
                + " th:" + Thread.currentThread().getName()
                + " isOpenedInOV(c):" + isOpenedInOV(c)
                + " openedComps.size:" + openedComps.size()); */
                if (isOpenedInOV(c) && (openedComps.size() > 1)) {
                    /* System.out.println("OutputView.requestVisible CALL OF setSelectedComponent"
                    + " c:" + c.getName()); */
                    JTabbedPane tab = getTabbedPane();
                    if (!c.equals(tab.getSelectedComponent())) {
                        //System.out.println("OutputView.requestVisible CALL OF");
                        tab.setSelectedComponent(c);
                        setActivatedNodes(((TopComponent) c).getActivatedNodes());
                    }
                } /*else {
                    System.out.println("OutputView.requestVisible NOT OPENED"
                    + " c:" + c.getName());
                }*/
                /*System.out.println("OutputView.requestVisible LEAVE DELAYED"
                + " c:" + c.getName()
                + " th:" + Thread.currentThread().getName());*/
            }
        });
    }
    
    /* Request focus on component in tab. */
    void requestFocus (final Component c) {
        Mutex.EVENT.readAccess(new Runnable() {
            public void run() {
                // The output TopComponent has to be active when the inner tab gets focus.
                OutputView.this.requestActive(false);
                if (isOpenedInOV(c)) {
                    if (openedComps.size() > 1) {
                        JTabbedPane tab = getTabbedPane();
                        if (!c.equals(tab.getSelectedComponent())) {
                            tab.setSelectedComponent(c);
                            setActivatedNodes(((TopComponent) c).getActivatedNodes());
                        }
                    }
                    c.requestFocusInWindow();
                }
            }
        });
    }
    
    /* Returns component selected in OutputView. Returns null when OutputView is empty
     * or when 'there is no tab selected' in JTabbedPane 
     * ie. return value from JTabbedPane.getSelectedComponent */
    Component getSelectedComponent () {
        if (openedComps.size() > 1) {
            return getTabbedPane().getSelectedComponent();
        } else if (openedComps.size() == 1) {
            return ((Component []) openedComps.toArray(new Component[1]))[0];
        } else {
            return null;
        }
    }
    
    /** Returns standard output top component */
    /*public static TopComponent getStdOutputTab() {
        System.out.println("++ OutputView.getStdOutputTab");
        return getFactory().getStdOutputTab();
    }*/
    
    private static OutputSettings outputSettings () {
        return (OutputSettings)OutputSettings.findObject (OutputSettings.class, true);
    }
    
    /* Returns text content of output tab. This is
    * content of terminal buffer, which is limited by
    * history size.
    * <p>
    * This function is no MT-safe call it from the AWT Event Dispatch thread.
    *
    * @return text content of output window buffer
    */
    public String toString () {
        return "";
    }
    
    static synchronized Factory getFactory () {
        if (factory == null) {
            factory = new Factory();
        }
        return factory;        
    }
    
    private static synchronized void initialize () {
        if (standard == null) {
            // create the tab for StdOut
            String name = NbBundle.getBundle(OutputView.class).getString("CTL_OutputWindow_OutputTab");
            standard = new OutputTabInner(name);
            // delete default behaviour of output tabs - remember even closed standard tab
            standard.putClientProperty("PersistenceType", null); // NOI18N
        }
    }
    
    public static class Factory { //implements OutputTabProvider {
        Factory () {
            //debug("OutputTabInner.Factory()"); // NOI18N
        }
        
        /** Print output writer.
         * @return default system output printer
         */
        public OutputWriter getStdOut() {
            //System.out.println("OutputView.Factory.getStdOut()");
            //debug("OutputTabInner.Factory.getStdOut()"); // NOI18N
            initialize();
            return standard.getOut();
        }
        
        /** creates new OutputWriter
         * @param name is a name of the writer
         * @return new OutputWriter with given name
         */
        public InputOutput getIO(String name, boolean newIO) {
            //System.out.println("OutputView.Factory.getIO("+ name + ", " + newIO + ")");
            //debug("OutputTabInner.Factory.getIO("+name+", "+newIO+")"); // NOI18N
            initialize();
            if (newIO) {
                //debug("..creating new"); // NOI18N
                /*System.out.println("OutputView.Factory.getIO"
                + " CREATE NEW 1");*/
                return new OutputTabInner(name);
            } else {
                InputOutput ino;
                synchronized(ioCache) {
                    ino = (InputOutput)ioCache.get(name);
                }
                if (ino == null) {
                    //debug("..cannot find - creating new"); // NOI18N
                    /*System.out.println("OutputView.Factory.getIO"
                    + " CREATE NEW 2");*/
                    ino = new OutputTabInner(name);
                }
                else {
                    //debug("using existing"); // NOI18N
                    /*System.out.println("OutputView.Factory.getIO"
                    + " USE EXISTING");*/
                }
                return ino;
            }
        }
        
        /** Returns standard output top component */
        public TopComponent getStdOutputTab() {
            //System.out.println("OutputView.Factory.getStdOutputTab");
            //debug("OutputTabInner.Factory.getStdOutputTab()"); // NOI18N
            initialize();
            return standard;
        }
        
    }

    // OutputTabs are not serialized
    // null is returned during deserialization
    // only standard output tab is dseserialized to
    // default instance

    /** This class is serializaed instead of OutputView */
    static class Replace implements java.io.Serializable {

        private static final long serialVersionUID =-3237844916624172415L;

        public Replace () {
        }
        
        /** Resolve as default singleton or null */
        public Object readResolve() throws java.io.ObjectStreamException {
            return OutputView.getDefault();
        }
    }
    
//    public boolean isClosed() {
//        //debug("isClosed()"); // NOI18N
//        Workspace wrkSpace = WindowManager.getDefault().getCurrentWorkspace();
//        return !isOpened(wrkSpace);
//    }
    
//    void ensureOpen() {
//        //debug("ensureOpen()"); // NOI18N
//        if (isClosed()) {
//            //debug("opening"); // NOI18N
//            open();
//        }
//    }
    
    /** Returns true if given component is opened in OutputView */
    public boolean isOpenedInOV (Component c) {
        return openedComps.contains(c);
    }
    
    /** Returns true if given component is closed and present in OutputView.
     * For example it could be opened and closed. */
    public boolean isClosedInOV (Component c) {
        return closedComps.contains(c);
    }
    
    /* Opens component in OutputView */
    public void openInOV (final Component c) {
        /* System.out.println("-- OutputView.openInOV ENTER"
        + " c:" + c.getName()
        + " [" + Integer.toHexString(System.identityHashCode(c)) + "]"
        + " th:" + Thread.currentThread().getName()); */
        //Open OutputView
        Mutex.EVENT.readAccess(new Runnable() {
            public void run() {
                /*System.out.println("-- OutputView.openInOV ENTER DELAYED"
                + " c:" + c.getName()
                + " [" + Integer.toHexString(System.identityHashCode(c)) + "]"
                + " th:" + Thread.currentThread().getName()); */
                //Open OutputView
                if (!isOpened()) {
                    open();
                }
                if (isOpenedInOV(c)) {
                    //Already opened
                    /*System.out.println("-- OutputView.openInOV LEAVE 1 DELAYED"
                    + " c:" + c.getName()); */
                    return;
                }
                /* System.out.println("-- OutputView.openInOV DELAYED"
                + " c:" + c.getName()
                + " th:" + Thread.currentThread().getName()
                + " CALL OF addTab openedComps.size=" + openedComps.size()); */
                if (openedComps.size() == 0) {
                    //Add component directly to OutputView
                    add(c);
                    revalidate();
                    setActivatedNodes(((TopComponent) c).getActivatedNodes());
                    setName(baseName + " - " + c.getName());
                    getInputMap(WHEN_ANCESTOR_OF_FOCUSED_COMPONENT).put(
                        KeyStroke.getKeyStroke(KeyEvent.VK_F4, KeyEvent.CTRL_DOWN_MASK), 
                        "discard"); //NOI18N
                    getActionMap().put("discard", new DiscardAction());
                } else if (openedComps.size() == 1) {
                    Component old = getComponents()[0];
                    remove(old);
                    JTabbedPane tab = getTabbedPane();
                    add(tab);
                    tab.addTab(old.getName(),old);
                    tab.addTab(c.getName(),c);
                    setActivatedNodes(((TopComponent) c).getActivatedNodes());
                    setName(baseName);
                    getInputMap(WHEN_ANCESTOR_OF_FOCUSED_COMPONENT).remove(
                        KeyStroke.getKeyStroke(KeyEvent.VK_F4, KeyEvent.CTRL_DOWN_MASK));
                    getTabbedPane().addTab(c.getName(),c);
                    setActivatedNodes(((TopComponent) c).getActivatedNodes());
                } 
                if (closedComps.contains(c)) {
                    closedComps.remove(c);
                }
                openedComps.add(c);
                if (openedComps.size() > 1) {
                    JTabbedPane tab = getTabbedPane();
                    if (tab.getTabCount() != openedComps.size()) {
                        tab.add (c.getName(), c);
                    }
                }                
                /*System.out.println("-- OutputView.openInOV LEAVE 2 DELAYED"
                + " c:" + c.getName());*/
            }
        });
        /*System.out.println("-- OutputView.openInOV LEAVE 2"
        + " c:" + c.getName()
        + " [" + Integer.toHexString(System.identityHashCode(c)) + "]");*/
    }
    
    /** Creates and returns popup menu for given top component,
     * filled with standard action presenters for window actions */
    protected JPopupMenu createPopupMenu () {
        JPopupMenu popup = new JPopupMenu();
        JMenuItem menuItem = new JMenuItem
        (NbBundle.getBundle(OutputView.class).getString("LBL_Discard"));
        menuItem.setActionCommand("Discard");
        menuItem.addActionListener(this);
        popup.add(menuItem);
        
        menuItem = new JMenuItem
        (NbBundle.getBundle(OutputView.class).getString("LBL_DiscardAll"));
        menuItem.setActionCommand("DiscardAll");
        menuItem.addActionListener(this);
        popup.add(menuItem);
        
        return popup;
    }

    /** Shows given popup on given coordinations and takes care about the
     * situation when menu can exceed screen limits */
    protected void showPopupMenu (JPopupMenu popup, Point p, Component comp) {
        SwingUtilities.convertPointToScreen (p, comp);
        Dimension popupSize = popup.getPreferredSize ();
        Rectangle screenBounds = Utilities.getUsableScreenBounds(getGraphicsConfiguration());
        
        if (p.x + popupSize.width > screenBounds.x + screenBounds.width) {
            p.x = screenBounds.x + screenBounds.width - popupSize.width;
        }
        if (p.y + popupSize.height > screenBounds.y + screenBounds.height) {
            p.y = screenBounds.y + screenBounds.height - popupSize.height;
        }
        
        SwingUtilities.convertPointFromScreen (p, comp);
        popup.show(comp, p.x, p.y);
    }
    
    //
    // Static stuff
    //
    
    static String getOutDisplayName() {
        return NbBundle.getBundle(OutputView.class).getString("CTL_OutputWindow");
    }
    
    //
    // TopComponent methods
    //
    
    /** always open this top component in output mode, if
    * no mode for this component is specified yet */
    public void open (Workspace workspace) {
        //debug("OutputTabInner.open():"+getName()); // NOI18N
        if (!isShowing()) {
            Workspace realWorkspace = (workspace == null)
                ? WindowManager.getDefault().getCurrentWorkspace()
                : workspace;
            // dock into outwin mode if not docked yet
            Mode mode = realWorkspace.findMode("output"); // NOI18N
            if (mode == null) {
                mode = realWorkspace.createMode("output", getOutDisplayName(), // NOI18N
                   OutputView.class.getResource(ICON_RESOURCE));
            }
            Mode tcMode = realWorkspace.findMode(this);
            if (tcMode == null) {
                mode.dockInto(this);
            }
            // behave like superclass
            super.open(workspace);
        } else {
            requestActive();
        }
    }
    
    //
    // inner classes
    //
    
    // Replacement for TopComponentListener. Listen on opened components.
    public void propertyChange(PropertyChangeEvent evt) {
    }    

    public void actionPerformed(ActionEvent e) {
        if ("Discard".equals(e.getActionCommand())) {
            discardTab();
        } else if ("DiscardAll".equals(e.getActionCommand())) {
            discardAllTabs();
        }
    }
    
    /* Close selected inner tab */
    void discardTab () {
        Mutex.EVENT.readAccess(new Runnable() {
            public void run() {
                Component c = getSelectedComponent();
                if (c == null) {
                    return;
                }
                discardTab(c);
            }
        });
    }
    
    /** Called by OutputTabInner in doClose() to close the tab when the
     * output is closed.  Must be called on the event thread */
    void discardTab (Component c) {
        if (openedComps.size() > 2) {
            getTabbedPane().remove(c);
            setActivatedNodes(((TopComponent) getSelectedComponent()).getActivatedNodes());
        } else if (openedComps.size() == 2) {
            JTabbedPane tab = getTabbedPane();
            remove(tab);
            tab.remove(c);
            Component old = tab.getComponentAt(0);
            tab.remove(old);
            add(old);
            setActivatedNodes(((TopComponent) old).getActivatedNodes());
            setName(baseName + " - " + old.getName());
            getInputMap(WHEN_ANCESTOR_OF_FOCUSED_COMPONENT).put(
                KeyStroke.getKeyStroke(KeyEvent.VK_F4, KeyEvent.CTRL_DOWN_MASK), 
                "discard"); //NOI18N
            getActionMap().put("discard", new DiscardAction());
        } else if (openedComps.size() == 1) {
            getInputMap(WHEN_ANCESTOR_OF_FOCUSED_COMPONENT).remove(
                KeyStroke.getKeyStroke(KeyEvent.VK_F4, KeyEvent.CTRL_DOWN_MASK)); 
            remove(c);
            setActivatedNodes(new Node[0]);
            setName(baseName);
        }
        revalidate();
        repaint();
        openedComps.remove(c);
        closedComps.add(c);
    }
    
    /* Close all inner tabs */
    void discardAllTabs () {
        Mutex.EVENT.readAccess(new Runnable() {
            public void run() {
                if (openedComps.size() > 1) {
                    JTabbedPane tab = getTabbedPane();
                    tab.removeAll();
                    remove(tab);
                } else if (openedComps.size() == 1) {
                    remove(((Component []) openedComps.toArray(new Component[1]))[0]);
                    setName(baseName);
                }
                setActivatedNodes(new Node[0]);
                revalidate();
                repaint();
                closedComps.addAll(openedComps);
                openedComps.clear();
            }
        });
    }
    
    /*private static void invokeLater(Runnable runnable) {
	if (SwingUtilities.isEventDispatchThread()) {
	    runnable.run();
	} else {
            SwingUtilities.invokeLater(runnable);
	}
    }*/

    /* DEBUG
    private static void ckEventDispatchThread() {
	if (!SwingUtilities.isEventDispatchThread()) {
	    System.out.println("OW: NOT IN EventDispatchThread");
	    Thread.dumpStack();
	}
    }
    */
}
