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

package org.netbeans.core.io.ui;

import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Component;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.Insets;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.FocusEvent;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.util.HashMap;
import java.util.Map;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.swing.AbstractAction;
import javax.swing.Action;
import javax.swing.BoxLayout;
import javax.swing.Icon;
import javax.swing.JButton;
import javax.swing.JComponent;
import javax.swing.JPopupMenu;
import javax.swing.JTabbedPane;
import javax.swing.JToolBar;
import javax.swing.SwingUtilities;
import javax.swing.UIManager;
import javax.swing.border.Border;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;
import org.openide.util.Utilities;
import org.openide.awt.MouseUtils;
import org.openide.awt.TabbedPaneFactory;
import org.openide.awt.ToolbarWithOverflow;
import org.openide.util.HelpCtx;
import org.openide.util.ImageUtilities;
import org.openide.util.NbBundle;
import org.openide.windows.IOContainer;
import org.openide.windows.IOContainer.CallBacks;
import org.openide.windows.TopComponent;
import org.openide.windows.WindowManager;


/**
 *
 * @author Tomas Holy
 */
@org.openide.util.lookup.ServiceProvider(service=org.openide.windows.IOContainer.Provider.class, position=100)
public final class IOWindow implements IOContainer.Provider {
    private static IOWindowImpl impl;
    
    IOWindowImpl impl() {
        if (impl == null) {
            impl = IOWindowImpl.findDefault();
        }
        return impl;
    }

    public void add(JComponent comp, CallBacks cb) {
        impl().addTab(comp, cb);
    }

    public JComponent getSelected() {
        return impl().getSelectedTab();
    }

    public boolean isActivated() {
        return impl().isActivated();
    }

    public void open() {
        impl().open();
    }

    public void remove(JComponent comp) {
        impl().removeTab(comp);
    }

    public void requestActive() {
        impl().requestActive();
    }

    public void requestVisible() {
        impl().requestVisible();
    }

    public void select(JComponent comp) {
        impl().selectTab(comp);
    }

    public void setIcon(JComponent comp, Icon icon) {
        impl().setIcon(comp, icon);
    }

    public void setTitle(JComponent comp, String name) {
        impl().setTitle(comp, name);
    }

    public void setToolTipText(JComponent comp, String text) {
        impl().setToolTipText(comp, text);
    }

    public void setToolbarActions(JComponent comp, Action[] toolbarActions) {
        impl().setToolbarActions(comp, toolbarActions);
    }

    public boolean isCloseable(JComponent comp) {
        return true;
    }

    public static final class IOWindowImpl extends TopComponent implements ChangeListener, PropertyChangeListener {

        public static IOWindowImpl DEFAULT;

        static synchronized IOWindowImpl findDefault() {
            if (DEFAULT == null) {
                TopComponent tc = WindowManager.getDefault().findTopComponent("output"); // NOI18N
                if (tc != null) {
                    if (tc instanceof IOWindowImpl) {
                        DEFAULT = (IOWindowImpl) tc;
                    } else {
                        //This should not happen. Possible only if some other module
                        //defines different settings file with the same name but different class.
                        //Incorrect settings file?
                        IllegalStateException exc = new IllegalStateException("Incorrect settings file. Unexpected class returned." // NOI18N
                                + " Expected: " + IOWindowImpl.class.getName() // NOI18N
                                + " Returned: " + tc.getClass().getName()); // NOI18N
                        Logger.getLogger(IOWindowImpl.class.getName()).log(Level.WARNING, null, exc);
                        //Fallback to accessor reserved for window system.
                        IOWindowImpl.getDefault();
                    }
                } else {
                    IOWindowImpl.getDefault();
                }
            }
            DEFAULT.getActionMap().remove("org.openide.actions.FindAction"); // NOI18N
            return DEFAULT;
        }

        /* Singleton accessor reserved for window system ONLY. Used by window system to create
         * IOWindowImpl instance from settings file when method is given. Use <code>findDefault</code>
         * to get correctly deserialized instance of IOWindowImpl. */
        public static synchronized IOWindowImpl getDefault() {
            if (DEFAULT == null) {
                DEFAULT = new IOWindowImpl();
            }
            return DEFAULT;
        }

