package speiger.src.coreengine.platform.graphics.api.texture.drawable;

import java.nio.ByteBuffer;
import java.util.Objects;

import org.lwjgl.stb.STBImage;
import org.lwjgl.stb.STBTTFontinfo;
import org.lwjgl.stb.STBTruetype;
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.FreeType;

import speiger.src.coreengine.math.color.ColorSpaces;
import speiger.src.coreengine.platform.graphics.api.core.GraphicsCommandQueue;
import speiger.src.coreengine.platform.graphics.api.shader.states.GraphicsDataType;
import speiger.src.coreengine.platform.graphics.api.texture.Texture;
import speiger.src.coreengine.platform.graphics.api.texture.states.TextureFormat;

public class Drawable implements IDrawable, AutoCloseable {
	TextureFormat format;
	int width;
	int height;
	long components;
	long pixels;
	boolean isSTB;
	
	public Drawable(TextureFormat format, int width, int height) {
		this(format, width, height, MemoryUtil.nmemAllocChecked(format.components() * width * height), false);
	}
	
	public Drawable(TextureFormat format, int width, int height, long pixels, boolean isSTB) {
		this.format = format;
		this.width = width;
		this.height = height;
		this.pixels = pixels;
		this.components = format.components();
		this.isSTB = isSTB;
	}
	
	@Override
	public ColorSpaces colorspace() { return ColorSpaces.ABGR; }
	@Override
	public int width() { return width; }
	@Override
	public int height() { return height; }
	public long pixels() { return pixels; }
	public boolean isSTBImage() { return isSTB; }
	public TextureFormat format() { return format; }
	
	@Override
	public void close() {
		if(pixels == 0L) return;
		if(isSTB) STBImage.nstbi_image_free(pixels);
		else MemoryUtil.nmemFree(pixels);
		pixels = 0L;
	}
	
	protected long offset(int x, int y) {
		return ((y * width()) + x) * components;
	}
	
	protected void ensureValid(int index) {
		ensureValid(index % width, index / width);
	}
	
	protected void ensureValid(int x, int y) {
		if(x < 0 || y < 0) throw new ArrayIndexOutOfBoundsException("Index out of bounds: X=["+x+"], Y=["+y+"]");
		if(x > width || y > height) throw new ArrayIndexOutOfBoundsException("Index out of bounds: X=["+x+"], Y=["+y+"], width=["+width+"], height=["+height+"]");
		if(pixels == 0L) throw new IllegalStateException("Pixel Data doesn't exist");
	}
	
	public boolean drawFont(FT_Face face, int glyth) {
		ensureValid(0);
		if(components != 1) throw new IllegalStateException("Format has to be 1 component");
		if(FreeType.FT_Load_Glyph(face, glyth, 4) != 0) return false;
		
		FT_GlyphSlot slot = Objects.requireNonNull(face.glyph());
		FT_Bitmap map = slot.bitmap();
		if(map.pixel_mode() != FreeType.FT_PIXEL_MODE_GRAY) throw new IllegalStateException("Pixel isn't a grayscale picture");
		if(map.width() != width() || map.rows() != height()) throw new IllegalStateException("Bounds do not match");
		int size = map.width() * map.rows();
		ByteBuffer buffer = Objects.requireNonNull(map.buffer(size));
		MemoryUtil.memCopy(MemoryUtil.memAddress(buffer), pixels(), size);
		return true;
	}
	
	public void drawFont(STBTTFontinfo info, int glyth, float sourceX, float sourceY, int targetX, int targetY, int width, int height, float scaleX, float scaleY) {
		ensureValid(targetX, targetY);
		ensureValid(targetX + width, targetY + height);
		if(components != 1) throw new IllegalStateException("Format has to be 1 component");
		STBTruetype.nstbtt_MakeGlyphBitmapSubpixel(info.address(), pixels + offset(targetX, targetY), width, height, width(), scaleX, scaleY, sourceX, sourceY, glyth);
	}
	
	public void upload(GraphicsCommandQueue queue, Texture texture) {
		if(texture.width() != width || texture.height() != height) throw new IllegalStateException("Texture[w="+texture.width()+",h="+texture.height()+"] bounds does not match the drawable [w="+width+",h="+height+"]");
		queue.writeToTexture(texture, format, GraphicsDataType.UNSIGNED_BYTE, pixels);
	}
	
