/**
 *  Licensed under GPL. For more information, see
 *    http://jaxodraw.sourceforge.net/license.html
 *  or the LICENSE file in the jaxodraw distribution.
 */
package net.sf.jaxodraw.gui.panel;

import java.awt.Color;
import java.awt.Component;
import java.awt.Dimension;
import java.awt.Font;
import java.awt.FontMetrics;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.GridLayout;
import java.awt.Insets;
import java.awt.RenderingHints;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.ComponentAdapter;
import java.awt.event.ComponentEvent;

import javax.swing.BorderFactory;
import javax.swing.ButtonGroup;
import javax.swing.Icon;
import javax.swing.ImageIcon;
import javax.swing.JButton;
import javax.swing.JCheckBox;
import javax.swing.JColorChooser;
import javax.swing.JComponent;
import javax.swing.JDialog;
import javax.swing.JOptionPane;
import javax.swing.JToggleButton;
import javax.swing.border.AbstractBorder;
import javax.swing.border.Border;
import javax.swing.colorchooser.AbstractColorChooserPanel;
import javax.swing.colorchooser.DefaultColorSelectionModel;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;
import javax.swing.event.EventListenerList;

import net.sf.jaxodraw.gui.JaxoDialogs;
import net.sf.jaxodraw.util.JaxoPrefs;
import net.sf.jaxodraw.util.JaxoColor;
import net.sf.jaxodraw.util.JaxoDictionary;
import net.sf.jaxodraw.util.JaxoLocalized;
import net.sf.jaxodraw.util.Location;
import net.sf.jaxodraw.util.JaxoUtils;


/** Color chooser dialog, a wrapper around JColorChooser.
 * Besides allowing to choose an arbitrary color, there are also modes where
 * the colors presented are those available in the colrdvi LaTex package,
 * plus a set of gray scales.
 * @since 2.0
 */
public class JaxoColorChooser implements JaxoLocalized {
    private static JaxoDictionary language = new JaxoDictionary(JaxoColorChooser.class);
    private final EventListenerList listeners;
    private ChangeEvent event;
    private boolean adjusting;
    private final Component parent;
    private boolean permanent;
    private JColorChooser chooser;
    private final JaxoColorSelectionModel selection;
    private JDialog dialog;
    private int mode;
    private boolean cancelled;
    private String dialogTitle;
    private boolean optional;
    private JCheckBox noColorBox;

    /**
     * Constructor.
     * @param parentc Component to take as context for the dialog.
     */
    public JaxoColorChooser(final Component parentc) {
        this.parent = parentc;
        this.selection = new JaxoColorSelectionModel();
        this.listeners = new EventListenerList();
        this.mode = JaxoColor.JAXO_COLORS_MODE;
    }

    /** Dispose existing cached components. Also close the dialog if
     * it is current visible.
     * @see #isPermanent()
     */
    public void dispose() {
        if (dialog != null) {
            dialog.dispose();
            dialog = null;
        }

        chooser = null;
    }

    /**
     * ChangeEvents will be fired everytime the selected color
     * is changed when the color chooser is on the screen.
     * Use @link{#isAdjusting} to see whether the change is final.
     * @param l The listener to add.
     */
    public void addChangeListener(final ChangeListener l) {
        listeners.add(ChangeListener.class, l);
    }

    /**
     * Removes a change listener.
     * @param l The listener to remove.
     */
    public void removeChangeListener(final ChangeListener l) {
        listeners.remove(ChangeListener.class, l);
    }

    /**
     * Notifies all listeners of a state change.
     */
    protected void fireStateChanged() {
        final Object[] pairs = listeners.getListenerList();

        for (int i = pairs.length - 2; i >= 0; i -= 2) {
            if (pairs[i] == ChangeListener.class) {
                if (event == null) {
                    event = new ChangeEvent(this);
                }
                ((ChangeListener) pairs[i + 1]).stateChanged(event);
            }
        }
    }


    private void updateNoColor() {
        if (noColorBox != null) {
            noColorBox.setSelected(selection.isNoColor());
            noColorBox.setVisible(optional);
            noColorBox.revalidate();
        }
    }


    /** Set color to be displayed.
        @param value color to set.
    */
    public void setColor(final Color value) {
        selection.setSelectedColor(value);
    }

