/*
 * Copyright (c) 2005-2020 Radiance Kirill Grouchnikov. All Rights Reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *
 *  o Redistributions of source code must retain the above copyright notice,
 *    this list of conditions and the following disclaimer.
 *
 *  o Redistributions in binary form must reproduce the above copyright notice,
 *    this list of conditions and the following disclaimer in the documentation
 *    and/or other materials provided with the distribution.
 *
 *  o Neither the name of the copyright holder nor the names of
 *    its contributors may be used to endorse or promote products derived
 *    from this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
 * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
 * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS;
 * OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
 * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
 * OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE,
 * EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */
package org.pushingpixels.substance.internal.utils;

import org.pushingpixels.substance.api.ComponentState;
import org.pushingpixels.substance.api.SubstanceCortex.ComponentOrParentChainScope;
import org.pushingpixels.substance.api.SubstanceSkin;
import org.pushingpixels.substance.api.SubstanceSlices.ColorSchemeAssociationKind;
import org.pushingpixels.substance.api.colorscheme.*;

import javax.swing.*;
import javax.swing.plaf.UIResource;
import java.awt.*;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * Utilities related to color schemes. This class is for internal use only.
 *
 * @author Kirill Grouchnikov
 */
public class SubstanceColorSchemeUtilities {
    /**
     * Cache of shifted schemes.
     */
    private final static LazyResettableHashMap<SubstanceColorScheme> shiftedCache =
            new LazyResettableHashMap<>("ShiftColorScheme.shiftedSchemes");

    private enum ColorSchemeKind {
        LIGHT, DARK
    }

    /**
     * Returns a colorized version of the specified color scheme.
     *
     * @param component Component.
     * @param scheme    Color scheme.
     * @param isEnabled Indicates whether the component is enabled.
     * @return Colorized version of the specified color scheme.
     */
    private static SubstanceColorScheme getColorizedScheme(Component component,
            SubstanceColorScheme scheme, boolean isEnabled) {
        Component forQuerying = component;
        if ((component != null) && (component.getParent() != null)
                && ((component.getClass().isAnnotationPresent(SubstanceInternalArrowButton.class)
                || (component instanceof SubstanceTitleButton)))) {
            forQuerying = component.getParent();
        }
        return getColorizedScheme(component, scheme,
                (forQuerying == null) ? null : forQuerying.getForeground(),
                (forQuerying == null) ? null : forQuerying.getBackground(), isEnabled);
    }

    /**
     * Returns a colorized version of the specified color scheme.
     *
     * @param component Component.
     * @param scheme    Color scheme.
     * @param isEnabled Indicates whether the component is enabled.
     * @return Colorized version of the specified color scheme.
     */
    private static SubstanceColorScheme getColorizedScheme(Component component,
            SubstanceColorScheme scheme, Color fgColor, Color bgColor, boolean isEnabled) {
        if ((scheme != null) && (component != null)) {
            // Support for enhancement 256 - colorizing
            // controls.
            if (bgColor instanceof UIResource) {
                bgColor = null;
            }
            if (fgColor instanceof UIResource) {
                fgColor = null;
            }
            if ((bgColor != null) || (fgColor != null)) {
                double colorization = SubstanceCoreUtilities.getColorizationFactor(component);
                if (!isEnabled) {
                    colorization /= 2.0;
                }
                if (colorization > 0.0) {
                    return SubstanceColorSchemeUtilities.getShiftedScheme(scheme, bgColor,
                            colorization, fgColor, colorization);
                }
            }
        }
        return scheme;
    }

