package org.ice1000.jimgui.util;

import org.ice1000.jimgui.JImGui;
import org.jetbrains.annotations.Contract;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.annotations.TestOnly;

import java.nio.charset.StandardCharsets;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.LongSupplier;

/**
 * @author ice1000
 * @since v0.1
 */
public class JImGuiUtil {
	/** defined in C++ float.h */
	public static final float FLT_MAX = 3.402823466e+38F;

	/**
	 * Run a GUI in a limited time period.
	 *
	 * @param millis   millis seconds to run
	 * @param runnable the task executed in each refreshing
	 */
	@TestOnly
	public static void runWithin(long millis, @NotNull Consumer<@NotNull JImGui> runnable) {
		try (JImGui imGui = new JImGui()) {
			long end = System.currentTimeMillis() + millis;
			imGui.initBeforeMainLoop();
			while (!imGui.windowShouldClose() && System.currentTimeMillis() < end) {
				imGui.initNewFrame();
				runnable.accept(imGui);
				imGui.render();
			}
		}
	}

	public static void run(@NotNull Consumer<@NotNull JImGui> runnable) {
		try (JImGui imGui = new JImGui()) {
			imGui.initBeforeMainLoop();
			while (!imGui.windowShouldClose()) {
				imGui.initNewFrame();
				runnable.accept(imGui);
				imGui.render();
			}
		}
	}

	public static void runPer(long millis, @NotNull Consumer<@NotNull JImGui> runnable) {
		try (JImGui imGui = new JImGui()) {
			long latestRefresh = System.currentTimeMillis();
			imGui.initBeforeMainLoop();
			while (!imGui.windowShouldClose()) {
				long currentTimeMillis = System.currentTimeMillis();
				long deltaTime = currentTimeMillis - latestRefresh;
				Thread.sleep(deltaTime / 2);
				if (deltaTime > millis) {
					imGui.initNewFrame();
					runnable.accept(imGui);
					imGui.render();
					latestRefresh = currentTimeMillis;
				}
			}
		} catch (@NotNull InterruptedException e) {
			throw new RuntimeException(e);
		}
	}

	@TestOnly
	public static void runWithinPer(long limit, long millis, @NotNull Consumer<@NotNull JImGui> runnable) {
		try (JImGui imGui = new JImGui()) {
			long latestRefresh = System.currentTimeMillis();
			long end = System.currentTimeMillis() + limit;
			imGui.initBeforeMainLoop();
			while (!imGui.windowShouldClose() && System.currentTimeMillis() < end) {
				long currentTimeMillis = System.currentTimeMillis();
				long deltaTime = currentTimeMillis - latestRefresh;
				Thread.sleep(deltaTime * 2 / 3);
				if (deltaTime > millis) {
					imGui.initNewFrame();
					runnable.accept(imGui);
					imGui.render();
					latestRefresh = currentTimeMillis;
				}
			}
		} catch (@NotNull InterruptedException e) {
			throw new RuntimeException(e);
		}
	}

	public static void runPer(@NotNull LongSupplier millisSupplier, @NotNull Consumer<@NotNull JImGui> runnable) {
		try (JImGui imGui = new JImGui()) {
			long latestRefresh = System.currentTimeMillis();
			long millis = millisSupplier.getAsLong();
			imGui.initBeforeMainLoop();
			while (!imGui.windowShouldClose()) {
				long currentTimeMillis = System.currentTimeMillis();
				long deltaTime = currentTimeMillis - latestRefresh;
				Thread.sleep(deltaTime / 2);
				if (deltaTime > millis) {
					imGui.initNewFrame();
					runnable.accept(imGui);
					imGui.render();
					latestRefresh = currentTimeMillis;
					millis = millisSupplier.getAsLong();
				}
			}
		} catch (@NotNull InterruptedException e) {
			throw new RuntimeException(e);
		}
	}

	@Contract(value = "!null -> !null; null -> null", pure = true)
	private static @Nullable byte[] getBytesDefaultImpl(@Nullable String text) {
		return text != null ? (text + '\0').getBytes(StandardCharsets.UTF_8) : null;
	}

	private static @Nullable Function<String, byte[]> STRING_TO_BYTES = null;

	/**
	 * Customize your string-to-bytes function.
	 * Because the default implementation is not very efficient.
	 *
	 * @param stringToBytes your conversion function.
	 */
	public static void setStringToBytes(@NotNull Function<@Nullable String, byte @Nullable []> stringToBytes) {
		STRING_TO_BYTES = stringToBytes;
	}

	@Contract(value = "!null -> !null; null -> null", pure = true)
	public static @Nullable byte[] getBytes(@Nullable String text) {
		return STRING_TO_BYTES != null ? STRING_TO_BYTES.apply(text) : getBytesDefaultImpl(text);
	}
}