/*
 * Copyright 2010-2015 Institut Pasteur.
 * 
 * This file is part of Icy.
 * 
 * Icy is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 * 
 * Icy is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU General Public License for more details.
 * 
 * You should have received a copy of the GNU General Public License
 * along with Icy. If not, see <http://www.gnu.org/licenses/>.
 */
package icy.gui.util;

import icy.gui.frame.IcyFrame;
import icy.network.NetworkUtil;
import icy.system.SystemUtil;

import java.awt.Color;
import java.awt.Component;
import java.awt.Container;
import java.awt.Dimension;
import java.awt.Font;
import java.awt.Frame;
import java.awt.GraphicsDevice;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.Window;
import java.awt.geom.Point2D;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;

import javax.swing.JInternalFrame;
import javax.swing.JMenu;
import javax.swing.JSlider;
import javax.swing.JTextPane;
import javax.swing.JTree;
import javax.swing.SwingConstants;
import javax.swing.SwingUtilities;
import javax.swing.event.HyperlinkEvent;
import javax.swing.event.HyperlinkListener;
import javax.swing.text.MutableAttributeSet;
import javax.swing.text.StyleConstants;
import javax.swing.text.StyledDocument;
import javax.swing.tree.TreeNode;
import javax.swing.tree.TreePath;

/**
 * General component utilities class.
 * 
 * @author Stephane
 */
public class ComponentUtil
{
    public static void setPreferredWidth(Component c, int w)
    {
        c.setPreferredSize(new Dimension(w, c.getPreferredSize().height));
    }

    public static void setPreferredHeight(Component c, int h)
    {
        c.setPreferredSize(new Dimension(c.getPreferredSize().width, h));
    }

    public static void setFixedSize(Component c, Dimension d)
    {
        c.setMinimumSize(d);
        c.setMaximumSize(d);
        c.setPreferredSize(d);
    }

    public static void setFixedWidth(Component c, int w)
    {
        c.setMinimumSize(new Dimension(w, 0));
        c.setMaximumSize(new Dimension(w, 65535));
        c.setPreferredSize(new Dimension(w, c.getPreferredSize().height));
    }

    public static void setFixedHeight(Component c, int h)
    {
        c.setMinimumSize(new Dimension(0, h));
        c.setMaximumSize(new Dimension(65535, h));
        c.setPreferredSize(new Dimension(c.getPreferredSize().width, h));
    }

    public static void setPreferredWidth(IcyFrame frm, int w)
    {
        frm.setPreferredSize(new Dimension(w, frm.getPreferredSize().height));
    }

    public static void setPreferredHeight(IcyFrame frm, int h)
    {
        frm.setPreferredSize(new Dimension(frm.getPreferredSize().width, h));
    }

    public static void setFixedSize(IcyFrame frm, Dimension d)
    {
        frm.setMinimumSize(d);
        frm.setMaximumSize(d);
        frm.setPreferredSize(d);
    }

    public static void setFixedWidth(IcyFrame frm, int w)
    {
        frm.setMinimumSize(new Dimension(w, 0));
        frm.setMaximumSize(new Dimension(w, 65535));
        frm.setPreferredSize(new Dimension(w, frm.getPreferredSize().height));
    }

    public static void setFixedHeight(IcyFrame frm, int h)
    {
        frm.setMinimumSize(new Dimension(0, h));
        frm.setMaximumSize(new Dimension(65535, h));
        frm.setPreferredSize(new Dimension(frm.getPreferredSize().width, h));
    }

    public static void removeFixedSize(Component c)
    {
        c.setMinimumSize(new Dimension(0, 0));
        c.setMaximumSize(new Dimension(65535, 65535));
    }

    /**
     * Center specified component relative to its parent
     */
    public static void center(Component comp)
    {
        final Container parent = comp.getParent();

        if (parent != null)
        {
            final int x = (parent.getWidth() - comp.getWidth()) / 2;
            final int y = (parent.getHeight() - comp.getHeight()) / 2;

            // avoid negative coordinates when centering
            comp.setLocation((x < 0) ? 0 : x, (y < 0) ? 0 : y);
        }
    }

    /**
     * Center specified windows relative to its parent
     */
    public static void center(Window window)
    {
        window.setLocationRelativeTo(window.getParent());
    }

    /**
     * Center specified JInternalFrame
     */
    public static void center(JInternalFrame frame)
    {
        center((Component) frame);
    }

    /**
     * Center the Window on specified point
     */
    public static void centerOn(Window window, Point position)
    {
        final int x = position.x - (window.getWidth() / 2);
        final int y = position.y - (window.getHeight() / 2);

        // avoid negative coordinates when centering
        window.setLocation((x < 0) ? 0 : x, (y < 0) ? 0 : y);
    }