        public Object readResolve() throws java.io.ObjectStreamException {
            return getDefault();
        }

        private static final String ICON_PROP = "tabIcon"; //NOI18N
        private static final String TOOLBAR_ACTIONS_PROP = "toolbarActions"; //NOI18N
        private static final String TOOLBAR_BUTTONS_PROP = "toolbarButtons"; //NOI18N
        private static final String ICON_RESOURCE = "org/netbeans/core/io/ui/output.png"; // NOI18N
        private static final boolean AQUA = "Aqua".equals(UIManager.getLookAndFeel().getID()); // NOI18N
        private JTabbedPane pane;
        private JComponent singleTab;
        private JToolBar toolbar;
        private JPopupMenu popupMenu;
        private Map<JComponent, CallBacks> tabToCb = new HashMap<JComponent, CallBacks>();

        public IOWindowImpl() {
            pane = TabbedPaneFactory.createCloseButtonTabbedPane();
            pane.addChangeListener(this);
            pane.addPropertyChangeListener(TabbedPaneFactory.PROP_CLOSE, this);
            setFocusable(true);

            toolbar = new ToolbarWithOverflow();
            toolbar.setOrientation(JToolBar.VERTICAL);
            toolbar.setLayout(new BoxLayout(toolbar, BoxLayout.Y_AXIS));
            toolbar.setFloatable(false);
            Insets ins = toolbar.getMargin();
            JButton sample = new JButton();
            sample.setBorderPainted(false);
            sample.setOpaque(false);
            sample.setText(null);
            sample.setIcon(new Icon() {

                public int getIconHeight() {
                    return 16;
                }

                public int getIconWidth() {
                    return 16;
                }

                public void paintIcon(Component c, Graphics g, int x, int y) {
                }
            });
            toolbar.add(sample);
            Dimension buttonPref = sample.getPreferredSize();
            Dimension minDim = new Dimension(buttonPref.width + ins.left + ins.right, buttonPref.height + ins.top + ins.bottom);
            toolbar.setMinimumSize(minDim);
            toolbar.setPreferredSize(minDim);
            toolbar.remove(sample);
            setLayout(new BorderLayout());
            add(toolbar, BorderLayout.WEST);
            toolbar.setBorder(new VariableRightBorder(pane));
            toolbar.setBorderPainted(true);

            popupMenu = new JPopupMenu();
            popupMenu.add(new Close());
            popupMenu.add(new CloseAll());
            popupMenu.add(new CloseOthers());
            pane.addMouseListener(new MouseUtils.PopupMouseAdapter() {

                @Override
                protected void showPopup(MouseEvent evt) {
                    popupMenu.show(IOWindowImpl.this, evt.getX(), evt.getY());
                }
            });
            pane.addMouseListener(new MouseAdapter() { // #221375
                @Override
                public void mouseClicked(MouseEvent e) {
                    if (SwingUtilities.isLeftMouseButton(e)) {
                        requestActive();
                    }
                }
            });

            String name = NbBundle.getMessage(IOWindow.class, "LBL_IO_WINDOW");
            setDisplayName(name); //NOI18N
            setToolTipText(name);
            // setting name to satisfy the accesible name requirement for window.
            setName(name); //NOI18N

            setIcon(ImageUtilities.loadImage(ICON_RESOURCE)); // NOI18N
            // special title for sliding mode
            // XXX - please rewrite to regular API when available - see issue #55955
            putClientProperty("SlidingName", getDisplayName()); //NOI18N
            if (AQUA) {
                setBackground(UIManager.getColor("NbExplorerView.background"));
                setOpaque(true);
                toolbar.setBackground(UIManager.getColor("NbExplorerView.background"));
                pane.setBackground(UIManager.getColor("NbExplorerView.background"));
                pane.setOpaque(true);
                setMinimumSize(new Dimension()); // #254566
            }
        }

        @Override
        public String getShortName() {
            return NbBundle.getMessage(IOWindow.class, "LBL_IO_WINDOW");
        }

        @Override
        public void open() {
            if (!isOpened())
		super.open();
        }

        @Override
        public void requestActive() {
            super.requestActive();
            JComponent tab = getSelectedTab();
            if (tab != null) {
                tab.requestFocus();
            }
        }

