/*
 * The contents of this file is dual-licensed under 2 
 * alternative Open Source/Free licenses: LGPL 2.1 or later and 
 * Apache License 2.0. (starting with JNA version 4.0.0).
 * 
 * You can freely decide which license you want to apply to 
 * the project.
 * 
 * You may obtain a copy of the LGPL License at:
 * 
 * http://www.gnu.org/licenses/licenses.html
 * 
 * A copy is also included in the downloadable source code package
 * containing JNA, in file "LGPL2.1".
 * 
 * You may obtain a copy of the Apache License at:
 * 
 * http://www.apache.org/licenses/
 * 
 * A copy is also included in the downloadable source code package
 * containing JNA, in file "AL2.0".
 */
package com.sun.jna.contrib.demo;

import java.awt.Color;
import java.awt.Component;
import java.awt.Dimension;
import java.awt.Font;
import java.awt.Graphics;
import java.awt.GraphicsConfiguration;
import java.awt.Insets;
import java.awt.Point;
import java.awt.Polygon;
import java.awt.Rectangle;
import java.awt.Shape;
import java.awt.Toolkit;
import java.awt.Window;
import java.awt.event.ActionEvent;
import java.awt.event.ComponentAdapter;
import java.awt.event.ComponentEvent;
import java.awt.event.FocusAdapter;
import java.awt.event.FocusEvent;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.geom.Area;
import java.awt.geom.RoundRectangle2D;

import javax.swing.AbstractAction;
import javax.swing.JLabel;
import javax.swing.JWindow;
import javax.swing.Popup;
import javax.swing.SwingUtilities;
import javax.swing.Timer;

import com.sun.jna.platform.WindowUtils;

/**
 * The BalloonTipManager class handles creation and disposal of balloon style 
 * tips that are typically used to display warning/error information based on
 * results from input validation.  The balloon tip location and direction are
 * computed based on the owner component location on the screen.  The balloon
 * tip is only displayed for a limited amount of time that is configurable.  
 * The balloon tip is also disposed when the owner component loses focus or the
 * mouse is pressed.
 */
public class BalloonTipManager {
  public static final Color DEFAULT_BORDER_COLOR = Color.BLACK;
  public static final Color DEFAULT_BACKGROUND_COLOR = new Color(255, 255, 225);
  public static final Color DEFAULT_TEXT_COLOR = Color.BLACK;
  
  private static final Integer VPOS_ABOVE = 0; // Positioned above component.
  private static final Integer VPOS_BELOW = 1; // Positioned below component.
  private static final Integer HPOS_LEFT = 0; // Arrow is on the left side.
  private static final Integer HPOS_RIGHT = 1; // Arros is on the right side.

  private static Integer vpos = null;
  private static Integer hpos = null;
  
  private static Timer hidePopupTimer = null;
  private static boolean isShowing = false;
  
  
  /*
   * The BalloonTip class defines the look of the BalloonTip object.
   */
  private static final class BalloonTip extends JWindow {
		private static final long serialVersionUID = 1L;
    private static final Integer HMARGIN = 10;
    private static final Integer VMARGIN = 6;
    private static final Integer VSPACER = 4;
    private static final int ARC_D = 16;
    
    private Area mask = null;
    private Dimension maskSize = null;
    private String[] textList = null;
    
    private Color backgroundColor = null;
    private Color borderColor = null;
    private Color textColor = null;
    
    /**
     * Create a BalloonTip object.
     * @param owner the parent window for the components
     * @param content the string for the balloon tip
     * @param position the position for the balloon; either above or below the
     * owner component
     * @param origin the origin point for the balloon tip
     * @param bordercolor the background color for the balloon tip
     * @param backgroundcolor the border color for the balloon tip
     * @param textcolor the text color for the balloon tip
     */
    public BalloonTip (
        Window owner, String content, Point origin, Color bordercolor,
        Color backgroundcolor, Color textcolor)
    {
      super(owner);
      textList = content.split("\n");
      borderColor = bordercolor;
      backgroundColor = backgroundcolor;
      textColor = textcolor;
      setFocusableWindowState(false);
      setName("###overrideRedirect###");
      setSize(getPreferredSize());
    }
    
