/*
 * 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.common.listener.weak.WeakListener;
import icy.image.ImageUtil;
import icy.preferences.GeneralPreferences;
import icy.system.IcyExceptionHandler;
import icy.system.SystemUtil;
import icy.system.thread.ThreadUtil;
import icy.util.ClassUtil;
import icy.util.ReflectionUtil;
import icy.util.StringUtil;
import ij.util.Java2;

import java.awt.Color;
import java.awt.Component;
import java.awt.Image;
import java.awt.Window;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.util.ArrayList;
import java.util.Map;

import javax.swing.JDialog;
import javax.swing.JFrame;
import javax.swing.JInternalFrame;
import javax.swing.PopupFactory;
import javax.swing.UIManager;
import javax.swing.UIManager.LookAndFeelInfo;
import javax.swing.plaf.ColorUIResource;
import javax.swing.plaf.InternalFrameUI;

import org.pushingpixels.flamingo.api.common.CommandToggleButtonGroup;
import org.pushingpixels.flamingo.api.common.JCommandToggleMenuButton;
import org.pushingpixels.flamingo.api.common.popup.JCommandPopupMenu;
import org.pushingpixels.substance.api.ColorSchemeAssociationKind;
import org.pushingpixels.substance.api.ComponentState;
import org.pushingpixels.substance.api.DecorationAreaType;
import org.pushingpixels.substance.api.SubstanceColorScheme;
import org.pushingpixels.substance.api.SubstanceLookAndFeel;
import org.pushingpixels.substance.api.SubstanceSkin;
import org.pushingpixels.substance.api.colorscheme.LightAquaColorScheme;
import org.pushingpixels.substance.api.fonts.FontPolicy;
import org.pushingpixels.substance.api.fonts.FontSet;
import org.pushingpixels.substance.api.fonts.SubstanceFontUtilities;
import org.pushingpixels.substance.api.skin.SkinChangeListener;
import org.pushingpixels.substance.api.skin.SkinInfo;
import org.pushingpixels.substance.internal.ui.SubstanceInternalFrameUI;
import org.pushingpixels.substance.internal.utils.SubstanceColorSchemeUtilities;
import org.pushingpixels.substance.internal.utils.SubstanceInternalFrameTitlePane;
import org.pushingpixels.substance.internal.utils.SubstanceTitlePane;

/**
 * @author Stephane
 */
public class LookAndFeelUtil
{
    /**
     * Weak listener wrapper for SkinChangeListener.
     * 
     * @author Stephane
     */
    public static class WeakSkinChangeListener extends WeakListener<SkinChangeListener> implements SkinChangeListener
    {
        public WeakSkinChangeListener(SkinChangeListener listener)
        {
            super(listener);
        }

        @Override
        public void removeListener(Object source)
        {
            removeSkinChangeListener(this);
        }

        @Override
        public void skinChanged()
        {
            final SkinChangeListener listener = getListener();

            if (listener != null)
                listener.skinChanged();
        }
    }

    static int defaultFontSize;
    static Map<String, SkinInfo> map;
    private static ArrayList<SkinInfo> skins;

    static int currentFontSize;

