package com.srbenoit.color;

import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Component;
import java.awt.Dimension;
import java.awt.FlowLayout;
import java.awt.Insets;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import javax.swing.BorderFactory;
import javax.swing.JButton;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.Popup;
import javax.swing.PopupFactory;
import com.srbenoit.ui.OffscreenImagePanel;

/**
 * A chooser window that presents a palate of colors for the user to select from. The dialog has
 * the option of displaying color information, and may or may not show color names. The size of the
 * color boxes can also be controlled.
 */
public class ColorNameChooser extends MouseAdapter implements ActionListener {

    /** the listener to notify when the user has made a selection */
    private final transient ColorNameChoiceListener listener;

    /** object on which to wait for popup to close */
    private final transient Object waiter;

    /** the width of color boxes */
    private final transient int boxWidth;

    /** the height of color boxes */
    private final transient int boxHeight;

    /** the vertical spacing between color boxes */
    private final transient int boxSpacing;

    /** <code>true</code> to include color names */
    private final transient boolean incNames;

    /** <code>true</code> to include color hex values */
    private final transient boolean incHex;

    /** <code>true</code> to include borders around boxes */
    private final transient boolean incBorders;

    /** the index of the color that is being clicked on */
    private transient int clicked = -1;

    /** the index of the color that has been selected */
    private transient int selected = -1;

    /** an offscreen image with the color boxes & names */
    private transient SwatchImage img = null;

    /** the GUI panel */
    private transient JPanel panel = null;

    /** the popup that is displaying the color chooser */
    private transient Popup popup = null;

    /**
     * Constructs a new <code>ColorNameChooser</code> with default settings. The defaults are to
     * have boxes sized at 12x12, with borders and names included, and box spacing set to 2.
     *
     * @param  theListener  the listener that is to be notified when the color choice has been made
     */
    public ColorNameChooser(final ColorNameChoiceListener theListener) {

        this(theListener, 12, 12, 2, true, true, true);
    }

    /**
     * Construct a new <code>ColorNameChooser</code> with particular settings.
     *
     * @param  theListener     the listener that is to be notified when the color choice has been
     *                         made
     * @param  swatchWidth     the width of boxes
     * @param  swatchHeight    the height of boxes
     * @param  swatchSpacing   the number of pixels between boxes
     * @param  includeNames    <code>true</code> to include names, <code>false</code> otherwise
     * @param  includeHex      <code>true</code> to include hex values, <code>false</code>
     *                         otherwise
     * @param  includeBorders  <code>true</code> to include black borders around each box; <code>
     *                         false</code> otherwise
     */
    public ColorNameChooser(final ColorNameChoiceListener theListener, final int swatchWidth,
        final int swatchHeight, final int swatchSpacing, final boolean includeNames,
        final boolean includeHex, final boolean includeBorders) {

        super();

        this.listener = theListener;
        this.boxWidth = swatchWidth;
        this.boxHeight = swatchHeight;
        this.boxSpacing = swatchSpacing;
        this.incNames = includeNames;
        this.incHex = includeHex;
        this.incBorders = includeBorders;

        this.waiter = new Object();
    }

    /**
     * Displays the popup.
     *
     * @param  owner  the owning <code>Component</code>
     * @param  xPos   the X location at which to show the popup
     * @param  yPos   the Y location at which to show the popup
     */
    public void show(final Component owner, final int xPos, final int yPos) {

        if (this.popup == null) {
            this.popup = PopupFactory.getSharedInstance().getPopup(owner, buildUI(), xPos, yPos);
        }

        this.popup.show();
    }

    /**
     * Hides the popup.
     */
    private void hide() {

        this.popup.hide();
        this.popup = null;

        synchronized (this.waiter) {
            this.waiter.notifyAll();
        }
    }

    /**
     * Waits for the user to make a selection and choose "OK", or to choose "Cancel".
     *
     * @return  the index of the selected color, or -1 if canceled
     */
    public int waitForSelection() {

        while (this.popup != null) {

            synchronized (this.waiter) {

                try {
                    this.waiter.wait();
                } catch (InterruptedException e) { }
            }
        }

        return this.selected;
    }

