package com.explodingpixels.widgets;

import java.awt.Graphics;
import java.awt.Insets;
import java.awt.Rectangle;
import java.awt.image.BufferedImage;

import javax.swing.BorderFactory;
import javax.swing.Icon;
import javax.swing.ImageIcon;
import javax.swing.JButton;
import javax.swing.JComponent;
import javax.swing.plaf.basic.BasicButtonUI;

/**
 * A button backed by an image. Additionally, a click mask can be provided. Any fully
 * non-transparent pixels in the mask will not be clickable.
 */
public class ImageButton extends JButton {

    // create a static index for the alpha channel of a raster image. i'm not exactly sure where
    // it's specified that red = channel 0, green = channel 1, blue = channel 2, and
    // alpha = channel 3, but this have been the values i've observed.
    private static final int ALPHA_BAND = 3;

    // a buffered image representing the mask for this button.
    private final BufferedImage fMask;

    private Icon fInactiveIcon;

    /**
     * Creates an image based button.
     *
     * @param icon the icon to use for the button.
     */
    public ImageButton(Icon icon) {
        this(icon, icon);
    }

    /**
     * Creates an image based button with the given click mask.
     *
     * @param icon the icon to use for the button.
     * @param mask the click mask to use for the button.
     * @throws IllegalArgumentException if the given icon is null, the given mask is null or
     *                                  the given mask's bounds do not match the given icons bounds.
     */
    public ImageButton(Icon icon, Icon mask) {
        super(icon);

        if (icon == null) {
            throw new IllegalArgumentException("The icon cannot be null.");
        }

        if (mask == null) {
            throw new IllegalArgumentException("The mask cannot be null.");
        }

        checkIconMatchesMaskBounds(icon, mask);

        // remove the margins from this button, request that the content area not be filled, and
        // indicate that the border not be painted.
        setMargin(new Insets(0, 0, 0, 0));
        setBorder(BorderFactory.createEmptyBorder());
        setContentAreaFilled(false);

        // create the mask from the supplied icon.
        fMask = createMask(mask);

        // repaint this button when the parent window's focus state changes so
        // that we can correctly show the active or inactive icon.
        WindowUtils.installJComponentRepainterOnWindowFocusChanged(this);
    }

    private BufferedImage createMask(Icon mask) {
        // create a BufferedImage to paint the mask into so that we can later retrieve pixel data
        // out of the image.
        BufferedImage image = new BufferedImage(
                mask.getIconWidth(), mask.getIconHeight(), BufferedImage.TYPE_INT_ARGB);

        Graphics graphics = image.getGraphics();
        mask.paintIcon(null, graphics, 0, 0);
        graphics.dispose();

        return image;
    }

    public Icon getIcon() {
        return WindowUtils.isParentWindowFocused(this) || fInactiveIcon == null
                ? super.getIcon() : fInactiveIcon;
    }

    @Override
    public void setIcon(Icon defaultIcon) {
        super.setIcon(defaultIcon);
        // if this class has already been initialized, ensure that the new icon matches the bounds
        // of the current mask.
        if (fMask != null) {
            checkIconMatchesMaskBounds(defaultIcon, new ImageIcon(fMask));
        }
    }

    public void setInactiveIcon(Icon inactiveIcon) {
        checkIconMatchesMaskBounds(inactiveIcon, new ImageIcon(fMask));
        fInactiveIcon = inactiveIcon;
    }

    @Override
    public void updateUI() {
        // install the custom ui delegate to track the icon rectangle and answer the contains
        // method.
        setUI(new CustomButtonUI());
    }

    private static void checkIconMatchesMaskBounds(Icon icon, Icon mask) {
        if (mask.getIconWidth() != icon.getIconWidth()
                || mask.getIconHeight() != icon.getIconHeight()) {
            throw new IllegalArgumentException("The mask must be the same size as the icon.");
        }
    }

    // CustomButtonUI implementation so that we can maintain the icon rectangle. //////////////////

    private class CustomButtonUI extends BasicButtonUI {

        private Rectangle fIconRect;

        private boolean maskContains(int x, int y) {
            return fIconRect != null && fIconRect.contains(x, y)
                    && fMask.getRaster().getSample(x - fIconRect.x, y - fIconRect.y, ALPHA_BAND) > 0;
        }

        @Override
        public boolean contains(JComponent c, int x, int y) {
            return maskContains(x, y);
        }

        @Override
        protected void paintIcon(Graphics g, JComponent c, Rectangle iconRect) {
            super.paintIcon(g, c, iconRect);
            // capture where the icon is being painted within the bounds of this button so we can
            // later use this information in the contains calculation.
            if (fIconRect == null || !fIconRect.equals(iconRect)) {
                // create a copy of the icon rectangle, as the given iconRect is a static variable
                // in BasicButtonUI that will be updated for each button painted.
                fIconRect = new Rectangle(iconRect);
            }
        }
    }

}