#define CC_DYNAMIC_VBS_ARE_STATIC
#include "../_GraphicsBase.h"
#include "../Errors.h"
#include "../Logger.h"
#include "../Window.h"

#include <3ds.h>
#define BUFFER_BASE_PADDR OS_VRAM_PADDR // VRAM physical address
#include "../../third_party/citro3d.c"

#include "gsp_gpu.h"
#include "pica_gpu.h"

// See the .v.pica shader files in misc/3ds
#define CONST_MVP 0 // c0-c3
#define CONST_TEX 4 // c4
#define CONST_255 5 // c5

// autogenerated from the .v.pica files by Makefile
extern const u8  coloured_shbin[];
extern const u32 coloured_shbin_size;

extern const u8  textured_shbin[];
extern const u32 textured_shbin_size;

extern const u8  offset_shbin[];
extern const u32 offset_shbin_size;
	
static void GPUBuffers_DeleteUnreferenced(void);
static void GPUTextures_DeleteUnreferenced(void);

static cc_uint32 frameCounter1;
static PackedCol clear_color;
static cc_bool rendering3D;

#define DISPLAY_TRANSFER_FLAGS \
	(GX_TRANSFER_FLIP_VERT(0) | GX_TRANSFER_OUT_TILED(0) | GX_TRANSFER_RAW_COPY(0) | \
	GX_TRANSFER_IN_FORMAT(GX_TRANSFER_FMT_RGBA8) | GX_TRANSFER_OUT_FORMAT(GX_TRANSFER_FMT_RGB8) | \
	GX_TRANSFER_SCALING(GX_TRANSFER_SCALE_NO))
	
	
/*########################################################################################################################*
*------------------------------------------------------Vertex shaders-----------------------------------------------------*
*#########################################################################################################################*/
static C3D_Mtx _mvp;
static int texOffset, dirty_mvp;

struct CCShader {
	DVLB_s* dvlb;
	shaderProgram_s program;
};
static struct CCShader* gfx_activeShader;
static struct CCShader shaders[3];

static void Shader_Alloc(struct CCShader* shader, const u8* binData, int binSize) {
	shader->dvlb = DVLB_ParseFile((u32*)binData, binSize);
	shaderProgramInit(&shader->program);
	shaderProgramSetVsh(&shader->program, &shader->dvlb->DVLE[0]);
}

// Switches program to one that can render current vertex format and state
// Loads program and reloads uniforms if needed
static void UpdateAttribFormat(void);
static void SwitchProgram(void) {
	struct CCShader* shader;
	int index = 0;

	if (gfx_format == VERTEX_FORMAT_TEXTURED) index++;
	if (texOffset) index = 2;

	shader = &shaders[index];
	if (shader != gfx_activeShader) {
		gfx_activeShader = shader;
		shaderProgramConfigure(&shader->program, true, false);
	}
	// TODO avoid explicitly calling this. but needed right now
	UpdateAttribFormat();
}


/*########################################################################################################################*
*------------------------------------------------------Command buffer-----------------------------------------------------*
*#########################################################################################################################*/
static u32* _cmdBuf;
static int  _cmdBufWords;
#define DEFAULT_MAX_CMDS (256 * 1024)

static void CmdBuf_Reset(void) {
	GPUCMD_SetBuffer(_cmdBuf, _cmdBufWords, 0);
}

static void CmdBuf_Alloc(int size) {
	size = (size + 0x0F) & ~0x0F; // 0x10-byte align
	_cmdBufWords = size / 4;
	_cmdBuf      = (u32*)linearAlloc(size);

	if (!_cmdBuf) Process_Abort("Failed to allocate command buffer");
	CmdBuf_Reset();
}


/*########################################################################################################################*
*---------------------------------------------------------General---------------------------------------------------------*
*#########################################################################################################################*/
static C3D_RenderTarget topTargetLeft;
static C3D_RenderTarget topTargetRight;
static C3D_RenderTarget bottomTarget;
static cc_bool createdTopTargetRight;
static C3D_RenderTarget* topTarget;

static void AllocShaders(void) {
	Shader_Alloc(&shaders[0], coloured_shbin, coloured_shbin_size);
	Shader_Alloc(&shaders[1], textured_shbin, textured_shbin_size);
	Shader_Alloc(&shaders[2], offset_shbin,   offset_shbin_size);
}

static void SetDefaultState(void) {
	Gfx_SetFaceCulling(false);
	Gfx_SetAlphaTest(false);
	Gfx_SetDepthWrite(true);
}

static aptHookCookie hookCookie;
static void AptEventHook(APT_HookType hookType, void* param) {
	if (hookType == APTHOOK_ONSUSPEND) {
		C3Di_RenderQueueWaitDone();
	} else if (hookType == APTHOOK_ONRESTORE) {
		C3Di_OnRestore();
	}
}

static void SetInitialStates(void) {
	extern void C3Di_UpdateContext(void);
	C3Di_UpdateContext();

	static C3D_FVec _1_div_255 = { .x = 1/255.0f, .y = 1/255.0f, .z = 1/255.0f, .w = 1/255.0f };
	pica_upload_vec4_constant(CONST_255, &_1_div_255);

	// NOTE: GPUREG_VERTEX_OFFSET only works when drawing non-indexed arrays
	GPUCMD_AddWrite(GPUREG_VERTEX_OFFSET, 0);

	// https://github.com/devkitPro/citro3d/issues/47
	// "Fyi the permutation specifies the order in which the attributes are stored in the buffer, LSB first. So 0x210 indicates attributes 0, 1 & 2."
	// This just maps array attrib 0 = vertex attrib 0, array attrib 1 = vertex attrib 1, etc
	pica_set_attrib_array0_mapping(0x210);
	pica_set_vsh_input_mapping(0x210, 0);
}