    /**
     * Returns the color scheme of the specified tabbed pane tab.
     *
     * @param jtp            Tabbed pane.
     * @param tabIndex       Tab index.
     * @param componentState Tab component state.
     * @return The color scheme of the specified tabbed pane tab.
     */
    public static SubstanceColorScheme getColorScheme(final JTabbedPane jtp, final int tabIndex,
            ColorSchemeAssociationKind associationKind, ComponentState componentState) {
        SubstanceSkin skin = SubstanceCoreUtilities.getSkin(jtp);
        if (skin == null) {
            SubstanceCoreUtilities.traceSubstanceApiUsage(jtp,
                    "Substance delegate used when Substance is not the current LAF");
        }
        SubstanceColorScheme nonColorized = skin.getColorScheme(jtp, associationKind,
                componentState);
        if (tabIndex >= 0) {
            Component component = jtp.getComponentAt(tabIndex);
            SubstanceColorScheme colorized = getColorizedScheme(component, nonColorized,
                    jtp.getForegroundAt(tabIndex), jtp.getBackgroundAt(tabIndex),
                    !componentState.isDisabled());
            return colorized;
        } else {
            return getColorizedScheme(jtp, nonColorized, !componentState.isDisabled());
        }
    }

    /**
     * Returns the color scheme of the specified component.
     *
     * @param component      Component.
     * @param componentState Component state.
     * @return Component color scheme.
     */
    public static SubstanceColorScheme getColorScheme(Component component,
            ComponentState componentState) {
        Component orig = component;
        // special case - if the component is marked as flat and
        // it is in the default state, or it is a button
        // that is never painting its background - get the color scheme of the
        // parent
        boolean isButtonThatIsNeverPainted = ((component instanceof AbstractButton)
                && SubstanceCoreUtilities.isButtonNeverPainted((AbstractButton) component));
        if (isButtonThatIsNeverPainted
                || (SubstanceCoreUtilities.hasFlatAppearance(component, false)
                && (componentState == ComponentState.ENABLED))) {
            component = component.getParent();
        }

        SubstanceSkin skin = SubstanceCoreUtilities.getSkin(component);
        if (skin == null) {
            SubstanceCoreUtilities.traceSubstanceApiUsage(component,
                    "Substance delegate used when Substance is not the current LAF");
        }
        SubstanceColorScheme nonColorized = skin.getColorScheme(component, componentState);

        return getColorizedScheme(orig, nonColorized, !componentState.isDisabled());
    }

    /**
     * Returns the color scheme of the component.
     *
     * @param component       Component.
     * @param associationKind Association kind.
     * @param componentState  Component state.
     * @return Component color scheme.
     */
    public static SubstanceColorScheme getColorScheme(Component component,
            ColorSchemeAssociationKind associationKind, ComponentState componentState) {
        // special case - if the component is marked as flat and
        // it is in the default state, get the color scheme of the parent.
        // However, flat toolbars should be ignored, since they are
        // the "top" level decoration area.
        if (!(component instanceof JToolBar)
                && SubstanceCoreUtilities.hasFlatAppearance(component, false)
                && (componentState == ComponentState.ENABLED)) {
            component = component.getParent();
        }

        SubstanceSkin skin = SubstanceCoreUtilities.getSkin(component);
        if (skin == null) {
            return null;
        }
        SubstanceColorScheme nonColorized = skin.getColorScheme(component, associationKind,
                componentState);
        return getColorizedScheme(component, nonColorized, !componentState.isDisabled());
    }

    /**
     * Returns the color scheme of the component.
     *
     * @param component       Component.
     * @param associationKind Association kind.
     * @param componentState  Component state.
     * @return Component color scheme.
     */
    public static SubstanceColorScheme getDirectColorScheme(Component component,
            ColorSchemeAssociationKind associationKind, ComponentState componentState) {
        // special case - if the component is marked as flat and
        // it is in the default state, get the color scheme of the parent.
        // However, flat toolbars should be ignored, since they are
        // the "top" level decoration area.
        if (!(component instanceof JToolBar)
                && SubstanceCoreUtilities.hasFlatAppearance(component, false)
                && (componentState == ComponentState.ENABLED)) {
            component = component.getParent();
        }

        SubstanceColorScheme nonColorized = SubstanceCoreUtilities.getSkin(component)
                .getDirectColorScheme(component, associationKind, componentState);
        return getColorizedScheme(component, nonColorized, !componentState.isDisabled());
    }