        @Override
        public void requestVisible() {
            if (!isShowing()) {
		super.requestVisible();
	    }
        }

        boolean activated;
        public boolean isActivated() {
            return activated;
        }


        public void addTab(JComponent comp, CallBacks cb) {
            if (cb != null) {
                tabToCb.put(comp, cb);
            }
            if (singleTab != null) {
                // only single tab, remove it from TopComp. and add it to tabbed pane
                assert pane.getParent() == null;
                assert pane.getTabCount() == 0;
                remove(singleTab);
                pane.add(singleTab);
                pane.setIconAt(0, (Icon) singleTab.getClientProperty(ICON_PROP));
                pane.setToolTipTextAt(0, singleTab.getToolTipText());
                singleTab = null;
                pane.add(comp);
                add(pane);
                updateWindowName(null);
            } else if (pane.getTabCount() > 0) {
                // already several tabs
                assert pane.getParent() != null;
                assert singleTab == null;
                pane.add(comp);
            } else {
                // nothing yet
                assert pane.getParent() == null;
                assert singleTab == null;
                setFocusable(false);
                singleTab = comp;
                add(comp);
                updateWindowName(singleTab.getName());
                checkTabSelChange();
            }
            revalidate();
        }

        public void removeTab(JComponent comp) {
            if (singleTab != null) {
                assert singleTab == comp;
                remove(singleTab);
                singleTab = null;
                updateWindowName(null);
                checkTabSelChange();
                setFocusable(true);
                revalidate();
                repaint();
            } else if (pane.getParent() == this) {
                assert pane.getTabCount() > 1;
                pane.remove(comp);
                if (pane.getTabCount() == 1) {
                    singleTab = (JComponent) pane.getComponentAt(0);
                    pane.remove(singleTab);
                    remove(pane);
                    add(singleTab);
                    updateWindowName(singleTab.getName());
                }
                revalidate();
            }
            CallBacks cb = tabToCb.remove(comp);
            if (cb != null) {
                cb.closed();
            }
        }

        public void selectTab(JComponent comp) {
//	    Calls to open/requestVisible() lifted into Controller, case CMD_SELECT.
//	    Tests pushed into this.open() and this.requestVisible().
//
//            if (!isOpened()) {
//                open();
//            }
//            if (!isShowing()) {
//                requestVisible();
//            }
            if (singleTab == null) {
                pane.setSelectedComponent(comp);
            }
            checkTabSelChange();
        }

        public JComponent getSelectedTab() {
            return singleTab != null ? singleTab : (JComponent) pane.getSelectedComponent();
        }

        public void setTitle(JComponent comp, String name) {
            comp.setName(name);
            if (singleTab != null) {
                assert singleTab == comp;
                updateWindowName(name);
            } else {
                assert pane.getParent() == this;
                int idx = pane.indexOfComponent(comp);
                assert idx >= 0;
                pane.setTitleAt(idx, name);
            }
        }

        public void setToolTipText(JComponent comp, String text) {
            comp.setToolTipText(text);
            if (singleTab != null) {
                assert singleTab == comp;
            } else {
                assert pane.getParent() == this;
                int idx = pane.indexOfComponent(comp);
                assert idx >= 0;
                pane.setToolTipTextAt(idx, text);
            }
        }

        public void setIcon(JComponent comp, Icon icon) {
            if (comp == singleTab) {
                comp.putClientProperty(ICON_PROP, icon);
                return;
            }
            int idx = pane.indexOfComponent(comp);
            if (idx < 0) {
                return;
            }
            comp.putClientProperty(ICON_PROP, icon);
            pane.setIconAt(idx, icon);
            pane.setDisabledIconAt(idx, icon);
        }

        void setToolbarActions(JComponent comp, Action[] toolbarActions) {
            if (toolbarActions != null && toolbarActions.length > 0) {
                comp.putClientProperty(TOOLBAR_ACTIONS_PROP, toolbarActions);
            }
            if (getSelectedTab() == comp) {
                updateToolbar(comp);
            }
        }

        @Override
        public int getPersistenceType() {
            return PERSISTENCE_ALWAYS;
        }

        @Override
        public String preferredID() {
            return "output"; //NOI18N
        }

