package main

import "base:runtime"
import "core:c"
import "core:log"
import "core:math/linalg/glsl"
import "core:mem/virtual"
import "core:slice"

import gl "vendor:OpenGL"
import "vendor:glfw"

@(export)
NvOptimusEnablement: u32 = 1
@(export)
AmdPowerXpressRequestHighPerformance: int = 1

TARGET_FPS :: 60

vertices := []struct {
	x, y: f32,
	u, v: f32,
}{{-1.0, -1.0, 0.0, 1.0}, {1.0, -1.0, 1.0, 1.0}, {-1.0, 1.0, 0.0, 0.0}, {1.0, 1.0, 1.0, 0.0}}

indices := []u32{0, 1, 2, 2, 3, 1}

Bitmap :: struct {
	width:     int,
	height:    int,
	buffer:    []u8,
	_internal: struct {
		texture: u32,
	},
}

create_bitmap :: proc(width, height: int) -> Bitmap {
	bitmap: Bitmap = {
		width  = width,
		height = height,
		buffer = make([]u8, width * height * 4),
	}

	gl.GenTextures(1, &bitmap._internal.texture)
	gl.BindTexture(gl.TEXTURE_2D, bitmap._internal.texture)

	gl.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST)
	gl.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST)
	gl.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_BORDER)
	gl.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_BORDER)

	gl.TexImage2D(gl.TEXTURE_2D, 0, gl.RGB, i32(width), i32(height), 0, gl.BGRA, gl.UNSIGNED_BYTE, nil)
	gl.TexSubImage2D(
		gl.TEXTURE_2D,
		0,
		0,
		0,
		i32(width),
		i32(height),
		gl.BGRA,
		gl.UNSIGNED_BYTE,
		raw_data(bitmap.buffer),
	)

	gl.BindBuffer(gl.TEXTURE_BUFFER, 0)

	return bitmap
}

destroy_bitmap :: proc(bitmap: ^Bitmap) {
	gl.DeleteTextures(1, &bitmap._internal.texture)
	delete(bitmap.buffer)
}

