/*
 * 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}.
     */
    @Nullable
    protected static final 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;
        }
    }

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