        @Override
        public void processFocusEvent(FocusEvent fe) {
            super.processFocusEvent(fe);
            if (Boolean.TRUE.equals(getClientProperty("isSliding"))) { //NOI18N
                repaint(200);
            }
        }

        @Override
        public void paintComponent(Graphics g) {
            if (AQUA) {
                g.setColor(getBackground());
                g.fillRect(0, 0, getWidth(), getHeight());
            }
            super.paintComponent(g);
            if (hasFocus()) {
                Insets ins = getInsets();
                Color col = UIManager.getColor("controlShadow"); //NOI18N
                //Draw *some* focus indication
                if (col == null) {
                    col = java.awt.Color.GRAY;
                }
                g.setColor(col);
                g.drawRect(
                        ins.left + 2,
                        ins.top + 2,
                        getWidth() - (ins.left + ins.right + 4),
                        getHeight() - (ins.top + ins.bottom + 4));
            }
        }

        void updateWindowName(String name) {
            String winName = NbBundle.getMessage(IOWindowImpl.class, "LBL_IO_WINDOW"); //NOI18N
            if (name != null) {
                String newName = NbBundle.getMessage(IOWindowImpl.class, "FMT_IO_WINDOW", new Object[]{winName, name}); //NOI18N
                if (newName.indexOf("<html>") != -1) {
                    newName = "<html>" + newName.replace("<html>", ""); //NOI18N
                    setHtmlDisplayName(newName); //NOI18N
                    setToolTipText(newName);
                } else {
                    setDisplayName(newName);
                    setHtmlDisplayName(null);
                    setToolTipText(newName);
                }
            } else {
                setDisplayName(winName);
                setToolTipText(winName);
                setHtmlDisplayName(null);
            }

        }

        private void updateToolbar(JComponent comp) {
            toolbar.removeAll();
            if (comp != null) {
                JButton[] buttons = getTabButtons(comp);
                for (int i = 0; i < buttons.length; i++) {
                    toolbar.add(buttons[i]);
                }
            }
            toolbar.validate();
            toolbar.repaint();
        }

        JButton[] getTabButtons(JComponent comp) {
            JButton[] buttons = (JButton[]) comp.getClientProperty(TOOLBAR_BUTTONS_PROP);
            if (buttons != null) {
                return buttons;
            }
            Action[] actions = (Action[]) comp.getClientProperty(TOOLBAR_ACTIONS_PROP);
            if (actions == null) {
                return new JButton[0];
            }

            buttons = new JButton[actions.length];
            for (int i=0; i < buttons.length; i++) {
                buttons[i] = new JButton(actions[i]);
                buttons[i].setBorderPainted(false);
                buttons[i].setOpaque(false);
                buttons[i].setText(null);
                buttons[i].putClientProperty("hideActionText", Boolean.TRUE); //NOI18N
                Object icon = actions[i].getValue(Action.SMALL_ICON);
                if (!(icon instanceof Icon)) {
                    throw new IllegalStateException ("No icon provided for " + actions[i]); //NOI18N
                }
                buttons[i].setDisabledIcon(ImageUtilities.createDisabledIcon((Icon) icon));
                String name = (String) actions[i].getValue(Action.NAME);
                String shortDescription = (String) actions[i].getValue(Action.SHORT_DESCRIPTION);
                String longDescription = (String) actions[i].getValue(Action.LONG_DESCRIPTION);
                if (name == null) name = shortDescription;
                if (longDescription == null) longDescription = shortDescription;
                buttons[i].getAccessibleContext().setAccessibleName(name);
                buttons[i].getAccessibleContext().setAccessibleDescription(longDescription);
            }
            return buttons;
        }

        @Override
        protected void componentActivated() {
            super.componentActivated();
            activated = true;
            JComponent comp = getSelectedTab();
            CallBacks cb = tabToCb.get(comp);
            if (cb != null) {
                cb.activated();
            }
        }

        @Override
        protected void componentDeactivated() {
            super.componentDeactivated();
            activated = false;
            JComponent comp = getSelectedTab();
            CallBacks cb = tabToCb.get(comp);
            if (cb != null) {
                cb.deactivated();
            }
        }

        public void stateChanged(ChangeEvent e) {
            checkTabSelChange();
        }