    /*
     * Sets the mask for the Balloon Tip.
     */
    private void setWindowMask () {
      mask = new Area(getMask());
      maskSize = getSize();
      WindowUtils.setWindowMask(BalloonTip.this, mask);
    }
    
    /*
     * (non-Javadoc)
     * @see java.awt.Container#paint(java.awt.Graphics)
     */
    public void paint (Graphics g) {
      super.paint(g);
      Dimension d = getMinimumWindowSize();
      int width = d.width + 2 * HMARGIN;
      int height = d.height + 2 * VMARGIN;
      int x = 0;
      int y = 0;
      if (vpos == VPOS_BELOW) {
        y += 15;
      }
      // Draw the filled rounded rectangle and clean up the missed pixels.
      g.setColor(backgroundColor);
      g.fillRoundRect(x, y, width, height, ARC_D, ARC_D);
      g.drawLine(x + 6, y + height - 1, x + 6, y + height - 1);
      g.drawLine(x + width - 1, y + 6, x + width - 1, y + 6);
      g.drawLine(x + width - 1, y + height - 6, x + width - 1, y + height - 7);
      g.drawLine(x + width - 2, y + height - 4, x + width - 4, y + height - 2);
      g.drawLine(x + width - 6, y + height - 1, x + width - 7, y + height - 1);
      g.clearRect(x + 2, y + 2, 1, 1);
      // Draw the border of the rounded rectangle.
      g.setColor(borderColor);
      g.drawRoundRect(x, y, width, height, ARC_D, ARC_D);
      // Draw the external triangle for the balloon.
      if (vpos == VPOS_BELOW) {
        if (hpos == HPOS_LEFT) {
          g.setColor(backgroundColor);
          int[] xPts = {16, 16, 31};
          int[] yPts = {0, 16, 16};
          g.fillPolygon(xPts, yPts, 3);
          g.setColor(borderColor);
          g.drawLine(16, 0, 16, 15);
          g.drawLine(16, 0, 31, 15);
          g.drawLine(16, 1, 30, 15);
        }
        else {
          g.setColor(backgroundColor);
          int[] xPts = {width - 16, width - 16, width - 31};
          int[] yPts = {0, 16, 16};
          g.fillPolygon(xPts, yPts, 3);
          g.setColor(borderColor);
          g.drawLine(width - 16, 0, width - 16, 15);
          g.drawLine(width - 16, 0, width - 31, 15);
          g.drawLine(width - 16, 1, width - 30, 15);
        }
      }
      else {
        if (hpos == HPOS_LEFT) {
          g.setColor(backgroundColor);
          int[] xPts = {16, 16, 31};
          int[] yPts = {height, height + 16, height};
          g.fillPolygon(xPts, yPts, 3);
          g.setColor(borderColor);
          g.drawLine(16, height, 16, height + 15);
          g.drawLine(16, height + 15, 31, height);
          g.drawLine(16, height + 14, 30, height);
        }
        else {
          g.setColor(backgroundColor);
          int[] xPts = {width - 16, width - 16, width - 31};
          int[] yPts = {height, height + 16, height};
          g.fillPolygon(xPts, yPts, 3);
          g.setColor(borderColor);
          g.drawLine(width - 16, height, width - 16, height + 15);
          g.drawLine(width - 16, height + 15, width - 31, height);
          g.drawLine(width - 16, height + 14, width - 30, height);
        }
      }
      // Draw the inner component for the balloon.
      g.setColor(textColor);
      g.setFont(new Font("Tahoma", Font.PLAIN, 11));
      int stringY = y + VMARGIN / 2;
      for (int i = 0; i < textList.length; i++) {
        stringY += new JLabel(textList[i]).getPreferredSize().height;
        if (i > 0) {
          stringY += VSPACER;
        }
        g.drawString(textList[i], HMARGIN, stringY);
      }
    }
    
