/*
 * Copyright 2020-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.widget.debug;

import java.util.ArrayList;
import java.util.List;

import ej.microui.display.Font;
import ej.mwt.Container;
import ej.mwt.Desktop;
import ej.mwt.Widget;
import ej.mwt.style.DefaultStyle;
import ej.mwt.style.Style;
import ej.mwt.style.background.Background;
import ej.mwt.style.dimension.Dimension;
import ej.mwt.style.outline.Outline;
import ej.mwt.stylesheet.cascading.CascadingStylesheet;
import ej.mwt.stylesheet.selector.Selector;

/**
 * Provides helpers to analyze a hierarchy of widgets.
 */
public class HierarchyInspector {

	private static final Selector[] EMPTY_STYLE_SOURCE = new Selector[0];

	private HierarchyInspector() {
		// Forbid instantiation
	}

	/**
	 * Visits a widget hierarchy.
	 * <p>
	 * If the root widget is a {@code Container}, this method calls:
	 *
	 * <pre>
	 * visitWidget(root);
	 * beginContainer();
	 * for (Widget child: getChildren(root)) { visitHierarchy(child, visitor); }
	 * endContainer()
	 * </pre>
	 *
	 * Otherwise, it simply calls `visitWidget(root)`.
	 *
	 * @param widget
	 *            the root of the widget hierarchy.
	 * @param visitor
	 *            the visitor.
	 */
	public static void visitHierarchy(Widget widget, WidgetVisitor visitor) {
		visitor.visitWidget(widget);
		if (widget instanceof Container) {
			visitor.beginContainer();

			Container container = (Container) widget;
			int childrenCount = container.getChildrenCount();
			for (int i = 0; i < childrenCount; i++) {
				Widget child = container.getChild(i);
				visitHierarchy(child, visitor);
			}

			visitor.endContainer();
		}
	}

	/**
	 * Counts instances of a {@link Widget} type in a widget hierarchy.
	 * <p>
	 * The method is recursive: if the given widget is a container, it browses its children.
	 *
	 * @param root
	 *            the root of the widget hierarchy.
	 * @param clazz
	 *            the widget class.
	 * @return the number of instances.
	 */
	public static int countInstances(Widget root, final Class<? extends Widget> clazz) {
		final IntHolder count = new IntHolder(0);
		visitHierarchy(root, new WidgetVisitor() {

			@Override
			public void beginContainer() {
				// do nothing
			}

			@Override
			public void visitWidget(Widget widget) {
				if (clazz.isInstance(widget)) {
					count.value++;
				}
			}

			@Override
			public void endContainer() {
				// do nothing
			}

		});
		return count.value;
	}

	/**
	 * Counts containers in a given widget hierarchy.
	 * <p>
	 * The method is recursive: if the given widget is a container, it browses its children.
	 *
	 * @param root
	 *            the root of the widget hierarchy.
	 * @return the number of containers.
	 */
	public static int countNumberOfContainers(Widget root) {
		return countInstances(root, Container.class);
	}

	/**
	 * Counts widgets in a widget hierarchy.
	 * <p>
	 * The method is recursive: if the given widget is a container, it browses its children.
	 *
	 * @param root
	 *            the root of the widget hierarchy.
	 * @return the number of widgets.
	 */
	public static int countNumberOfWidgets(Widget root) {
		return countInstances(root, Widget.class);
	}

	/**
	 * Counts the maximum depth of a widget hierarchy.
	 * <p>
	 * The method is recursive: if the given widget is a container, it browses its children.
	 *
	 * @param root
	 *            the root of the widget hierarchy.
	 * @return the maximum depth.
	 */
	public static int countMaxDepth(Widget root) {
		final IntHolder count = new IntHolder(0);
		visitHierarchy(root, new WidgetVisitor() {
			int depth = 0;

			@Override
			public void beginContainer() {
				this.depth++;
			}

			@Override
			public void visitWidget(Widget widget) {
				int depth = this.depth;
				if (depth > count.value) {
					count.value = depth;
				}
			}

			@Override
			public void endContainer() {
				this.depth--;
			}

		});
		return count.value;
	}

	/**
	 * Prints a widget hierarchy.
	 * <p>
	 * Prints the root widget and its children recursively in a tree format.
	 *
	 * @param root
	 *            the root of the widget hierarchy.
	 * @return the widget hierarchy serialized in a tree format.
	 */
	public static String hierarchyToString(Widget root) {
		return hierarchyToString(root, false);
	}

	/**
	 * Prints the widget hierarchy of a desktop on the "standard" output stream.
	 * <p>
	 * For each widget and its children recursively in a tree format, prints its class name.
	 * <p>
	 * This method writes on the "standard" output stream, for use during time-sensitive operations, prefer
	 * {@link #hierarchyStyleToString(Widget)}.
	 *
	 * @param root
	 *            the root of the widget hierarchy.
	 * @throws IllegalArgumentException
	 *             if {@code root} is {@code null} or is not attached.
	 * @see System#out standard output stream
	 * @see Widget#isAttached()
	 */
	public static void printHierarchy(Widget root) {
		System.out.println(hierarchyToString(root)); // NOSONAR: this is for debugging purposes only
	}