    public static void init()
    {
        try
        {
            // so ImageJ won't change look and feel later
            ReflectionUtil.getField(Java2.class, "lookAndFeelSet", true).set(null, Boolean.valueOf(true));
        }
        catch (Exception e)
        {
            // do it in another way (slower)
            Java2.setSystemLookAndFeel();
        }

        // enable or not EDT checking in substance
        SystemUtil.setProperty("insubstantial.checkEDT", "false");
        SystemUtil.setProperty("insubstantial.logEDT", "true");

        // funny features of substance

        // AnimationConfigurationManager.getInstance().allowAnimations(AnimationFacet.FOCUS_LOOP_ANIMATION);
        // AnimationConfigurationManager.getInstance().allowAnimations(AnimationFacet.GHOSTING_BUTTON_PRESS);
        // AnimationConfigurationManager.getInstance().allowAnimations(AnimationFacet.GHOSTING_ICON_ROLLOVER);
        // AnimationConfigurationManager.getInstance().allowAnimations(AnimationFacet.ICON_GLOW);

        // UIManager.put(SubstanceLookAndFeel.USE_THEMED_DEFAULT_ICONS, Boolean.TRUE);
        // UIManager.put(SubstanceLookAndFeel.SHOW_EXTRA_WIDGETS, Boolean.TRUE);
        // UIManager.put(SubstanceLookAndFeel.WATERMARK_VISIBLE, Boolean.TRUE);

        // SubstanceLookAndFeel.setWidgetVisible(null, true, SubstanceWidgetType.MENU_SEARCH);
        // SubstanceLookAndFeel.setWidgetVisible(null, false,
        // SubstanceWidgetType.TITLE_PANE_HEAP_STATUS);

        // enabled LAF decoration instead of native ones
        JFrame.setDefaultLookAndFeelDecorated(true);
        JDialog.setDefaultLookAndFeelDecorated(true);

        map = SubstanceLookAndFeel.getAllSkins();
        skins = new ArrayList<SkinInfo>(map.values());

        final LookAndFeelInfo[] lafInfos = new LookAndFeelInfo[skins.size()];

        // install Substance look and feel
        for (int i = 0; i < skins.size(); i++)
        {
            final SkinInfo skin = skins.get(i);
            final String className = skin.getClassName();
            final String simpleName = ClassUtil.getSimpleClassName(className);
            final int len = simpleName.length();
            // remove the "skin" suffix from simple name
            final String lafClassName = ClassUtil.getPackageName(className) + ".Substance"
                    + simpleName.substring(0, len - 4) + "LookAndFeel";

            lafInfos[i] = new LookAndFeelInfo(skin.getDisplayName(), lafClassName);
        }

        // replace system LAF by substance LAF
        UIManager.setInstalledLookAndFeels(lafInfos);
        // set default skin
        setSkin(GeneralPreferences.getGuiSkin());

        // get default font size
        final FontPolicy fontPolicy = SubstanceFontUtilities.getDefaultFontPolicy();
        final FontSet fontSet = fontPolicy.getFontSet("Substance", null);

        defaultFontSize = fontSet.getMessageFont().getSize();
        currentFontSize = defaultFontSize;

        // set default font size
        setFontSize(GeneralPreferences.getGuiFontSize());

        // Define custom PopupFactory :
        // That fix Ribbon repaint bugs on Medium Weight components for OSX
        PopupFactory.setSharedInstance(new CustomPopupFactory());
    }

    /**
     * Add skin change listener (automatically add weak listener)
     */
    public static void addSkinChangeListener(SkinChangeListener listener)
    {
        SubstanceLookAndFeel.registerSkinChangeListener(listener);
    }

    /**
     * Remove skin change listener
     */
    public static void removeSkinChangeListener(SkinChangeListener listener)
    {
        SubstanceLookAndFeel.unregisterSkinChangeListener(listener);
    }

    /**
     * get current Substance skin
     */
    public static SubstanceSkin getCurrentSkin()
    {
        return SubstanceLookAndFeel.getCurrentSkin();
    }

    /**
     * get current Substance skin display name
     */
    public static String getCurrentSkinName()
    {
        final SubstanceSkin skin = getCurrentSkin();

        if (skin != null)
            return skin.getDisplayName();

        return null;
    }

    // /**
    // * get current Look And Feel
    // */
    // public static LookAndFeel getLookAndFeel()
    // {
    // return UIManager.getLookAndFeel();
    // }
    //
    // /**
    // * get current Look And Feel name
    // */
    // public static String getLookAndFeelName()
    // {
    // final LookAndFeel laf = getLookAndFeel();
    //
    // if (laf instanceof SubstanceLookAndFeel)
    // return SubstanceLookAndFeel.getCurrentSkin().getDisplayName();
    //
    // return UIManager.getLookAndFeel().getName();
    // }

