/*
 * 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.modules.progress.ui;

import java.awt.AWTEvent;
import java.awt.Component;
import java.awt.Container;
import java.awt.Cursor;
import java.awt.Dimension;
import java.awt.FlowLayout;
import java.awt.Frame;
import java.awt.Graphics;
import java.awt.GraphicsEnvironment;
import java.awt.Image;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.Toolkit;
import java.awt.event.AWTEventListener;
import java.awt.event.ActionEvent;
import java.awt.event.ComponentAdapter;
import java.awt.event.ComponentEvent;
import java.awt.event.KeyEvent;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.awt.event.WindowEvent;
import java.awt.event.WindowStateListener;
import java.util.HashMap;
import java.util.Map;
import javax.swing.AbstractAction;
import javax.swing.Action;
import javax.swing.BorderFactory;
import javax.swing.Icon;
import javax.swing.ImageIcon;
import javax.swing.JButton;
import javax.swing.JComponent;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JPopupMenu;
import javax.swing.JSeparator;
import javax.swing.JWindow;
import javax.swing.KeyStroke;
import javax.swing.SwingUtilities;
import javax.swing.UIManager;
import javax.swing.event.ListDataEvent;
import javax.swing.event.ListDataListener;
import javax.swing.event.ListSelectionEvent;
import javax.swing.event.ListSelectionListener;
import org.netbeans.modules.progress.spi.InternalHandle;
import org.netbeans.modules.progress.spi.ProgressEvent;
import org.netbeans.modules.progress.spi.ProgressUIWorkerWithModel;
import org.netbeans.modules.progress.spi.TaskModel;
import org.openide.DialogDisplayer;
import org.openide.NotifyDescriptor;
import org.openide.util.ImageUtilities;
import org.openide.util.NbBundle;
import org.openide.util.Utilities;
import org.openide.windows.WindowManager;

/**
 *
 * @author Milos Kleint (mkleint@netbeans.org)
 */
public class StatusLineComponent extends JPanel implements ProgressUIWorkerWithModel {
    private NbProgressBar bar;
    private JLabel label;
    private JSeparator separator;
    private InternalHandle handle;
    private boolean showingPopup = false;
    private TaskModel model;
    private MouseListener mouseListener;
    private HideAWTListener hideListener;
    private JWindow popupWindow;
    private PopupPane pane;
    private Map<InternalHandle, ListComponent> handleComponentMap;
    private final int preferredHeight;
    private JButton closeButton;
    private JLabel extraLabel;
    public StatusLineComponent() {
        handleComponentMap = new HashMap<InternalHandle, ListComponent>();
        FlowLayout flay = new FlowLayout();
        flay.setVgap(1);
        flay.setHgap(5);
        setLayout(flay);
        mouseListener = new MListener();
        addMouseListener(mouseListener);
        hideListener = new HideAWTListener();
        
        createLabel();
        createBar();
        // tricks to figure out correct height.
        bar.setStringPainted(true);
        bar.setString("@@@"); // NOI18N
        label.setText("@@@"); // NOI18N
        preferredHeight = Math.max(label.getPreferredSize().height, bar.getPreferredSize().height) + 3;
        setOpaque(false);
        discardLabel();
        discardBar();
        
        pane = new PopupPane();
        pane.getActionMap().put("HidePopup", new AbstractAction() {
            public @Override void actionPerformed(ActionEvent actionEvent) {
//                System.out.println("escape pressed - hiding");
                hidePopup();
            }
        });
        pane.getInputMap().put(KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0), "HidePopup");
        pane.getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT).put(KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0), "HidePopup");
        
        
    }
    
    private void createLabel() {
        discardLabel();
        label = new JLabel();
        label.setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR));
        label.addMouseListener(mouseListener);
    }
    
    private void discardLabel() {
        if (label != null) {
            label.removeMouseListener(mouseListener);
            label = null;
        }
    }

    private void createExtraLabel() {
        discardExtraLabel();
        extraLabel = new JLabel();
        extraLabel.setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR));
        extraLabel.addMouseListener(mouseListener);
    }

    private void discardExtraLabel() {
        if (extraLabel != null) {
            extraLabel.removeMouseListener(mouseListener);
            extraLabel = null;
        }
    }

    private void createBar() {
        discardBar();
        bar = new NbProgressBar();
        bar.setUseInStatusBar(true);
        bar.setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR));
//        setBorder(BorderFactory.createLineBorder(Color.BLUE, 1));
        // HACK - put smaller font inside the progress bar to keep
        // the height of the progressbar constant for determinate and indeterminate bars