    /**
     * The currently selected color (or null) - updated while
     * the chooser is shown.
     * @return The selected color.
     */
    public Color getColor() {
        return selection.getColor();
    }


    /**
     * Current optional property.
     * @return The current optional value.
     */
    public final boolean isOptional() {
        return optional;
    }

    /**
     * Sets the optional property. When set, the user may also
     * choose 'no color'.
     * @param value The value to set.
     */
    public void setOptional(final boolean value) {
        optional = value;
        updateNoColor();
    }


    /**
     * Sets the optional and color properties. 'Optional' is set
     * to true iff the color is 'null'.
     * @param value The color to  set.
     */
    public void setOptionalColor(final Color value) {
        setOptional(value == null);
        setColor(value);
    }




    /** Determines if the color change is temporary. At the end of a
     *   color choosing process, this will become true.
     * @return True if still adjusting.
     */
    public final boolean isAdjusting() {
        return adjusting;
    }

    /** Determines if the chooser was cancelled the last time it was shown.
     * @return True if the chooser was cancelled.
     */
    public boolean isCancelled() {
        return cancelled;
    }

    /** Determines if the color was changed the last time it was shown.
     * @return True if the color was changed.
     */
    public boolean hasChanged() {
        return !isCancelled() && selection.hasChanged();
    }

    /**
     * The colors that can be chosen. This is any of the mode
     * constants, the default is
     * {@link net.sf.jaxodraw.util.JaxoColor#JAXO_COLORS_MODE}.
     *
     * @return The current mode.
     */
    public final int getMode() {
        return mode;
    }

    /** Sets the current mode.
     * @param value The mode to set.
     */
    public void setMode(final int value) {
        if (mode != value) {
            final int old = mode;

            mode = value;

            if ((old == JaxoColor.ALL_COLORS_MODE) || (mode == JaxoColor.ALL_COLORS_MODE)) {
                // could do better
                dispose();
            } else {
                if (chooser != null) {
                    ((JaxoColorPanel) chooser.getChooserPanels()[0])
                    .setGrayScalesAllowed(mode == JaxoColor.JAXO_COLORS_MODE);
                }
            }
        }
    }

    /** {@inheritDoc} */
    public void updateLanguage() {
        // It is easiest to recreate the cached components
        // (JColorChooser also has problems)
        dispose();
    }

    /** Whether the chooser stays alive after showing for further requests.
     * If this is true, the chooser must be disposed if not needed anymore.
     * (This will happen automatically if the owner window (via 'parent')
     * is disposed). The default is false.
     * @return True if the window is permanent.
     * @see #dispose()
     */
    public final boolean isPermanent() {
        return permanent;
    }

    /**
     * Whether the chooser stays alive after showing for further requests.
     * @param value True if the chooser is permanent.
     */
    public void setPermanent(final boolean value) {
        if (value != permanent) {
            permanent = value;

            if (!permanent && (dialog != null) && !dialog.isVisible()) {
                dispose();
            }
        }
    }

    /**
     * Dialog title - if null (default), a default title will be chosen.
     * @return The dialog title.
     */
    public final String getDialogTitle() {
        return dialogTitle;
    }

    /**
     * Sets the dialog title.
     * @param value The dialog title to set.
     */
    public void setDialogTitle(final String value) {
        dialogTitle = value;
    }

