/*
 * 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.mwt.render;

import ej.annotation.Nullable;
import ej.bon.Constants;
import ej.microui.display.GraphicsContext;
import ej.mwt.Desktop;
import ej.mwt.Widget;

/**
 * A render policy is responsible of rendering the widgets of a desktop on the display.
 * <p>
 * A render policy is associated to a single desktop.
 * <p>
 * A render policy provides a method to render the desktop on the display. This method is used by the desktop in order
 * to perform the initial rendering of the complete widget hierarchy.
 * <p>
 * A render policy also provides a method to handle render requests targeting specific widgets of the desktop. The goal
 * of this method is to render only a part of the widget hierarchy considering that an old representation of the desktop
 * is already visible on the display. The most naive implementation would be to render the whole hierarchy of the
 * desktop.
 */
public abstract class RenderPolicy {

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

	/**
	 * {@link Constants#getClass(String) BON Class constant} to set the render monitor.
	 * <p>
	 * Set it to the FQN of an implementation of {@link RenderListener} to monitor rendering.
	 */
	protected static final String DEBUG_RENDER_MONITOR_CONSTANT = "ej.mwt.debug.render.monitor";

	/**
	 * {@link RenderListener} instance to be used by all {@link RenderPolicy} implementations.
	 * <p>
	 * It is {@code null} if {@value #DEBUG_RENDER_ENABLED_CONSTANT} is {@code false}.
	 */
	protected static final @Nullable RenderListener MONITOR;

	static {
		if (Constants.getBoolean(DEBUG_RENDER_ENABLED_CONSTANT)) {
			Class<?> clazz = Constants.getClass(DEBUG_RENDER_MONITOR_CONSTANT);
			if (!RenderListener.class.isAssignableFrom(clazz)) {
				throw new IllegalArgumentException("Class set to " + DEBUG_RENDER_MONITOR_CONSTANT + " ( "
						+ clazz.getName() + " ) does not implement " + RenderListener.class.getSimpleName());
			}

			Object o;
			try {
				o = clazz.newInstance();
			} catch (InstantiationException | IllegalAccessException e) {
				throw new IllegalArgumentException(
						"Class set to " + DEBUG_RENDER_MONITOR_CONSTANT + " cannot be instantiated", e);
			}

			assert o instanceof RenderListener;
			MONITOR = (RenderListener) o;
		} else {
			MONITOR = null;
		}
	}

	private final Desktop desktop;

	/**
	 * Creates a render policy.
	 *
	 * @param desktop
	 *            the desktop to render.
	 */
	protected RenderPolicy(Desktop desktop) {
		this.desktop = desktop;
	}

	/**
	 * Returns the desktop managed by this render policy.
	 *
	 * @return the desktop managed by this render policy.
	 */
	public Desktop getDesktop() {
		return this.desktop;
	}

	/**
	 * Renders the desktop on the display. This method is used by the desktop in order to perform the initial rendering
	 * of the complete widget hierarchy.
	 * <p>
	 * This method should be called in the MicroUI thread as the rendering of the widget is performed synchronously.
	 */
	public abstract void renderDesktop();

	/**
	 * Requests a rendering of the given widget on the display. The goal of this method is to render only a part of the
	 * widget hierarchy considering that an old representation of the desktop is already visible on the display.
	 * <p>
	 * This method returns immediately and the rendering of the widget is performed asynchronously in the MicroUI
	 * thread.
	 * <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.
	 */
	public abstract void requestRender(Widget widget, int x, int y, int width, int height);

	/**
	 * Performs the increment render of the widget.
	 *
	 * @param g
	 *            the graphics context.
	 * @param widget
	 *            the widget to render.
	 */
	protected void renderWidget(GraphicsContext g, Widget widget) {
		if (Constants.getBoolean(DEBUG_RENDER_ENABLED_CONSTANT)) {
			// render area relative to the widget
			int clipX = g.getClipX();
			int clipY = g.getClipY();
			int clipWidth = g.getClipWidth();
			int clipHeight = g.getClipHeight();
			int widgetX = widget.getX();
			int widgetY = widget.getY();
			// Check that the widget is in the clip.
			// The test is equivalent to the one in Widget.paint(GraphicsContext) done later.
			if (widgetX >= clipX + clipWidth || widgetX + widget.getWidth() <= clipX || widgetY >= clipY + clipHeight
					|| widgetY + widget.getHeight() <= clipY) {
				return;
			}

			assert MONITOR != null;
			MONITOR.onRenderExecuted(widget, clipX - widget.getAbsoluteX(), clipY - widget.getAbsoluteY(), clipWidth,
					clipHeight);
		}

		this.desktop.renderWidget(g, widget);
	}

	/**
	 * Listener for render events (when it is requested and when it is actually executed).
	 */
	public interface RenderListener {

		/**
		 * Handles a render request notification.
		 * <p>
		 * Called by the {@link RenderPolicy} when a widget render is requested, in application thread.<br>
		 * It is not called for each successive rendering relative to the {@link RenderPolicy}.<br>
		 * It is not called for each successive rendering relative to the widget hierarchy.
		 *
		 * @param widget
		 *            the widget requested to be rendered.
		 * @param x
		 *            the x coordinate of the area requested to be rendered.
		 * @param y
		 *            the y coordinate of the area requested to be rendered.
		 * @param width
		 *            the width of the area requested to be rendered.
		 * @param height
		 *            the height of the area requested to be rendered.
		 */
		void onRenderRequested(Widget widget, int x, int y, int width, int height);

		/**
		 * Handles a render execution notification.
		 * <p>
		 * Called by the {@link RenderPolicy} when a widget is rendered, in MicroUI thread.<br>
		 * It is called for each successive rendering relative to the {@link RenderPolicy}.<br>
		 * It is not called for each successive rendering relative to the widget hierarchy.
		 *
		 * @param widget
		 *            the rendered widget.
		 * @param x
		 *            the x coordinate of the rendered area.
		 * @param y
		 *            the y coordinate of the rendered area.
		 * @param width
		 *            the width of the rendered area.
		 * @param height
		 *            the height of the rendered area.
		 */
		void onRenderExecuted(Widget widget, int x, int y, int width, int height);

	}

}