    public static JCommandPopupMenu getLookAndFeelMenu()
    {
        final JCommandPopupMenu result = new JCommandPopupMenu();

        final CommandToggleButtonGroup buttonGroup = new CommandToggleButtonGroup();
        final String currentSkinName = getCurrentSkinName();

        for (SkinInfo skin : skins)
        {
            final String skinName = skin.getDisplayName();

            final JCommandToggleMenuButton button = new JCommandToggleMenuButton(skinName, null);

            button.addActionListener(new ActionListener()
            {
                @Override
                public void actionPerformed(ActionEvent e)
                {
                    // set LAF
                    LookAndFeelUtil.setSkin(skinName);
                    // and save to preferences
                    GeneralPreferences.setGuiSkin(skinName);
                }
            });

            result.addMenuButton(button);

            // TODO: why this is needed ? ribbon bug ?
            button.getUI().installUI(button);

            buttonGroup.add(button);
            buttonGroup.setSelected(button, button.getText().equals(currentSkinName));
        }

        return result;
    }

    /**
     * get default Look And Feel font size
     */
    public static int getDefaultFontSize()
    {
        return defaultFontSize;
    }

    /**
     * get default Substance skin
     */
    public static String getDefaultSkin()
    {
        // should be Cerulean
        return skins.get(4).getDisplayName();
    }

    // /**
    // * get default Look And Feel
    // */
    // public static String getDefaultLookAndFeel()
    // {
    // return "org.pushingpixels.substance.api.skin.SubstanceCremeCoffeeLookAndFeel";
    // // return UIManager.getSystemLookAndFeelClassName();
    // }

    /**
     * Get current LookAndFeel font size
     */
    public static int getFontSize()
    {
        return currentFontSize;
    }

    /**
     * Set LookAndFeel font size
     */
    public static void setFontSize(final int size)
    {
        if (size != currentFontSize)
        {
            ThreadUtil.invokeLater(new Runnable()
            {
                @Override
                public void run()
                {
                    final float scaleFactor = (float) size / (float) defaultFontSize;

                    try
                    {
                        // will fail here if look and feel is not a substance one
                        SubstanceLookAndFeel.setFontPolicy(SubstanceFontUtilities.getScaledFontPolicy(scaleFactor));
                        currentFontSize = size;
                    }
                    catch (Exception e)
                    {
                        System.err.println("LookAndFeelUtil.setFontSize(...) error :");
                        IcyExceptionHandler.showErrorMessage(e, false);
                    }
                }
            });
        }
    }

    /**
     * Set the specified LookAndFeel skin (skin display name)
     */
    public static void setSkin(final String skinName)
    {
        if (!StringUtil.equals(skinName, getCurrentSkinName()))
        {
            ThreadUtil.invokeLater(new Runnable()
            {
                @Override
                public void run()
                {
                    try
                    {
                        SubstanceLookAndFeel.setSkin(map.get(skinName).getClassName());
                    }
                    catch (Exception e)
                    {
                        System.err.println("LookAndFeelUtil.setSkin(...) error :");
                        IcyExceptionHandler.showErrorMessage(e, false);
                    }
                }
            });
        }
    }

    /**
     * Return a foreground component color image depending original alpha intensity image
     */
    public static Image getForegroundImageFromAlphaImage(Component c, Image alphaImage)
    {
        return paintForegroundImageFromAlphaImage(c, alphaImage, null);
    }

    /**
     * Return a background component color image depending original alpha intensity image
     */
    public static Image getBackgroundImageFromAlphaImage(Component c, Image alphaImage)
    {
        return paintBackgroundImageFromAlphaImage(c, alphaImage, null);
    }

    /**
     * Paint foreground component color in 'out' image<br>
     * depending original alpha intensity from 'alphaImage'
     */
    public static Image paintForegroundImageFromAlphaImage(Component c, Image alphaImage, Image out)
    {
        return ImageUtil.paintColorImageFromAlphaImage(alphaImage, out, getForeground(c));
    }