    /*
     * Returns the mask for the balloon tip window.
     */
    private Shape getMask () {
      Dimension d = getMinimumWindowSize();
      int width = d.width + 2 * HMARGIN;
      int height = d.height + 2 * VMARGIN;
      int x = 0;
      int y = 0;
      if (vpos == VPOS_BELOW) {
        y += 15;
      }
      // Start by creating the area of the main rounded rectangle.
      Area area = new Area(
        new RoundRectangle2D.Float(x, y, width + 1, height + 1, ARC_D, ARC_D));
      // Add in the remaining pixels that are not included by default due to
      // the differences between Graphics drawing and Shape creation.
      area.add(new Area(new Rectangle(0, y + 6, 1, 2)));
      area.add(new Area(new Rectangle(0, y + height - 7, 1, 2)));
      area.add(new Area(new Rectangle(1, y + 4, 1, 1)));
      area.add(new Area(new Rectangle(1, y + height - 5, 1, 2)));
      area.add(new Area(new Rectangle(4, y + 1, 1, 1)));
      area.add(new Area(new Rectangle(6, y, 2, 1)));
      area.add(new Area(new Rectangle(width - 7, y, 2, 1)));
      // Subtract the extra pixels that are not included by default due to the
      // differences between Graphics drawing and Shape creation.
      area.subtract(new Area(new Rectangle(2, y + height - 2, 1, 1)));
      area.subtract(new Area(new Rectangle(3, y + height - 1, 1, 1)));
      area.subtract(new Area(new Rectangle(5, y + height, 1, 1)));
      area.subtract(new Area(new Rectangle(width - 5, y + height, 2, 1)));
      area.subtract(new Area(new Rectangle(width - 3, y + height - 1, 2, 1)));
      area.subtract(new Area(new Rectangle(width - 2, y + 2, 1, 1)));
      area.subtract(new Area(new Rectangle(width - 2, y + height - 2, 2, 1)));
      area.subtract(new Area(new Rectangle(width - 1, y + 3, 1, 1)));
      area.subtract(new Area(new Rectangle(width - 1, y + height - 3, 1, 1)));
      area.subtract(new Area(new Rectangle(width, y + 5, 1, 1)));
      area.subtract(new Area(new Rectangle(width, y + height - 5, 1, 2)));
      // Add in the triangle piece for the balloon.
      if (vpos == VPOS_BELOW) {
        if (hpos == HPOS_LEFT) {
          int[] xPts = {16, 16, 32};
          int[] yPts = {-1, 16, 16};
          area.add(new Area(new Polygon(xPts, yPts, 3)));
        }
        else {
          int[] xPts = {width - 15, width - 15, width - 32};
          int[] yPts = {-1, 16, 16};
          area.add(new Area(new Polygon(xPts, yPts, 3)));
        }
      }
      else {
        if (hpos == HPOS_LEFT) {
          int[] xPts = {16, 16, 31};
          int[] yPts = {height, height + 16, height};
          area.add(new Area(new Polygon(xPts, yPts, 3)));
        }
        else {
          int[] xPts = {width - 15, width - 15, width - 32};
          int[] yPts = {height, height + 16, height};
          area.add(new Area(new Polygon(xPts, yPts, 3)));
        }
      }
      return area;
    }
    
    /*
     * Returns the dimension of the window based on the preferred component
     * sizes.
     */
    private Dimension getMinimumWindowSize () {
      int maxWidth = 0;
      int textHeight = 0;
      JLabel tempLabel = null;
      for (int i = 0; i < textList.length; i++) {
        tempLabel = new JLabel(textList[i]);
        maxWidth = Math.max(maxWidth, tempLabel.getPreferredSize().width);
        textHeight += tempLabel.getPreferredSize().height;
      }
      int w = Math.max(maxWidth, 32);
      int h = Math.max(textHeight + (textList.length - 1) * VSPACER, 8);
      return new Dimension(w, h);
    }
    