static void InitCitro3D(void) {	
	CmdBuf_Alloc(DEFAULT_MAX_CMDS * 4);
	C3D_Init();
	aptHook(&hookCookie, AptEventHook, NULL);

	C3D_RenderTargetInit(&topTargetLeft,  240, 400);
	C3D_RenderTargetColor(&topTargetLeft, GPU_RB_RGBA8);
	C3D_RenderTargetDepth(&topTargetLeft, GPU_RB_DEPTH24);
	C3D_RenderTargetSetOutput(&topTargetLeft, GFX_TOP, GFX_LEFT, DISPLAY_TRANSFER_FLAGS);

	// Even though the bottom screen is 320 pixels wide, we use 400 here so that the same ortho matrix
	// can be used for both screens. The output is clipped to the actual screen width, anyway.
	// TODO avoid this	
	C3D_RenderTargetInit(&bottomTarget, 240, 400);
	C3D_RenderTargetColor(&bottomTarget, GPU_RB_RGBA8);
	C3D_RenderTargetSetOutput(&bottomTarget, GFX_BOTTOM, GFX_LEFT, DISPLAY_TRANSFER_FLAGS);

	// Allocate right framebuffer (for stereoscopic 3D) in advance even when it doesn't actually used
	//  Although this means a bit less VRAM available for textures, this ensures that if the user later 
	//  turns on stereoscopic 3D, don't have to try handling the case when insufficient VRAM for it
	C3D_RenderTargetInit(&topTargetRight,  240, 400);
	C3D_RenderTargetColor(&topTargetRight, GPU_RB_RGBA8);
	C3D_RenderTargetDepth(&topTargetRight, GPU_RB_DEPTH24);

	gfxSetDoubleBuffering(GFX_TOP, true);
	SetDefaultState();
	AllocShaders();

	GSP_setup();
	SetInitialStates();
}

static GfxResourceID white_square;
void Gfx_Create(void) {
	if (!Gfx.Created) InitCitro3D();
	else C3Di_RenderQueueInit();
	
	Gfx.MaxTexWidth  = 1024;
	Gfx.MaxTexHeight = 1024;
	Gfx.MaxTexSize   = 512 * 512;
	
	Gfx.MinTexWidth  = 8;
	Gfx.MinTexHeight = 8;
	Gfx.Created      = true;
	gfx_vsync        = true;
	
	Gfx.NonPowTwoTexturesSupport = GFX_NONPOW2_UPLOAD;
	Gfx_RestoreState();
}

void Gfx_Free(void) {
	Gfx_FreeState();
	C3Di_RenderQueueExit();
	gfxSet3D(false);

	// C3D_Fini()
	// aptUnhook(&hookCookie);

	frameCounter1 += 5;
	GPUBuffers_DeleteUnreferenced();
	GPUTextures_DeleteUnreferenced();
}

cc_bool Gfx_TryRestoreContext(void) { return true; }

static void Gfx_RestoreState(void) {
	InitDefaultResources();
 	
	// 8x8 dummy white texture
	//  (textures must be at least 8x8, see C3D_TexInitWithParams source)
	struct Bitmap bmp;
	BitmapCol pixels[8*8];
	Mem_Set(pixels, 0xFF, sizeof(pixels));
	Bitmap_Init(bmp, 8, 8, pixels);
	white_square = Gfx_CreateTexture(&bmp, 0, false);
}

static void Gfx_FreeState(void) {
	FreeDefaultResources(); 
	Gfx_DeleteTexture(&white_square);
}

void Gfx_3DS_SetRenderScreen(enum Screen3DS screen) {
	C3D_FrameDrawOn(screen == TOP_SCREEN ? topTarget : &bottomTarget);
}


/*########################################################################################################################*
*----------------------------------------------------Stereoscopic support-------------------------------------------------*
*#########################################################################################################################*/
static void Calc3DProjection(int dir, struct Matrix* proj) {
	struct Matrix proj_adj = *proj;

	// See mtx_perspstereotilt
	float slider = osGet3DSliderState();
	float iod    = (slider / 3) * dir;
	float shift  = iod / (2.0f * 2.0f);

	proj_adj.row3.y = 1.0f * shift * -proj->row1.y;
	Gfx.Projection = proj_adj;
}

void Gfx_Set3DLeft(struct Matrix* proj, struct Matrix* view) {
	Calc3DProjection(-1, proj);
	rendering3D = true;
}

void Gfx_Set3DRight(struct Matrix* proj, struct Matrix* view) {
	Calc3DProjection(+1, proj);

	if (!createdTopTargetRight) {
		C3D_RenderTargetSetOutput(&topTargetRight, GFX_TOP, GFX_RIGHT, DISPLAY_TRANSFER_FLAGS);
		createdTopTargetRight = true;
	}

	C3D_RenderTargetClear(&topTargetRight, C3D_CLEAR_ALL, clear_color, 0);
	topTarget = &topTargetRight;
	C3D_FrameDrawOn(topTarget);
}

void Gfx_End3D(struct Matrix* proj, struct Matrix* view) {
	Gfx.Projection = *proj;

	topTarget = &topTargetLeft;
	C3D_FrameDrawOn(topTarget);
}

