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

import java.awt.AlphaComposite;
import java.awt.Color;
import java.awt.Component;
import java.awt.Composite;
import java.awt.Container;
import java.awt.Cursor;
import java.awt.Dimension;
import java.awt.GradientPaint;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.GridBagConstraints;
import java.awt.GridBagLayout;
import java.awt.Insets;
import java.awt.Rectangle;
import java.awt.RenderingHints;
import java.awt.Shape;
import java.awt.Window;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.ComponentEvent;
import java.awt.event.ComponentListener;
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.awt.geom.AffineTransform;
import java.awt.geom.Area;
import java.awt.geom.GeneralPath;
import java.awt.geom.Point2D;
import java.awt.geom.RoundRectangle2D;
import javax.swing.BorderFactory;
import javax.swing.Icon;
import javax.swing.JButton;
import javax.swing.JComponent;
import javax.swing.JLayeredPane;
import javax.swing.JPanel;
import javax.swing.SwingUtilities;
import javax.swing.Timer;
import javax.swing.ToolTipManager;
import javax.swing.UIManager;
import javax.swing.plaf.metal.MetalLookAndFeel;
import org.openide.util.ImageUtilities;

/**
 * Shows, hides balloon-like tooltip windows.
 * 
 * @author S. Aubrecht
 */
class BalloonManager {

    private static Balloon currentBalloon;
    private static JLayeredPane currentPane;
    private static ComponentListener listener;
    private static WindowStateListener windowListener;
    private static Window ownerWindow;
    
    /**
     * Show balloon-like tooltip pointing to the given component. The balloon stays
     * visible until dismissed by clicking its 'close' button or by invoking its default action.
     * @param owner The component which the balloon will point to
     * @param content Content to be displayed in the balloon.
     * @param defaultAction Action to invoked when the balloon is clicked, can be null.
     * @param timeoutMillies Number of milliseconds before the balloon disappears, 0 to keep it visible forever
     */
    public static synchronized void show( final JComponent owner, JComponent content, ActionListener defaultAction, ActionListener dismissAction, int timeoutMillis ) {
        assert null != owner;
        assert null != content;
        
        //hide current balloon (if any)
        dismiss();
            
        currentBalloon = new Balloon( content, defaultAction, dismissAction, timeoutMillis );
        currentPane = JLayeredPane.getLayeredPaneAbove( owner );
        
        listener = new ComponentListener() {
            public void componentResized(ComponentEvent e) {
                dismiss();
            }

            public void componentMoved(ComponentEvent e) {
                dismiss();
            }

            public void componentShown(ComponentEvent e) {
            }

            public void componentHidden(ComponentEvent e) {
                dismiss();
            }
        };
        windowListener = new WindowStateListener() {
            public void windowStateChanged(WindowEvent e) {
                dismiss();
            }
        };
        ownerWindow = SwingUtilities.getWindowAncestor(owner);
        if( null != ownerWindow ) {
            ownerWindow.addWindowStateListener(windowListener);
        }
        currentPane.addComponentListener( listener );
        configureBalloon( currentBalloon, currentPane, owner );
        currentPane.add( currentBalloon, new Integer(JLayeredPane.POPUP_LAYER-1) );
    }
    
    /**
     * Dismiss currently showing balloon tooltip (if any)
     */
    public static synchronized void dismiss() {
        if( null != currentBalloon ) {
            currentBalloon.setVisible( false );
            currentBalloon.stopDismissTimer();
            currentPane.remove( currentBalloon );
            currentPane.repaint();
            currentPane.removeComponentListener( listener );
            if( null != ownerWindow ) {
                ownerWindow.removeWindowStateListener(windowListener);
            }
            currentBalloon.content.removeMouseListener (currentBalloon.mouseListener);
            currentBalloon = null;
            currentPane = null;
            listener = null;
            ownerWindow = null;
            windowListener = null;
        }
    }
    
    public static synchronized void dismissSlowly (final int timeout) {
        if( null != currentBalloon ) {
            if( currentBalloon.timeoutMillis > 0 ) {
                SwingUtilities.invokeLater( new Runnable() {
                    public void run() {
                        if (currentBalloon != null) {
                            currentBalloon.startDismissTimer (timeout);
                        }
                    }
                });
            } else {
                dismiss ();
            }
        }
    }
    