	/**
	 * Prints the widget hierarchy of a desktop.
	 * <p>
	 * For each widget and its children recursively in a tree format, prints its class name and current style.
	 *
	 * @param root
	 *            the root of the widget hierarchy.
	 * @return the hierarchy.
	 * @throws IllegalArgumentException
	 *             if {@code root} is {@code null} or is not attached.
	 * @see Widget#isAttached()
	 */
	public static String hierarchyStyleToString(Widget root) {
		if (!root.isAttached()) {
			throw new IllegalArgumentException();
		}
		return hierarchyToString(root, true);
	}

	/**
	 * Prints the widget hierarchy of a desktop on the "standard" output stream.
	 * <p>
	 * For each widget and its children recursively in a tree format, prints its class name and current style.
	 * <p>
	 * This method writes on the "standard" output stream, for use during time-sensitive operations, prefer
	 * {@link #hierarchyStyleToString(Widget)}.
	 *
	 * @param root
	 *            the root of the widget hierarchy.
	 * @throws IllegalArgumentException
	 *             if {@code root} is {@code null} or is not attached.
	 * @see System#out standard output stream
	 * @see Widget#isAttached()
	 */
	public static void printHierarchyStyle(Widget root) {
		System.out.println(hierarchyStyleToString(root)); // NOSONAR: this is for debugging purposes only
	}

	private static String hierarchyToString(Widget root, final boolean appendStyle) {
		final StringBuilder builder = new StringBuilder();
		WidgetVisitor visitor = new WidgetVisitor() {
			int depth = 0;

			@Override
			public void beginContainer() {
				this.depth++;
			}

			@Override
			public void visitWidget(Widget widget) {
				appendDepth(builder, this.depth);
				appendElement(builder, widget);
				appendPosition(builder, widget);
				if (appendStyle) {
					Style style = widget.getStyle();
					appendStyle(builder, style);
				}
				builder.append("\n");
			}

			@Override
			public void endContainer() {
				this.depth--;
			}
		};

		visitHierarchy(root, visitor);

		return builder.toString();
	}

	private static void appendPosition(StringBuilder builder, Widget widget) {
		builder.append('{');
		builder.append("x=").append(widget.getAbsoluteX());
		builder.append(',');
		builder.append("y=").append(widget.getAbsoluteY());
		builder.append(',');
		builder.append("w=").append(widget.getWidth());
		builder.append(',');
		builder.append("h=").append(widget.getHeight());
		builder.append('}');
	}

	private static void appendHierarchyRecursive(StringBuilder builder, Widget widget, int depth) {
		appendDepth(builder, depth);
		appendElement(builder, widget);
		builder.append('\n');
		if (widget instanceof Container) {
			appendChildrenHierarchy(builder, (Container) widget, depth);
		}
	}

	private static void appendChildrenHierarchy(StringBuilder builder, Container container, int depth) {
		int childrenCount = container.getChildrenCount();
		for (int i = 0; i < childrenCount; i++) {
			Widget child = container.getChild(i);
			appendHierarchyRecursive(builder, child, depth + 1);
		}
	}

	static StringBuilder appendDepth(StringBuilder builder, int depth) {
		while (depth > 1) {
			builder.append('|').append(' ').append(' ');
			depth--;
		}
		if (depth > 0) {
			builder.append('+').append('-').append('-');
		}
		return builder;
	}