//        Font fnt = UIManager.getFont("ProgressBar.font");
//        bar.setFont(fnt.deriveFont(fnt.getStyle(), fnt.getSize() - 3));
        bar.addMouseListener(mouseListener);
        
    }
    
    private void discardBar() {
        if (bar != null) {
            bar.removeMouseListener(mouseListener);
            //EMI: On OSX, an animation thread is started when the progressbar is created, even if not displayed (or added to a container).
            // The following line is needed to kill the animation thread otherwise this tends to be alive for the rest of the JVM execution
            // pumping a lot of repaing events in the event queue (in my tests, about 50% of the CPU while idle).
            // The culprit apple.laf.CUIAquaProgressBar$Animator was discovered with the normal Profiler, while Tim Boudreau told me about a similar
            // problem with OSX and the pulsating button (that also has a thread for that animation).
            // Finally, btrace and this IDEA bug report (http://www.jetbrains.net/jira/browse/IDEADEV-25376) connected the dots.
            bar.getUI().uninstallUI(bar);
            bar = null;
        }
    }
    
    private void createCloseButton() {
        discardCloseButton();
        closeButton = new JButton();
        closeButton.setBorderPainted(false);
        closeButton.setBorder(BorderFactory.createEmptyBorder());
        closeButton.setOpaque(false);
        closeButton.setContentAreaFilled(false);
        
        Image img = (Image)UIManager.get("nb.progress.cancel.icon");
        if( null != img ) {
            closeButton.setIcon( new ImageIcon( img ) );
        }
        img = (Image)UIManager.get("nb.progress.cancel.icon.mouseover");
        if( null != img ) {
            closeButton.setRolloverEnabled(true);
            closeButton.setRolloverIcon( new ImageIcon( img ) );
        }
        img = (Image)UIManager.get("nb.progress.cancel.icon.pressed");
        if( null != img ) {
            closeButton.setPressedIcon( new ImageIcon( img ) );
        }
    }
    
    private void setCloseButtonNameAndTooltip() {
        closeButton.setName(NbBundle.getMessage(ListComponent.class, "ListComponent.btnClose.name"));
        closeButton.setToolTipText(NbBundle.getMessage(ListComponent.class, "ListComponent.btnClose.tooltip"));
    }
    
    private void discardCloseButton() {
        closeButton = null;
    }

    private void createSeparator() {
        discardSeparator();
        separator = new JSeparator(JSeparator.VERTICAL);
//        separator.setPreferredSize(new Dimension(5, prefferedHeight));
        separator.setBorder(BorderFactory.createEmptyBorder(1, 0, 2, 0));
    }
    
    private void discardSeparator() {
        separator = null;
    }
    
    @Override
    public Dimension getPreferredSize() {
        Dimension retValue;
        retValue = super.getPreferredSize();
        retValue.height = preferredHeight;
        return retValue;
    }

    @Override
    public Dimension getMinimumSize() {
        Dimension retValue;
        retValue = super.getMinimumSize();
        retValue.height = preferredHeight;
        return retValue;
    }        
    
    @Override
    public Dimension getMaximumSize() {
        Dimension retValue;
        retValue = super.getMaximumSize();
        retValue.height = preferredHeight;
        return retValue;
    }        
    
    public @Override void setModel(TaskModel mod) {
        model = mod;
        model.addListDataListener(new Listener());
        model.addListSelectionListener(new ListSelectionListener() {
            public @Override void valueChanged(ListSelectionEvent e) {
                pane.updateBoldFont(model.getSelectedHandle());
            }
        });
    }
    
    private void setTooltipForAll() {
        int size = model.getSize();
        String key = "NbProgressBar.tooltip1"; //NOI18N
        if (size == 1) {
            key = "NbProgressBar.tooltip2"; //NOI18N
        }
        String text = NbBundle.getMessage(StatusLineComponent.class, key, size);
        setToolTipText(text);
        if (label != null) {
            label.setToolTipText(text);
        }
        if (bar != null) {
            bar.setToolTipText(text);
        }
    }
    
    public @Override void processProgressEvent(ProgressEvent event) {
        if (event.getType() == ProgressEvent.TYPE_START) {
            createListItem(event.getSource());
        } else if (event.getType() == ProgressEvent.TYPE_PROGRESS || 
                   event.getType() == ProgressEvent.TYPE_SWITCH || 
                   event.getType() == ProgressEvent.TYPE_SILENT) {
            ListComponent comp = handleComponentMap.get(event.getSource());
            if (comp == null) {
                createListItem(event.getSource());
                comp = handleComponentMap.get(event.getSource());
            }
            comp.processProgressEvent(event);
        } else if (event.getType() == ProgressEvent.TYPE_FINISH) {
            removeListItem(event.getSource());
            if (model.getSelectedHandle() != null && handle != model.getSelectedHandle()) {
                ProgressEvent snap = model.getSelectedHandle().requestStateSnapshot();
                initializeComponent(snap);
                if (snap.getSource().isInSleepMode()) {
                    bar.setString(snap.getMessage());
                }
                
            }
            //release handle reference if it is the last one
            if (model.getSize() == 0 && event.getSource().equals(handle)) {
                handle = null;
            }
        }
        
    }
    
    public @Override void processSelectedProgressEvent(ProgressEvent event) {
        if (event.getType() == ProgressEvent.TYPE_START) {
            initializeComponent(event);
            return;
        } else if (event.getType() == ProgressEvent.TYPE_FINISH) {
            //happens only when there's no more handles.
            hidePopup();
            removeAll();
            discardSeparator();
            discardCloseButton();
            discardBar();
            discardLabel();
            discardExtraLabel();
            //#63393, 61940 fix - removeAll() just invalidates. seems to work without revalidate/repaint on some platforms, fail on others.
            revalidate();
            repaint();
            return;
        } else {
            if (event.getSource() != handle || event.isSwitched() || 
                event.getType() == ProgressEvent.TYPE_SILENT ||                    // the following condition re-initiates the bar when going from/to sleep mode..
                    (event.getSource().isInSleepMode() != (bar.getClientProperty(NbProgressBar.SLEEPY) != null))) { //NIO18N
                initializeComponent(event);
            }
            if (bar != null) {
                if (event.getWorkunitsDone() > 0) {
                   bar.setValue(event.getWorkunitsDone());
                }
                bar.setString(getBarString(event.getPercentageDone(), event.getEstimatedCompletion()));
                if (event.getDisplayName() != null) {
                    label.setText(event.getDisplayName());
                }
                if (event.getSource().isInSleepMode()) {
                    bar.setString(event.getMessage());
                }
            }
        } 
    }
    
    static String formatEstimate(long estimate) {
        long minutes = estimate / 60;
        long seconds = estimate - (minutes * 60);
        return "" + minutes + (seconds < 10 ? ":0" : ":") + seconds;
    }
    
    static String getBarString(double percentage, long estimatedCompletion) {
        if (estimatedCompletion != -1) {
            return formatEstimate(estimatedCompletion);
        }
        if (percentage != -1) {
            int rounded = (int) Math.round(percentage);
            if (rounded > 100) {
                rounded = 100;
            }
            return "" + rounded + "%"; //NOI18N
        }
        return "";
    }

    private void updateExtraLabel() {
        if (extraLabel != null) {
            if (handleComponentMap.size() > 1) {
                extraLabel.setText(NbBundle.getMessage(StatusLineComponent.class, "StatusLineComponent.extra", handleComponentMap.size() - 1));
            } else {
                extraLabel.setText(null);
            }
        }
    }
    
    private void initializeComponent(ProgressEvent event) {
        handle = event.getSource();
        boolean toShow = false;
        if (label == null) {
            createLabel();
            add(label);
            toShow = true;
            label.setToolTipText(getToolTipText());
        }
        label.setText(handle.getDisplayName());
        
        if (bar == null) {
            createBar();
            add(bar);
            toShow = true;
            bar.setToolTipText(getToolTipText());
            
        }
        NbProgressBar.setupBar(event.getSource(), bar);
        
        if (closeButton == null) {
            createCloseButton();
            add(closeButton);
            toShow = true;
        }
        if (extraLabel == null) {
            createExtraLabel();
            updateExtraLabel();
            add(extraLabel);
            toShow = true;
        }
        if (separator == null) {
            createSeparator();
            add(separator);
            toShow = true;
        }
        if (handle.isAllowCancel()) {
            closeButton.setAction(new CancelAction(false));
        } else {
            closeButton.setAction(new EmptyCancelAction());
        }
        
        // #200126: tooltip property must be set following the action or it will be overwritten
        setCloseButtonNameAndTooltip();
        
        if (toShow) {
            revalidate();
            repaint();
        }
    }
    
    private class Listener implements ListDataListener {
        public @Override void intervalAdded(ListDataEvent e) {
            setTooltipForAll();
        }
        public @Override void intervalRemoved(ListDataEvent e) {
            setTooltipForAll();
        }
        public @Override void contentsChanged(ListDataEvent e) {
            setTooltipForAll();
        }
    }
    
    public void hidePopup() {
        if (GraphicsEnvironment.isHeadless()) {
            return;
        }
        if (popupWindow != null) {
//            popupWindow.getContentPane().removeAll();
            popupWindow.setVisible(false);
        }
        Toolkit.getDefaultToolkit().removeAWTEventListener(hideListener);
        WindowManager.getDefault().getMainWindow().removeWindowStateListener(hideListener);
        WindowManager.getDefault().getMainWindow().removeComponentListener(hideListener);
        showingPopup = false;
    }
    
    private void createListItem(InternalHandle handle) {
        ListComponent comp;
        if (handleComponentMap.containsKey(handle)) {
            // happens when we click to display on popup and there is a 
            // new handle waiting in the queue.
            comp = handleComponentMap.get(handle);
        } else {
            comp = new ListComponent(handle);
            handleComponentMap.put(handle, comp);
        }
        pane.addListComponent(comp);            
        pane.updateBoldFont(model.getSelectedHandle());
        if (showingPopup) {
            resizePopup();
        }
        updateExtraLabel();
    }
    
    private void removeListItem(InternalHandle handle) {
        ListComponent c = handleComponentMap.remove(handle);
        pane.removeListComponent(handle);
        pane.updateBoldFont(model.getSelectedHandle());
        if (showingPopup) {
            resizePopup();
        }
        if (c != null) {
            c.clearProgressBarOSX();
        }
        updateExtraLabel();
    }

    
    public @Override void showPopup() {
        if (GraphicsEnvironment.isHeadless()) {
            return;
        }
        if (showingPopup) {
            return;
        }
        InternalHandle[] handles = model.getHandles();
        if (handles.length == 0) {
            // just in case..
            return;
        }
        showingPopup = true;
        
        // NOT using PopupFactory
        // 1. on linux, creates mediumweight popup taht doesn't refresh behind visible glasspane
        // 2. on mac, needs an owner frame otherwise hiding tooltip also hides the popup. (linux requires no owner frame to force heavyweight)
        // 3. the created window is not focusable window
        if (popupWindow == null) {
            popupWindow = new JWindow(WindowManager.getDefault().getMainWindow());
            popupWindow.getContentPane().add(pane);
        }
        Toolkit.getDefaultToolkit().addAWTEventListener(hideListener, AWTEvent.MOUSE_EVENT_MASK);
        WindowManager.getDefault().getMainWindow().addWindowStateListener(hideListener);
        WindowManager.getDefault().getMainWindow().addComponentListener(hideListener);
        resizePopup();
        popupWindow.setVisible(true);
        pane.requestFocus();
        pane.repaint();
//        System.out.println("     window focusable=" + popupWindow.isFocusableWindow());
    }
    
    private void resizePopup() {
        popupWindow.pack();
        Point point = new Point(0,0);
        SwingUtilities.convertPointToScreen(point, this);
        Dimension dim = popupWindow.getSize();
        //#63265 
        Rectangle usableRect = Utilities.getUsableScreenBounds();
        int sepShift = separator != null ? separator.getSize().width : 0;
        Point loc = new Point(point.x + this.getSize().width - dim.width - sepShift - 5 * 2  , point.y - dim.height - 5);
        // -5 in x coordinate is becuase of the hgap between the separator and button and separator and edge
        //#JDEV #17370036
        if( loc.x < usableRect.x )
            loc.x = Math.max( loc.x, usableRect.x );
        if( loc.x+dim.width > usableRect.x+usableRect.width )
            loc.x = usableRect.x + usableRect.width - dim.width;
        if (! usableRect.contains(loc)) {
            loc = new Point(loc.x, point.y + 5 + this.getSize().height);
        }
            // +4 here because of the width of the close button in popup, we
            // want the progress bars to align visually.. but there's separator in status now..
        popupWindow.setLocation(loc);
//        System.out.println("count=" + count);
//        System.out.println("offset =" + offset);
    }
    
    private class HideAWTListener extends ComponentAdapter implements  AWTEventListener, WindowStateListener {
        public @Override void eventDispatched(java.awt.AWTEvent aWTEvent) {
            if (aWTEvent instanceof MouseEvent) {
                MouseEvent mv = (MouseEvent)aWTEvent;
                if (mv.getClickCount() > 0) {
                    //#118828
                    if (! (aWTEvent.getSource() instanceof Component)) {
                        return;
                    }
                    Component comp = (Component)aWTEvent.getSource();
                    Container par = SwingUtilities.getAncestorNamed("progresspopup", comp); //NOI18N
                    Container barpar = SwingUtilities.getAncestorOfClass(StatusLineComponent.class, comp);
                    if (par == null && barpar == null) {
                        hidePopup();
                    }
                }
            }
        }

        public @Override void windowStateChanged(WindowEvent windowEvent) {
            if (showingPopup) {
                int oldState = windowEvent.getOldState();
                int newState = windowEvent.getNewState();
            
                if (((oldState & Frame.ICONIFIED) == 0) &&
                    ((newState & Frame.ICONIFIED) == Frame.ICONIFIED)) {
                    hidePopup();
//                } else if (((oldState & Frame.ICONIFIED) == Frame.ICONIFIED) && 
//                           ((newState & Frame.ICONIFIED) == 0 )) {
//                    //TODO remember we showed before and show again? I guess not worth the efford, not part of spec.
                }
            }

        }
        
        @Override
        public void componentResized(ComponentEvent evt) {
            if (showingPopup) {
                resizePopup();
            }
        }
        
        @Override
        public void componentMoved(ComponentEvent evt) {
            if (showingPopup) {
                resizePopup();
            }
        }        
        
    }
    
    private class MListener extends MouseAdapter {
        @Override
        public void mouseClicked(java.awt.event.MouseEvent e) {
            if (e.getButton() != MouseEvent.BUTTON1) {
                showMenu(e);
            } else {
                if (showingPopup) {
                    hidePopup();
                } else {
                    showPopup();
                }
            }
        }
        
    }
    
    private void showMenu(MouseEvent e) {
        JPopupMenu popup = new JPopupMenu();
        popup.add(new ProgressListAction(NbBundle.getMessage(StatusLineComponent.class, "StatusLineComponent.ShowProcessList"))); 
        popup.add(new ViewAction());
        popup.add(new CancelAction(true));
        popup.show((Component)e.getSource(), e.getX(), e.getY());
    }
    
  private class CancelAction extends AbstractAction {
        public CancelAction(boolean text) {
            if (text) {
                putValue(Action.NAME, NbBundle.getMessage(StatusLineComponent.class, "StatusLineComponent.Cancel"));
            } else {
                Image icon = (Image)UIManager.get("nb.progress.cancel.icon");
                if (icon == null) {
                       // for custom L&F?
                    putValue(Action.SMALL_ICON, ImageUtilities.loadImageIcon("org/netbeans/progress/module/resources/buton.png", true));
                } else {
                    putValue(Action.SMALL_ICON, new ImageIcon(icon));
                }
            }
            setEnabled(handle == null ? false : handle.isAllowCancel());
        }
        public @Override void actionPerformed(ActionEvent actionEvent) {
            InternalHandle hndl = handle;
            if (hndl !=null && hndl.getState() == InternalHandle.STATE_RUNNING) {
                String message = NbBundle.getMessage(StatusLineComponent.class, "Cancel_Question", handle.getDisplayName());
                String title = NbBundle.getMessage(StatusLineComponent.class, "Cancel_Question_Title");
                NotifyDescriptor dd = new NotifyDescriptor(message, title, 
                                           NotifyDescriptor.YES_NO_OPTION,
                                           NotifyDescriptor.QUESTION_MESSAGE, null, null);
                Object retType = DialogDisplayer.getDefault().notify(dd);
                if (retType == NotifyDescriptor.YES_OPTION && hndl.getState() == InternalHandle.STATE_RUNNING) {
                    hndl.requestCancel();
                }
            }
        }
    }

    private class ViewAction extends AbstractAction {
        public ViewAction() {
            putValue(Action.NAME, NbBundle.getMessage(StatusLineComponent.class, "StatusLineComponent.View"));
            setEnabled(handle == null ? false : handle.isAllowView());
            
        }
        public @Override void actionPerformed(ActionEvent actionEvent) {
            if (handle != null) {
                handle.requestView();
            }
        }
    }    
    
    
    private class EmptyCancelAction extends AbstractAction {
        public EmptyCancelAction() {
            setEnabled(false);
            putValue(Action.SMALL_ICON, new Icon() {
                public @Override int getIconHeight() {
                    return 12;
                }
                public @Override int getIconWidth() {
                    return 12;
                }
                public @Override void paintIcon(Component c, Graphics g, int x, int y) {
                }
            });
            putValue(Action.NAME, "");
        }

        public @Override void actionPerformed(ActionEvent e) {
        }
    }

}
