/*
 * 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";

	private static final int FIELDS_COUNT = 16;

	private static final Map<CascadingStyle, Selector[]> styleSources = new WeakHashMap<>();
	private static Selector[] currentSelectors;

	/**
	 * Default style.
	 */
	private EditableStyle defaultStyle;
	/**
	 * Rules.
	 */
	// The list ensures the definition order.
	private Object[] selectorStyles; // contains each rule sequentially (selector and style)

	/**
	 * 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;
	}

	private void mergeSelectors(Widget widget, CascadingStyle resultingStyle) {
		Object[] selectorStyles = this.selectorStyles;

		for (int i = 0; i < selectorStyles.length; i += 2) {
			if (((Selector) selectorStyles[i]).appliesToWidget(widget)) {
				CascadingStyle style = (CascadingStyle) selectorStyles[i + 1];
				assert (style != null);
				resultingStyle.merge(style);

				if (Constants.getBoolean(DEBUG_CASCADINGSTYLE_ENABLED_CONSTANT)) {
					updateSource(style, (Selector) selectorStyles[i]);
				}
			}
		}
	}

	/**
	 * 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];
	}

	private static int getIndex(Selector selector, Object[] selectorStyles) {
		int specificity = selector.getSpecificity();
		int minIndex = 0;
		int maxIndex = selectorStyles.length / 2;
		while (maxIndex - minIndex > 1) {
			int currentIndex = (maxIndex - minIndex) / 2 + minIndex;
			if (isMoreSpecific(selectorStyles, currentIndex * 2, specificity)) {
				minIndex = currentIndex;
			} else {
				maxIndex = currentIndex;
			}
		}
		if (maxIndex > minIndex) {
			if (isMoreSpecific(selectorStyles, minIndex * 2, specificity)) {
				return maxIndex * 2;
			} else {
				return minIndex * 2;
			}
		}
		return minIndex * 2;
	}

	private static boolean isMoreSpecific(Object[] selectorStyles, int index, int specificity) {
		int specificityCandidate = ((Selector) selectorStyles[index]).getSpecificity();
		// If equals, the first added remains first.
		return specificityCandidate > specificity;
	}

	private static int getSelectorIndex(Selector selector, Object[] selectorStyles) {
		for (int i = 0; i < selectorStyles.length; i += 2) {
			if (selector.equals(selectorStyles[i])) {
				return i;
			}
		}
		return -1;
	}

	private static void createCurrentStyleSources() {
		currentSelectors = new Selector[FIELDS_COUNT];
	}

	/**
	 * Updates the sources of a style from a selector.
	 * <p>
	 * Updates the entries matching the newly set fields.
	 */
	private void updateSource(CascadingStyle style, Selector selector) {
		Selector[] sources = currentSelectors;
		assert sources != null;
		short styleMap = style.map;
		for (int i = 0; i < FIELDS_COUNT; i++) {
			updateSource(styleMap, sources, selector, i);
		}
	}

	/**
	 * Updates the sources of a style from another style.
	 * <p>
	 * Updates the entries matching the newly set fields.
	 */
	private void updateSource(CascadingStyle style, Style fromStyle) {
		Selector[] fromSources = getStyleSources(fromStyle);
		if (fromSources != null) {
			Selector[] sources = currentSelectors;
			assert sources != null;
			short styleMap = style.map;
			for (int i = 0; i < FIELDS_COUNT; i++) {
				updateSource(styleMap, sources, fromSources[i], i);
			}
		}
	}

	/**
	 * Updates the source of a field of a style.
	 * <p>
	 * The entry is updated if it was not already set and if the field is set.
	 */
	private void updateSource(short styleMap, Selector[] sources, final Selector selector, final int field) {
		// Update the selector the first time the field is set.
		if (sources[field] == null && (styleMap & (0x1 << field)) != 0x0) {
			sources[field] = selector;
		}
	}

	/**
	 * 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();
	}

}