void Gfx_SetTopRight(void) {
	topTarget = &topTargetRight;
	C3D_FrameDrawOn(topTarget);
}


/*########################################################################################################################*
*--------------------------------------------------------GPU Textures-----------------------------------------------------*
*#########################################################################################################################*/
struct GPUTexture;
struct GPUTexture {
	cc_uint32* data;
	C3D_Tex texture;
	struct GPUTexture* next;
	cc_uint32 lastFrame;
};
static struct GPUTexture* del_textures_head;
static struct GPUTexture* del_textures_tail;

// can't delete textures until not used in any frames
static void GPUTexture_Unref(GfxResourceID* resource) {
	struct GPUTexture* tex = (struct GPUTexture*)(*resource);
	if (!tex) return;
	*resource = NULL;
	
	LinkedList_Append(tex, del_textures_head, del_textures_tail);
}

static void GPUTexture_Free(struct GPUTexture* tex) {
	void* addr = tex->texture.data;
	if (addrIsVRAM(addr)) {
		vramFree(addr);
	} else {
		linearFree(addr);
	}

	Mem_Free(tex);
}

static void GPUTextures_DeleteUnreferenced(void) {
	if (!del_textures_head) return;
	
	struct GPUTexture* tex;
	struct GPUTexture* next;
	struct GPUTexture* prev = NULL;
	
	for (tex = del_textures_head; tex != NULL; tex = next)
	{
		next = tex->next;
		
		if (tex->lastFrame + 4 > frameCounter1) {
			// texture was used within last 4 fames
			prev = tex;
		} else {
			// advance the head of the linked list
			if (del_textures_head == tex) 
				del_textures_head = next;
			// update end of linked list if necessary
			if (del_textures_tail == tex)
				del_textures_tail = prev;
			
			// unlink this texture from the linked list
			if (prev) prev->next = next;
			
			GPUTexture_Free(tex);
		}
	}
}


/*########################################################################################################################*
*---------------------------------------------------------Textures--------------------------------------------------------*
*#########################################################################################################################*/
static bool CreateNativeTexture(C3D_Tex* tex, u32 width, u32 height, int vram) {
	width    = Math_NextPowOf2(width);
	height   = Math_NextPowOf2(height);
	u32 size = width * height * 4;

	tex->data = vram ? vramAlloc(size) : NULL;
	if (!tex->data) tex->data = linearAlloc(size);
	if (!tex->data) return false;

	tex->width  = width;
	tex->height = height;
	tex->param  = GPU_TEXTURE_MODE(GPU_TEX_2D) |
					GPU_TEXTURE_MAG_FILTER(GPU_NEAREST) | GPU_TEXTURE_MIN_FILTER(GPU_NEAREST) |
					GPU_TEXTURE_WRAP_S(GPU_REPEAT)      | GPU_TEXTURE_WRAP_T(GPU_REPEAT);
	tex->fmt    = GPU_RGBA8;
	tex->size   = size;

	tex->border   = 0;
	tex->lodBias  = 0;
	tex->maxLevel = 0;
	tex->minLevel = 0;
	return true;
}

// Pixels are arranged in a recursive Z-order curve / Morton offset
// They are arranged into 8x8 tiles, where each 8x8 tile is composed of
//  four 4x4 subtiles, which are in turn composed of four 2x2 subtiles
// http://problemkaputt.de/gbatek-3ds-video-texture-swizzling.htm
static CC_INLINE void TwiddleCalcFactors(unsigned w, unsigned h, 
										unsigned* maskX, unsigned* maskY) {
	*maskX = 0b010101; // 3 interleaved X bits
	*maskY = 0b101010; // 3 interleaved Y bits

	// Lower 3 X and Y bits are always interleaved
	w >>= 4;
	h >>= 4;
	int shift = 6;

	for (; w > 0; w >>= 1) {
		*maskX += 0x01 << shift;
		shift  += 1;
	}

	for (; h > 0; h >>= 1) {
		*maskY += 0x01 << shift;
		shift  += 1;
	}
}

static void ToMortonTexture(C3D_Tex* tex, int originX, int originY, 
							struct Bitmap* bmp, int rowWidth) {
	int src_w = bmp->width,  dst_w = tex->width;
	int src_h = bmp->height, dst_h = tex->height;
	cc_uint32* dst = tex->data;
	cc_uint32* src = bmp->scan0;

	unsigned maskX, maskY;
	TwiddleCalcFactors(dst_w, dst_h, &maskX, &maskY);

	unsigned begX = 0, begY = 0;
	// Calculate start twiddled X and Y values
	for (int x = 0; x < originX; x++) { begX = (begX - maskX) & maskX; }
	for (int y = 0; y < originY; y++) { begY = (begY - maskY) & maskY; }

	unsigned Y = begY;
	for (int y = 0; y < src_h; y++)
	{
		unsigned X = begX;
		for (int x = 0; x < src_w; x++)
		{
			// need to flip image vertically
			dst[X | (maskY - Y)] = src[x];
			X = (X - maskX) & maskX;
		}
		Y = (Y - maskY) & maskY;
		src += rowWidth;
	}
	// TODO flush data cache GSPGPU_FlushDataCache
}


GfxResourceID Gfx_AllocTexture(struct Bitmap* bmp, int rowWidth, cc_uint8 flags, cc_bool mipmaps) {
	struct GPUTexture* tex = Mem_TryAllocCleared(1, sizeof(struct GPUTexture));
	if (!tex) return NULL;

	int can_vram = !(flags & TEXTURE_FLAG_DYNAMIC);
	bool success = CreateNativeTexture(&tex->texture, bmp->width, bmp->height, can_vram);
	if (!success) return NULL;
	
	ToMortonTexture(&tex->texture, 0, 0, bmp, rowWidth);
    return tex;
}

