/*
 * Copyright (c) 2007, 2018, Oracle and/or its affiliates. All rights reserved.
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
 *
 * This code is free software; you can redistribute it and/or modify it
 * under the terms of the GNU General Public License version 2 only, as
 * published by the Free Software Foundation.  Oracle designates this
 * particular file as subject to the "Classpath" exception as provided
 * by Oracle in the LICENSE file that accompanied this code.
 *
 * This code 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
 * version 2 for more details (a copy is included in the LICENSE file that
 * accompanied this code).
 *
 * You should have received a copy of the GNU General Public License version
 * 2 along with this work; if not, write to the Free Software Foundation,
 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
 *
 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
 * or visit www.oracle.com if you need additional information or have any
 * questions.
 */

package org.graalvm.visualvm.modules.mbeans;

import java.awt.*;
import java.awt.event.*;

import javax.swing.*;
import javax.swing.border.*;
import javax.swing.plaf.*;
import javax.swing.plaf.basic.BasicGraphicsUtils;

import static javax.swing.SwingConstants.*;

import static org.graalvm.visualvm.modules.mbeans.Resources.*;
import static org.graalvm.visualvm.modules.mbeans.Utilities.*;

@SuppressWarnings("serial")
class BorderedComponent extends JPanel implements ActionListener {
    JButton moreOrLessButton;
    String valueLabelStr;
    JLabel label;
    JComponent comp;
    boolean collapsed = false;

    private JPopupMenu popupMenu;

    private Icon collapseIcon;
    private Icon expandIcon;

    private static Image getImage(String name) {
        Toolkit tk = Toolkit.getDefaultToolkit();
        name = "resources/" + name + ".png"; // NOI18N
        return tk.getImage(BorderedComponent.class.getResource(name));
    }

    public BorderedComponent(String text) {
        this(text, null, false);
    }

    public BorderedComponent(String text, JComponent comp) {
        this(text, comp, false);
    }

    public BorderedComponent(String text, JComponent comp, boolean collapsible) {
        super(null);

        this.comp = comp;

        // Only add border if text is not null
        if (text != null) {
            TitledBorder border;
            if (collapsible) {
                final JLabel textLabel = new JLabel(text);
                JPanel borderLabel = new JPanel(new FlowLayout(FlowLayout.LEFT, 2, 0)) {
                    public int getBaseline(int w, int h) {
                        Dimension dim = textLabel.getPreferredSize();
                        return textLabel.getBaseline(dim.width, dim.height) + textLabel.getY();
                    }
                };
                borderLabel.add(textLabel);
                border = new LabeledBorder(borderLabel);
                textLabel.setForeground(border.getTitleColor());

                collapseIcon = new ArrowIcon(SOUTH, textLabel);
                expandIcon = new ArrowIcon(EAST, textLabel);

                moreOrLessButton = new JButton(collapseIcon);
                moreOrLessButton.setContentAreaFilled(false);
                moreOrLessButton.setBorderPainted(false);
                moreOrLessButton.setMargin(new Insets(0, 0, 0, 0));
                moreOrLessButton.addActionListener(this);
                String toolTip =
                    getText("LBL_BorderedComponent.moreOrLessButton.toolTip"); // NOI18N
                moreOrLessButton.setToolTipText(toolTip);
                borderLabel.add(moreOrLessButton);
                borderLabel.setSize(borderLabel.getPreferredSize());
                add(borderLabel);
            } else {
                border = new TitledBorder(text);
            }
            setBorder(new CompoundBorder(new FocusBorder(this), border));
        } else {
            setBorder(new FocusBorder(this));
        }
        if (comp != null) {
            add(comp);
        }
    }

    public void setComponent(JComponent comp) {
        if (this.comp != null) {
            remove(this.comp);
        }
        this.comp = comp;
        if (!collapsed) {
            LayoutManager lm = getLayout();
            if (lm instanceof BorderLayout) {
                add(comp, BorderLayout.CENTER);
            } else {
                add(comp);
            }
        }
        revalidate();
    }

