/*
 * Copyright 2015-2024 MicroEJ Corp. All rights reserved.
 * This library is provided in source code for use, modification and test, subject to license terms.
 * Any modification of the source code will break MicroEJ Corp. warranties on the whole library.
 */
package ej.mwt.stylesheet.cascading;

import java.util.Map;
import java.util.WeakHashMap;
import ej.annotation.Nullable;
import ej.basictool.ArrayTools;
import ej.bon.Constants;
import ej.mwt.Container;
import ej.mwt.Widget;
import ej.mwt.style.EditableStyle;
import ej.mwt.style.Style;
import ej.mwt.stylesheet.Stylesheet;
import ej.mwt.stylesheet.selector.Selector;

/**
 * Cascading stylesheet implementation strongly inspired by CSS.
 * <p>
 * This stylesheet contains:
 * <ul>
 * <li>a default style that defines all the attributes,</li>
 * <li>a set of rules with a selector and a partial style.</li>
 * </ul>
 * <p>
 * The style of a widget is determined following these steps:
 * <ol>
 * <li>create an empty result style (no attributes set),</li>
 * <li>merge widget with matching selectors rules (set by {@link #getSelectorStyle(Selector)}),</li>
 * <li>recursively merge with inherited attributes of parents (repeat previous step for parent recursively) (see
 * {@link Widget#getParent()}),</li>
 * <li>merge global style (set by {@link #getDefaultStyle()}).</li>
 * </ol>
 * The merge consists in completing the result style with all the set attributes of another style (see {@link Style}).
 * The result style is complete at the end of the resolution.
 * <p>
 * The implementation assumes that the style of the parent is resolved prior to the resolution of its children. It
 * simplifies the cascading resolution because it avoids recursive resolution upward. In other words, that means that
 * the resolution of the styles in a hierarchy must be done from top to bottom. That is the case for the widgets since
 * {@link Widget#updateStyle()} is recursive (see {@link Container#updateStyle()}).
 */
public class CascadingStylesheet implements Stylesheet {

    /**
     * Dimension field position in the array of selectors.
     *
     * @see #getStyleSources(Style)
     */
    public static final int DIMENSION_INDEX = 0;

    /**
     * Horizontal alignment field position in the array of selectors.
     *
     * @see #getStyleSources(Style)
     */
    public static final int HORIZONTAL_ALIGNMENT_INDEX = 1;

    /**
     * Vertical alignment field position in the array of selectors.
     *
     * @see #getStyleSources(Style)
     */
    public static final int VERTICAL_ALIGNMENT_INDEX = 2;

    /**
     * Margin field position in the array of selectors.
     *
     * @see #getStyleSources(Style)
     */
    public static final int MARGIN_INDEX = 3;

    /**
     * Border field position in the array of selectors.
     *
     * @see #getStyleSources(Style)
     */
    public static final int BORDER_INDEX = 4;

    /**
     * Padding field position in the array of selectors.
     *
     * @see #getStyleSources(Style)
     */
    public static final int PADDING_INDEX = 5;

    /**
     * Background field position in the array of selectors.
     *
     * @see #getStyleSources(Style)
     */
    public static final int BACKGROUND_INDEX = 6;

    /**
     * Color field position in the array of selectors.
     *
     * @see #getStyleSources(Style)
     */
    public static final int COLOR_INDEX = 7;

    /**
     * Font field position in the array of selectors.
     *
     * @see #getStyleSources(Style)
     */
    public static final int FONT_INDEX = 8;

    /**
     * First extra field position in the array of selectors.
     *
     * @see #getStyleSources(Style)
     */
    public static final int EXTRA_FIELD_1_INDEX = 9;

    /**
     * Second extra field position in the array of selectors.
     *
     * @see #getStyleSources(Style)
     */
    public static final int EXTRA_FIELD_2_INDEX = EXTRA_FIELD_1_INDEX + 1;

    /**
     * Third extra field position in the array of selectors.
     *
     * @see #getStyleSources(Style)
     */
    public static final int EXTRA_FIELD_3_INDEX = EXTRA_FIELD_1_INDEX + 2;

    /**
     * Fourth extra field position in the array of selectors.
     *
     * @see #getStyleSources(Style)
     */
    public static final int EXTRA_FIELD_4_INDEX = EXTRA_FIELD_1_INDEX + 3;

    /**
     * Fifth extra field position in the array of selectors.
     *
     * @see #getStyleSources(Style)
     */
    public static final int EXTRA_FIELD_5_INDEX = EXTRA_FIELD_1_INDEX + 4;

    /**
     * Sixth extra field position in the array of selectors.
     *
     * @see #getStyleSources(Style)
     */
    public static final int EXTRA_FIELD_6_INDEX = EXTRA_FIELD_1_INDEX + 5;

    /**
     * Seventh extra field position in the array of selectors.
     *
     * @see #getStyleSources(Style)
     */
    public static final int EXTRA_FIELD_7_INDEX = EXTRA_FIELD_1_INDEX + 6;

