#+build linux
#+feature dynamic-literals
package main

import "core:fmt"
import "core:sys/posix"

import "platform/linux/X11"
import "platform/linux/shm"
import "platform/linux/xcb"
import xcberrors "platform/linux/xcb/errors"
import xcbkeysyms "platform/linux/xcb/keysyms"
import xcbshm "platform/linux/xcb/shm"

foreign import unistd "system:c"
@(default_calling_convention = "std")
foreign unistd {
	usleep :: proc(usecs: u64) -> i32 ---
}

DEBUG_DRAW_TIMINGS :: #config(DEBUG_DRAW_TIMINGS, false)

// TODO: Improve with create/destroy etc.
Backbuffer :: struct {
	memory:     rawptr,
	shm_seg_id: xcbshm.Seg,
	pixmap_id:  xcb.Pixmap,
	shm_id:     i32,
	width:      u16,
	height:     u16,
	bps:        u8,
	pitch:      u32,
}

resize_backbuffer :: proc(
	backbuffer: ^Backbuffer,
	connection: ^xcb.Connection,
	window: xcb.Window,
	width, height: u16,
) {
	if backbuffer.shm_seg_id == 0 {
		backbuffer.shm_seg_id = xcbshm.Seg(xcb.generate_id(connection))
		backbuffer.pixmap_id = xcb.Pixmap(xcb.generate_id(connection))
	} else {
		shm.ctl(backbuffer.shm_id, shm.IPC_RMID, nil)
		shm.dt(backbuffer.memory)
		xcbshm.detach(connection, backbuffer.shm_seg_id)
		xcb.free_pixmap(connection, backbuffer.pixmap_id)
	}
	xcb.flush(connection)

	backbuffer.width = width
	backbuffer.height = height
	backbuffer.bps = 4
	backbuffer.pitch = u32(backbuffer.width) * u32(backbuffer.height)

	backbuffer.shm_id = shm.get(
		shm.IPC_PRIVATE,
		u32(backbuffer.bps) * backbuffer.pitch,
		shm.IPC_CREAT | 0o600, // | 0o600 is file permissions
	)
	backbuffer.memory = shm.at(backbuffer.shm_id, nil, 0)

	xcbshm.attach(connection, backbuffer.shm_seg_id, u32(backbuffer.shm_id), 0)
	screen := xcb.setup_roots_iterator(xcb.get_setup(connection)).data
	xcbshm.create_pixmap(
		connection,
		backbuffer.pixmap_id,
		xcb.Drawable(window),
		backbuffer.width,
		backbuffer.height,
		screen.root_depth,
		backbuffer.shm_seg_id,
		0,
	)
}

get_clock_value :: #force_inline proc() -> posix.timespec {
	t: posix.timespec
	posix.clock_gettime(.MONOTONIC, &t)
	return t
}

get_seconds_elapsed :: #force_inline proc(start, end: posix.timespec) -> f32 {
	return f32(end.tv_sec - start.tv_sec) + (f32(end.tv_nsec - start.tv_nsec) / 1000000000.0)
}

input_key_translation := map[X11.Key_Code]App_Input_Key {
	.Escape = .Escape,
	.Num0   = .Num0,
	.Num1   = .Num1,
	.Num2   = .Num2,
	.Num3   = .Num3,
	.Num4   = .Num4,
	.Num5   = .Num5,
	.Num6   = .Num6,
	.Num7   = .Num7,
	.Num8   = .Num8,
	.Num9   = .Num9,
	.Left   = .ArrowLeft,
	.Right  = .ArrowRight,
}