	public void upload(GraphicsCommandQueue queue, Texture texture, int targetX, int targetY, int sourceX, int sourceY, int width, int height) {
		upload(queue, texture, 0, targetX, targetY, sourceX, sourceY, width, height);
	}
	
	public void upload(GraphicsCommandQueue queue, Texture texture, int level, int targetX, int targetY, int sourceX, int sourceY, int width, int height) {
		ensureValid(sourceX, sourceY);
		ensureValid(sourceX + width, sourceY + height);
		queue.writeToTexture(texture, level, sourceX, sourceY, targetX, targetY, width, height, format, GraphicsDataType.UNSIGNED_BYTE, pixels);
	}
	
	@Override
	public void set(int index, int data) {
		ensureValid(index);
		if(components != 4) throw new IllegalStateException("Format has to be 4 components");
		MemoryUtil.memPutInt(this.pixels + index * 4L, data);
	}

	@Override
	public void setR(int index, int red) {
		if(!format.hasRed()) throw new IllegalArgumentException("Format doesn't support Red/Luminance");
		ensureValid(index);
		MemoryUtil.memPutByte(this.pixels + index * components + format.redOffset(), (byte)(red & 0xFF));
	}

	@Override
	public void setG(int index, int green) {
		if(!format.hasGreen()) throw new IllegalArgumentException("Format doesn't support Green");
		ensureValid(index);
		MemoryUtil.memPutByte(this.pixels + index * components + format.greenOffset(), (byte)(green & 0xFF));
	}

	@Override
	public void setB(int index, int blue) {
		if(!format.hasBlue()) throw new IllegalArgumentException("Format doesn't support Blue");
		ensureValid(index);
		MemoryUtil.memPutByte(this.pixels + index * components + format.blueOffset(), (byte)(blue & 0xFF));
	}

	@Override
	public void setA(int index, int alpha) {
		if(!format.hasAlpha()) throw new IllegalArgumentException("Format doesn't support Alpha");
		ensureValid(index);
		MemoryUtil.memPutByte(this.pixels + index * components + format.alphaOffset(), (byte)(alpha & 0xFF));
	}

	@Override
	public void fill(int x, int y, int width, int height, int data) {
		if(components != 4 && components != 1) throw new IllegalStateException("Format has to be 1 or 4 components");
		ensureValid(x, y);
		ensureValid(x+width, y+height);
		if(components == 1) {
			for(int xOff = 0;xOff<width;xOff++) {
				for(int yOff = 0;yOff<height;yOff++) {
					MemoryUtil.memPutByte(this.pixels + offset(x+xOff, y+yOff), (byte)(data & 0xFF));
				}
			}
			return;
		}
		
		for(int xOff = 0;xOff<width;xOff++) {
			for(int yOff = 0;yOff<height;yOff++) {
				MemoryUtil.memPutInt(this.pixels + offset(x+xOff, y+yOff), data);
			}
		}
	}
	
	@Override
	public int get(int index) {
		if(components != 4) throw new IllegalStateException("Format has to be 4 components");
		ensureValid(index);
		return MemoryUtil.memGetInt(this.pixels + index * 4L);
	}
	
	@Override
	public int getR(int index) {
		if(!format.hasRed()) throw new IllegalArgumentException("Format doesn't support Red/Luminance");
		ensureValid(index);
		return MemoryUtil.memGetByte(this.pixels + index * components + format.redOffset());
	}
	
	@Override
	public int getG(int index) {
		if(!format.hasGreen()) throw new IllegalArgumentException("Format doesn't support Green");
		ensureValid(index);
		return MemoryUtil.memGetByte(this.pixels + index * components + format.greenOffset());
	}
	
	@Override
	public int getB(int index) {
		if(!format.hasBlue()) throw new IllegalArgumentException("Format doesn't support Blue");
		ensureValid(index);
		return MemoryUtil.memGetByte(this.pixels + index * components + format.blueOffset());
	}
	
	@Override
	public int getA(int index) {
		if(!format.hasAlpha()) throw new IllegalArgumentException("Format doesn't support Alpha");
		ensureValid(index);
		return MemoryUtil.memGetByte(this.pixels + index * components + format.alphaOffset());
	}
}