    /**
     * Paint background component color in 'out' image<br>
     * depending original alpha intensity from 'alphaImage'
     */
    public static Image paintBackgroundImageFromAlphaImage(Component c, Image alphaImage, Image out)
    {
        return ImageUtil.paintColorImageFromAlphaImage(alphaImage, out, getBackground(c));
    }

    public static SubstanceColorScheme getActiveColorScheme(DecorationAreaType d)
    {
        return getCurrentSkin().getActiveColorScheme(d);
    }

    /**
     * @deprecated Use {@link #getActiveColorScheme(DecorationAreaType)} instead.
     */
    @Deprecated
    public static SubstanceColorScheme getActiveColorSheme(DecorationAreaType d)
    {
        return getActiveColorScheme(d);
    }

    public static SubstanceColorScheme getBackgroundColorScheme(DecorationAreaType d)
    {
        final SubstanceSkin skin = getCurrentSkin();
        if (skin != null) return skin.getBackgroundColorScheme(d);

        // Arrive only when using designer --> use a random color theme, we don't care
        return new LightAquaColorScheme();
    }

    public static SubstanceColorScheme getDisabledColorScheme(DecorationAreaType d)
    {
        final SubstanceSkin skin = getCurrentSkin();
        if (skin != null) return skin.getDisabledColorScheme(d);
        
        // Arrive only when using designer --> use a random color theme, we don't care
        return new LightAquaColorScheme();
    }

    public static SubstanceColorScheme getEnabledColorScheme(DecorationAreaType d)
    {
        final SubstanceSkin skin = getCurrentSkin();
        if (skin != null) return skin.getEnabledColorScheme(d);
        
        // Arrive only when using designer --> use a random color theme, we don't care
        return new LightAquaColorScheme();
    }

    public static SubstanceSkin getSkin()
    {
        return SubstanceLookAndFeel.getCurrentSkin();
    }

    public static SubstanceSkin getSkin(Component c)
    {
        return SubstanceLookAndFeel.getCurrentSkin(c);
    }

    public static DecorationAreaType getDecoration(Component c)
    {
        return SubstanceLookAndFeel.getDecorationType(c);
    }

    public static SubstanceColorScheme getColorScheme(Component c, ComponentState state)
    {
        return getSkin().getColorScheme(c, state);
    }

    public static SubstanceColorScheme getColorScheme(Component c, ColorSchemeAssociationKind kind, ComponentState state)
    {
        return getSkin().getColorScheme(c, kind, state);
    }

    public static SubstanceColorScheme getActiveColorScheme(Component c)
    {
        final SubstanceSkin skin = getSkin(c);
        final DecorationAreaType decoration = getDecoration(c);

        if ((skin != null) && (decoration != null))
            return skin.getActiveColorScheme(decoration);

        return null;
    }

    public static SubstanceColorScheme getActiveColorScheme(Component c, ComponentState state)
    {
        return SubstanceColorSchemeUtilities.getActiveColorScheme(c, state);
    }

    /**
     * @deprecated Use {@link #getActiveColorScheme(Component)} instead.
     */
    @Deprecated
    public static SubstanceColorScheme getActiveColorSheme(Component c)
    {
        return getActiveColorScheme(c);
    }

    /**
     * @deprecated Use {@link #getActiveColorScheme(Component, ComponentState)} instead.
     */
    @Deprecated
    public static SubstanceColorScheme getActiveColorSheme(Component c, ComponentState state)
    {
        return getActiveColorScheme(c, state);
    }

    public static SubstanceColorScheme getBackgroundColorScheme(Component c)
    {
        final SubstanceSkin skin = getSkin(c);
        final DecorationAreaType decoration = getDecoration(c);

        if ((skin != null) && (decoration != null))
            return skin.getBackgroundColorScheme(decoration);

        return null;
    }

    public static SubstanceColorScheme getDisabledColorScheme(Component c)
    {
        final SubstanceSkin skin = getSkin(c);
        final DecorationAreaType decoration = getDecoration(c);

        if ((skin != null) && (decoration != null))
            return skin.getDisabledColorScheme(decoration);

        return null;
    }