main :: proc() {
	fmt.println("Hellope!")

	window_width: u16 = 1280
	window_height: u16 = 720

	connection := xcb.connect(nil, nil)
	defer xcb.disconnect(connection)
	fmt.printf("connection %x\n", connection)

	setup := xcb.get_setup(connection)
	fmt.printf("setup %v\n", setup)

	screen := xcb.setup_roots_iterator(setup).data
	fmt.printf("screen %v\n", screen)

	window := xcb.Window(xcb.generate_id(connection))
	fmt.printf("window %v\n", window)

	mask: u32
	values: [2]u32
	mask = u32(xcb.Cw.Event_Mask)
	values[0] = u32(xcb.EventMask.Exposure | xcb.EventMask.Key_Press | xcb.EventMask.Key_Release)
	xcb.create_window(
		connection,
		xcb.COPY_FROM_PARENT,
		window,
		screen.root,
		0,
		0,
		window_width,
		window_height,
		10,
		.Input_Output,
		screen.root_visual,
		mask,
		&values[0],
	)
	defer xcb.destroy_window(connection, window)

	// NOTE: This seems to be set to 0 until something makes us wait for a bit.
	// For example: a fmt.printf. Simply moving the fmt.printf to after the
	// xcb.create_gc will break it, resulting in an invalid gc. Moreover it seems
	// that gcontext is overridden to 0 if xcb.create_gc is called immediately
	// after xcb.generate_id. This happens only when all optimizations are turned
	// off. Slows down earlier calls? Moving the entire block down closer to the
	// loop start also seems to fix this. Related to window creation flush?
	// gcontext := xcb.Gcontext(xcb.generate_id(connection))
	// fmt.printf("gcontext: %d\n", gcontext)
	// xcb.create_gc(connection, gcontext, xcb.Drawable(window), 0, nil)

	// NOTE: Make sure we get the close window event (when letting decorations close the window etc).
	protocol_reply := xcb.intern_atom_reply(connection, xcb.intern_atom(connection, 1, 12, "WM_PROTOCOLS"), nil)
	delete_window_reply := xcb.intern_atom_reply(
		connection,
		xcb.intern_atom(connection, 0, 16, "WM_DELETE_WINDOW"),
		nil,
	)
	xcb.change_property(connection, .Replace, window, protocol_reply.atom, .Atom, 32, 1, &delete_window_reply.atom)

	xcb.map_window(connection, window)
	xcb.flush(connection)

	// NOTE: SHM support check.
	if reply := xcbshm.query_version_reply(connection, xcbshm.query_version(connection), nil);
	   reply == nil || reply.shared_pixmaps == 0 {
		fmt.printf("Shm missing?\n")
	} else {
		fmt.printf("Shm: %v\n", reply)
	}

	key_syms := xcbkeysyms.symbols_alloc(connection)
	defer xcbkeysyms.symbols_free(key_syms)

	backbuffers: [2]Backbuffer
	backbuffer_index := 0
	resize_backbuffer(&backbuffers[0], connection, window, window_width, window_height)
	resize_backbuffer(&backbuffers[1], connection, window, window_width, window_height)
	defer shm.ctl(backbuffers[0].shm_id, shm.IPC_RMID, nil)
	defer shm.dt(backbuffers[0].memory)
	defer xcbshm.detach(connection, backbuffers[0].shm_seg_id)
	defer xcb.free_pixmap(connection, backbuffers[0].pixmap_id)

	defer shm.ctl(backbuffers[1].shm_id, shm.IPC_RMID, nil)
	defer shm.dt(backbuffers[1].memory)
	defer xcbshm.detach(connection, backbuffers[1].shm_seg_id)
	defer xcb.free_pixmap(connection, backbuffers[1].pixmap_id)
	fmt.printf("backbuffer[0]: %+v\n", backbuffers[0])
	fmt.printf("backbuffer[1]: %+v\n", backbuffers[1])

	shm_completion_event := xcb.get_extension_data(connection, &xcbshm.Id).first_event + xcbshm.COMPLETION
	fmt.printf("completion event: %d\n", shm_completion_event)

	game_update_hz := f32(30)
	target_seconds_per_frame := 1.0 / game_update_hz
	last_counter := get_clock_value()

	when DEBUG_DRAW_TIMINGS {
		debug_frame_timings: [256]f32
		debug_render_timings: [256]f32
		debug_highest_timing := f32(1)
	}

	gcontext := xcb.Gcontext(xcb.generate_id(connection))
	xcb.create_gc(connection, gcontext, xcb.Drawable(window), 0, nil)

	is_running := true
	ready_to_blit := true
	backbuffer := backbuffers[backbuffer_index]
	input: [2]App_Input
	curr_input := &input[0]
	prev_input := &input[1]
	for is_running {
		event := xcb.poll_for_event(connection)
		for ; event != nil; event = xcb.poll_for_event(connection) {
			switch (event.response_type & ~u8(0x80)) {
			case 0:
				err := (^xcb.GenericError)(event)
				err_ctx: ^xcberrors.Context
				xcberrors.context_new(connection, &err_ctx)
				major := xcberrors.get_name_for_major_code(err_ctx, u8(err.major_code))
				minor := xcberrors.get_name_for_minor_code(err_ctx, u8(err.major_code), err.minor_code)
				extension: cstring
				error := xcberrors.get_name_for_error(err_ctx, err.error_code, &extension)
				fmt.printf(
					"XCB Error: %s:%s, %s:%s, resource %u sequence %u\n",
					error,
					extension != nil ? extension : "no_extension",
					major,
					minor != nil ? minor : "no_minor",
					err.resource_id,
					err.sequence,
				)
				xcberrors.context_free(err_ctx)

			case xcb.EXPOSE:
				fmt.printf("XCB_EXPOSE\n")

			case xcb.KEY_PRESS, xcb.KEY_RELEASE:
				// fmt.printf("XCB_KEY_PRESS/RELEASE\n")
				if (event.response_type == xcb.KEY_PRESS) {
					evt := (^xcb.KeyPressEvent)(event)
					key_sym := X11.Key_Code(xcbkeysyms.press_lookup_keysym(key_syms, evt, 0))

					// fmt.printf("KEY DOWN: %v %d ?= %d\n", evt, key_sym, X11.Key_Code.Escape)
					if key_sym == X11.Key_Code.Escape {
						is_running = false
					}

					/*if translated_key, ok := input_key_translation[key_sym]; ok {
						curr_input.keyboard[translated_key].down = true
					}*/
					/*} else {
					evt := (^xcb.KeyReleaseEvent)(event)
					key_sym := X11.Key_Code(xcbkeysyms.release_lookup_keysym(key_syms, evt, 0))

					if translated_key, ok := input_key_translation[key_sym]; ok {
						curr_input.keyboard[translated_key].down = false
					}*/
				}

			case xcb.CLIENT_MESSAGE:
				fmt.printf("XCB_CLIENT_MESSAGE\n")
				evt := (^xcb.ClientMessageEvent)(event)
				if evt.data.data32[0] == u32(delete_window_reply.atom) {
					is_running = false
				}

			case shm_completion_event:
				ready_to_blit = true

			case:
				fmt.printf("unexpected event %v\n", event)
			}

			xcb.free_generic_event(event)
		}

		/*for i in 0 ..< len(curr_input.keyboard) {
			curr_input.keyboard[i].transition = false
		}
		for i in 0 ..< len(curr_input.keyboard) {
			curr_input.keyboard[i].transition = (curr_input.keyboard[i].down != prev_input.keyboard[i].down)
		}
		fmt.printf("num1:\n%v\n%v\n", curr_input.keyboard[App_Input_Key.Num1], prev_input.keyboard[App_Input_Key.Num1])*/

		keymap_reply := xcb.query_keymap_reply(connection, xcb.query_keymap(connection), nil)
		for i in 0 ..< 256 {
			state := bool((keymap_reply.keys[i / 8] >> u32(i % 8)) & 1)
			key_sym := X11.Key_Code(xcbkeysyms.symbols_get_keysym(key_syms, xcb.Keycode(i), 0))
			if translated_key, ok := input_key_translation[key_sym]; ok {
				curr_input.keyboard[translated_key].down = state
				curr_input.keyboard[translated_key].transition =
					(curr_input.keyboard[translated_key].down != prev_input.keyboard[translated_key].down)

				if curr_input.keyboard[translated_key].transition {
					fmt.printf("key %s->%v\n", translated_key, curr_input.keyboard[translated_key])
				}
			}
			/*if X11.Key_Code(key_sym) == X11.Key_Code.Num1 {
				fmt.printf("key %s %t\n", X11.Key_Code(key_sym), state)
			}*/
		}
		xcb.free(keymap_reply)

		screen_buffer := Bitmap {
			buffer = ([^]u32)(backbuffer.memory)[0:backbuffer.pitch],
			width  = u32(backbuffer.width),
			height = u32(backbuffer.height),
		}
		when DEBUG_DRAW_TIMINGS {
			update_and_render_timing_start := get_clock_value()
		}
		app_update_and_render(&screen_buffer, curr_input)
		when DEBUG_DRAW_TIMINGS {
			update_and_render_timing_stop := get_clock_value()
		}

		curr_input, prev_input = prev_input, curr_input

		when DEBUG_DRAW_TIMINGS {
			pix_step := 128 / debug_highest_timing
			target_ms := 1000 * target_seconds_per_frame
			target_ms_line := int(((target_ms * (target_ms / debug_highest_timing)) * pix_step) + 0.5)
			for x := 0; x < 256; x += 1 {
				frame_height: int
				render_height: int
				{
					scaling := debug_frame_timings[x] / debug_highest_timing
					frame_height = int(((debug_frame_timings[x] * scaling) * pix_step) + 0.5)
				}
				{
					scaling := debug_render_timings[x] / debug_highest_timing
					render_height = int(((debug_render_timings[x] * scaling) * pix_step) + 0.5)
				}

				for y := 0; y < 128; y += 1 {
					i := ((127 - y) * int(screen_buffer.width)) + x

					c: u32 = 0xFF000000
					if y == target_ms_line {
						c = 0xFFFF00FF
					} else if y <= render_height {
						if render_height > target_ms_line {
							c = 0xFFFF0000
						} else {
							c = 0xFF00AA00
						}
					} else if y <= frame_height {
						c = 0xFF0000AA
					}
					screen_buffer.buffer[i] = c
				}
			}
		}

		if ready_to_blit {
			ready_to_blit = false

			xcbshm.put_image(
				connection,
				xcb.Drawable(window),
				gcontext,
				backbuffer.width,
				backbuffer.height,
				0,
				0,
				backbuffer.width,
				backbuffer.height,
				0,
				0,
				screen.root_depth,
				.z_pixmap,
				1,
				backbuffer.shm_seg_id,
				0,
			)

			xcb.flush(connection)

			backbuffer_index = (backbuffer_index + 1) % 2
			backbuffer = backbuffers[backbuffer_index]
		}

		elapsed := get_seconds_elapsed(last_counter, get_clock_value())
		if elapsed < target_seconds_per_frame {
			sleep_mics := u64(1000000 * (target_seconds_per_frame - elapsed))
			if sleep_mics > 0 {
				usleep(sleep_mics)
			}

			for elapsed < target_seconds_per_frame {
				elapsed = get_seconds_elapsed(last_counter, get_clock_value())
			}
		} else {
			// fmt.printf("missed sleep\n")
		}

		end_counter := get_clock_value()

		when DEBUG_DRAW_TIMINGS {
			ms_per_frame := 1000.0 * get_seconds_elapsed(last_counter, end_counter)
			frame_render_ms :=
				1000.0 * get_seconds_elapsed(update_and_render_timing_start, update_and_render_timing_stop)

			debug_highest_timing = 0
			for i := 0; i < 255; i += 1 {
				debug_frame_timings[i] = debug_frame_timings[i + 1]
				if debug_frame_timings[i] > debug_highest_timing {
					debug_highest_timing = debug_frame_timings[i]
				}
				debug_render_timings[i] = debug_render_timings[i + 1]
				if debug_render_timings[i] > debug_highest_timing {
					debug_highest_timing = debug_render_timings[i]
				}
			}
			debug_frame_timings[255] = ms_per_frame
			if ms_per_frame > debug_highest_timing {
				debug_highest_timing = ms_per_frame
			}
			debug_render_timings[255] = frame_render_ms
			if frame_render_ms > debug_highest_timing {
				debug_highest_timing = frame_render_ms
			}
		}

		last_counter = end_counter
	}
}