package speiger.src.coreengine.ui.gui.font.providers;

import java.nio.ByteBuffer;
import java.nio.IntBuffer;

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

import com.google.gson.JsonObject;

import speiger.src.collections.ints.sets.IntOpenHashSet;
import speiger.src.collections.ints.sets.IntSet;
import speiger.src.coreengine.assets.api.IAssetProvider;
import speiger.src.coreengine.assets.api.ID;
import speiger.src.coreengine.assets.impl.parsers.Buffers;
import speiger.src.coreengine.platform.graphics.api.core.GraphicsDevice;
import speiger.src.coreengine.platform.graphics.api.texture.Texture;
import speiger.src.coreengine.platform.graphics.api.texture.drawable.Drawable;
import speiger.src.coreengine.ui.gui.font.FontTexture;
import speiger.src.coreengine.ui.gui.font.glyth.Glyth;
import speiger.src.coreengine.ui.gui.font.glyth.IGlythSheetInfo;
import speiger.src.coreengine.ui.gui.font.glyth.UnbakedGlyth;
import speiger.src.coreengine.ui.gui.font.glyth.UnbakedGlyth.EmptyGlythData;
import speiger.src.coreengine.utils.helpers.JsonUtil;

public class STBTrueTypeProvider implements IFontProvider {
	TrueTypeInstance[] instances;
	
	public STBTrueTypeProvider(TrueTypeInstance[] instances) {
		this.instances = instances;
	}
	
	public static IFontProvider create(ID location, IAssetProvider provider) {
		try {
			return create(provider.get(location).jsonObj(), provider);
		}
		catch(Exception e) { e.printStackTrace(); }
		return null;
	}
	
	public static IFontProvider create(JsonObject data, IAssetProvider provider) {
		TrueTypeInstance[] instances = new TrueTypeInstance[4];
		instances[0] = create(0, data.getAsJsonObject("regular"), provider);
		if(instances[0] == null) return null;
		instances[1] = create(1, data.getAsJsonObject("bold"), provider);
		instances[2] = create(2, data.getAsJsonObject("italic"), provider);
		instances[3] = create(3, data.getAsJsonObject("bold_italic"), provider);
		return new STBTrueTypeProvider(instances);
	}
	
	private static TrueTypeInstance create(int style, JsonObject obj, IAssetProvider provider) {
		if(obj == null || !obj.has("file")) return null;
		ID location = ID.of(obj.get("file").getAsString());
		float oversample = JsonUtil.getOrDefault(obj, "oversample", 1F);
		float shadowOffset = JsonUtil.getOrDefault(obj, "shadowOffset", 1F);
		float xOff = 0;
		float yOff = 0;
		StringBuilder builder = new StringBuilder();
		JsonObject shift = obj.getAsJsonObject("offset");
		if(shift != null) {
			xOff = JsonUtil.getOrDefault(shift, "x", 0);
			yOff = JsonUtil.getOrDefault(shift, "y", 0);
		}
		JsonUtil.iterateValues(obj.get("skip"), T -> builder.append(T.getAsString()));
		try {
			ByteBuffer buffer = provider.get(location).parse(Buffers.DIRECT);
			STBTTFontinfo info = STBTTFontinfo.create();
			if(!STBTruetype.stbtt_InitFont(info, buffer)) {
				System.out.println("Couldn't load font");
				MemoryUtil.memFree(buffer);
				info.free();
				return null;
			}
			return new TrueTypeInstance(style, MemoryUtil.memAddress(buffer), info, oversample, xOff, yOff, shadowOffset, builder.toString());
		}
		catch(Exception e) {
			e.printStackTrace();
		}
		return null;
	}
	
	public static class TrueTypeInstance {
		final int style;
		long data;
		final STBTTFontinfo info;
		IntSet skip = new IntOpenHashSet();
		final float oversample;
		final float xOff;
		final float yOff;
		final float shadowOffset;
		final float ascent;
		
		public TrueTypeInstance(int style, long data, STBTTFontinfo info, float oversample, float xOff, float yOff, float shadowOffset, String skip) {
			this.style = style;
			this.data = data;
			this.info = info;
			skip.codePoints().forEach(this.skip::add);
			this.oversample = oversample;
			this.xOff = xOff;
			this.yOff = yOff;
			this.shadowOffset = shadowOffset;
			int[] ascent = new int[1];
			STBTruetype.stbtt_GetFontVMetrics(info, ascent, new int[1], new int[1]);
			this.ascent = ascent[0];
		}
		