    /*
     * (non-Javadoc)
     * @see java.awt.Window#setBounds(int, int, int, int)
     */
    public void setBounds (int x, int y, int w, int h) {
      super.setBounds(x, y, w, h);
      Dimension size = new Dimension(w, h);
      if (mask != null && !size.equals(maskSize)) {
        mask.subtract(mask);
        mask.add(new Area(getMask()));
        maskSize = size;
      }
    }
    
    /*
     * (non-Javadoc)
     * @see java.awt.Container#getPreferredSize()
     */
    public Dimension getPreferredSize () {
      Dimension d = getMinimumWindowSize();
      int w = d.width + 2 * HMARGIN + 1;
      int h = d.height + 2 * VMARGIN + 16;
      return new Dimension(w, h);
    }
  }
  

  /**
   * Returns the popup window of the balloon tip.
   * @param owner the owner component for the balloon tip
   * @param content the text string to display in the balloon tip
   * @param x the x coordinate for the origin for the balloon tip in relation
   * to the owner component
   * @param y the y coordinate for the origin for the balloon tip in relation
   * to the owner component
   * @param position the position for the balloon; either above or below the
     * owner component
   * @param duration the duration in milliseconds to display balloon tip
   * @return the popup window of the balloon tip
   */
  public static Popup getBalloonTip (final Component owner,
      final String content, int x, int y, final int duration)
  {
    return getBalloonTip(owner, content, x, y, duration, DEFAULT_BORDER_COLOR,
      DEFAULT_BACKGROUND_COLOR, DEFAULT_TEXT_COLOR);
  }
  
  /**
   * Returns whether the popup is showing or not.
   * @return true if the popup is showing, else false
   */
  public static boolean isShowing () {
    return isShowing;
  }
  
  /**
   * Restarts the popup timer.
   */
  public static void restartTimer () {
    hidePopupTimer.restart();
  }
  