void Gfx_UpdateTexture(GfxResourceID texId, int x, int y, struct Bitmap* part, int rowWidth, cc_bool mipmaps) {
 	struct GPUTexture* tex = (struct GPUTexture*)texId;
	ToMortonTexture(&tex->texture, x, y, part, rowWidth);
}

void Gfx_DeleteTexture(GfxResourceID* texId) {
	GPUTexture_Unref(texId);
}

void Gfx_EnableMipmaps(void) { }
void Gfx_DisableMipmaps(void) { }

void Gfx_BindTexture(GfxResourceID texId) {
	if (!texId) texId = white_square; 
 	struct GPUTexture* tex = (struct GPUTexture*)texId;
	
	tex->lastFrame = frameCounter1;
	C3D_TexBind(0, &tex->texture);
}


/*########################################################################################################################*
*-----------------------------------------------------State management----------------------------------------------------*
*#########################################################################################################################*/
void Gfx_SetFaceCulling(cc_bool enabled) { 
	C3D_CullFace(enabled ? GPU_CULL_BACK_CCW : GPU_CULL_NONE);
}

void Gfx_SetAlphaArgBlend(cc_bool enabled) { }

static void SetAlphaBlend(cc_bool enabled) { 
	if (enabled) {
		C3D_AlphaBlend(GPU_BLEND_ADD, GPU_BLEND_ADD, GPU_SRC_ALPHA, GPU_ONE_MINUS_SRC_ALPHA, GPU_SRC_ALPHA, GPU_ONE_MINUS_SRC_ALPHA);
	} else {
		C3D_ColorLogicOp(GPU_LOGICOP_COPY);
	}
}

static void SetAlphaTest(cc_bool enabled) {
	C3D_AlphaTest(enabled, GPU_GREATER, 0x7F);
}

void Gfx_DepthOnlyRendering(cc_bool depthOnly) {
	cc_bool enabled = !depthOnly;
	SetColorWrite(enabled & gfx_colorMask[0], enabled & gfx_colorMask[1], 
				  enabled & gfx_colorMask[2], enabled & gfx_colorMask[3]);
}

void Gfx_ClearColor(PackedCol color) {
	// TODO find better way?
	clear_color = (PackedCol_R(color) << 24) | (PackedCol_G(color) << 16) | (PackedCol_B(color) << 8) | 0xFF;
}

static cc_bool depthTest, depthWrite;
static int colorWriteMask = GPU_WRITE_COLOR;

static void UpdateWriteState(void) {
	//C3D_EarlyDepthTest(true, GPU_EARLYDEPTH_GREATER, 0);
	//C3D_EarlyDepthTest(false, GPU_EARLYDEPTH_GREATER, 0);
	int writeMask = colorWriteMask;
	if (depthWrite) writeMask |= GPU_WRITE_DEPTH;
	C3D_DepthTest(depthTest, GPU_GEQUAL, writeMask);
}

void Gfx_SetDepthWrite(cc_bool enabled) {
	depthWrite = enabled;
	UpdateWriteState();
}

void Gfx_SetDepthTest(cc_bool enabled) { 
	depthTest = enabled;
	UpdateWriteState();
}

static void SetColorWrite(cc_bool r, cc_bool g, cc_bool b, cc_bool a) {
	int mask = 0;
	if (r) mask |= GPU_WRITE_RED;
	if (g) mask |= GPU_WRITE_GREEN;
	if (b) mask |= GPU_WRITE_BLUE;
	if (a) mask |= GPU_WRITE_ALPHA;
	
	colorWriteMask = mask;
	UpdateWriteState();
}


/*########################################################################################################################*
*-----------------------------------------------------------Misc----------------------------------------------------------*
*#########################################################################################################################*/
static BitmapCol* _3DS_GetRow(struct Bitmap* bmp, int y, void* ctx) {
	u8* fb    = (u8*)ctx;
	// Framebuffer is rotated 90 degrees	
	int width = bmp->width, height = bmp->height;

	for (int x = 0; x < width; x++)
	{
		int addr = (height - 1 - y + x * height) * 3; // TODO -1 or not
		int b = fb[addr + 0];
		int g = fb[addr + 1];
		int r = fb[addr + 2];
		bmp->scan0[x] = BitmapColor_RGB(r, g, b);
	}
	return bmp->scan0;
}

cc_result Gfx_TakeScreenshot(struct Stream* output) {
	BitmapCol tmp[512];
	u16 width, height;
	u8* fb = gfxGetFramebuffer(GFX_TOP, GFX_LEFT, &width, &height);

	// Framebuffer is rotated 90 degrees
	struct Bitmap bmp;
	bmp.scan0  = tmp;
	bmp.width  = height; 
	bmp.height = width;

	return Png_Encode(&bmp, output, _3DS_GetRow, false, fb);
}

void Gfx_GetApiInfo(cc_string* info) {
	String_Format1(info, "-- Using 3DS --\n", NULL);
	PrintMaxTextureInfo(info);

	int total = (int)vramSpaceFree();
	float totalMB = total / (1024.0f * 1024.0f);
	String_Format1(info, "VRAM free: %f2 MB", &totalMB);
}

void Gfx_SetVSync(cc_bool vsync) {
	gfx_vsync = vsync;
}