main :: proc() {
	context.logger = log.create_console_logger(.Debug, log.Default_Console_Logger_Opts, "MAIN")

	log.info("booting up...")

	if ok := glfw.Init(); !ok {
		log.panic("fail :: init glfw")
	}
	defer glfw.Terminate()

	glfw.WindowHint(glfw.RESIZABLE, false)
	glfw.WindowHint(glfw.CONTEXT_VERSION_MAJOR, 4)
	glfw.WindowHint(glfw.CONTEXT_VERSION_MINOR, 1)
	glfw.WindowHint(glfw.OPENGL_PROFILE, glfw.OPENGL_CORE_PROFILE)
	glfw.WindowHint(glfw.OPENGL_FORWARD_COMPAT, true)
	glfw.WindowHint(glfw.OPENGL_DEBUG_CONTEXT, true)

	glfw.SetErrorCallback(proc "c" (error: c.int, description: cstring) {
		context = runtime.default_context()
		context.logger = log.create_console_logger(.Debug, log.Default_Console_Logger_Opts, "GLFW")
		log.errorf("%d:%s", error, description)
	})

	window := glfw.CreateWindow(1280, 720, "Cellular Automata", nil, nil)
	if window == nil {
		log.panic("fail :: create window")
	}
	defer glfw.DestroyWindow(window)

	glfw.MakeContextCurrent(window)
	glfw.SwapInterval(0)
	glfw.SetKeyCallback(window, key_callback)

	gl.load_up_to(4, 1, glfw.gl_set_proc_address)

	log.debugf("GL Context: %s", gl.GetString(gl.VERSION))

	/*if glfw.ExtensionSupported("GL_KHR_debug") != 0 {
		// NOTE: Not supported on macos?
		gl.Enable(gl.DEBUG_OUTPUT)
		gl.Enable(gl.DEBUG_OUTPUT_SYNCHRONOUS)
		gl.DebugMessageCallback(debug_message_proc, nil)
		gl.DebugMessageControl(gl.DONT_CARE, gl.DONT_CARE, gl.DONT_CARE, 0, nil, true)
		gl.DebugMessageInsert(
			gl.DEBUG_SOURCE_APPLICATION,
			gl.DEBUG_TYPE_ERROR,
			0,
			gl.DEBUG_SEVERITY_NOTIFICATION,
			-1,
			"Vary dangerous error",
		)
	}*/

	width, height := glfw.GetFramebufferSize(window)
	gl.Viewport(0, 0, width, height)
	log.debugf("viewport %dx%d", width, height)

	frame_vb, frame_ib: u32
	gl.GenBuffers(1, &frame_vb)
	gl.GenBuffers(1, &frame_ib)
	gl.BindBuffer(gl.ARRAY_BUFFER, frame_vb)
	gl.BufferData(gl.ARRAY_BUFFER, size_of(vertices[0]) * len(vertices), raw_data(vertices), gl.STATIC_DRAW)
	gl.BindBuffer(gl.ELEMENT_ARRAY_BUFFER, frame_ib)
	gl.BufferData(gl.ELEMENT_ARRAY_BUFFER, size_of(indices[0]) * len(indices), raw_data(indices), gl.STATIC_DRAW)

	bitmap := create_bitmap(int(width), int(height))
	defer destroy_bitmap(&bitmap)
	log.debugf("bitmap: %dx%d, %d bytes", bitmap.width, bitmap.height, len(bitmap.buffer))

	program, ok := gl.load_shaders_file("src/simple.vs", "src/simple.fs")
	//msg, _, _, _ := gl.get_last_error_messages()
	//log.errorf("msg: %s", msg)
	assert(ok, "shader failed")
	log.infof("program %d %b", program, ok)

	mvp_location := gl.GetUniformLocation(program, "MVP")
	vpos_location := gl.GetAttribLocation(program, "vPos")
	uv_location := gl.GetAttribLocation(program, "vTexCoord")
	log.debugf("locs %d %d %d", mvp_location, vpos_location, uv_location)

	frame_vao: u32
	gl.GenVertexArrays(1, &frame_vao)
	gl.BindVertexArray(frame_vao)
	gl.BindBuffer(gl.ARRAY_BUFFER, frame_vb)
	gl.BindBuffer(gl.ELEMENT_ARRAY_BUFFER, frame_ib)

	gl.EnableVertexAttribArray(u32(vpos_location))
	gl.EnableVertexAttribArray(u32(uv_location))

	gl.VertexAttribPointer(u32(vpos_location), 2, gl.FLOAT, false, size_of(vertices[0]), 0)
	gl.VertexAttribPointer(u32(uv_location), 2, gl.FLOAT, false, size_of(vertices[0]), size_of(f32) * 2)
	gl.BindVertexArray(0)

	gl.ClearColor(0.5, 0.5, 1.0, 1.0)

	projection: glsl.mat4
	projection = glsl.mat4Ortho3d(-1, 1, -1, 1, 1, -1)

	app_memory: virtual.Arena
	if err := virtual.arena_init_static(&app_memory); err != .None {
		log.panicf("fail :: arena_init, %v", err)
	}
	defer virtual.arena_destroy(&app_memory)

	app: ^App
	{
		context.allocator = virtual.arena_allocator(&app_memory)
		app = app_init()
	}
	log.debugf("app_memory: %v", app_memory)

	max_tick := 1.0 / TARGET_FPS
	log.debugf("max_tick: %f (target %d fps)", max_tick, TARGET_FPS)

	tick := glfw.GetTime()
	draws_tick := glfw.GetTime()
	draws: int
	for !glfw.WindowShouldClose(window) {
		gl.Clear(gl.COLOR_BUFFER_BIT)

		gl.UseProgram(program)
		defer gl.UseProgram(0)

		gl.UniformMatrix4fv(mvp_location, 1, false, &projection[0, 0])

		{
			new_tick := glfw.GetTime()
			gl.BindTexture(gl.TEXTURE_2D, bitmap._internal.texture)
			if (new_tick - tick > max_tick) {
				tick = new_tick

				slice.fill(bitmap.buffer, 0)
				{
					context.allocator = virtual.arena_allocator(&app_memory)
					app_update_and_draw(app, &bitmap)
				}

				gl.TexSubImage2D(
					gl.TEXTURE_2D,
					0,
					0,
					0,
					i32(bitmap.width),
					i32(bitmap.height),
					gl.BGRA,
					gl.UNSIGNED_INT_8_8_8_8_REV,
					raw_data(bitmap.buffer),
				)
			}
			defer gl.BindTexture(gl.TEXTURE_2D, 0)

			gl.BindVertexArray(frame_vao)
			defer gl.BindVertexArray(0)
			gl.DrawElements(gl.TRIANGLES, i32(len(indices)), gl.UNSIGNED_INT, nil)
		}


		glfw.SwapBuffers(window)
		glfw.PollEvents()

		draws += 1
		new_draws_tick := glfw.GetTime()
		if (new_draws_tick - draws_tick > 1) {
			draws_tick = new_draws_tick
			log.debugf("draws: %d", draws)
			draws = 0
		}
	}
}

debug_message_proc :: proc "c" (
	source: u32,
	type: u32,
	id: u32,
	severity: u32,
	length: i32,
	message: cstring,
	user_param: rawptr,
) {
	context = runtime.default_context()
	context.logger = log.create_console_logger(.Debug, log.Default_Console_Logger_Opts, "GL")
	log.debugf("%d %d %d %d %d %s %p", source, type, id, severity, length, message, user_param)
}

key_callback :: proc "c" (window: glfw.WindowHandle, key, scancode, action, mods: i32) {
	// Exit program on escape pressed
	if key == glfw.KEY_ESCAPE {
		glfw.SetWindowShouldClose(window, true)
	}
}