  /**
   * Returns the popup window of the balloon tip.
   * @param owner the owner component for the balloon tip
   * @param content the text string to display in the balloon tip
   * @param x the x coordinate for the origin for the balloon tip in relation
   * to the owner component
   * @param y the y coordinate for the origin for the balloon tip in relation
   * to the owner component
   * @param position the position for the balloon; either above or below the
     * owner component
   * @param duration the duration in milliseconds to display balloon tip
   * @param bordercolor the background color for the balloon tip
   * @param backgroundcolor the border color for the balloon tip
   * @param textcolor the text color for the balloon tip
   * @return the popup window of the balloon tip
   */
  public static Popup getBalloonTip (final Component owner,
      final String content, int x, int y, final Integer duration,
      final Color bordercolor, final Color backgroundcolor,
      final Color textcolor)
  {
    final Point origin =
      owner == null ? new Point(0, 0) : owner.getLocationOnScreen();
    final Window parent =
      owner != null ? SwingUtilities.getWindowAncestor(owner) : null;
    final String text =
      content != null ? content : "";
    final Integer timerDuration =
      duration != null ? duration : 10000;
    origin.translate(x, y);
    vpos = VPOS_BELOW;
    hpos = HPOS_LEFT;
    return new Popup () {
      private BalloonTip bt = null;
      final ComponentEar componentEar = new ComponentEar();
      final MouseEar mouseEar = new MouseEar();
      final FocusEar focusEar = new FocusEar();
      
      /*
       * (non-Javadoc)
       * @see javax.swing.Popup#show()
       */
      public void show () {
        hidePopupTimer = new Timer(timerDuration, new TimerAction());
        bt = new BalloonTip(parent, text, origin, bordercolor,
          backgroundcolor, textcolor);
        bt.pack();
        Point pt = new Point(origin);
        pt.translate(10, owner.getHeight());
        bt.setLocation(getAdjustedOrigin(pt));
        bt.setWindowMask();
        bt.setVisible(true);
        owner.addFocusListener(focusEar);
        owner.addMouseListener(mouseEar);
        parent.addMouseListener(mouseEar);
        parent.addComponentListener(componentEar);
        hidePopupTimer.start();
        isShowing = true;
      }
      
      /*
       * (non-Javadoc)
       * @see javax.swing.Popup#hide()
       */
      public void hide () {
        if (bt != null) {
          isShowing = false;
          hidePopupTimer.stop();
          parent.removeComponentListener(componentEar);
          parent.removeMouseListener(mouseEar);
          owner.removeMouseListener(mouseEar);
          owner.removeFocusListener(focusEar);
          bt.setVisible(false);
          bt.dispose();
        }
      }
      
      /*
       * Adjust the location of the balloon popup so that is drawn completely
       * on the screen and specify the orientation.
       */
      private Point getAdjustedOrigin (Point pt) {
        Point ret = new Point(pt.x, pt.y);
        GraphicsConfiguration gc = owner.getGraphicsConfiguration();
        Rectangle sBounds = gc.getBounds();
        Insets sInsets = Toolkit.getDefaultToolkit().getScreenInsets(gc);
        sBounds.x += sInsets.left;
        sBounds.y += sInsets.top;
        sBounds.width -= (sInsets.left + sInsets.right);
        sBounds.height -= (sInsets.top + sInsets.bottom);
        
        if (ret.x < sBounds.x) {
          ret.x = sBounds.x;
        }
        else if (ret.x - sBounds.x + bt.getWidth() > sBounds.width) {
          ret.x = owner.getLocationOnScreen().x - bt.getWidth() + 43;
        }
        if (ret.x >= pt.x) {
          hpos = HPOS_LEFT;
        }
        else {
          hpos = HPOS_RIGHT;
        }

        if (ret.y < sBounds.y) {
          ret.y = sBounds.y;
        }
        else if (ret.y - sBounds.y + bt.getHeight() > sBounds.height) {
          ret.y = owner.getLocationOnScreen().y - bt.getHeight();
        }
        if (ret.y >= pt.y) {
          vpos = VPOS_BELOW;
        }
        else {
          vpos = VPOS_ABOVE;
        }
        return ret;
      }
      
      /*
       * This class handles actions from the balloon tip timer.
       */
      @SuppressWarnings("serial")
      final class TimerAction extends AbstractAction {
        /*
         * (non-Javadoc)
         * @see java.awt.event.ActionListener#actionPerformed(
         * java.awt.event.ActionEvent)
         */
        public void actionPerformed (ActionEvent e) {
          hide();
        }
      }
      
      /*
       * This class handles events spawned from moving the component.
       */
      final class ComponentEar extends ComponentAdapter {
        /*
         * (non-Javadoc)
         * @see java.awt.event.ComponentAdapter#componentMoved(
         * java.awt.event.ComponentEvent)
         */
        public void componentMoved (ComponentEvent e) {
          hide();
        }
      }
      
      /*
       * This class handles events spawned when a mouse button is pressed.
       */
      final class MouseEar extends MouseAdapter {
        /*
         * (non-Javadoc)
         * @see java.awt.event.MouseAdapter#mousePressed(
         * java.awt.event.MouseEvent)
         */
        public void mousePressed (MouseEvent e) {
          hide();
        }
      }

      /*
       * This class handles events spawned when the component loses focus.
       */
      final class FocusEar extends FocusAdapter {
        /*
         * (non-Javadoc)
         * @see java.awt.event.FocusAdapter#focusLost(
         * java.awt.event.FocusEvent)
         */
        public void focusLost (FocusEvent e) {
          hide();
        }
      }
    };
  }
}