    public static synchronized void stopDismissSlowly () {
        if( null != currentBalloon ) {
            if( currentBalloon.timeoutMillis > 0 ) {
                currentBalloon.timeoutMillis = ToolTipManager.sharedInstance ().getDismissDelay (); // on MouseEnter cut timeout on 100ms
                SwingUtilities.invokeLater( new Runnable() {
                    public void run() {
                        if (currentBalloon != null) {
                            currentBalloon.stopDismissTimer ();
                        }
                    }
                });
            }
        }
    }
    
    private static void configureBalloon( Balloon balloon, JLayeredPane pane, JComponent ownerComp ) {
        Rectangle ownerCompBounds = ownerComp.getBounds();
        ownerCompBounds = SwingUtilities.convertRectangle( ownerComp.getParent(), ownerCompBounds, pane );
        
        int paneWidth = pane.getWidth();
        int paneHeight = pane.getHeight();
        
        Dimension balloonSize = balloon.getPreferredSize();
        balloonSize.height += Balloon.ARC;
        
        //first try lower right corner
        if( ownerCompBounds.x + ownerCompBounds.width + balloonSize.width < paneWidth
            && 
            ownerCompBounds.y + ownerCompBounds.height + balloonSize.height + Balloon.ARC < paneHeight ) {
            
            balloon.setArrowLocation( GridBagConstraints.SOUTHEAST );
            balloon.setBounds( ownerCompBounds.x+ownerCompBounds.width-Balloon.ARC/2, 
                    ownerCompBounds.y+ownerCompBounds.height, balloonSize.width+Balloon.ARC, balloonSize.height );
        
        //upper right corner
        } else  if( ownerCompBounds.x + ownerCompBounds.width + balloonSize.width < paneWidth
                    && 
                    ownerCompBounds.y - balloonSize.height - Balloon.ARC > 0 ) {
            
            balloon.setArrowLocation( GridBagConstraints.NORTHEAST );
            balloon.setBounds( ownerCompBounds.x+ownerCompBounds.width-Balloon.ARC/2, 
                    ownerCompBounds.y-balloonSize.height, balloonSize.width+Balloon.ARC, balloonSize.height );
        
        //lower left corner
        } else  if( ownerCompBounds.x - balloonSize.width > 0
                    && 
                    ownerCompBounds.y + ownerCompBounds.height + balloonSize.height + Balloon.ARC < paneHeight ) {
            
            balloon.setArrowLocation( GridBagConstraints.SOUTHWEST );
            balloon.setBounds( ownerCompBounds.x-balloonSize.width+Balloon.ARC/2, 
                    ownerCompBounds.y+ownerCompBounds.height, balloonSize.width+Balloon.ARC, balloonSize.height );
        //upper left corent
        } else {
            balloon.setArrowLocation( GridBagConstraints.NORTHWEST );
            balloon.setBounds( ownerCompBounds.x-balloonSize.width/*+Balloon.ARC/2*/, 
                    ownerCompBounds.y-balloonSize.height, balloonSize.width+Balloon.ARC, balloonSize.height );
        }
    }

    private static class Balloon extends JPanel {

        private static final int Y_OFFSET = 8;
        private static final int ARC = 15;
        private static final int SHADOW_SIZE = 3;


        private JComponent content;
        private MouseListener mouseListener;
        private ActionListener defaultAction;
        private JButton btnDismiss;
        private int arrowLocation = GridBagConstraints.SOUTHEAST;
        private float currentAlpha = 1.0f;
        private Timer dismissTimer;
        private int timeoutMillis;
        private boolean isMouseOverEffect = false;

