/*
 * Copyright 2022-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.logging.Logger;

import ej.bon.Constants;
import ej.mwt.Desktop;
import ej.mwt.Widget;
import ej.mwt.render.RenderPolicy;

/**
 * Monitor for Widget rendering.
 * <p>
 * It uses a {@link Logger} with following format:
 *
 * <pre>
 * Render requested on Path &gt; To &gt; MyWidget at {0,1 2x3} of {10,20 30x40}
 * 	by my.package.MyClass.myMethod(MyClass.java:0)
 * 	at my.package.MyClass.myCaller(MyClass.java:1)
 * 	at my.package.MyClass.myCaller(MyClass.java:2)
 * </pre>
 */
public class RenderMonitor implements RenderPolicy.RenderListener {

	private static final String DEBUG_RENDER_FRAMES_CONSTANT = "ej.mwt.debug.render.frames";
	private static final Logger LOGGER = Logger.getLogger(RenderMonitor.class.getSimpleName());

	@Override
	public void onRenderRequested(Widget widget, int x, int y, int width, int height) {
		LOGGER.info(buildMessage("Render requested on ", widget, x, y, width, height, true));
	}

	@Override
	public void onRenderExecuted(Widget widget, int x, int y, int width, int height) {
		LOGGER.info(buildMessage("Render executed on  ", widget, x, y, width, height, false));
	}

	private static String buildMessage(String prefix, Widget widget, int x, int y, int width, int height,
			boolean appendCaller) {
		StringBuilder builder = new StringBuilder();
		builder.append(prefix);
		builder.append(HierarchyInspector.pathToWidgetToString(widget));
		builder.append(" at {").append(x).append(',').append(y).append(' ').append(width).append('x').append(height)
				.append("} of {");
		BoundsInspector.appendAbsolutePosition(builder, widget);
		builder.append(' ');
		BoundsInspector.appendSize(builder, widget);
		builder.append('}');
		if (appendCaller) {
			appendCaller(builder);
		}
		return builder.toString();
	}

	private static void appendCaller(StringBuilder builder) {
		int frames = Constants.getInt(DEBUG_RENDER_FRAMES_CONSTANT);
		if (frames <= 0) {
			return;
		}

		StackTraceElement[] stacktrace = Thread.currentThread().getStackTrace();
		int start = skipInternalFrames(stacktrace);
		StackTraceElement frame;

		builder.append(frames == 1 ? " " : "\n\t"); // inline when only one frame
		builder.append("by ");
		frame = stacktrace[start];
		assert frame != null;
		appendFrame(builder, frame);

		for (int i = 1, n = stacktrace.length; i < frames && start + i < n; i++) {
			builder.append("\n\tat ");
			frame = stacktrace[start + i];
			assert frame != null;
			appendFrame(builder, frame);
		}
	}

	private static void appendFrame(StringBuilder builder, StackTraceElement frame) {
		String className = frame.getClassName();
		String methodName = frame.getMethodName();
		className = HierarchyInspector.canonizeClassName(className);
		builder.append(className).append('.').append(methodName).append('(');
		String fileName = frame.getFileName();
		if (fileName != null) {
			int lineNumber = frame.getLineNumber();
			builder.append(fileName).append(':').append(lineNumber);
		}
		builder.append(')');
	}

	private static int skipInternalFrames(StackTraceElement[] stacktrace) {
		String renderMonitor = RenderMonitor.class.getName();
		String desktop = Desktop.class.getName();
		String widget = Widget.class.getName();

		int i = 0;
		// skip getStackTrace() implementation frames
		while (!stacktrace[i].getClassName().equals(renderMonitor)) {
			i++;
		}
		assert i < stacktrace.length;

		// skip render policy implementation frames
		while (!stacktrace[i].getClassName().equals(desktop)) {
			i++;
		}
		assert i < stacktrace.length;

		// skip ej.mwt.Desktop.requestRender(Widget, int, int, int, int) frame
		i++;
		assert i < stacktrace.length;

		// skip ej.mwt.Widget.* frames
		while (stacktrace[i].getClassName().equals(widget)) {
			i++;
		}
		assert i < stacktrace.length;

		return i;
	}

}