    /**
     * Sets up all components of this color chooser.
     */
    protected void createComponents() {
        if (chooser != null) {
            return;
        }

        noColorBox = new JCheckBox(language.value("noColor"));
        noColorBox.setAlignmentX(0.5f);
        noColorBox.addActionListener(new ActionListener() {
                public void actionPerformed(final ActionEvent e) {
                    selection.setNoColor(noColorBox.isSelected());
                }
            });

        chooser =
            new JColorChooser(selection) {
                    private static final long serialVersionUID = 7526471155622L;
                    // Fix JColorChooser not to throw if the panel is not contained
                    // Otherwise changing the LookAndFeel breaks
                    // (really a BasicColorChooserUI bug).
                    @Override
                    public AbstractColorChooserPanel removeChooserPanel(
                        final AbstractColorChooserPanel p) {
                        final AbstractColorChooserPanel[] old = getChooserPanels();

                        for (int i = 0; i < old.length; i++) {
                            if (old[i] == p) {
                                super.removeChooserPanel(p);
                                break;
                            }
                        }

                        return p;

                    }

                    // Always take only the JaxoColorPanel
                    @Override
                    public void updateUI() {
                        super.updateUI();

                        if (mode != JaxoColor.ALL_COLORS_MODE) {
                            setChooserPanels(new AbstractColorChooserPanel[]{
                                    new JaxoColorPanel()
                                });
                        }
                    }
                };

        if (mode != JaxoColor.ALL_COLORS_MODE) {
            ((JaxoColorPanel) chooser.getChooserPanels()[0])
            .setGrayScalesAllowed(mode == JaxoColor.JAXO_COLORS_MODE);

            //JaxoColorPreviewPanel Jcpp = new JaxoColorPreviewPanel(chooser);
            chooser.setPreviewPanel(new JaxoColorPreviewPanel(chooser));
        }

        final JButton ok = new JButton(language.value("/Accept"));

        ok.addActionListener(new ActionListener() {
                public final void actionPerformed(final ActionEvent evt) {
                    cancelled = false;
                    dialog.setVisible(false);
                }
            });

        final JButton reset = new JButton(language.value("/Reset"));

        reset.addActionListener(new ActionListener() {
                public void actionPerformed(final ActionEvent e) {
                    selection.reset();
                }
            });

        final Object[] optionButtons = {
            ok,
            reset,
            language.value("/Cancel")
        };

        final Object[] message = {
            chooser,
            noColorBox
        };

        final JOptionPane p =
            new JOptionPane(message, JOptionPane.PLAIN_MESSAGE,
                            JOptionPane.OK_CANCEL_OPTION, null, optionButtons,
                            optionButtons[0]);

        dialog = p.createDialog(parent, ""); // title will be set later
        dialog.addComponentListener(new ComponentAdapter() {
                @Override
                public void componentHidden(final ComponentEvent e) {
                    if (!permanent) {
                        dispose();
                    }
                }
            });

        dialog.setDefaultCloseOperation(JDialog.HIDE_ON_CLOSE);
    }




    /**
     * See the variant that takes a Location argument, which here
     * is taken to be JaxoUtils.RelativeTo('parent').
     */
    public void show() {
        show(new Location.RelativeTo(parent));
    }

    /**
     * Brings up a color chooser menu to select the color of the
     * specified JaxoObject.
     * @param l The location of the dialog.
     */
    public void show(final Location l) {
        createComponents();
        selection.commit();
        updateNoColor();
        adjusting = true;

        cancelled = true;
        dialog.setTitle(JaxoDialogs.translatedWindowTitle((
                    dialogTitle == null
                ) ? language.value("defaultTitle")
                  : dialogTitle));
        dialog.pack();
        l.setLocation(dialog);
        fireStateChanged();
        dialog.setVisible(true);

        if (cancelled) {
            selection.reset();
        }

        adjusting = false;
        fireStateChanged();
    }

    private class JaxoColorSelectionModel
        extends DefaultColorSelectionModel {
        private static final long serialVersionUID = 7526471155622776147L;

        private Color committedColor;
        private boolean noColor;

        JaxoColorSelectionModel() {
            super(Color.black);
            committedColor = getColor();
            noColor = false;
        }


        public void reset() {
            setSelectedColor(committedColor);
        }

        public void commit() {
            committedColor = getColor();
        }

        public boolean isEqualColor(final Color c) {
            return c == null ? noColor : c.equals(getSelectedColor());
        }

        @Override
        public void setSelectedColor(final Color c) {
            if ((dialog == null || !dialog.isVisible()) && c != null
                && c.equals(getSelectedColor())) {
                return;
            }
            final boolean oldNoColor = noColor;

            noColor = c == null;

            super.setSelectedColor(noColor ? Color.black : c);

            if (noColor != oldNoColor) {
                fireStateChanged();
                updateNoColor();
            }
        }

        public boolean hasChanged() {
            return noColor ? committedColor != null
                : !getSelectedColor().equals(committedColor);
        }

        public void setNoColor(final boolean value) {
            setSelectedColor(value ? null : Color.black);
        }

        public boolean isNoColor() {
            return noColor;
        }

        public final Color getColor() {
            return noColor ? null : getSelectedColor();
        }

        @Override
        protected void fireStateChanged() {
            super.fireStateChanged();
            JaxoColorChooser.this.fireStateChanged();
        }
    }