    /**
     * Returns the active color scheme of the component.
     *
     * @param component      Component.
     * @param componentState Component state.
     * @return Component color scheme.
     */
    public static SubstanceColorScheme getActiveColorScheme(Component component,
            ComponentState componentState) {
        // special case - if the component is marked as flat and
        // it is in the default state, get the color scheme of the parent.
        // However, flat toolbars should be ignored, since they are
        // the "top" level decoration area.
        if (!(component instanceof JToolBar)
                && SubstanceCoreUtilities.hasFlatAppearance(component, false)
                && (componentState == ComponentState.ENABLED)) {
            component = component.getParent();
        }

        SubstanceColorScheme nonColorized = SubstanceCoreUtilities.getSkin(component)
                .getActiveColorScheme(ComponentOrParentChainScope.getDecorationType(component));
        return getColorizedScheme(component, nonColorized, !componentState.isDisabled());
    }

    /**
     * Returns the alpha channel of the highlight color scheme of the component.
     *
     * @param component      Component.
     * @param componentState Component state.
     * @return Highlight color scheme alpha channel.
     */
    public static float getHighlightAlpha(Component component, ComponentState componentState) {
        return SubstanceCoreUtilities.getSkin(component).getHighlightAlpha(component, componentState);
    }

    /**
     * Returns the alpha channel of the color scheme of the component.
     *
     * @param component      Component.
     * @param componentState Component state.
     * @return Color scheme alpha channel.
     */
    public static float getAlpha(Component component, ComponentState componentState) {
        return SubstanceCoreUtilities.getSkin(component).getAlpha(component, componentState);
    }

    /**
     * Used as reference in attention-drawing animations. This field is <b>for internal use
     * only</b>.
     */
    public final static SubstanceColorScheme YELLOW = new SunGlareColorScheme();

    /**
     * Used as reference in attention-drawing animations. This field is <b>for internal use
     * only</b>.
     */
    public final static SubstanceColorScheme ORANGE = new SunfireRedColorScheme();

    /**
     * Used as reference to the green color scheme. This field is <b>for internal use only</b>.
     */
    public final static SubstanceColorScheme GREEN = new BottleGreenColorScheme();

    public static SubstanceColorScheme getLightColorScheme(String name, final Color[] colors) {
        if (colors == null) {
            throw new IllegalArgumentException("Color encoding cannot be null");
        }
        if (colors.length != 7) {
            throw new IllegalArgumentException("Color encoding must have 7 components");
        }
        return new BaseLightColorScheme(name) {
            public Color getUltraLightColor() {
                return colors[0];
            }

            public Color getExtraLightColor() {
                return colors[1];
            }

            public Color getLightColor() {
                return colors[2];
            }

            public Color getMidColor() {
                return colors[3];
            }

            public Color getDarkColor() {
                return colors[4];
            }

            public Color getUltraDarkColor() {
                return colors[5];
            }

            public Color getForegroundColor() {
                return colors[6];
            }
        };
    }

    public static SubstanceColorScheme getDarkColorScheme(String name, final Color[] colors) {
        if (colors == null) {
            throw new IllegalArgumentException("Color encoding cannot be null");
        }
        if (colors.length != 7) {
            throw new IllegalArgumentException("Color encoding must have 7 components");
        }
        return new BaseDarkColorScheme(name) {
            public Color getUltraLightColor() {
                return colors[0];
            }

            public Color getExtraLightColor() {
                return colors[1];
            }

            public Color getLightColor() {
                return colors[2];
            }

            public Color getMidColor() {
                return colors[3];
            }

            public Color getDarkColor() {
                return colors[4];
            }

            public Color getUltraDarkColor() {
                return colors[5];
            }

            public Color getForegroundColor() {
                return colors[6];
            }
        };
    }

    private static Color decodeColor(String value, Map<String, Color> colorMap) {
        if (value.startsWith("@")) {
            return colorMap.get(value.substring(1));
        }
        return Color.decode(value);
    }

