/*
 * Copyright 2020-2023 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.render;

import ej.bon.Constants;
import ej.microui.MicroUI;
import ej.microui.display.Display;
import ej.microui.display.GraphicsContext;
import ej.mwt.Container;
import ej.mwt.Desktop;
import ej.mwt.Widget;

/**
 * This render policy renders the requested widget or its parent if transparent.
 * <p>
 * When a widget is asked to be rendered:
 * <ul>
 * <li>if the widget is transparent, its parent is asked to be rendered (and recursively),</li>
 * <li>otherwise, the widget is rendered asynchronously.</li>
 * </ul>
 * <p>
 * When one widget is asked to be rendered, a flush is requested to the display. When several widgets are asked to be
 * rendered in a row, only one flush is done at the end.
 *
 * @see Widget#isTransparent()
 * @see Display#requestFlush()
 */
public class DefaultRenderPolicy extends RenderPolicy {

	// Number of pending repaints in the display pump.
	private int pendingRepaints;

	/**
	 * Creates a default render policy.
	 *
	 * @param desktop
	 *            the desktop.
	 */
	public DefaultRenderPolicy(Desktop desktop) {
		super(desktop);
	}

	/**
	 * {@inheritDoc}
	 * <p>
	 * The graphics context is translated to the position of the parent of the widget. The clipping area is set to the
	 * intersection of the parent's bounds and the given bounds.
	 */
	@Override
	public void renderDesktop() {
		Desktop desktop = getDesktop();
		Widget widget = desktop.getWidget();
		if (widget != null) {
			// reset translation and clip
			GraphicsContext g = Display.getDisplay().getGraphicsContext();
			g.resetTranslation();
			g.resetClip();

			// render widget
			if (widget.isShown()) {
				renderWidget(g, widget);
			}
		}
	}

	@Override
	public void requestRender(Widget widget, int x, int y, int width, int height) {
		if (Constants.getBoolean(DEBUG_RENDER_ENABLED_CONSTANT)) {
			assert MONITOR != null;
			MONITOR.onRenderRequested(widget, x, y, width, height);
		}
		while (widget.isTransparent()) {
			Container parent = widget.getParent();
			if (parent == null) {
				break;
			}
			// Render the parent.
			x += parent.getContentX() + widget.getX();
			y += parent.getContentY() + widget.getY();
			widget = parent;
		}
		asynchronousRender(widget, x, y, width, height);
	}

	private void asynchronousRender(final Widget widget, final int x, final int y, final int width, final int height) {
		// Calling the call serially before increasing the pending repaints counter ensures that even if an
		// OutOfEventException occurs, the pendingRepaints remains consistent.
		MicroUI.callSerially(new Runnable() {
			@Override
			public void run() {
				executeRender(widget, x, y, width, height);
			}
		});

		synchronized (this) {
			this.pendingRepaints++;
		}
	}

	private void executeRender(Widget widget, int x, int y, int width, int height) {
		if (!widget.isShown()) {
			synchronized (this) {
				--this.pendingRepaints;
			}
			return;
		}

		try {
			renderWidget(widget, x, y, width, height);
		} finally {
			int pendingRepaints;
			synchronized (this) {
				--this.pendingRepaints;
				pendingRepaints = this.pendingRepaints;
			}

			Display display = Display.getDisplay();
			if (pendingRepaints == 0) {
				display.flush();
			} else {
				display.requestFlush();
			}
		}
	}

	/**
	 * This method performs the increment render of the widget. Its implementation only renders the widget and not its
	 * siblings, but this behavior may be changed by overriding this method.
	 * <p>
	 * The given bounds are relative to the widget.
	 *
	 * @param widget
	 *            the widget to render.
	 * @param x
	 *            the x coordinate of the area to render.
	 * @param y
	 *            the y coordinate of the area to render.
	 * @param width
	 *            the width of the area to render.
	 * @param height
	 *            the height of the area to render.
	 */
	protected void renderWidget(Widget widget, int x, int y, int width, int height) {
		// reset translation and clip
		GraphicsContext g = Display.getDisplay().getGraphicsContext();
		g.resetTranslation();
		g.resetClip();

		// compute translation and clip
		setParentClip(widget, g);
		g.intersectClip(widget.getX() + x, widget.getY() + y, width, height);

		// render widget
		renderWidget(g, widget);
	}

	/**
	 * Clips and translates a graphics context to the absolute bounds of the parent of a widget.
	 * <p>
	 * It applies recursively a translation to each parent's coordinates, and a clip to its content size.
	 *
	 * @param widget
	 *            the widget to clip from.
	 * @param g
	 *            the graphics context to clip on.
	 */
	protected void setParentClip(Widget widget, GraphicsContext g) {
		Container parent = widget.getParent();
		if (parent != null) {
			setParentClip(parent, g);
			g.translate(parent.getX() + parent.getContentX(), parent.getY() + parent.getContentY());
			g.intersectClip(0, 0, parent.getContentWidth(), parent.getContentHeight());
		}
	}

}