        public Balloon( final JComponent content, final ActionListener defaultAction, final ActionListener dismissAction, final int timeoutMillis ) {
            super( new GridBagLayout() );
            this.content = content;
            this.defaultAction = defaultAction;
            this.timeoutMillis = timeoutMillis;
            content.setOpaque( false );

            btnDismiss = new DismissButton();
            btnDismiss.addActionListener( new ActionListener() {
                public void actionPerformed(ActionEvent e) {
                    BalloonManager.dismiss();
                }
            });
            if( null != dismissAction )
                btnDismiss.addActionListener(dismissAction);

            add( content, new GridBagConstraints(0,0,1,1,1.0,1.0,GridBagConstraints.NORTH,GridBagConstraints.BOTH,new Insets(0,0,0,0),0,0)); 
            add( btnDismiss, new GridBagConstraints(1,0,1,1,0.0,0.0,GridBagConstraints.NORTHEAST,GridBagConstraints.NONE,new Insets(7,0,0,7),0,0)); 

            setOpaque( false );

            mouseListener = new MouseListener() {

                public void mouseClicked(MouseEvent e) {
                    BalloonManager.dismiss();
                    if( null != defaultAction )
                        defaultAction.actionPerformed( new ActionEvent( Balloon.this, 0, "", e.getWhen(), e.getModifiers() ) );
                }

                public void mousePressed(MouseEvent e) {
                }

                public void mouseReleased(MouseEvent e) {
                }

                public void mouseEntered(MouseEvent e) {
                    if( null != defaultAction )
                        content.setCursor( Cursor.getPredefinedCursor( Cursor.HAND_CURSOR ) );
                    stopDismissTimer();
                    repaint();
                }

                public void mouseExited(MouseEvent e) {
                    content.setCursor( Cursor.getDefaultCursor() );
                    if( Balloon.this.timeoutMillis > 0 )
                        startDismissTimer (ToolTipManager.sharedInstance ().getDismissDelay ());
                }
            };
            content.addMouseListener(mouseListener);
            
            if( timeoutMillis > 0 ) {
                SwingUtilities.invokeLater( new Runnable() {
                    public void run() {
                        startDismissTimer (timeoutMillis);
                    }
                });
            }
            
            MouseListener mouseOverAdapter = new MouseAdapter() {
                @Override
                public void mouseEntered(MouseEvent e) {
                    isMouseOverEffect = true;
                    repaint();
                }

                @Override
                public void mouseExited(MouseEvent e) {
                    isMouseOverEffect = false;
                    repaint();
                }
            };
            
            addMouseListener(mouseOverAdapter);
            content.addMouseListener(mouseOverAdapter);
            btnDismiss.addMouseListener(mouseOverAdapter);

            handleMouseOver( content, mouseOverAdapter );
        }
        
        private static final float ALPHA_DECREMENT = 0.03f;
        private static final int DISMISS_REPAINT_REPEAT = 100;

        private void handleMouseOver( Container c, MouseListener ml ) {
            c.addMouseListener(ml);
            for( Component child : c.getComponents() ) {
                child.addMouseListener(ml);
                if( child instanceof Container )
                    handleMouseOver((Container)child, ml);
            }
        }
        
        synchronized void startDismissTimer (int timeout) {
            stopDismissTimer();
            currentAlpha = 1.0f;
            dismissTimer = new Timer(DISMISS_REPAINT_REPEAT, new ActionListener() {
                public void actionPerformed(ActionEvent e) {
                    currentAlpha -= ALPHA_DECREMENT;
                    if( currentAlpha <= ALPHA_DECREMENT ) {
                        stopDismissTimer();
                        dismiss();
                    }
                    repaint();
                }
            });
            dismissTimer.setInitialDelay (timeout);
            dismissTimer.start();
        }
        
        synchronized void stopDismissTimer() {
            if( null != dismissTimer ) {
                dismissTimer.stop();
                dismissTimer = null;
                currentAlpha = 1.0f;
            }
        }
        
        void setArrowLocation( int arrowLocation) {
            this.arrowLocation = arrowLocation;
            if( arrowLocation == GridBagConstraints.NORTHEAST || arrowLocation == GridBagConstraints.NORTHWEST ) {
                setBorder( BorderFactory.createEmptyBorder(0, 0, Y_OFFSET, btnDismiss.getWidth()));
            } else {
                setBorder( BorderFactory.createEmptyBorder(Y_OFFSET, 0, 0, btnDismiss.getWidth()));
            }
        }
        