    public static SubstanceSkin.ColorSchemes getColorSchemes(InputStream inputStream) {
        List<SubstanceColorScheme> schemes = new ArrayList<>();

        Map<String, Color> colorMap = new HashMap<>();

        Color ultraLight = null;
        Color extraLight = null;
        Color light = null;
        Color mid = null;
        Color dark = null;
        Color ultraDark = null;
        Color foreground = null;
        Color background = null;
        String name = null;
        ColorSchemeKind kind = null;
        boolean inColorSchemeBlock = false;
        boolean inColorsBlock = false;
        try (BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream))) {
            while (true) {
                String line = reader.readLine();
                if (line == null)
                    break;

                line = line.trim();
                if (line.length() == 0)
                    continue;

                if (line.startsWith("#")) {
                    // enhancement 476 - allow comments
                    continue;
                }

                if (line.contains("{")) {
                    if (inColorSchemeBlock || inColorsBlock) {
                        throw new IllegalArgumentException("Already in color scheme or colors definition");
                    }
                    name = line.substring(0, line.indexOf("{")).trim();
                    if (name.equals("@colors")) {
                        inColorsBlock = true;
                    } else {
                        inColorSchemeBlock = true;
                    }
                    continue;
                }

                if (line.contains("}")) {
                    if (!inColorSchemeBlock && !inColorsBlock) {
                        throw new IllegalArgumentException("Not in color scheme or colors definition");
                    }

                    if (inColorsBlock) {
                        // Colors have already been processed
                        inColorsBlock = false;
                        continue;
                    }

                    inColorSchemeBlock = false;

                    if (background == null) {
                        if ((name == null) || (kind == null) || (ultraLight == null)
                                || (extraLight == null) || (light == null) || (mid == null)
                                || (dark == null) || (ultraDark == null) || (foreground == null)) {
                            throw new IllegalArgumentException("Incomplete specification of '" + name + "'");
                        }
                    } else {
                        if ((name == null) || (foreground == null)) {
                            throw new IllegalArgumentException("Incomplete specification '" + name + "'");
                        }
                    }
                    Color[] colors = (background != null)
                            ? new Color[] {background, background, background, background, background, background,
                            foreground}
                            : new Color[] {ultraLight, extraLight, light, mid, dark, ultraDark, foreground};

                    if (kind == ColorSchemeKind.LIGHT) {
                        schemes.add(getLightColorScheme(name, colors));
                    } else {
                        schemes.add(getDarkColorScheme(name, colors));
                    }
                    name = null;
                    kind = null;
                    ultraLight = null;
                    extraLight = null;
                    light = null;
                    mid = null;
                    dark = null;
                    ultraDark = null;
                    foreground = null;
                    background = null;
                    continue;
                }

                String[] split = line.split("=");
                if (split.length != 2) {
                    throw new IllegalArgumentException("Unsupported format in line " + line);
                }

                String key = split[0].trim();
                String value = split[1].trim();

                if (inColorsBlock) {
                    colorMap.put(key, Color.decode(value));
                    continue;
                }

                if ("kind".equals(key)) {
                    if (kind == null) {
                        if ("Light".equals(value)) {
                            kind = ColorSchemeKind.LIGHT;
                            continue;
                        }
                        if ("Dark".equals(value)) {
                            kind = ColorSchemeKind.DARK;
                            continue;
                        }
                        throw new IllegalArgumentException("Unsupported format in line " + line);
                    }
                    throw new IllegalArgumentException("'kind' should only be defined once");
                }
                if ("colorUltraLight".equals(key)) {
                    if (ultraLight == null) {
                        ultraLight = decodeColor(value, colorMap);
                        continue;
                    }
                    throw new IllegalArgumentException("'ultraLight' should only be defined once");
                }
                if ("colorExtraLight".equals(key)) {
                    if (extraLight == null) {
                        extraLight = decodeColor(value, colorMap);
                        continue;
                    }
                    throw new IllegalArgumentException("'extraLight' should only be defined once");
                }
                if ("colorLight".equals(key)) {
                    if (light == null) {
                        light = decodeColor(value, colorMap);
                        continue;
                    }
                    throw new IllegalArgumentException("'light' should only be defined once");
                }
                if ("colorMid".equals(key)) {
                    if (mid == null) {
                        mid = decodeColor(value, colorMap);
                        continue;
                    }
                    throw new IllegalArgumentException("'mid' should only be defined once");
                }
                if ("colorDark".equals(key)) {
                    if (dark == null) {
                        dark = decodeColor(value, colorMap);
                        continue;
                    }
                    throw new IllegalArgumentException("'dark' should only be defined once");
                }
                if ("colorUltraDark".equals(key)) {
                    if (ultraDark == null) {
                        ultraDark = decodeColor(value, colorMap);
                        continue;
                    }
                    throw new IllegalArgumentException("'ultraDark' should only be defined once");
                }
                if ("colorForeground".equals(key)) {
                    if (foreground == null) {
                        foreground = decodeColor(value, colorMap);
                        continue;
                    }
                    throw new IllegalArgumentException("'foreground' should only be defined once");
                }
                if ("colorBackground".equals(key)) {
                    if (value.contains("->")) {
                        String[] splitInner = value.split("->");
                        Color colorStart = decodeColor(splitInner[0].trim(), colorMap);
                        Color colorEnd = decodeColor(splitInner[1].trim(), colorMap);
                        ultraLight = colorStart;
                        extraLight = SubstanceColorUtilities.getInterpolatedColor(colorStart, colorEnd, 0.9f);
                        light = SubstanceColorUtilities.getInterpolatedColor(colorStart, colorEnd, 0.7f);
                        mid = SubstanceColorUtilities.getInterpolatedColor(colorStart, colorEnd, 0.5f);
                        dark = SubstanceColorUtilities.getInterpolatedColor(colorStart, colorEnd, 0.2f);
                        ultraDark = colorEnd;
                        continue;
                    } else {
                        if (background == null) {
                            background = decodeColor(value, colorMap);
                            continue;
                        }
                    }
                    throw new IllegalArgumentException("'foreground' should only be defined once");
                }
                throw new IllegalArgumentException("Unsupported format in line " + line);
            }
        } catch (IOException ioe) {
            throw new IllegalArgumentException(ioe);
        }
        return new SubstanceSkin.ColorSchemes(schemes);
    }

    /**
     * Returns a shifted color scheme. This method is for internal use only.
     *
     * @param orig                  The original color scheme.
     * @param backgroundShiftColor  Shift color for the background color scheme colors. May be <code>null</code> - in
     *                              this case, the background color scheme colors will not be shifted.
     * @param backgroundShiftFactor Shift factor for the background color scheme colors. If the shift color for the
     *                              background color scheme colors is <code>null</code>, this value is ignored.
     * @param foregroundShiftColor  Shift color for the foreground color scheme colors. May be <code>null</code> - in
     *                              this case, the foreground color scheme colors will not be shifted.
     * @param foregroundShiftFactor Shift factor for the foreground color scheme colors. If the shift color for the
     *                              foreground color scheme colors is <code>null</code>, this value is ignored.
     * @return Shifted scheme.
     */
    public static SubstanceColorScheme getShiftedScheme(SubstanceColorScheme orig,
            Color backgroundShiftColor, double backgroundShiftFactor, Color foregroundShiftColor,
            double foregroundShiftFactor) {
        HashMapKey key = SubstanceCoreUtilities.getHashKey(orig.getDisplayName(),
                backgroundShiftColor == null ? "" : backgroundShiftColor.getRGB(),
                backgroundShiftFactor,
                foregroundShiftColor == null ? "" : foregroundShiftColor.getRGB(),
                foregroundShiftFactor);
        SubstanceColorScheme result = shiftedCache.get(key);
        if (result == null) {
            result = orig.shift(backgroundShiftColor, backgroundShiftFactor, foregroundShiftColor,
                    foregroundShiftFactor);
            shiftedCache.put(key, result);
        }
        return result;
    }
}