void Gfx_BeginFrame(void) {
	rendering3D = false;

	C3D_FrameBegin(0);
	topTarget = &topTargetLeft;
	C3D_FrameDrawOn(topTarget);
}

void Gfx_ClearBuffers(GfxBuffers buffers) {
	int targets = 0;
	if (buffers & GFX_BUFFER_COLOR) targets |= C3D_CLEAR_COLOR;
	if (buffers & GFX_BUFFER_DEPTH) targets |= C3D_CLEAR_DEPTH;
	
	C3D_RenderTargetClear(&topTargetLeft, targets, clear_color, 0);
}

void Gfx_EndFrame(void) {
	gfxSet3D(rendering3D);
	C3D_FrameFinish(0);
	//gfxFlushBuffers();
	//gfxSwapBuffers();
		
	GPUBuffers_DeleteUnreferenced();
	GPUTextures_DeleteUnreferenced();
	frameCounter1++;

	// wait for vblank for both screens
	if (gfx_vsync) GSP_wait_for_full_vblank();

	C3Di_WaitAndClearQueue(-1);
	CmdBuf_Reset();
}

void Gfx_OnWindowResize(void) {
	Gfx_SetViewport(0, 0, Game.Width, Game.Height);
}

void Gfx_SetViewport(int x, int y, int w, int h) {
	// x and y are swapped and start from right/bottom instead of left/top
	x = Game.Width  - w - x;
	y = Game.Height - h - y;

	C3D_SetViewport(y, x, h, w); 
}
void Gfx_SetScissor (int x, int y, int w, int h) { }


/*########################################################################################################################*
*----------------------------------------------------------Buffers--------------------------------------------------------*
*#########################################################################################################################*/
struct GPUBuffer {
	cc_uint32 lastFrame;
	struct GPUBuffer* next;
	int pad1, pad2;
	cc_uint8 data[]; // aligned to 16 bytes
};
static struct GPUBuffer* del_buffers_head;
static struct GPUBuffer* del_buffers_tail;

static struct GPUBuffer* GPUBuffer_Alloc(int count, int elemSize) {
	void* ptr = linearAlloc(16 + count * elemSize);
	return (struct GPUBuffer*)ptr;
}

// can't delete buffers until not used in any frames
static void GPUBuffer_Unref(GfxResourceID* resource) {
	struct GPUBuffer* buf = (struct GPUBuffer*)(*resource);
	if (!buf) return;
	*resource = NULL;
	
	LinkedList_Append(buf, del_buffers_head, del_buffers_tail);
}

static void GPUBuffer_Free(struct GPUBuffer* buf) {
	linearFree(buf);
}

static void GPUBuffers_DeleteUnreferenced(void) {
	if (!del_buffers_head) return;
	
	struct GPUBuffer* buf;
	struct GPUBuffer* next;
	struct GPUBuffer* prev = NULL;
	
	for (buf = del_buffers_head; buf != NULL; buf = next)
	{
		next = buf->next;
		
		if (buf->lastFrame + 4 > frameCounter1) {
			// texture was used within last 4 fames
			prev = buf;
		} else {
			// advance the head of the linked list
			if (del_buffers_head == buf) 
				del_buffers_head = next;
			// update end of linked list if necessary
			if (del_buffers_tail == buf)
				del_buffers_tail = prev;
			
			// unlink this texture from the linked list
			if (prev) prev->next = next;
			
			GPUBuffer_Free(buf);
		}
	}
}


/*########################################################################################################################*
*-------------------------------------------------------Index buffers-----------------------------------------------------*
*#########################################################################################################################*/
static cc_uint16* gfx_indices;

GfxResourceID Gfx_CreateIb2(int count, Gfx_FillIBFunc fillFunc, void* obj) {
	if (!gfx_indices) {
		gfx_indices = linearAlloc(count * sizeof(cc_uint16));
		if (!gfx_indices) Process_Abort("Failed to allocate memory for index buffer");
	}

	fillFunc(gfx_indices, count, obj);
	return gfx_indices;
}

void Gfx_BindIb(GfxResourceID ib) {
	u32 phys_addr = osConvertVirtToPhys(ib);
	pica_set_index_buffer_address(phys_addr);
}

void Gfx_DeleteIb(GfxResourceID* ib) { }


/*########################################################################################################################*
*-------------------------------------------------------Vertex buffers----------------------------------------------------*
*#########################################################################################################################*/
static cc_uint8* gfx_vertices;
static int vb_size;

static GfxResourceID Gfx_AllocStaticVb(VertexFormat fmt, int count) {
	return GPUBuffer_Alloc(count, strideSizes[fmt]);
}

void Gfx_BindVb(GfxResourceID vb) {
	struct GPUBuffer* buffer = (struct GPUBuffer*)vb;
	buffer->lastFrame = frameCounter1;
	gfx_vertices = buffer->data;
}

void Gfx_DeleteVb(GfxResourceID* vb) { GPUBuffer_Unref(vb); }

void* Gfx_LockVb(GfxResourceID vb, VertexFormat fmt, int count) {
	struct GPUBuffer* buffer = (struct GPUBuffer*)vb;
	vb_size = count * strideSizes[fmt];
	return buffer->data;
}

void Gfx_UnlockVb(GfxResourceID vb) {
	CPU_FlushDataCache(vb, vb_size);
}


/*########################################################################################################################*
*-----------------------------------------------------State management----------------------------------------------------*
*#########################################################################################################################*/
static u32 fogColor;
static int fogMode = -1;
static float fogDensity = 1.0f;
static float fogEnd = 32.0f;