    /**
     * {@link Constants#getBoolean(String) BON boolean constant} to enable/disable the cascading stylesheet debug.
     * <p>
     * If enabled, {@value #DEBUG_CASCADINGSTYLE_ENABLED_CONSTANT} must also be set.
     */
    public static final String DEBUG_CASCADINGSTYLE_ENABLED_CONSTANT = "ej.mwt.debug.cascadingstyle.enabled";

    /**
     * Rules.
     */
    // The list ensures the definition order.
    /**
     * Creates a new cascading stylesheet.
     */
    public CascadingStylesheet() {
        // Equivalent of #reset()
        this.defaultStyle = new EditableStyle();
        this.selectorStyles = new Object[0];
    }

    @Override
    public Style getStyle(Widget widget) {
        CascadingStyle resultingStyle = new CascadingStyle(this.defaultStyle);
        if (Constants.getBoolean(DEBUG_CASCADINGSTYLE_ENABLED_CONSTANT)) {
            createCurrentStyleSources();
        }
        // Global style selectors only.
        mergeSelectors(widget, resultingStyle);
        // Merge with parents inherited attributes.
        Widget parentWidget = widget.getParent();
        if (parentWidget != null) {
            Style parentStyle = parentWidget.getStyle();
            resultingStyle.inheritMerge(parentStyle);
            if (Constants.getBoolean(DEBUG_CASCADINGSTYLE_ENABLED_CONSTANT)) {
                updateSource(resultingStyle, parentStyle);
            }
        }
        resultingStyle.updateHashCode();
        if (Constants.getBoolean(DEBUG_CASCADINGSTYLE_ENABLED_CONSTANT)) {
            styleSources.put(resultingStyle, currentSelectors);
        }
        return resultingStyle;
    }

    /**
     * Gets the default style. The style can be modified.
     * <p>
     * This style is used as the root style of the cascading resolution. Its initial attributes are equal to the values
     * defined in {@link ej.mwt.style.DefaultStyle}.
     *
     * @return the editable default style.
     */
    public EditableStyle getDefaultStyle() {
        return this.defaultStyle;
    }

    /**
     * Resets the default style attributes to their initial value.
     */
    public void resetDefaultStyle() {
        this.defaultStyle = new EditableStyle();
    }

    /**
     * Gets the style for a selector. The style can be modified.
     * <p>
     * This style is applied to the widgets matching the selector.
     *
     * @param selector
     *            the selector.
     * @return the editable style for the given selector.
     */
    public EditableStyle getSelectorStyle(Selector selector) {
        // Get the array in local to avoid synchronization (with add and remove).
        Object[] selectorStyles = this.selectorStyles;
        int selectorIndex = getSelectorIndex(selector, selectorStyles);
        if (selectorIndex != -1) {
            CascadingStyle style = (CascadingStyle) selectorStyles[selectorIndex + 1];
            assert (style != null);
            return style;
        } else {
            CascadingStyle style = new CascadingStyle();
            int index = getIndex(selector, selectorStyles);
            // Add the selector at the first place to ensure order (last added is resolved first).
            selectorStyles = ArrayTools.grow(selectorStyles, index, 2);
            selectorStyles[index] = selector;
            selectorStyles[index + 1] = style;
            this.selectorStyles = selectorStyles;
            return style;
        }
    }

    /**
     * Resets the style attributes for a selector.
     *
     * @param selector
     *            the selector.
     */
    public void resetSelectorStyle(Selector selector) {
        // Get the array in local to avoid synchronization (with add and remove).
        Object[] selectorStyles = this.selectorStyles;
        int selectorIndex = getSelectorIndex(selector, selectorStyles);
        if (selectorIndex != -1) {
            this.selectorStyles = ArrayTools.shrink(selectorStyles, selectorIndex, 2);
        }
    }

    /**
     * Resets the stylesheet to its initial state.
     */
    public void reset() {
        // Duplicated in the constructor.
        this.defaultStyle = new EditableStyle();
        this.selectorStyles = new Object[0];
    }

    /**
     * Gets the selectors used to create the given style.
     * <p>
     * The returned array contains 16 entries: one for each parameter of the style. For each entry, the selector belongs
     * to the rule selected to fill the matching parameter. A {@code null} entry means that the parameter is from the
     * default style.
     * <p>
     * The BON boolean constant {@link #DEBUG_CASCADINGSTYLE_ENABLED_CONSTANT} must be set to {@code true} for this
     * method to work. Beware that enabling that feature may downgrade the performances (more time to compute a style
     * and more Java heap used).
     *
     * @param style
     *            the style to get the sources for
     * @return the array of selectors
     * @throws IllegalArgumentException
     *             if the given style does not come from a {@link CascadingStylesheet}
     */
    @Nullable
    public static Selector[] getStyleSources(Style style) {
        if (style instanceof CascadingStyle) {
            return styleSources.get(style);
        }
        throw new IllegalArgumentException();
    }
}
