package speiger.src.coreengine.rendering.guiOld.helper;

import java.awt.Color;
import java.awt.Font;
import java.awt.FontMetrics;
import java.awt.Graphics2D;
import java.awt.Rectangle;
import java.awt.RenderingHints;
import java.awt.image.BufferedImage;
import java.io.InputStream;
import java.nio.ByteBuffer;
import java.nio.IntBuffer;
import java.nio.charset.Charset;
import java.nio.charset.CharsetEncoder;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.List;
import java.util.Map;
import java.util.function.Consumer;

import org.lwjgl.glfw.GLFW;
import org.lwjgl.stb.STBTTFontinfo;
import org.lwjgl.stb.STBTruetype;
import org.lwjgl.system.MemoryStack;
import org.lwjgl.system.MemoryUtil;

import com.google.gson.JsonArray;
import com.google.gson.JsonObject;

import speiger.src.collections.ints.collections.IntIterator;
import speiger.src.collections.ints.utils.IntIterators;
import speiger.src.collections.objects.lists.ObjectArrayList;
import speiger.src.collections.objects.maps.impl.hash.Object2ObjectLinkedOpenHashMap;
import speiger.src.collections.objects.misc.pairs.ObjectObjectPair;
import speiger.src.coreengine.assets.AssetLocation;
import speiger.src.coreengine.math.vector.ints.Vec2i;
import speiger.src.coreengine.rendering.guiOld.renderer.IFontRenderer.CharInstance;
import speiger.src.coreengine.rendering.textures.custom.AtlasStitcher;
import speiger.src.coreengine.rendering.textures.custom.AtlasStitcher.Entry;
import speiger.src.coreengine.rendering.texturesOld.custom.TextureAtlas;
import speiger.src.coreengine.rendering.texturesOld.custom.TextureAtlas.AtlasEntry;
import speiger.src.coreengine.rendering.texturesOld.custom.TextureAtlas.Builder;

public class FontBuilder {
	public static final int LITERAL = 1;
	public static final int PLAIN = 2;
	public static final int BOLD = 4;
	
	public static void main(String...args) {
		generateFont();
	}
	
	public static void generateFont() {
		float size = 48F;
		Path path = Paths.get("resources/font/Roboto-Medium.ttf");
		try(STBTTFontinfo info = STBTTFontinfo.malloc()) {
			byte[] data = Files.readAllBytes(path);
			ByteBuffer buffer = MemoryUtil.memAlloc(data.length).put(data).flip();
			STBTruetype.stbtt_InitFont(info, buffer);
			float point = STBTruetype.stbtt_ScaleForPixelHeight(info, size);
			float ascent = 0F;
			int index = 0;
			try(MemoryStack stack = MemoryStack.stackPush()) {
				IntBuffer ascentB = stack.mallocInt(1);
				IntBuffer descentB = stack.mallocInt(1);
				IntBuffer lineGapB = stack.mallocInt(1);
				STBTruetype.stbtt_GetFontVMetrics(info, ascentB, descentB, lineGapB);
				ascent = ascentB.get(0) * point;
				index = STBTruetype.nstbtt_FindGlyphIndex(info.address(), GLFW.GLFW_KEY_C);
			}
			System.out.println("Testing: Scale="+point+", Ascent="+ascent+", index="+index);
		}
		catch(Exception e) {
			e.printStackTrace();
		}
	}
	
	public static ObjectObjectPair<BufferedImage, JsonObject> createBitmapFont(InputStream stream, float size) {
		return createBitmapFont(stream, size, "UTF-8");
	}
	
	public static ObjectObjectPair<BufferedImage, JsonObject> createBitmapFont(InputStream stream, float size, String charset) {
		ObjectObjectPair<ObjectObjectPair<Vec2i, BufferedImage>, List<WrittenChar>> result = createBitmapFont(stream, charset, PLAIN | BOLD, size);
		if(result == null) return null;
		JsonArray array = new JsonArray();
		result.getValue().forEach(T -> array.add(T.seralize()));
		JsonObject info = new JsonObject();
		ObjectObjectPair<Vec2i, BufferedImage> key = result.getKey();
		info.addProperty("width", key.getValue().getWidth());
		info.addProperty("height", key.getValue().getHeight());
		info.addProperty("base", key.getKey().x());
		info.addProperty("charHeight", key.getKey().y());
		info.addProperty("tabs", 4);
		
		JsonObject data = new JsonObject();
		data.addProperty("type", "bitmap");
		data.addProperty("file", "?");
		data.add("info", info);
		data.add("chars", array);
		return ObjectObjectPair.of(key.getValue(), data);
	}
	