    public static SubstanceColorScheme getEnabledColorScheme(Component c)
    {
        final SubstanceSkin skin = getSkin(c);
        final DecorationAreaType decoration = getDecoration(c);

        if ((skin != null) && (decoration != null))
            return skin.getEnabledColorScheme(decoration);

        return null;
    }

    /**
     * Return the foreground color for the specified component
     */
    public static Color getForeground(Component c)
    {
        SubstanceColorScheme colorScheme;

        if (c == null)
            colorScheme = getEnabledColorScheme(DecorationAreaType.GENERAL);
        else if (c.isEnabled())
            colorScheme = getEnabledColorScheme(c);
        else
            colorScheme = getDisabledColorScheme(c);

        if (colorScheme == null)
            getEnabledColorScheme(DecorationAreaType.GENERAL);

        if (colorScheme != null)
            return new ColorUIResource(colorScheme.getForegroundColor());

        if (c != null)
            return c.getForeground();

        return Color.white;
    }

    /**
     * Return the selected foreground color for the specified component
     */
    public static Color getSelectedForeground(Component c)
    {
        SubstanceColorScheme colorScheme;

        if (c == null)
            colorScheme = getEnabledColorScheme(DecorationAreaType.GENERAL);
        else if (c.isEnabled())
            colorScheme = getEnabledColorScheme(c);
        else
            colorScheme = getDisabledColorScheme(c);

        if (colorScheme == null)
            getEnabledColorScheme(DecorationAreaType.GENERAL);

        if (colorScheme != null)
            return new ColorUIResource(colorScheme.getSelectionForegroundColor());

        if (c != null)
            return c.getForeground();

        return Color.gray;
    }

    /**
     * Return the background color for the specified component
     */
    public static Color getBackground(Component c)
    {
        SubstanceColorScheme colorScheme;

        if (c == null)
            colorScheme = getEnabledColorScheme(DecorationAreaType.GENERAL);
        else if (c.isEnabled())
            colorScheme = getEnabledColorScheme(c);
        else
            colorScheme = getDisabledColorScheme(c);

        if (colorScheme == null)
            getEnabledColorScheme(DecorationAreaType.GENERAL);

        if (colorScheme != null)
            return new ColorUIResource(colorScheme.getBackgroundFillColor());

        if (c != null)
            return c.getBackground();

        return Color.lightGray;
    }

    /**
     * Return the selected background color for the specified component
     */
    public static Color getSelectedBackground(Component c)
    {
        SubstanceColorScheme colorScheme;

        if (c == null)
            colorScheme = getEnabledColorScheme(DecorationAreaType.GENERAL);
        else if (c.isEnabled())
            colorScheme = getEnabledColorScheme(c);
        else
            colorScheme = getDisabledColorScheme(c);

        if (colorScheme == null)
            getEnabledColorScheme(DecorationAreaType.GENERAL);

        if (colorScheme != null)
            return new ColorUIResource(colorScheme.getSelectionBackgroundColor());

        if (c != null)
            return c.getBackground();

        return Color.darkGray;
    }

    public static SubstanceTitlePane getTitlePane(Window window)
    {
        return (SubstanceTitlePane) SubstanceLookAndFeel.getTitlePaneComponent(window);
    }

    public static SubstanceInternalFrameTitlePane getTitlePane(JInternalFrame frame)
    {
        final InternalFrameUI ui = frame.getUI();

        if (ui instanceof SubstanceInternalFrameUI)
            return ((SubstanceInternalFrameUI) ui).getTitlePane();

        return null;
    }

    public static void setTitlePane(JInternalFrame frame, SubstanceInternalFrameTitlePane titlePane)
    {
        final InternalFrameUI ui = frame.getUI();

        if (ui instanceof SubstanceInternalFrameUI)
            ((SubstanceInternalFrameUI) ui).setNorthPane(titlePane);
    }
}