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

import java.nio.ByteBuffer;

import org.lwjgl.PointerBuffer;
import org.lwjgl.system.MemoryStack;
import org.lwjgl.system.MemoryUtil;
import org.lwjgl.util.freetype.FT_Bitmap;
import org.lwjgl.util.freetype.FT_Face;
import org.lwjgl.util.freetype.FT_GlyphSlot;
import org.lwjgl.util.freetype.FT_Vector;
import org.lwjgl.util.freetype.FreeType;
import org.lwjgl.util.harfbuzz.HarfBuzz;
import org.lwjgl.util.harfbuzz.hb_glyph_position_t;

import com.google.gson.JsonObject;

import speiger.src.collections.ints.sets.IntOpenHashSet;
import speiger.src.collections.ints.sets.IntSet;
import speiger.src.collections.longs.misc.pairs.LongObjectPair;
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 FreeTypeProvider implements IFontProvider {
	
	FreeTypeInstance[] instance;
	
	public FreeTypeProvider(FreeTypeInstance[] instance) {
		this.instance = instance;
	}
	
	public static IFontProvider create(ID location, IAssetProvider provider) {
		try {
			return load(provider.get(location).jsonObj(), provider);
		}
		catch(Exception e) { e.printStackTrace(); }
		return null;
	}
	
	public static IFontProvider load(JsonObject data, IAssetProvider provider) {
		long library = FreeTypeLibrary.get();
		if(library == 0L) return null;
		FreeTypeInstance[] instances = new FreeTypeInstance[4];
		instances[0] = create(library, 0, data.getAsJsonObject("regular"), provider);
		if(instances[0] == null) return null;
		instances[1] = create(library, 1, data.getAsJsonObject("bold"), provider);
		instances[2] = create(library, 2, data.getAsJsonObject("italic"), provider);
		instances[3] = create(library, 3, data.getAsJsonObject("bold_italic"), provider);
		return new FreeTypeProvider(instances);
	}
	
	private static FreeTypeInstance create(long library, 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", 0F);
			yOff = JsonUtil.getOrDefault(shift, "y", 0F);
		}
		JsonUtil.iterateValues(obj.get("skip"), T -> builder.append(T.getAsString()));
		LongObjectPair<FT_Face> value = parse(location, provider, library);
		if(value == null) return null;
		return new FreeTypeInstance(style, value.getLongKey(), value.getValue(), oversample, xOff, yOff, shadowOffset, builder.toString());
	}
	
	private static LongObjectPair<FT_Face> parse(ID location, IAssetProvider provider, long library) {
		try(MemoryStack stack = MemoryStack.stackPush()) {
			ByteBuffer buffer = provider.get(location).parse(Buffers.DIRECT);
			PointerBuffer facePointer = stack.mallocPointer(1);
			if(FreeTypeLibrary.parseError(FreeType.FT_New_Memory_Face(library, buffer, 0L, facePointer), "Creating Font Face")) {
				MemoryUtil.memFree(buffer);
				return null;
			}
			FT_Face face = FT_Face.create(facePointer.get());
            String s = FreeType.FT_Get_Font_Format(face);
            if(!"TrueType".equals(s)) {
				MemoryUtil.memFree(buffer);
				throw new IllegalStateException("Font type ["+s+"] is not true type");
            }
            if(FreeTypeLibrary.parseError(FreeType.FT_Select_Charmap(face, FreeType.FT_ENCODING_UNICODE), "Applying Unicode Encoding")) {
				MemoryUtil.memFree(buffer);
				return null;
            }
			return LongObjectPair.of(MemoryUtil.memAddress(buffer), face);
		}
		catch(Exception exception) {
			exception.printStackTrace();
			return null;
		}
	}
	
	@Override
	public UnbakedGlyth glythData(int codepoint, int style, float size, float oversample) {
		FreeTypeInstance instance = this.instance[style & 0x3];
		return instance != null ? instance.gylthData(codepoint, size, oversample) : null;
	}
	
	@Override
	public void close() {
		for(int i = 0;i<4;i++) {
			if(instance[i] != null) {
				instance[i].free();
			}
		}
		instance = null;
	}
	
	public static class FreeTypeGlyth implements UnbakedGlyth {
		final FT_Face face;
        final int width;
        final int height;
        final float xOff;
        final float yOff;
        final float oversample;
        private final float advance;
        final int glyth;
        final int glythCodepoint;
        final long harfBuzzFont;
        
		public FreeTypeGlyth(FT_Face face, long harfBuzzFont, float xOff, float yOff, int width, int height, float advance, float oversample, int glyth, int glythCodepoint) {
			this.face = face;
			this.harfBuzzFont = harfBuzzFont;
			this.width = width;
			this.height = height;
			this.oversample = oversample;
			this.advance = advance / oversample;
			this.xOff = xOff / oversample;
			this.yOff = yOff / oversample;
			this.glyth = glyth;
			this.glythCodepoint = glythCodepoint;
		}

		@Override
		public float advance() {
			return advance;
		}

		@Override
		public float kerning(int codepoint) {
			int index = FreeType.FT_Get_Char_Index(face, codepoint);
			if(index == 0) return 0;
			StringBuilder builder = new StringBuilder();
			builder.append(Character.toChars(codepoint));
			builder.append(Character.toChars(glythCodepoint));
			float hbresult = (getAdvance(Character.toString(codepoint)) - getAdvance(builder.toString())) / 64F;
			return hbresult;
		}
		
		private float getAdvance(String adv) {
			long id = HarfBuzz.hb_buffer_create();
			try {
				HarfBuzz.hb_buffer_add_utf8(id, adv, 0, adv.length());
				HarfBuzz.hb_buffer_guess_segment_properties(id);
				HarfBuzz.hb_shape(harfBuzzFont, id, null);
				hb_glyph_position_t.Buffer positions = HarfBuzz.hb_buffer_get_glyph_positions(id);
				float result = positions.hasRemaining() ? positions.x_advance() : 0F;
				HarfBuzz.hb_buffer_destroy(positions.address());
				return result;
			}
			finally {
				HarfBuzz.hb_buffer_destroy(id);
			}
		}
		
		@Override
		public Glyth bake(GlythBaker baker) {
			return baker.bake(new IGlythSheetInfo() {
				@Override
				public float xOffset() { return xOff; }
				@Override
				public float yOffset() { return yOff; }
				@Override
				public int width() { return width; }
				@Override
				public int height() { return height; }
				@Override
				public float oversample() { return oversample; }
				@Override
				public void upload(Texture texture, int x, int y) {
					Drawable drawable = new Drawable(FontTexture.formatByColor(false), width, height);
					if(drawable.drawFont(face, glyth)) {
						drawable.upload(GraphicsDevice.get().queue(), texture, x, y, 0, 0, width, height);
					}
					drawable.close();
				}
				
				@Override
				public boolean isColored() { return false; }
			});
		}
		
	}
	
	public static class FreeTypeInstance {
		final int style;
		long data;
		final FT_Face face;
		IntSet skip = new IntOpenHashSet();
		final float oversample;
		final float xOff;
		final float yOff;
		final float shadowOffset;
		final long harfBuzzFont;
		
		public FreeTypeInstance(int style, long data, FT_Face face, float oversample, float xOff, float yOff, float shadowOffset, String skip) {
			this.style = style;
			this.data = data;
			this.face = face;
			this.harfBuzzFont = HarfBuzz.hb_ft_font_create_referenced(face.address());
			skip.codePoints().forEach(this.skip::add);
			this.oversample = oversample;
			this.xOff = xOff;
			this.yOff = yOff;
			this.shadowOffset = shadowOffset;
	        try(MemoryStack stack = MemoryStack.stackPush()) {
	            FT_Vector ft_vector = FT_Vector.malloc(stack).set(Math.round(oversample * xOff * 64F), Math.round(oversample * -yOff * 64F)); 
	            FreeType.FT_Set_Transform(face, null, ft_vector);
			}
		}
		
		public void free() {
			if(data == 0L) return;
			FreeType.FT_Done_Face(face);
			MemoryUtil.nmemFree(data);
			data = 0L;
		}
		
		public UnbakedGlyth gylthData(int codepoint, float size, float oversample) {
			if(skip.contains(codepoint)) return null;
			int index = FreeType.FT_Get_Char_Index(face, codepoint);
			if(index == 0) return null;
			oversample *= this.oversample;
	        int pixels = Math.round(size * oversample);
	        if(FreeTypeLibrary.parseError(FreeType.FT_Set_Pixel_Sizes(face, 0, pixels), "Set Pixel Size")) return null;
			if(FreeTypeLibrary.parseError(FreeType.FT_Load_Glyph(face, index, FreeType.FT_LOAD_NO_BITMAP | FreeType.FT_LOAD_BITMAP_METRICS_ONLY), "Loading Glyth")) return null;
			FT_GlyphSlot slot = face.glyph();
			if(slot == null) {
				System.out.println("Glyth didn't load for some reason");
				return null;
			}
			float advance = slot.advance().x() / 64F;
			FT_Bitmap bitmap = slot.bitmap();
			
			int left = slot.bitmap_left();
			int top = slot.bitmap_top();
			int width = bitmap.width();
			int height = bitmap.rows();
			if(width > 0 && height > 0) return new FreeTypeGlyth(face, harfBuzzFont, left, -top, width, height, advance, oversample, index, codepoint);
			return new EmptyGlythData(advance / oversample);
		}
	}
	
	public static class FreeTypeLibrary {
		private static long pointer = 0L;
		
		public static long get() {
			if(pointer == 0L) {
				try(MemoryStack stack = MemoryStack.stackPush()) {
					PointerBuffer pointBuffer = stack.callocPointer(1);
					int result = FreeType.FT_Init_FreeType(pointBuffer);
					if(result != 0) {
						throw new IllegalStateException(FreeType.FT_Error_String(result));
					}
					pointer = pointBuffer.get();
				}
			}
			return pointer;
		}
		
		public static boolean parseError(int result, String action) {
			if(result == 0) return false;
			String error = FreeType.FT_Error_String(result);
			System.out.println("Couldn't do ["+action+"] because of "+error);
			return true;
		}
		
		public static void close() {
			if(pointer == 0L) return;
			FreeType.FT_Done_Library(pointer);
			pointer = 0L;
		}
	}
}