		public void free() {
			if(data == 0L) return;
			info.free();
			MemoryUtil.nmemFree(data);
			data = 0L;
		}
		
		public UnbakedGlyth glythData(int codepoint, float size, float oversample) {
			if(skip.contains(codepoint)) return null;
			int glyth = STBTruetype.nstbtt_FindGlyphIndex(info.address(), codepoint);
			if(glyth == 0) return null;
			oversample *= this.oversample;
			float scale = STBTruetype.stbtt_ScaleForPixelHeight(info, size * oversample);
			try(MemoryStack stack = MemoryStack.stackPush()) {
				IntBuffer left = stack.mallocInt(1);
				IntBuffer bottom = stack.mallocInt(1);
				IntBuffer right = stack.mallocInt(1);
				IntBuffer top = stack.mallocInt(1);
				IntBuffer advance = stack.mallocInt(1);
				IntBuffer leftSideBearing = stack.mallocInt(1);	
				STBTruetype.stbtt_GetGlyphHMetrics(info, glyth, advance, leftSideBearing);
				STBTruetype.stbtt_GetGlyphBitmapBoxSubpixel(info, glyth, scale, scale, xOff, yOff, left, bottom, right, top);
				int minX = left.get(0);
				int minY = -top.get(0);
				int maxX = right.get(0);
				int maxY = -bottom.get(0);
				if(maxX - minX <= 0 || maxY - minY <= 0) return new EmptyGlythData(advance.get(0) * scale / oversample);
				return new STBGlyth(this, minX, minY, maxX, maxY, advance.get(0), leftSideBearing.get(0), scale, oversample, glyth);
			}
			catch(Exception e) {
				e.printStackTrace();
				return null;
			}
		}
	}
	
	@Override
	public UnbakedGlyth glythData(int codepoint, int style, float size, float oversample) {
		TrueTypeInstance instance = instances[style & 0x3];
		return instance == null ? null : instance.glythData(codepoint, size, oversample);
	}

	@Override
	public void close() {
		if(instances == null) return;
		for(int i = 0;i<4;i++) {
			if(instances[i] == null) continue;
			instances[i].free();
		}
		instances = null;
	}
	
	private static class STBGlyth implements UnbakedGlyth {
		final TrueTypeInstance owner;
		final float xOffset;
		final float yOffset;
		final int width;
		final int height;
		final float oversample;
		final float scale;
		final float advance;
		final int glyth;
		
		public STBGlyth(TrueTypeInstance owner, int minX, int minY, int maxX, int maxY, float advance, float leftPadding, float scale, float oversample, int glyth) {
			this.owner = owner;
			this.width = maxX - minX;
			this.height = maxY - minY;
			this.scale = scale;
			this.oversample = oversample;
	        this.xOffset = ((leftPadding * scale) + minX + owner.xOff) / oversample;
	        this.yOffset = ((owner.ascent * scale) - maxY + owner.yOff) / oversample;
			this.advance = advance * scale / oversample;
			this.glyth = glyth;
		}
		
		@Override
		public float advance() { return advance; }
		@Override
		public float shadowOffset() { return owner.shadowOffset; }
		@Override
		public float kerning(int codepoint) { return STBTruetype.stbtt_GetCodepointKernAdvance(owner.info, codepoint, glyth) * scale / oversample; }
		@Override
		public Glyth bake(GlythBaker baker) {
			return baker.bake(new IGlythSheetInfo() {
				@Override
				public float xOffset() { return xOffset; }
				@Override
				public float yOffset() { return yOffset; }
				@Override
				public int width() { return width; }
				@Override
				public int height() { return height; }
				@Override
				public float oversample() { return oversample; }
				@Override
				public boolean isColored() { return false; }
				@Override
				public void upload(Texture texture, int x, int y) {
					Drawable drawable = new Drawable(FontTexture.formatByColor(false), width, height);
					drawable.drawFont(owner.info, glyth, owner.xOff, owner.yOff, 0, 0, width, height, scale, scale);
					drawable.upload(GraphicsDevice.get().queue(), texture, x, y, 0, 0, width, height);
					drawable.close();
				}
			});
		}
	}
}