void Gfx_SetFog(cc_bool enabled) {
	pica_update_fog_mode(enabled, false);
	gfx_fogEnabled = enabled;
}

void Gfx_SetFogCol(PackedCol color) {
	// TODO find better way?
	u32 c = (0xFFu << 24) | (PackedCol_B(color) << 16) | (PackedCol_G(color) << 8) | PackedCol_R(color);
	if (c == fogColor) return;

	fogColor = c;
	pica_set_fog_color(c);
}

static float GetFogValue(float c) {
	if (fogMode == FOG_LINEAR) {
		return (fogEnd - c) / fogEnd;
	} else if (fogMode == FOG_EXP) {
		return expf(-(fogDensity * c));
	} else {
		return expf(-(fogDensity * c) * (fogDensity * c));
	}
}

static void UpdateFog(void) {
	float near = 0.01f;
	float far  = Game_ViewDistance;
	float values[129];

	// TODO: Exp calculation isn't right for lava ???
	for (int i = 0; i <= 128; i ++)
	{
		float c = FogLut_CalcZ(i / 128.0f, near, far);
		values[i] = GetFogValue(c);
	}

	C3D_FogLut fog_lut;
	FogLut_FromArray(&fog_lut, values);
	pica_set_fog_table(fog_lut.data);
}

void Gfx_SetFogDensity(float value) {
	if (fogDensity == value) return;

	fogDensity = value;
	if (fogMode != FOG_LINEAR) UpdateFog();
}

void Gfx_SetFogEnd(float value) {
	if (fogEnd == value) return;

	fogEnd = value;
	if (fogMode == FOG_LINEAR) UpdateFog();
}

void Gfx_SetFogMode(FogFunc func) {
	if (fogMode == func) return;

	fogMode = func;
	UpdateFog();
}


/*########################################################################################################################*
*---------------------------------------------------------Matrices--------------------------------------------------------*
*#########################################################################################################################*/
static C3D_Mtx _view, _proj;

void Gfx_CalcOrthoMatrix(struct Matrix* matrix, float width, float height, float zNear, float zFar) {
	// See Mtx_OrthoTilt in Citro3D for the original basis
	// (it's mostly just a standard orthograph matrix rotated by 90 degrees)
	// Note: The rows/columns are "flipped" over the diagonal axis compared to original basis
	Mem_Set(matrix, 0, sizeof(struct Matrix));
	
	matrix->row2.x = -2.0f / height;
	matrix->row4.x =  1.0f;
	matrix->row1.y = -2.0f / width;
	matrix->row4.y =  1.0f;
	
	matrix->row3.z = 1.0f / (zNear - zFar);
	matrix->row4.z = 0.5f * (zNear + zFar) / (zNear - zFar) - 0.5f;
	matrix->row4.w = 1.0f;
}

void Gfx_CalcPerspectiveMatrix(struct Matrix* matrix, float fov, float aspect, float zFar) {
	// See Mtx_PerspTilt in Citro3D for the original basis
	// (it's mostly just a standard perspective matrix rotated by 90 degrees) 
	// Note: The rows/columns are "flipped" over the diagonal axis compared to original basis
	float zNear = 0.1f;
	fov = tanf(fov / 2.0f);	 
	Mem_Set(matrix, 0, sizeof(struct Matrix));

	matrix->row2.x =  1.0f / fov;
	matrix->row1.y = -1.0f / (fov * aspect);
	matrix->row4.z = zFar * zNear  / (zNear - zFar);
	matrix->row3.w = -1.0f;
	matrix->row3.z =  1.0f * zNear / (zNear - zFar);
}

void Gfx_LoadMatrix(MatrixType type, const struct Matrix* matrix) {
	C3D_Mtx* dst = type == MATRIX_VIEW ? &_view : &_proj;
	float* src   = (float*)matrix;
	
	// Transpose
	for (int i = 0; i < 4; i++)
	{
		dst->r[i].x = src[0  + i];
		dst->r[i].y = src[4  + i];
		dst->r[i].z = src[8  + i];
		dst->r[i].w = src[12 + i];
	}

	Mtx_Multiply(&_mvp, &_proj, &_view);
	pica_upload_mat4_constant(CONST_MVP, &_mvp);
}


/*void Gfx_LoadMatrix(MatrixType type, const struct Matrix* matrix) {
	if (type == MATRIX_VIEW) _view = *matrix;
	
	// Provided projection matrix assumes landscape display, but 3DS framebuffer
	//  is a rotated portrait display, so need to swap pixel X/Y values to correct that
	// 
	// This can be done by rotating the projection matrix 90 degrees around Z axis
	// https://open.gl/transformations
	if (type == MATRIX_PROJ) {
		struct Matrix rot = Matrix_Identity;
		rot.row1.x =  0; rot.row1.y = 1;
		rot.row2.x = -1; rot.row2.y = 0;
		//Matrix_RotateZ(&rot, 90 * MATH_DEG2RAD);
		//Matrix_Mul(&_proj, &_proj, &rot); // TODO avoid Matrix_Mul ??
		Matrix_Mul(&_proj, matrix, &rot); // TODO avoid Matrix_Mul ?
	}

	Mtx_Multiply(&_mvp, &_proj, &_view);
	UpdateMVP();
}*/

void Gfx_LoadMVP(const struct Matrix* view, const struct Matrix* proj, struct Matrix* mvp) {
	Gfx_LoadMatrix(MATRIX_VIEW, view);
	Gfx_LoadMatrix(MATRIX_PROJ, proj);
	Matrix_Mul(mvp, view, proj);
}