	public static ObjectObjectPair<ObjectObjectPair<Vec2i, BufferedImage>, List<WrittenChar>> createBitmapFont(InputStream ttf, String characters, int flags, float size) {
		try {
			Map<AssetLocation, CharData> toDraw = new Object2ObjectLinkedOpenHashMap<>();
			Builder builder = TextureAtlas.create();
			AtlasStitcher<CharData> newBuilder = new AtlasStitcher<>(Integer.MAX_VALUE, Integer.MAX_VALUE);
			
			Consumer<CharData> data = T -> {
				AssetLocation location = T.asLocation();
				toDraw.put(location, T);
				if(T.width > 0 && !builder.add(location, T.width, T.getExtraY(T.height), 2)) throw new IllegalStateException("Character: "+location+" isnt Accepted, W="+T.width+", H="+T.height);
				newBuilder.add(T);
			};
			
			Font font = Font.createFont(Font.TRUETYPE_FONT, ttf).deriveFont(size);
			String validChars = (flags & LITERAL) != 0 ? characters : getChars(characters, font);
			if((flags & PLAIN) != 0) loadFontData(font, validChars, data);
			if((flags & BOLD) != 0) loadFontData(font.deriveFont(Font.BOLD), validChars, data);
			
			newBuilder.stitch();
			if(newBuilder.isValid()) {
				BufferedImage image = new BufferedImage(newBuilder.width(), newBuilder.height(), BufferedImage.TYPE_INT_ARGB);
				Graphics2D graphics = image.createGraphics();
				graphics.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
				graphics.setRenderingHint(RenderingHints.KEY_ALPHA_INTERPOLATION, RenderingHints.VALUE_ALPHA_INTERPOLATION_QUALITY);
				graphics.setFont(font);
				FontMetrics metric = graphics.getFontMetrics();
				ObjectObjectPair<ObjectObjectPair<Vec2i, BufferedImage>, List<WrittenChar>> result = ObjectObjectPair.mutableValue(new ObjectArrayList<>());
				int[] extra = new int[1];
				newBuilder.process((D, X, Y) -> {
					extra[0] = D.extraY;
					graphics.setFont(D.getFont());
					graphics.setColor(Color.WHITE);
					graphics.drawString(new String(Character.toChars(D.getLetter())), X - D.xOffset, D.getExtraY(Y + metric.getAscent()));
					result.getValue().add(new WrittenChar(D.getLetter(), X, Y, D.width(), D.height(), D.isBold()));
				});
				result.setKey(ObjectObjectPair.of(Vec2i.of(metric.getAscent() + extra[0], metric.getHeight() + extra[0]), image));
				graphics.dispose();
				return result;
			}
			ObjectObjectPair<ObjectObjectPair<Vec2i, BufferedImage>, List<WrittenChar>> result = ObjectObjectPair.mutableValue(new ObjectArrayList<>());
			builder.buildHollow((K, V) -> {
				BufferedImage image = new BufferedImage(K.x(), K.y(), BufferedImage.TYPE_INT_ARGB);
				Graphics2D graphics = image.createGraphics();
				graphics.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
				graphics.setRenderingHint(RenderingHints.KEY_ALPHA_INTERPOLATION, RenderingHints.VALUE_ALPHA_INTERPOLATION_QUALITY);
				graphics.setFont(font);
				
				FontMetrics metric = graphics.getFontMetrics();
				int extra = 0;
				
				for(AtlasEntry entry : V) {
					CharData pair = toDraw.remove(entry.getLocation());
					extra = pair.extraY;
					graphics.setFont(pair.getFont());
					graphics.setColor(Color.WHITE);
					graphics.drawString(new String(Character.toChars(pair.getLetter())), entry.getX() - pair.xOffset, pair.getExtraY(entry.getY() + metric.getAscent()));
					result.getValue().add(new WrittenChar(pair.getLetter(), entry.getX(), entry.getY(), entry.getWidth(), entry.getHeight(), pair.isBold()));
				}
				toDraw.values().forEach(T -> result.getValue().add(new WrittenChar(T.getLetter(), 0, 0, 0, 0, T.isBold())));
				result.setKey(ObjectObjectPair.of(Vec2i.of(metric.getAscent() + extra, metric.getHeight() + extra), image));
				graphics.dispose();
			});
			return result;
		}
		catch(Exception e) {
			e.printStackTrace();
			return null;
		}
	}
	