    public void setValueLabel(String str) {
        this.valueLabelStr = str;
        if (label != null) {
            label.setText(Resources.getText("LBL_CurrentValue",valueLabelStr)); // NOI18N
        }
    }

    public void actionPerformed(ActionEvent ev) {
        if (collapsed) {
            if (label != null) {
                remove(label);
            }
            add(comp);
            moreOrLessButton.setIcon(collapseIcon);
        } else {
            remove(comp);
            if (valueLabelStr != null) {
                if (label == null) {
                    label = new JLabel(Resources.getText("LBL_CurrentValue", // NOI18N
                                                         valueLabelStr));
                }
                add(label);
            }
            moreOrLessButton.setIcon(expandIcon);
        }
        collapsed = !collapsed;

        JComponent container = (JComponent)getParent();
        if (container != null &&
            container.getLayout() instanceof VariableGridLayout) {

            ((VariableGridLayout)container.getLayout()).setFillRow(this, !collapsed);
            container.revalidate();
        }
    }

    public Dimension getMinimumSize() {
        if (getLayout() != null) {
            // A layout manager has been set, so delegate to it
            return super.getMinimumSize();
        }

        if (moreOrLessButton != null) {
            Dimension d = moreOrLessButton.getMinimumSize();
            Insets i = getInsets();
            d.width  += i.left + i.right;
            d.height += i.top + i.bottom;
            return d;
        } else {
            return super.getMinimumSize();
        }
    }

    public void doLayout() {
        if (getLayout() != null) {
            // A layout manager has been set, so delegate to it
            super.doLayout();
            return;
        }

        Dimension d = getSize();
        Insets i = getInsets();

        if (collapsed) {
            if (label != null) {
                Dimension p = label.getPreferredSize();
                label.setBounds(i.left,
                                i.top + (d.height - i.top - i.bottom - p.height) / 2,
                                p.width,
                                p.height);
            }
        } else {
            if (comp != null) {
                comp.setBounds(i.left,
                               i.top,
                               d.width - i.left - i.right,
                               d.height - i.top - i.bottom);
            }
        }
    }

    private static class ArrowIcon implements Icon {
        private int direction;
        private JLabel textLabel;

        public ArrowIcon(int direction, JLabel textLabel) {
            this.direction = direction;
            this.textLabel = textLabel;
        }

        public void paintIcon(Component c, Graphics g, int x, int y) {
            int w = getIconWidth();
            int h = w;
            Polygon p = new Polygon();
            switch (direction) {
              case EAST:
                p.addPoint(x + 2,     y);
                p.addPoint(x + w - 2, y + h / 2);
                p.addPoint(x + 2,     y + h - 1);
                break;

              case SOUTH:
                p.addPoint(x,         y + 2);
                p.addPoint(x + w / 2, y + h - 2);
                p.addPoint(x + w - 1, y + 2);
                break;
            }
            g.fillPolygon(p);
        }

        public int getIconWidth() {
            return getIconHeight();
        }

        public int getIconHeight() {
            Graphics g = textLabel.getGraphics();
            if (g != null) {
                int h = g.getFontMetrics(textLabel.getFont()).getAscent() * 6/10;
                if (h % 2 == 0) {
                    h += 1;     // Make it odd
                }
                return h;
            } else {
                return 7;
            }
        }
    }


    /**
     * A subclass of <code>TitledBorder</code> which implements an arbitrary border
     * with the addition of a JComponent (JLabel, JPanel, etc) in the
     * default position.
     * <p>
     * If the border property value is not
     * specified in the constuctor or by invoking the appropriate
     * set method, the property value will be defined by the current
     * look and feel, using the following property name in the
     * Defaults Table:
     * <ul>
     * <li>&quot;TitledBorder.border&quot;
     * </ul>
     */
    protected static class LabeledBorder extends TitledBorder {
        protected JComponent label;

        private Point compLoc = new Point();

        /**
         * Creates a LabeledBorder instance.
         *
         * @param label  the label the border should display
         */
        public LabeledBorder(JComponent label)     {
            this(null, label);
        }