static C3D_FVec tex_offset;
void Gfx_EnableTextureOffset(float x, float y) {
	texOffset    = true;
	tex_offset.x = x;
	tex_offset.y = y;
	
	SwitchProgram();
	pica_upload_vec4_constant(CONST_TEX, &tex_offset);
}

void Gfx_DisableTextureOffset(void) {
	texOffset  = false;
	SwitchProgram();
}



/*########################################################################################################################*
*---------------------------------------------------------Drawing---------------------------------------------------------*
*#########################################################################################################################*/
cc_bool Gfx_WarnIfNecessary(void) { return false; }
cc_bool Gfx_GetUIOptions(struct MenuOptionsScreen* s) { return false; }

static void UpdateAttribFormat(void) {
	C3D_AttrInfo* attrInfo = C3D_GetAttrInfo();
	AttrInfo_Init(attrInfo);
	
	AttrInfo_AddLoader(attrInfo, 0, GPU_FLOAT, 3);         // in_pos
	AttrInfo_AddLoader(attrInfo, 1, GPU_UNSIGNED_BYTE, 4); // in_col
	if (gfx_format == VERTEX_FORMAT_TEXTURED) {
		AttrInfo_AddLoader(attrInfo, 2, GPU_FLOAT, 2); // in_tex
	}

	cc_bool textured = gfx_format == VERTEX_FORMAT_TEXTURED;
	pica_set_vsh_input_count(textured ? 3 : 2);
}

static void UpdateAttribConfig(void) {
	if (gfx_format == VERTEX_FORMAT_TEXTURED) {
		pica_set_attrib_array0_format(3, SIZEOF_VERTEX_TEXTURED);
	} else {
		pica_set_attrib_array0_format(2, SIZEOF_VERTEX_COLOURED);
	}
}

static void UpdateTexEnv(VertexFormat fmt) {
	int func, source;
	
	if (fmt == VERTEX_FORMAT_TEXTURED) {
		// Configure the first fragment shading substage to blend the texture color with
  		// the vertex color (calculated by the vertex shader using a lighting algorithm)
  		// See https://www.opengl.org/sdk/docs/man2/xhtml/glTexEnv.xml for more insight	
  		source = GPU_TEVSOURCES(GPU_TEXTURE0, GPU_PRIMARY_COLOR, 0);
 		func   = GPU_MODULATE;
 	} else {
 		// Configure the first fragment shading substage to just pass through the vertex color
		// See https://www.opengl.org/sdk/docs/man2/xhtml/glTexEnv.xml for more insight
		source = GPU_TEVSOURCES(GPU_PRIMARY_COLOR, 0, 0);
		func   = GPU_REPLACE;
 	}

	GPUCMD_AddWrite(GPUREG_TEXENV0_SOURCE,   source | (source << 16));
	GPUCMD_AddWrite(GPUREG_TEXENV0_COMBINER, func   | (func   << 16));
}

void Gfx_SetVertexFormat(VertexFormat fmt) {
	if (fmt == gfx_format) return;
	gfx_format = fmt;
	gfx_stride = strideSizes[fmt];
	
	SwitchProgram();
	//UpdateAttribFormat();
	UpdateAttribConfig();
	UpdateTexEnv(fmt);
}

void Gfx_DrawVb_Lines(int verticesCount) {
	/* TODO */
}

static void SetVertexSource(int startVertex) {
	const void* data = (cc_uint8*)gfx_vertices + startVertex * gfx_stride;

	u32 pa = osConvertVirtToPhys(data);
	GPUCMD_AddWrite(GPUREG_ATTRIBBUFFER0_OFFSET, pa - BUFFER_BASE_PADDR);
}

void Gfx_DrawVb_IndexedTris_Range(int verticesCount, int startVertex, DrawHints hints) {
	SetVertexSource(startVertex);
	C3D_DrawElements(GPU_TRIANGLES, ICOUNT(verticesCount));
}

void Gfx_DrawVb_IndexedTris(int verticesCount) {
	SetVertexSource(0);
	C3D_DrawElements(GPU_TRIANGLES, ICOUNT(verticesCount));
}

void Gfx_DrawIndexedTris_T2fC4b(int verticesCount, int startVertex) {
	SetVertexSource(startVertex);
	C3D_DrawElements(GPU_TRIANGLES, ICOUNT(verticesCount));
}

// TODO: TEMP HACK !!
void Gfx_Draw2DFlat(int x, int y, int width, int height, PackedCol color) {
	struct VertexColoured v1, v2, v3, v4;
	v1.x = (float)x;           v1.y = (float)y;
	v2.x = (float)(x + width); v2.y = (float)y;
	v3.x = (float)(x + width); v3.y = (float)(y + height);
	v4.x = (float)x;           v4.y = (float)(y + height);
	Gfx_SetVertexFormat(VERTEX_FORMAT_COLOURED);
	C3D_ImmDrawBegin(GPU_TRIANGLES);
		C3D_ImmSendAttrib(v1.x, v1.y, 0.0f, 1.0f);
		C3D_ImmSendAttrib(PackedCol_R(color), PackedCol_G(color), PackedCol_B(color), PackedCol_A(color));
		C3D_ImmSendAttrib(v2.x, v2.y, 0.0f, 1.0f);
		C3D_ImmSendAttrib(PackedCol_R(color), PackedCol_G(color), PackedCol_B(color), PackedCol_A(color));
		C3D_ImmSendAttrib(v3.x, v3.y, 0.0f, 1.0f);
		C3D_ImmSendAttrib(PackedCol_R(color), PackedCol_G(color), PackedCol_B(color), PackedCol_A(color));
		C3D_ImmSendAttrib(v3.x, v3.y, 0.0f, 1.0f);
		C3D_ImmSendAttrib(PackedCol_R(color), PackedCol_G(color), PackedCol_B(color), PackedCol_A(color));
		C3D_ImmSendAttrib(v4.x, v4.y, 0.0f, 1.0f);
		C3D_ImmSendAttrib(PackedCol_R(color), PackedCol_G(color), PackedCol_B(color), PackedCol_A(color));
		C3D_ImmSendAttrib(v1.x, v1.y, 0.0f, 1.0f);
		C3D_ImmSendAttrib(PackedCol_R(color), PackedCol_G(color), PackedCol_B(color), PackedCol_A(color));
	C3D_ImmDrawEnd();
}