        public void propertyChange(PropertyChangeEvent evt) {
            if (TabbedPaneFactory.PROP_CLOSE.equals(evt.getPropertyName())) {
                JComponent comp = (JComponent) evt.getNewValue();
                removeTab(comp);
            }
        }

        JComponent lastSelTab;
        void checkTabSelChange() {
            JComponent sel = getSelectedTab();
            if (sel != lastSelTab) {
                lastSelTab = sel;
                updateToolbar(sel);
                getActionMap().setParent(sel != null ? sel.getActionMap() : null);
            }
        }

        private JComponent[] getTabs() {
            if (singleTab != null) {
                return new JComponent[] {singleTab};
            }

            JComponent[] tabs = new JComponent[pane.getTabCount()];
            for (int i = 0; i < pane.getTabCount(); i++) {
                tabs[i] = (JComponent) pane.getComponentAt(i);
            }
            return tabs;
        }

        @Override
        public SubComponent[] getSubComponents() {
            if( singleTab != null )
                return new SubComponent[0];
            JComponent[] tabs = getTabs();
            SubComponent[] res = new SubComponent[tabs.length];
            for( int i=0; i<res.length; i++ ) {
                final JComponent theTab = tabs[i];
                String title = pane.getTitleAt( i );
                res[i] = new SubComponent( title, new ActionListener() {

                    @Override
                    public void actionPerformed( ActionEvent e ) {
                        if( singleTab != null || pane.indexOfComponent( theTab ) < 0 )
                            return; //the tab is gone already
                        selectTab( theTab );
                    }
                }, theTab == getSelectedTab() );
            }
            return res;
        }


        private void closeOtherTabs() {
            assert pane.getParent() == this;
            JComponent sel = getSelectedTab();
            for (JComponent tab : getTabs()) {
                if (tab != sel) {
                    removeTab(tab);
                }
            }
        }

        private void closeAllTabs() {
            for (JComponent tab : getTabs()) {
                removeTab(tab);
            }
        }

        @Override
        public HelpCtx getHelpCtx() {
            return new HelpCtx("org.netbeans.core.io.ui.IOWindow");
        }

        private class Close extends AbstractAction {

            public Close() {
                super(NbBundle.getMessage(IOWindowImpl.class, "LBL_Close"));
            }

            public void actionPerformed(ActionEvent e) {
                removeTab(getSelectedTab());
            }
        }

        private class CloseAll extends AbstractAction {

            public CloseAll() {
                super(NbBundle.getMessage(IOWindowImpl.class, "LBL_CloseAll"));
            }

            public void actionPerformed(ActionEvent e) {
                closeAllTabs();
            }
        }

        private class CloseOthers extends AbstractAction {

            public CloseOthers() {
                super(NbBundle.getMessage(IOWindowImpl.class, "LBL_CloseOthers"));
            }

            public void actionPerformed(ActionEvent e) {
                closeOtherTabs();
            }
        }

        private class VariableRightBorder implements Border {

            private JTabbedPane pane;

            public VariableRightBorder(JTabbedPane pane) {
                this.pane = pane;
            }

            public void paintBorder(Component c, Graphics g, int x, int y, int width, int height) {
                if (pane.getParent() != IOWindowImpl.this) {
                    Color old = g.getColor();
                    g.setColor(getColor());
                    g.drawLine(x + width - 1, y, x + width - 1, y + height);
                    g.setColor(old);
                }
            }

            public Color getColor() {
                if (Utilities.isMac()) {
                    Color c1 = UIManager.getColor("controlShadow");
                    Color c2 = UIManager.getColor("control");
                    return new Color((c1.getRed() + c2.getRed()) / 2,
                            (c1.getGreen() + c2.getGreen()) / 2,
                            (c1.getBlue() + c2.getBlue()) / 2);
                } else {
                    return UIManager.getColor("controlShadow");
                }
            }

            public Insets getBorderInsets(Component c) {
                if (pane.getParent() == IOWindowImpl.this) {
                    return new Insets(0, 0, 0, 0);
                }
                return new Insets(0, 0, 0, 2);
            }

            public boolean isBorderOpaque() {
                return true;
            }
        }
    }
}