    /**
     * Center the JInternalFrame on specified point
     */
    public static void centerOn(JInternalFrame f, Point position)
    {
        centerOn((Component) f, position);
    }

    /**
     * Center specified component relative to its parent
     */
    public static void centerOn(Component comp, Point position)
    {
        final int x = position.x - (comp.getWidth() / 2);
        final int y = position.y - (comp.getHeight() / 2);

        // avoid negative coordinates when centering
        comp.setLocation((x < 0) ? 0 : x, (y < 0) ? 0 : y);
    }

    /**
     * Use f.center() instead
     * 
     * @deprecated
     */
    @Deprecated
    public static void center(IcyFrame f)
    {
        f.center();
    }

    public static void center(Component dst, Component src)
    {
        dst.setLocation(src.getX() + ((src.getWidth() - dst.getWidth()) / 2),
                src.getY() + ((src.getHeight() - dst.getHeight()) / 2));
    }

    public static void center(IcyFrame dst, Component src)
    {
        dst.setLocation(src.getX() + ((src.getWidth() - dst.getWidth()) / 2),
                src.getY() + ((src.getHeight() - dst.getHeight()) / 2));
    }

    public static void center(Component dst, IcyFrame src)
    {
        dst.setLocation(src.getX() + ((src.getWidth() - dst.getWidth()) / 2),
                src.getY() + ((src.getHeight() - dst.getHeight()) / 2));
    }

    public static void center(IcyFrame dst, IcyFrame src)
    {
        dst.setLocation(src.getX() + ((src.getWidth() - dst.getWidth()) / 2),
                src.getY() + ((src.getHeight() - dst.getHeight()) / 2));
    }

    /**
     * Returns the center position of the specified component.
     */
    public static Point2D.Double getCenter(Component c)
    {
        if (c != null)
        {
            final Rectangle r = c.getBounds();
            return new Point2D.Double(r.getX() + (r.getWidth() / 2d), r.getY() + (r.getHeight() / 2d));
        }

        return new Point2D.Double(0d, 0d);
    }

    /**
     * Returns all screen device where the specified component is currently displayed.<br>
     * Can return an empty list if given region do not intersect any screen device.
     * 
     * @see #getScreen(Component)
     * @see SystemUtil#getScreenDevices(Rectangle)
     */
    public static List<GraphicsDevice> getScreens(Component c)
    {
        return SystemUtil.getScreenDevices(c.getBounds());
    }

    /**
     * Returns the main screen device where the specified component is currently displayed.<br>
     * Can return <code>null</code> if component is not located on any screen device.
     * 
     * @see #getScreens(Component)
     * @see SystemUtil#getScreenDevice(Rectangle)
     * @see SystemUtil#getScreenDevice(Point)
     */
    public static GraphicsDevice getScreen(Component c)
    {
        final Point2D.Double pos2d = getCenter(c);
        final Point pos = new Point((int) pos2d.getX(), (int) pos2d.getY());

        // get screen on Component center first (better for multi screen)
        GraphicsDevice result = SystemUtil.getScreenDevice(pos);

        // cannot retrieve screen on center, just use component bounds then
        if (result == null)
            result = SystemUtil.getScreenDevice(c.getBounds());

        return result;
    }

    /**
     * Returns the new location of wanted bounds so it does not go outside the specified screen bounds.<br>
     * Returns <code>null</code> if the wanted bounds doesn't need position adjustment.
     */
    public static Point fixPosition(Rectangle wantedBounds, Rectangle screenBounds)
    {
        if (screenBounds.isEmpty())
            return null;

        final int margeX = 80;
        final int margeY = 40;

        int x = wantedBounds.x;
        int y = wantedBounds.y;
        int sx = screenBounds.x;
        int sy = screenBounds.y;
        int minX = (sx - wantedBounds.width) + margeX;
        int maxX = (sx + screenBounds.width) - margeX;
        int minY = sy;
//        int minY = (sy - wantedBounds.height) + margeY;
        int maxY = (sy + screenBounds.height) - margeY;

        if (y < minY)
            y = minY;
        else if (y > maxY)
            y = maxY;
        if (x < minX)
            x = minX;
        else if (x > maxX)
            x = maxX;

        final Point pos = wantedBounds.getLocation();

        // position changed ?
        if ((pos.x != x) || (pos.y != y))
            return new Point(x, y);

        return null;
    }