    /**
     * Builds the chooser UI.
     *
     * @return  the constructed panel
     */
    private JPanel buildUI() {

        this.img = new SwatchImage(this.boxWidth, this.boxHeight, this.boxSpacing, this.incNames,
                this.incHex, this.incBorders);

        this.panel = new JPanel(new BorderLayout());
        this.panel.setBorder(BorderFactory.createCompoundBorder(
                BorderFactory.createLineBorder(Color.black, 1),
                BorderFactory.createRaisedBevelBorder()));

        buildScroll();
        buildButtonBar();

        return this.panel;
    }

    /**
     * Constructs the scroll pane that contains the offscreen image with the swatches.
     */
    private void buildScroll() {

        OffscreenImagePanel offscreen;
        JScrollPane scroll;
        Dimension pref;

        offscreen = new OffscreenImagePanel(this.img.getImage());
        scroll = new JScrollPane(offscreen);
        offscreen.addMouseListener(this);

        if (this.img.getHeight() > 400) {
            pref = new Dimension(this.img.getWidth() + 24, 300);
        } else {
            pref = new Dimension(this.img.getWidth() + 4, this.img.getHeight() + 4);
        }

        scroll.setPreferredSize(pref);
        scroll.getVerticalScrollBar().setUnitIncrement(36);
        this.panel.add(scroll, BorderLayout.CENTER);
    }

    /**
     * Constructs the button bar and adds it to the panel.
     */
    private void buildButtonBar() {

        JPanel buttonBar;
        JButton button;

        buttonBar = new JPanel(new FlowLayout(FlowLayout.CENTER, 10, 0));
        button = new JButton("Ok");
        button.setMargin(new Insets(0, 3, 0, 5));
        button.addActionListener(this);
        buttonBar.add(button);
        button = new JButton("Cancel");
        button.setMargin(new Insets(0, 3, 0, 5));
        button.addActionListener(this);
        buttonBar.add(button);
        this.panel.add(buttonBar, BorderLayout.SOUTH);
    }

    /**
     * Gets the selected color index, which is an index into the <code>ColorNames.COLORNAMES</code>
     * array of color names.
     *
     * @return  the selected color index, or -1 if nothing selected
     */
    public int getSelected() {

        return this.selected;
    }

    /**
     * Sets the selected color by index.
     *
     * @param  index  the index of the selected color, which is an index into the <code>
     *                ColorNames.CNAMES</code> array of color names, or -1 to indicate no selection
     */
    public void setSelected(final int index) {

        this.selected = index;

        if (this.img != null) {
            this.img.setSelected(index);
            this.panel.repaint();
        }
    }

    /**
     * Called when the mouse is pressed. Tests whether the click occurs on a color, and if so,
     * records the color under the mouse cursor. If a subsequent release is on the same color, that
     * color will be assumed to have been "clicked".
     *
     * @param  evt  the mouse event
     */
    @Override public void mousePressed(final MouseEvent evt) {

        this.clicked = this.img.getHitIndex(evt.getX(), evt.getY());
    }

    /**
     * Called when the mouse is released. Tests whether the click occurs on a color, and if so,
     * checks whether that color was under the mouse when it was last pressed. If so, that color
     * will be assumed to have been "clicked".
     *
     * @param  evt  the mouse event
     */
    @Override public void mouseReleased(final MouseEvent evt) {

        if ((this.clicked != -1)
                && (this.clicked == this.img.getHitIndex(evt.getX(), evt.getY()))) {
            setSelected(this.clicked);
        }

        this.clicked = -1;
    }

    /**
     * Handler for action events generated when the user clicks the OK or Cancel buttons.
     *
     * @param  evt  the action event
     */
    public void actionPerformed(final ActionEvent evt) {

        String cmd;

        cmd = evt.getActionCommand();

        if ("Cancel".equals(cmd)) {
            this.selected = -1;

            if (this.listener != null) {
                this.listener.colorNameChosen(null);
            }
        } else if (this.selected == -1) {

            if (this.listener != null) {
                this.listener.colorNameChosen(null);
            }
        } else {

            if (this.listener != null) {
                this.listener.colorNameChosen(ColorNames.getInstance().getColorName(
                        this.selected));
            }
        }

        hide();
    }

    /**
     * Main method to bring up an instance of the dialog.
     *
     * @param  args  command-line arguments
     */
    public static void main(final String... args) {

        new ColorNameChooser(null).show(null, 100, 100);
    }
}