	private static float convert(float pos, float width) {
		return pos / width;
	}
	
	private static String getChars(String s, Font font) {
		CharsetEncoder encoder = Charset.forName(s).newEncoder();
		StringBuilder builder = new StringBuilder();
		for(int c = 0;c < 0x110000;c++) {
			if(!Character.isDefined(c)) continue;
			char[] chars = Character.toChars(c);
			if(encoder.canEncode(new String(chars)) && font.canDisplay(c)) builder.append(chars);
		}
		return builder.toString();
	}
	
	private static void loadFontData(Font font, String chars, Consumer<CharData> listener) {
		BufferedImage image = new BufferedImage(1, 1, BufferedImage.TYPE_INT_ARGB);
		Graphics2D graphics = (Graphics2D)image.getGraphics();
		graphics.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
		graphics.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
		graphics.setFont(font);
		FontMetrics metric = graphics.getFontMetrics();
		int extraHeight = 0;
		List<CharData> data = new ObjectArrayList<>();
		int c = 0;
		for(IntIterator iter = IntIterators.wrap(chars.codePoints().iterator());iter.hasNext();c++) {
			int next = iter.nextInt();
			Rectangle rect = font.layoutGlyphVector(graphics.getFontRenderContext(), Character.toChars(next), 0, Character.charCount(next), 0).getGlyphPixelBounds(0, graphics.getFontRenderContext(), 0.0F, 0.0F);
			extraHeight = Math.min(extraHeight, rect.y);
			data.add(new CharData(font, next, rect, metric.charWidth(next), metric.getHeight(), font.isBold()));
			if(c % 100 == 0) System.out.println("Rendered: "+c+" / "+chars.length()+" Chars");
		}
		extraHeight = -(extraHeight + metric.getAscent());
		for(int i = 0,m = data.size();i < m;listener.accept(data.get(i++).offset(extraHeight)));
		graphics.dispose();
	}
	
	public static class WrittenChar {
		int letter;
		int x;
		int y;
		int width;
		int height;
		boolean bold;
		
		public WrittenChar(int letter, int x, int y, int width, int height, boolean bold) {
			this.letter = letter;
			this.x = x;
			this.y = y;
			this.width = width;
			this.height = height;
			this.bold = bold;
		}
		
		public JsonObject seralize() {
			JsonObject obj = new JsonObject();
			obj.addProperty("char", letter);
			obj.addProperty("minX", x);
			obj.addProperty("minY", y);
			obj.addProperty("maxX", x + width);
			obj.addProperty("maxY", y + height);
			obj.addProperty("bold", bold);
			return obj;
		}
		
		public CharInstance create(int textureWidth, int textureHeight) {
			return new CharInstance(letter, width, height, convert(x, textureWidth), convert(y, textureHeight), convert(x + width, textureWidth), convert(y + height, textureHeight), width, bold);
		}
	}
	
	private static class CharData implements Entry {
		Font font;
		int letter;
		int xOffset;
		int width;
		int height;
		boolean bold;
		int extraY = 0;
		
		public CharData(Font font, int letter, Rectangle bounds, int width, int height, boolean bold) {
			this.font = font;
			this.letter = letter;
			if(bounds.x >= 0) this.width = width;
			else {
				this.width = bounds.width == 0 ? width : bounds.width;
				xOffset = bounds.x;
			}
			this.height = height;
			this.bold = bold;
		}
		
		private CharData offset(int offset) {
			extraY += offset;
			return this;
		}
		
		public boolean isBold() { return bold; }
		public Font getFont() { return font; }
		public int getExtraY(int asent) { return extraY + asent; }
		public int getLetter() { return letter; }
		public AssetLocation asLocation() { return AssetLocation.of("base", font.getFontName().replaceAll(" ", "_")+(bold ? "_Bold" : "")+"_"+letter); }

		@Override
		public AssetLocation id() {
			return asLocation();
		}

		@Override
		public int width() {
			return this.width + 2;
		}

		@Override
		public int height() {
			return this.height + 2;
		}
		
		
	}
}