        /**
         * Creates a LabeledBorder instance with the specified border
         * and an empty label.
         *
         * @param border  the border
         */
        public LabeledBorder(Border border)       {
            this(border, null);
        }

        /**
         * Creates a LabeledBorder instance with the specified border and
         * label.
         *
         * @param border  the border
         * @param label  the label the border should display
         */
        public LabeledBorder(Border border, JComponent label) {
            super(border);

            this.label = label;

            if (label instanceof JLabel &&
                label.getForeground() instanceof ColorUIResource) {

                label.setForeground(getTitleColor());
            }

        }

        /**
         * Paints the border for the specified component with the
         * specified position and size.
         * @param c the component for which this border is being painted
         * @param g the paint graphics
         * @param x the x position of the painted border
         * @param y the y position of the painted border
         * @param width the width of the painted border
         * @param height the height of the painted border
         */
        public void paintBorder(Component c, Graphics g, int x, int y, int width, int height) {

            Border border = getBorder();

            if (label == null) {
                if (border != null) {
                    border.paintBorder(c, g, x, y, width, height);
                }
                return;
            }

            Rectangle grooveRect = new Rectangle(x + EDGE_SPACING, y + EDGE_SPACING,
                                                 width - (EDGE_SPACING * 2),
                                                 height - (EDGE_SPACING * 2));

            Dimension   labelDim = label.getPreferredSize();
            int baseline = label.getBaseline(labelDim.width, labelDim.height);
            int         ascent = Math.max(0, baseline);
            int         descent = labelDim.height - ascent;
            int         diff;
            Insets      insets;

            if (border != null) {
                insets = border.getBorderInsets(c);
            } else {
                insets = new Insets(0, 0, 0, 0);
            }

            diff = Math.max(0, ascent/2 + TEXT_SPACING - EDGE_SPACING);
            grooveRect.y += diff;
            grooveRect.height -= diff;
            compLoc.y = grooveRect.y + insets.top/2 - (ascent + descent) / 2 - 1;

            int justification;
            if (c.getComponentOrientation().isLeftToRight()) {
                justification = LEFT;
            } else {
                justification = RIGHT;
            }

            switch (justification) {
                case LEFT:
                    compLoc.x = grooveRect.x + TEXT_INSET_H + insets.left;
                    break;
                case RIGHT:
                    compLoc.x = (grooveRect.x + grooveRect.width
                                 - (labelDim.width + TEXT_INSET_H + insets.right));
                    break;
            }

            // If title is positioned in middle of border AND its fontsize
            // is greater than the border's thickness, we'll need to paint
            // the border in sections to leave space for the component's background
            // to show through the title.
            //
            if (border != null) {
                if (grooveRect.y > compLoc.y - ascent) {
                    Rectangle clipRect = new Rectangle();

                    // save original clip
                    Rectangle saveClip = g.getClipBounds();

                    // paint strip left of text
                    clipRect.setBounds(saveClip);
                    if (computeIntersection(clipRect, x, y, compLoc.x-1-x, height)) {
                        g.setClip(clipRect);
                        border.paintBorder(c, g, grooveRect.x, grooveRect.y,
                                      grooveRect.width, grooveRect.height);
                    }

                    // paint strip right of text
                    clipRect.setBounds(saveClip);
                    if (computeIntersection(clipRect, compLoc.x+ labelDim.width +1, y,
                                   x+width-(compLoc.x+ labelDim.width +1), height)) {
                        g.setClip(clipRect);
                        border.paintBorder(c, g, grooveRect.x, grooveRect.y,
                                      grooveRect.width, grooveRect.height);
                    }

                    // paint strip below text
                    clipRect.setBounds(saveClip);
                    if (computeIntersection(clipRect,
                                            compLoc.x - 1, compLoc.y + ascent + descent,
                                            labelDim.width + 2,
                                            y + height - compLoc.y - ascent - descent)) {
                        g.setClip(clipRect);
                        border.paintBorder(c, g, grooveRect.x, grooveRect.y,
                                  grooveRect.width, grooveRect.height);
                    }

                    // restore clip
                    g.setClip(saveClip);

                } else {
                    border.paintBorder(c, g, grooveRect.x, grooveRect.y,
                                      grooveRect.width, grooveRect.height);
                }

                label.setLocation(compLoc);
                label.setSize(labelDim);
            }
        }