	private static void appendStyle(StringBuilder builder, Style style) {
		Dimension dimension = style.getDimension(); // warning: this is not widget size
		int color = style.getColor();
		Background background = style.getBackground();
		Outline border = style.getBorder();
		Outline padding = style.getPadding();
		Outline margin = style.getMargin();
		Font font = style.getFont();
		int horizontalAlignment = style.getHorizontalAlignment();
		int verticalAlignment = style.getVerticalAlignment();

		Selector[] styleSource = getStyleSources(style);
		List<String> entries = new ArrayList<>();
		if (!DefaultStyle.DIMENSION.equals(dimension)) {
			entries.add("dimension=" + Stringifier.toString(dimension)
					+ from(styleSource, CascadingStylesheet.DIMENSION_INDEX));
		}
		if (DefaultStyle.COLOR != color) {
			entries.add(
					"color=" + Stringifier.colorToString(color) + from(styleSource, CascadingStylesheet.COLOR_INDEX));
		}
		if (!DefaultStyle.BACKGROUND.equals(background)) {
			entries.add("background=" + Stringifier.toString(background)
					+ from(styleSource, CascadingStylesheet.BACKGROUND_INDEX));
		}
		if (!DefaultStyle.BORDER.equals(border)) {
			entries.add("border=" + Stringifier.toString(border) + from(styleSource, CascadingStylesheet.BORDER_INDEX));
		}
		if (!DefaultStyle.PADDING.equals(padding)) {
			entries.add(
					"padding=" + Stringifier.toString(padding) + from(styleSource, CascadingStylesheet.PADDING_INDEX));
		}
		if (!DefaultStyle.MARGIN.equals(margin)) {
			entries.add("margin=" + Stringifier.toString(margin) + from(styleSource, CascadingStylesheet.MARGIN_INDEX));
		}
		if (!Font.getDefaultFont().equals(font)) {
			entries.add("font=" + Stringifier.toString(font) + from(styleSource, CascadingStylesheet.FONT_INDEX));
		}
		if (DefaultStyle.HORIZONTAL_ALIGNMENT != horizontalAlignment) {
			entries.add("horizontalAlignment=" + Stringifier.alignmentToString(horizontalAlignment)
					+ from(styleSource, CascadingStylesheet.HORIZONTAL_ALIGNMENT_INDEX));
		}
		if (DefaultStyle.VERTICAL_ALIGNMENT != verticalAlignment) {
			entries.add("verticalAlignment=" + Stringifier.alignmentToString(verticalAlignment)
					+ from(styleSource, CascadingStylesheet.VERTICAL_ALIGNMENT_INDEX));
		}

		if (!entries.isEmpty()) {
			builder.append(" (");
			int n = entries.size() - 1;
			for (int i = 0; i < n; i++) {
				builder.append(entries.get(i)).append(", ");
			}
			builder.append(entries.get(n)).append(")");
		}
	}

	private static Selector[] getStyleSources(final Style style) {
		try {
			return CascadingStylesheet.getStyleSources(style);
		} catch (IllegalArgumentException e) {
			return EMPTY_STYLE_SOURCE;
		}
	}

	private static String from(final Selector[] styleSource, int index) {
		if (styleSource != null && styleSource.length != 0) {
			return " from " + getSelector(styleSource, index);
		} else {
			return "";
		}
	}

	private static String getSelector(final Selector[] styleSource, int index) {
		Selector selector = styleSource[index];
		if (selector == null) {
			return "default";
		}
		return Stringifier.toString(selector);
	}

	/**
	 * Prints the path to the given widget with <code>&gt;</code> separator.
	 *
	 * @param widget
	 *            the widget to inspect.
	 * @return the path.
	 * @see HierarchyInspector#pathToWidgetToString(Widget, char)
	 */
	public static String pathToWidgetToString(Widget widget) {
		return pathToWidgetToString(widget, '>');
	}

	/**
	 * Prints the path to the given widget.
	 * <p>
	 * Prints the containers from the root (including the desktop) to the widget, separated with the given separator.
	 * Example: <code>Desktop &gt; Scroll &gt; ScrollableList &gt; Label</code>
	 *
	 * @param widget
	 *            the widget to inspect.
	 * @param separator
	 *            the separator between the items.
	 * @return the path.
	 */
	public static String pathToWidgetToString(Widget widget, char separator) {
		StringBuilder builder = new StringBuilder();

		if (widget.isAttached()) {
			Desktop desktop = widget.getDesktop();
			appendElement(builder, desktop);
			appendSeparator(builder, separator);
		}

		pathToWidgetRecursive(builder, widget, separator);

		return builder.toString();
	}

	/**
	 * Prints the root parent first then all the children.
	 */
	private static void pathToWidgetRecursive(StringBuilder builder, Widget widget, char separator) {
		Container parent = widget.getParent();
		if (parent != null) {
			pathToWidgetRecursive(builder, parent, separator);
			appendSeparator(builder, separator);
		}
		appendElement(builder, widget);
	}

	/* package */ static StringBuilder appendElement(StringBuilder builder, Object object) {
		Class<?> clazz = object.getClass();
		String name = clazz.getSimpleName();
		if (name.equals("@")) { // anonymous class on S3
			name = clazz.getName();
		}
		name = canonizeClassName(name);
		builder.append(name);
		return builder;
	}

	// Takes advantage of S3 capability to get class name even when the type name is not "embedded"
	/* package */ static String canonizeClassName(String className) {
		if (className.startsWith("@T:")) {
			className = className.substring(3);
		}
		int length = className.length();
		if (length > 1 && className.charAt(length - 1) == '@') {
			className = className.substring(0, length - 1);
		}
		return className;
	}

	private static void appendSeparator(StringBuilder builder, char separator) {
		builder.append(' ').append(separator).append(' ');
	}

	private static class IntHolder {

		private int value;

		public IntHolder(int initialValue) {
			this.value = initialValue;
		}

	}

}