    private static class JaxoColorPanel extends AbstractColorChooserPanel {
        private static final long serialVersionUID = 7526471155622776147L;
        private static final Dimension ICON_SIZE = new Dimension(25, 25);
        private boolean grayScalesAllowed;
        private JToggleButton[] jaxButton;
        private JToggleButton extra;

        JaxoColorPanel() {
            super();
            grayScalesAllowed = true;
            setLayout(new GridLayout(7, 12));
            setBorder(BorderFactory.createEmptyBorder(4, 4, 4, 4));
        }

        /** line colors of fill objects cannot be grayscales. The default is true. */
        public final boolean areGrayScalesAllowed() {
            return grayScalesAllowed;
        }

        public void setGrayScalesAllowed(final boolean value) {
            if (value != grayScalesAllowed) {
                grayScalesAllowed = value;
                updateGrayScales();
            }
        }

        public final void updateChooser() {
            final Color color = getColorFromModel();

            for (int i = 0; i < jaxButton.length; i++) {
                if (color.equals(JaxoColor.getColor(i))) {
                    jaxButton[i].setSelected(true);
                    return;
                }
            }

            extra.setSelected(true);

        }

        protected void updateGrayScales() {
            int i = 0;

            // GridLayout does ignore whether the components are
            // visible, so have to remove/add.
            do {
                if (JaxoColor.isGrayScale(JaxoColor.getColor(i))) {
                    if (grayScalesAllowed) {
                        add(jaxButton[i], i);
                    } else {
                        remove(jaxButton[i]);
                    }
                }
                i++;
            } while (i < JaxoColor.getColorCount());

            revalidate();
            repaint();
        }

        protected void buildChooser() {
            jaxButton = new JToggleButton[JaxoColor.getColorCount()];

            final JaxoColorListener jcl = new JaxoColorListener();
            final ButtonGroup boxOfJaxoColors = new ButtonGroup();

            final Border border =
                new AbstractBorder() {
                    private static final long serialVersionUID = 7526471155622L;
                    private Border line =
                        BorderFactory.createLineBorder(Color.black, 4);

                    @Override
                    public boolean isBorderOpaque() {
                        return false;
                    }

                    @Override
                    public Insets getBorderInsets(final Component c) {
                        return line.getBorderInsets(c);
                    }

                    @Override
                    public Insets getBorderInsets(final Component c, final Insets insets) {
                        return ((AbstractBorder) line).getBorderInsets(c,
                            insets);
                    }

                    @Override
                    public void paintBorder(final Component c, final Graphics g, final int x,
                        final int y, final int width, final int height) {
                        if (c instanceof JToggleButton
                                && ((JToggleButton) c).isSelected()) {
                            line.paintBorder(c, g, x, y, width, height);
                        }
                    }
                };

            for (int i = 0; i < JaxoColor.getColorCount(); i++) {
                final ImageIcon jaxBox =
                    JaxoUtils.getChooserImageIcon(JaxoColor.getColor(i),
                        ICON_SIZE);
                final String colorName =
                    language.value("/" + JaxoColor.getColorName(
                            JaxoColor.getColor(i)));
                jaxButton[i] = new JToggleButton(jaxBox);
                jaxButton[i].putClientProperty("Jaxo.color",
                    JaxoColor.getColor(i));
                jaxButton[i].addActionListener(jcl);
                jaxButton[i].setBorder(border);
                jaxButton[i].setContentAreaFilled(false);
                jaxButton[i].setToolTipText(colorName);
                boxOfJaxoColors.add(jaxButton[i]);
                add(jaxButton[i]);
            }

            // extra button is used for a Color that is not in the list
            extra = new JToggleButton();
            boxOfJaxoColors.add(extra);
            updateGrayScales();
        }

        public final String getDisplayName() {
            return language.value("chooserPanelTitle");
        }

        // these two are needed for AbstractColorChooserPanel
        public final Icon getSmallDisplayIcon() {
            return null;
        }

        public final Icon getLargeDisplayIcon() {
            return null;
        }

        public boolean isExtraSelected() {
            updateChooser();
            return extra.isSelected();
        }