        private Shape getMask( int w, int h ) {
            w--;
            w -= SHADOW_SIZE;
            GeneralPath path = new GeneralPath();
            Area area = null;
            switch( arrowLocation ) {
            case GridBagConstraints.SOUTHEAST: 
                area = new Area(new RoundRectangle2D.Float(0, Y_OFFSET, w, h-Y_OFFSET-SHADOW_SIZE, ARC, ARC));
                path.moveTo(ARC/2, 0);
                path.lineTo(ARC/2, Y_OFFSET);
                path.lineTo(ARC/2+Y_OFFSET, Y_OFFSET);
                break;
            case GridBagConstraints.NORTHEAST: 
                area = new Area(new RoundRectangle2D.Float(0, SHADOW_SIZE, w, h-Y_OFFSET-SHADOW_SIZE, ARC, ARC));
                path.moveTo(ARC/2, h-1);
                path.lineTo(ARC/2, h-1-Y_OFFSET);
                path.lineTo(ARC/2+Y_OFFSET, h-1-Y_OFFSET);
                break;
            case GridBagConstraints.SOUTHWEST: 
                area = new Area(new RoundRectangle2D.Float(0, Y_OFFSET, w, h-Y_OFFSET-SHADOW_SIZE, ARC, ARC));
                path.moveTo(w-ARC/2, 0);
                path.lineTo(w-ARC/2, Y_OFFSET);
                path.lineTo(w-ARC/2-Y_OFFSET, Y_OFFSET);
                break;
            case GridBagConstraints.NORTHWEST: 
                area = new Area(new RoundRectangle2D.Float(0, SHADOW_SIZE, w, h-Y_OFFSET-SHADOW_SIZE, ARC, ARC));
                path.moveTo(w-ARC/2, h-1);
                path.lineTo(w-ARC/2-Y_OFFSET, h-1-Y_OFFSET);
                path.lineTo(w-ARC/2, h-1-Y_OFFSET);
                break;
            }
                
            path.closePath();
            area.add(new Area(path));
            return area;
        }
        
        private Shape getShadowMask( Shape parentMask ) {
            Area area = new Area(parentMask);

            AffineTransform tx = new AffineTransform();
            tx.translate(SHADOW_SIZE, SHADOW_SIZE );//Math.sin(ANGLE)*(getHeight()+SHADOW_SIZE), 0);
            area.transform(tx);
            area.subtract(new Area(parentMask));
            return area;
        }


        @Override
        protected void paintBorder(Graphics g) {
        }

        @Override
        protected void paintComponent(Graphics g) {
            Graphics2D g2d = (Graphics2D)g;
            
            g2d.setRenderingHint( RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON );
            
            Composite oldC = g2d.getComposite();
            Shape s = getMask( getWidth(), getHeight() );

            g2d.setComposite( AlphaComposite.getInstance( AlphaComposite.SRC_OVER, 0.25f*currentAlpha ) );
            g2d.setColor( Color.black );
            g2d.fill( getShadowMask(s) );
            
            g2d.setColor( UIManager.getColor( "ToolTip.background" ) ); //NOI18N
            g2d.setComposite( AlphaComposite.getInstance( AlphaComposite.SRC_OVER, currentAlpha ) );
            Point2D p1 = s.getBounds().getLocation();
            Point2D p2 = new Point2D.Double(p1.getX(), p1.getY()+s.getBounds().getHeight());
            if( isMouseOverEffect )
                g2d.setPaint( new GradientPaint( p2, getMouseOverGradientStartColor(), p1, getMouseOverGradientFinishColor() ) );
            else
                g2d.setPaint( new GradientPaint( p2, getDefaultGradientStartColor(), p1, getDefaultGradientFinishColor() ) );
            g2d.fill(s);
            g2d.setColor( Color.black );
            g2d.draw(s);
            g2d.setComposite( oldC );
        }

        @Override
        protected void paintChildren(Graphics g) {
            Graphics2D g2d = (Graphics2D)g;
            Composite oldC = g2d.getComposite();
            g2d.setComposite( AlphaComposite.getInstance( AlphaComposite.SRC_OVER, currentAlpha ) );
            super.paintChildren(g);
            g2d.setComposite( oldC );
        }