    /**
     * Fix the given bounds of specified component so it does not go completely off screen.<br>
     * Returns <code>true</code> if the bounds position has be adjusted.
     */
    public static boolean fixPosition(Component component, Rectangle wantedBounds)
    {
      final List<GraphicsDevice> screens = SystemUtil.getScreenDevices();

      // headless mode probably
      if (screens.isEmpty())
          return false;

      Point newPos = null;
      boolean useMainScreen = false;

      for (GraphicsDevice screen : screens)
      {
          final Point pt = fixPosition(wantedBounds, SystemUtil.getScreenBounds(screen, true));

          // this screen accept current position --> no need to adjust position
          if (pt == null)
              return false;
          
          // we already have an adjusted position ?
          if (newPos != null)
              useMainScreen = true;
          else
              newPos = pt;
      }

      // multiple possible position adjustment ? --> use main screen
      if (useMainScreen)
          newPos = fixPosition(wantedBounds, SystemUtil.getScreenBounds(getScreen(component), true));

      // got a new position ? --> set it
      if (newPos != null)
      {
          wantedBounds.setLocation(newPos);
          return true;
      }

      return false;

//        final List<GraphicsDevice> screens = getScreens(component);
//        Point newPos = null;
//        boolean useMainScreen = false;
//
//        for (GraphicsDevice screen : screens)
//        {
//            final Point pt = fixPosition(wantedBounds, SystemUtil.getScreenBounds(screen, true));
//
//            // this screen accept current position --> no need to adjust position
//            if (pt == null)
//                return false;
//
//            // we already have an adjusted position ?
//            if (newPos != null)
//                useMainScreen = true;
//            else
//                newPos = pt;
//        }
//
//        // use main screen
//        if (screens.isEmpty())
//            useMainScreen = true;
//
//        // multiple possible position adjustment ? --> use main screen
//        if (useMainScreen)
//            newPos = fixPosition(wantedBounds, SystemUtil.getScreenBounds(getScreen(component), true));
//
//        // got a new position ? --> set it
//        if (newPos != null)
//            wantedBounds.setLocation(newPos);
//
//        return true;
    }

    /**
     * Fix the given bounds of specified component so it does not go completely off screen.<br>
     * Returns <code>true</code> if component position has be adjusted.
     */
    public static boolean fixPosition(Component component)
    {
        final Rectangle bounds = component.getBounds();

        if (fixPosition(component, bounds))
        {
            component.setBounds(bounds);
            return true;
        }

        return false;
    }

    public static int getComponentIndex(Component c)
    {
        if (c != null)
        {
            final Container container = c.getParent();

            if (container != null)
                for (int i = 0; i < container.getComponentCount(); i++)
                    if (container.getComponent(i) == c)
                        return i;
        }

        return -1;
    }

    public static Point convertPoint(Component src, Point p, Component dst)
    {
        return SwingUtilities.convertPoint(src, p, dst);
    }

    public static Point convertPointFromScreen(Point p, Component c)
    {
        final Point result = new Point(p);

        SwingUtilities.convertPointFromScreen(result, c);

        return result;
    }

    public static Point convertPointToScreen(Point p, Component c)
    {
        final Point result = new Point(p);

        SwingUtilities.convertPointToScreen(result, c);

        return result;
    }

    public static boolean isOutside(Component c, Rectangle r)
    {
        return !r.intersects(c.getBounds());
    }

    public static boolean isInside(Component c, Rectangle r)
    {
        return r.contains(c.getBounds());
    }

    public static void increaseFontSize(Component c, int value)
    {
        setFontSize(c, c.getFont().getSize() + value);
    }

    public static void decreaseFontSize(Component c, int value)
    {
        setFontSize(c, c.getFont().getSize() - value);
    }

    public static void setFontSize(Component c, int fontSize)
    {
        c.setFont(FontUtil.setSize(c.getFont(), fontSize));
    }

    public static void setFontStyle(Component c, int fontStyle)
    {
        c.setFont(FontUtil.setStyle(c.getFont(), fontStyle));
    }

    public static void setFontBold(Component c)
    {
        setFontStyle(c, c.getFont().getStyle() | Font.BOLD);
    }

    public static void setJTextPaneFont(JTextPane tp, Font font, Color c)
    {
        final MutableAttributeSet attrs = tp.getInputAttributes();

        // Set the font family, size, and style, based on properties of
        // the Font object. Note that JTextPane supports a number of
        // character attributes beyond those supported by the Font class.
        // For example, underline, strike-through, super- and sub-script.
        StyleConstants.setFontFamily(attrs, font.getFamily());
        StyleConstants.setFontSize(attrs, font.getSize());
        StyleConstants.setItalic(attrs, (font.getStyle() & Font.ITALIC) != 0);
        StyleConstants.setBold(attrs, (font.getStyle() & Font.BOLD) != 0);

        // Set the font color
        StyleConstants.setForeground(attrs, c);

        // Retrieve the pane's document object
        StyledDocument doc = tp.getStyledDocument();

        // Replace the style for the entire document. We exceed the length
        // of the document by 1 so that text entered at the end of the
        // document uses the attributes.
        doc.setCharacterAttributes(0, doc.getLength() + 1, attrs, false);
    }