void Gfx_Draw2DGradient(int x, int y, int width, int height, PackedCol top, PackedCol bottom) {
	struct VertexColoured v1, v2, v3, v4;
	v1.x = (float)x;           v1.y = (float)y;
	v2.x = (float)(x + width); v2.y = (float)y;
	v3.x = (float)(x + width); v3.y = (float)(y + height);
	v4.x = (float)x;           v4.y = (float)(y + height);
	Gfx_SetVertexFormat(VERTEX_FORMAT_COLOURED);
	C3D_ImmDrawBegin(GPU_TRIANGLES);
		C3D_ImmSendAttrib(v1.x, v1.y, 0.0f, 1.0f);
		C3D_ImmSendAttrib(PackedCol_R(top), PackedCol_G(top), PackedCol_B(top), PackedCol_A(top));
		C3D_ImmSendAttrib(v2.x, v2.y, 0.0f, 1.0f);
		C3D_ImmSendAttrib(PackedCol_R(top), PackedCol_G(top), PackedCol_B(top), PackedCol_A(top));
		C3D_ImmSendAttrib(v3.x, v3.y, 0.0f, 1.0f);
		C3D_ImmSendAttrib(PackedCol_R(bottom), PackedCol_G(bottom), PackedCol_B(bottom), PackedCol_A(bottom));
		C3D_ImmSendAttrib(v3.x, v3.y, 0.0f, 1.0f);
		C3D_ImmSendAttrib(PackedCol_R(bottom), PackedCol_G(bottom), PackedCol_B(bottom), PackedCol_A(bottom));
		C3D_ImmSendAttrib(v4.x, v4.y, 0.0f, 1.0f);
		C3D_ImmSendAttrib(PackedCol_R(bottom), PackedCol_G(bottom), PackedCol_B(bottom), PackedCol_A(bottom));
		C3D_ImmSendAttrib(v1.x, v1.y, 0.0f, 1.0f);
		C3D_ImmSendAttrib(PackedCol_R(top), PackedCol_G(top), PackedCol_B(top), PackedCol_A(top));
	C3D_ImmDrawEnd();
}

void Gfx_Draw2DTexture(const struct Texture* tex, PackedCol color) {
	struct VertexTextured v[4];
	struct VertexTextured* ptr = v;
	Gfx_Make2DQuad(tex, color, &ptr);
	Gfx_SetVertexFormat(VERTEX_FORMAT_TEXTURED);
	C3D_ImmDrawBegin(GPU_TRIANGLES);
		C3D_ImmSendAttrib(v[0].x, v[0].y, 0.0f, 1.0f);
		C3D_ImmSendAttrib(PackedCol_R(color), PackedCol_G(color), PackedCol_B(color), PackedCol_A(color));
		C3D_ImmSendAttrib(v[0].U, v[0].V, 0.0f, 0.0f);
		C3D_ImmSendAttrib(v[1].x, v[1].y, 0.0f, 1.0f);
		C3D_ImmSendAttrib(PackedCol_R(color), PackedCol_G(color), PackedCol_B(color), PackedCol_A(color));
		C3D_ImmSendAttrib(v[1].U, v[1].V, 0.0f, 0.0f);
		C3D_ImmSendAttrib(v[2].x, v[2].y, 0.0f, 1.0f);
		C3D_ImmSendAttrib(PackedCol_R(color), PackedCol_G(color), PackedCol_B(color), PackedCol_A(color));
		C3D_ImmSendAttrib(v[2].U, v[2].V, 0.0f, 0.0f);
		C3D_ImmSendAttrib(v[2].x, v[2].y, 0.0f, 1.0f);
		C3D_ImmSendAttrib(PackedCol_R(color), PackedCol_G(color), PackedCol_B(color), PackedCol_A(color));
		C3D_ImmSendAttrib(v[2].U, v[2].V, 0.0f, 0.0f);
		C3D_ImmSendAttrib(v[3].x, v[3].y, 0.0f, 1.0f);
		C3D_ImmSendAttrib(PackedCol_R(color), PackedCol_G(color), PackedCol_B(color), PackedCol_A(color));
		C3D_ImmSendAttrib(v[3].U, v[3].V, 0.0f, 0.0f);
		C3D_ImmSendAttrib(v[0].x, v[0].y, 0.0f, 1.0f);
		C3D_ImmSendAttrib(PackedCol_R(color), PackedCol_G(color), PackedCol_B(color), PackedCol_A(color));
		C3D_ImmSendAttrib(v[0].U, v[0].V, 0.0f, 0.0f);
	C3D_ImmDrawEnd();
}