        /**
         * Reinitialize the insets parameter with this Border's current Insets.
         * @param c the component for which this border insets value applies
         * @param insets the object to be reinitialized
         */
        public Insets getBorderInsets(Component c, Insets insets) {
            int height = 16;

            Border border = getBorder();
            if (border != null) {
                if (border instanceof AbstractBorder) {
                    ((AbstractBorder)border).getBorderInsets(c, insets);
                } else {
                    // Can't reuse border insets because the Border interface
                    // can't be enhanced.
                    Insets i = border.getBorderInsets(c);
                    insets.top = i.top;
                    insets.right = i.right;
                    insets.bottom = i.bottom;
                    insets.left = i.left;
                }
            } else {
                insets.left = insets.top = insets.right = insets.bottom = 0;
            }

            insets.left += EDGE_SPACING + TEXT_SPACING;
            insets.right += EDGE_SPACING + TEXT_SPACING;
            insets.top += EDGE_SPACING + TEXT_SPACING;
            insets.bottom += EDGE_SPACING + TEXT_SPACING;

            if (c == null || label == null) {
                return insets;
            }

            insets.top += label.getHeight();

            return insets;
        }

        /**
         * Returns the label of the labeled border.
         */
        public JComponent getLabel() {
            return label;
        }


        /**
         * Sets the title of the titled border.
         * param title the title for the border
         */
        public void setLabel(JComponent label) {
            this.label = label;
        }



        /**
         * Returns the minimum dimensions this border requires
         * in order to fully display the border and title.
         * @param c the component where this border will be drawn
         */
        public Dimension getMinimumSize(Component c) {
            Insets insets = getBorderInsets(c);
            Dimension minSize = new Dimension(insets.right + insets.left,
                                              insets.top + insets.bottom);
            minSize.width += label.getWidth();

            return minSize;
        }


        private static boolean computeIntersection(Rectangle dest,
                                                   int rx, int ry, int rw, int rh) {
            int x1 = Math.max(rx, dest.x);
            int x2 = Math.min(rx + rw, dest.x + dest.width);
            int y1 = Math.max(ry, dest.y);
            int y2 = Math.min(ry + rh, dest.y + dest.height);
            dest.x = x1;
            dest.y = y1;
            dest.width = x2 - x1;
            dest.height = y2 - y1;

            if (dest.width <= 0 || dest.height <= 0) {
                return false;
            }
            return true;
        }
    }


    protected static class FocusBorder extends AbstractBorder implements FocusListener {
        private Component comp;
        private Color focusColor;
        private boolean focusLostTemporarily = false;

        public FocusBorder(Component comp) {
            this.comp = comp;

            comp.addFocusListener(this);

            // This is the best guess for a L&F specific color
            focusColor = UIManager.getColor("TabbedPane.focus"); // NOI18N
        }

        public void paintBorder(Component c, Graphics g, int x, int y, int width, int height) {
            if (comp.hasFocus() || focusLostTemporarily) {
                Color color = g.getColor();
                g.setColor(focusColor);
                BasicGraphicsUtils.drawDashedRect(g, x, y, width, height);
                g.setColor(color);
            }
        }

        public Insets getBorderInsets(Component c, Insets insets) {
            insets.set(2, 2, 2, 2);
            return insets;
        }


        public void focusGained(FocusEvent e) {
            comp.repaint();
        }

        public void focusLost(FocusEvent e) {
            // We will still paint focus even if lost temporarily
            focusLostTemporarily = e.isTemporary();
            if (!focusLostTemporarily) {
                comp.repaint();
            }
        }
    }
}