        class JaxoColorListener implements ActionListener {
            public final void actionPerformed(final ActionEvent e) {
                final JToggleButton source = (JToggleButton) e.getSource();
                getColorSelectionModel().setSelectedColor((Color) source
                    .getClientProperty("Jaxo.color"));
            }
        }
    }


    private static class JaxoColorPreviewPanel extends JComponent {
        private static final long serialVersionUID = 7526471155622776147L;
        private final JColorChooser chooser;
        private int cachedWidth = -1;

        JaxoColorPreviewPanel(final JColorChooser colorChooser) {
            super();
            this.chooser = colorChooser;

            setFont(new Font("SansSerif", Font.BOLD, 24));

            // Bug workaround for several BasicColorChooserUI bugs
            // 1. must have size != (0, 0) on install
            // 2. must have Insets != (0, 0, 0, 0) on install
            setBorder(BorderFactory.createEmptyBorder(1, 1, 1, 1));
            setSize(getPreferredSize());
        }

        @Override
        public void removeNotify() {
            cachedWidth = -1;
            super.removeNotify();
        }

        @Override
        public Dimension getMinimumSize() {
            return isMinimumSizeSet() ? super.getMinimumSize()
                                      : getPreferredSize();
        }

        @Override
        public final Dimension getPreferredSize() {
            if (isPreferredSizeSet()) {
                return super.getPreferredSize();
            }

            final Insets n = getInsets();
            final FontMetrics m = getFontMetrics(getFont());

            int width = cachedWidth;
            if (width == -1) {
                width = 0;
                for (int i = 0; i < JaxoColor.getColorCount(); i++) {
                    width =
                        Math.max(width,
                            m.stringWidth(language.message(
                                    "previewText1%0",
                                    language.value(
                                        "/" + JaxoColor.getColorName(i)))));
                }
                cachedWidth = width;
            }

            // Somewhat heuristical multipliers.
            // It is unfortunate that BasicColorChooserUI lays the preview panel
            // out at its preferred size, even if there would be more space
            // available.
            // That means the preferred width must be large enough to display
            // all color names, or they may be cut off, even though there is
            // space.
            return new Dimension(n.left + n.right + ((width * 5) / 4),
                n.top + n.bottom + ((m.getHeight() * 3) / 2));
        }

        @Override
        protected final void paintComponent(final Graphics g) {
            final Graphics2D g2 = (Graphics2D) g;
            String name = "";
            final Color clr = chooser.getColor();

            g2.setFont(getFont());
            g2.setColor(clr);

            if (JaxoPrefs.getBooleanPref(JaxoPrefs.PREF_ANTIALIAS)) {
                g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
                    RenderingHints.VALUE_ANTIALIAS_ON);
            } else {
                g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
                    RenderingHints.VALUE_ANTIALIAS_OFF);
            }

            final FontMetrics m = g2.getFontMetrics();
            final Insets n = getInsets();
            final int y = (getHeight() - n.top - n.bottom - m.getHeight()) / 2;

            if (JaxoColor.isDefinedColor(clr)) {
                name =
                    language.message("previewText%0",
                        language.value("/" + JaxoColor.getColorName(clr)));
                g2.drawString(name, n.left + y, n.top + y + m.getAscent());
            } else {
                name = language.value("previewText");
                g2.drawString(name, n.left + y, n.top + y + m.getAscent());
                if (((JaxoColorPanel) chooser.getChooserPanels()[0])
                        .areGrayScalesAllowed()) {
                    final Color col =
                        JaxoColor.getClosestColorTo(chooser.getColor(),
                            JaxoColor.JAXO_COLORS_MODE);
                    g2.setColor(col);
                    g2.drawString(language.value(
                        "/" + JaxoColor.getColorName(col)),
                        n.left + y + m.stringWidth(name),
                        n.top + y + m.getAscent());
                } else {
                    final Color col =
                        JaxoColor.getClosestColorTo(chooser.getColor(),
                            JaxoColor.JAXO_COLORS_NO_GRAYSCALES_MODE);
                    g2.setColor(col);
                    g2.drawString(language.value("/" + JaxoColor.getColorName(col)),
                        n.left + y + m.stringWidth(name),
                        n.top + y + m.getAscent());
                }
            }
        }
    }
}