    public static void setTickMarkers(JSlider slider)
    {
        final int min = slider.getMinimum();
        final int max = slider.getMaximum();
        final int delta = max - min;

        if (delta > 0)
        {
            final int sliderSize;
            if (slider.getOrientation() == SwingConstants.HORIZONTAL)
                sliderSize = slider.getPreferredSize().width;
            else
                sliderSize = slider.getPreferredSize().height;

            // adjust ticks space on slider
            final int majTick = findBestMajTickSpace(sliderSize, delta);

            slider.setMinorTickSpacing(Math.max(1, majTick / 5));
            slider.setMajorTickSpacing(majTick);
            slider.setLabelTable(slider.createStandardLabels(slider.getMajorTickSpacing(), majTick));
        }
    }

    private static int findBestMajTickSpace(int sliderSize, int delta)
    {
        final int values[] = {1, 2, 5, 10, 20, 25, 50, 100, 200, 250, 500, 1000, 2000, 2500, 5000};
        // wanted a major tick each ~40 pixels
        final int wantedMajTickSpace = delta / (sliderSize / 40);

        int min = Integer.MAX_VALUE;
        int bestValue = 1;

        // try with our predefined values
        for (int value : values)
        {
            final int dx = Math.abs(value - wantedMajTickSpace);

            if (dx < min)
            {
                min = dx;
                bestValue = value;
            }
        }

        return bestValue;
    }

    /**
     * Breaks the list of items in the specified menu, by creating sub-menus containing the
     * specified number of items, and a "More..." menu to access subsequent items.
     * 
     * @param menu
     *        the menu to break into smaller sub-menus
     * @param maxItemsPerMenu
     *        the maximum number of items to display in each sub-menu
     */
    public static void split(JMenu menu, int maxItemsPerMenu)
    {
        ArrayList<Component> components = new ArrayList<Component>(Arrays.asList(menu.getPopupMenu().getComponents()));

        if (components.size() > maxItemsPerMenu)
        {
            menu.removeAll();

            JMenu currentMenu = menu;

            while (components.size() > 0)
            {
                int n = Math.min(components.size(), maxItemsPerMenu - 1);

                for (int i = 0; i < n; i++)
                    currentMenu.add(components.remove(0));

                if (components.size() > 0)
                    currentMenu = (JMenu) currentMenu.add(new JMenu("More..."));
            }

            if (components.size() > 0)
                System.err.println(components.size() + " are remaining !!");
        }

        // do this recursively for sub-menus
        for (Component component : menu.getPopupMenu().getComponents())
        {
            if (component instanceof JMenu)
                split((JMenu) component, maxItemsPerMenu);
        }
    }

    public static TreePath buildTreePath(TreeNode node)
    {
        final ArrayList<TreeNode> nodes = new ArrayList<TreeNode>();

        nodes.add(node);

        TreeNode n = node;
        while (n.getParent() != null)
        {
            n = n.getParent();
            nodes.add(n);
        }

        Collections.reverse(nodes);

        return new TreePath(nodes.toArray());
    }

    public static void expandAllTree(JTree tree)
    {
        for (int i = 0; i < tree.getRowCount(); i++)
            tree.expandRow(i);
    }

    public static HyperlinkListener getDefaultHyperlinkListener()
    {
        return new HyperlinkListener()
        {
            @Override
            public void hyperlinkUpdate(HyperlinkEvent e)
            {
                if (e.getEventType() == HyperlinkEvent.EventType.ACTIVATED)
                    NetworkUtil.openURL(e.getURL());
            }
        };
    }

    public static boolean isMaximized(Frame f)
    {
        return (f.getExtendedState() & Frame.MAXIMIZED_BOTH) == Frame.MAXIMIZED_BOTH;
    }

    public static void setMaximized(Frame f, boolean b)
    {
        // only relevant if state changed
        if (isMaximized(f) ^ b)
        {
            if (b)
                f.setExtendedState(Frame.MAXIMIZED_BOTH);
            else
                f.setExtendedState(Frame.NORMAL);
        }
    }

    public static boolean isMinimized(Frame f)
    {
        return (f.getExtendedState() & Frame.ICONIFIED) == Frame.ICONIFIED;
    }

    public static void setMinimized(Frame f, boolean b)
    {
        // only relevant if state changed
        if (isMinimized(f) ^ b)
        {
            if (b)
                f.setExtendedState(Frame.ICONIFIED);
            else
                f.setExtendedState(Frame.NORMAL);
        }
    }
}