        private static Color mouseOverGradientStartColor = null;
        private static Color mouseOverGradientFinishColor = null;

        private static Color defaultGradientStartColor = null;
        private static Color defaultGradientFinishColor = null;

        private static final boolean isMetal = UIManager.getLookAndFeel() instanceof MetalLookAndFeel;
        private static final boolean isNimbus = "Nimbus".equals( UIManager.getLookAndFeel().getID() ); //NOI18N

        private static Color getMouseOverGradientStartColor() {
            if( null == mouseOverGradientStartColor ) {
                mouseOverGradientStartColor = UIManager.getColor("nb.core.ui.balloon.mouseOverGradientStartColor"); //NOI18N
                if( null == mouseOverGradientStartColor ) {
                    mouseOverGradientStartColor = new Color(224,224,185);
                    if( isMetal || isNimbus ) {
                        Color c = UIManager.getColor( "ToolTip.background" ); //NOI18N
                        if( null != c ) {
                            mouseOverGradientStartColor = c.darker();
                        }
                    }
                }
            }
            return mouseOverGradientStartColor;
        }

        private static Color getMouseOverGradientFinishColor() {
            if( null == mouseOverGradientFinishColor ) {
                mouseOverGradientFinishColor = UIManager.getColor("nb.core.ui.balloon.mouseOverGradientFinishColor"); //NOI18N
                if( null == mouseOverGradientFinishColor ) {
                    mouseOverGradientFinishColor = new Color(255,255,241);
                    if( isMetal || isNimbus ) {
                        Color c = UIManager.getColor( "ToolTip.background" ); //NOI18N
                        if( null != c ) {
                            mouseOverGradientFinishColor = c.brighter();
                        }
                    }
                }
            }
            return mouseOverGradientFinishColor;
        }

        private static Color getDefaultGradientStartColor() {
            if( null == defaultGradientStartColor ) {
                defaultGradientStartColor = UIManager.getColor("nb.core.ui.balloon.defaultGradientStartColor"); //NOI18N
                if( null == defaultGradientStartColor ) {
                    defaultGradientStartColor = new Color(225,225,225);
                    if( isMetal || isNimbus ) {
                        Color c = UIManager.getColor( "ToolTip.background" ); //NOI18N
                        if( null != c ) {
                            defaultGradientStartColor = c.darker();
                        }
                    }
                }
            }
            return defaultGradientStartColor;
        }


        private static Color getDefaultGradientFinishColor() {
            if( null == defaultGradientFinishColor ) {
                defaultGradientFinishColor = UIManager.getColor("nb.core.ui.balloon.defaultGradientFinishColor"); //NOI18N
                if( null == defaultGradientFinishColor ) {
                    defaultGradientFinishColor = new Color(255,255,255);
                    if( isMetal || isNimbus ) {
                        Color c = UIManager.getColor( "ToolTip.background" ); //NOI18N
                        if( null != c ) {
                            defaultGradientFinishColor = c;
                        }
                    }
                }
            }
            return defaultGradientFinishColor;
        }
    }
    
    static class DismissButton extends JButton {

        public DismissButton() {
            setIcon( ImageUtilities.loadImageIcon( "org/netbeans/core/ui/resources/dismiss_enabled.png", true ) );
            setRolloverIcon(ImageUtilities.loadImageIcon( "org/netbeans/core/ui/resources/dismiss_rollover.png", true ));
            setPressedIcon(ImageUtilities.loadImageIcon( "org/netbeans/core/ui/resources/dismiss_pressed.png", true ));

            setBorder( BorderFactory.createEmptyBorder() );
            setBorderPainted( false );
            setFocusable( false );
            setOpaque( false );
            setRolloverEnabled( true );
        }
        
        @Override
        public void paint(Graphics g) {
            Icon icon = null;
            if( getModel().isArmed() && getModel().isPressed() ) {
                icon = getPressedIcon();
            } else if( getModel().isRollover() ) {
                icon = getRolloverIcon();
            } else {
                icon = getIcon();
            }
            icon.paintIcon( this, g, 0, 0 );
        }
        
    }
}
