#+build wasm32, wasm64p32

package main

import "core:log"
import "core:mem"
import "core:c"
import "base:intrinsics"
import "base:runtime"
import "core:strings"
import "core:fmt"

@(private="file") web_context: runtime.Context

@export main_start :: proc "c" () {
	context = runtime.default_context()

	// The WASM allocator doesn't seem to work properly in combination with
	// emscripten. There is some kind of conflict with how the manage memory.
	// So this sets up an allocator that uses emscripten's malloc.
	context.allocator = emscripten_allocator()
	runtime.init_global_temporary_allocator(1*mem.Megabyte)

	// Since we now use js_wasm32 we should be able to remove this and use
	// context.logger = log.create_console_logger(). However, that one produces
	// extra newlines on web. So it's a bug in that core lib.
	context.logger = create_emscripten_logger()

	web_context = context

	init()
}

@export main_update :: proc "c" () -> bool {
	context = web_context
	update()
	return should_run()
}

@export main_end :: proc "c" () {
	context = web_context
	shutdown()
}

@export web_window_size_changed :: proc "c" (w: c.int, h: c.int) {
	context = web_context
	parent_window_size_changed(int(w), int(h))
}

///////////////////////////////////////////////////////
///              EMSCRIPTEN ALLOCATOR               ///
///////////////////////////////////////////////////////

// This will create bindings to emscripten's implementation of libc
// memory allocation features.
@(default_calling_convention = "c")
foreign {
	calloc  :: proc(num, size: c.size_t) -> rawptr ---
	free    :: proc(ptr: rawptr) ---
	malloc  :: proc(size: c.size_t) -> rawptr ---
	realloc :: proc(ptr: rawptr, size: c.size_t) -> rawptr ---
}

emscripten_allocator :: proc "contextless" () -> mem.Allocator {
	return mem.Allocator{emscripten_allocator_proc, nil}
}

emscripten_allocator_proc :: proc(allocator_data: rawptr,mode: mem.Allocator_Mode,size, alignment: int,old_memory: rawptr,old_size: int,location := #caller_location) -> (data: []byte, err: mem.Allocator_Error)  {
	// These aligned alloc procs are almost indentical those in
	// `_heap_allocator_proc` in `core:os`. Without the proper alignment you
	// cannot use maps and simd features.

	aligned_alloc :: proc(size, alignment: int, zero_memory: bool, old_ptr: rawptr = nil) -> ([]byte, mem.Allocator_Error) {
		a := max(alignment, align_of(rawptr))
		space := size + a - 1

		allocated_mem: rawptr
		if old_ptr != nil {
			original_old_ptr := mem.ptr_offset((^rawptr)(old_ptr), -1)^
			allocated_mem = realloc(original_old_ptr, c.size_t(space+size_of(rawptr)))
		} else if zero_memory {
			// calloc automatically zeros memory, but it takes a number + size
			// instead of just size.
			allocated_mem = calloc(c.size_t(space+size_of(rawptr)), 1)
		} else {
			allocated_mem = malloc(c.size_t(space+size_of(rawptr)))
		}
		aligned_mem := rawptr(mem.ptr_offset((^u8)(allocated_mem), size_of(rawptr)))

		ptr := uintptr(aligned_mem)
		aligned_ptr := (ptr - 1 + uintptr(a)) & -uintptr(a)
		diff := int(aligned_ptr - ptr)
		if (size + diff) > space || allocated_mem == nil {
			return nil, .Out_Of_Memory
		}

		aligned_mem = rawptr(aligned_ptr)
		mem.ptr_offset((^rawptr)(aligned_mem), -1)^ = allocated_mem

		return mem.byte_slice(aligned_mem, size), nil
	}

	aligned_free :: proc(p: rawptr) {
		if p != nil {
			free(mem.ptr_offset((^rawptr)(p), -1)^)
		}
	}

	aligned_resize :: proc(p: rawptr, old_size: int, new_size: int, new_alignment: int) -> ([]byte, mem.Allocator_Error) {
		if p == nil {
			return nil, nil
		}
		return aligned_alloc(new_size, new_alignment, true, p)
	}

	switch mode {
	case .Alloc:
		return aligned_alloc(size, alignment, true)

	case .Alloc_Non_Zeroed:
		return aligned_alloc(size, alignment, false)

	case .Free:
		aligned_free(old_memory)
		return nil, nil

	case .Resize:
		if old_memory == nil {
			return aligned_alloc(size, alignment, true)
		}

		bytes := aligned_resize(old_memory, old_size, size, alignment) or_return

		// realloc doesn't zero the new bytes, so we do it manually.
		if size > old_size {
			new_region := raw_data(bytes[old_size:])
			intrinsics.mem_zero(new_region, size - old_size)
		}

		return bytes, nil

	case .Resize_Non_Zeroed:
		if old_memory == nil {
			return aligned_alloc(size, alignment, false)
		}

		return aligned_resize(old_memory, old_size, size, alignment)

	case .Query_Features:
		set := (^mem.Allocator_Mode_Set)(old_memory)
		if set != nil {
			set^ = {.Alloc, .Free, .Resize, .Query_Features}
		}
		return nil, nil

	case .Free_All, .Query_Info:
		return nil, .Mode_Not_Implemented
	}
	return nil, .Mode_Not_Implemented
}

///////////////////////////////////////////////////////
///              EMSCRIPTEN LOGGER                  ///
///////////////////////////////////////////////////////
Emscripten_Logger_Opts :: log.Options{.Level, .Short_File_Path, .Line}

create_emscripten_logger :: proc (lowest := log.Level.Debug, opt := Emscripten_Logger_Opts) -> log.Logger {
	return log.Logger{data = nil, procedure = logger_proc, lowest_level = lowest, options = opt}
}

// This create's a binding to `puts` which will be linked in as part of the
// emscripten runtime.
@(default_calling_convention = "c")
foreign {
	puts :: proc(buffer: cstring) -> c.int ---
}

@(private="file") logger_proc :: proc(logger_data: rawptr,level: log.Level,text: string,options: log.Options,location := #caller_location) {
	b := strings.builder_make(context.temp_allocator)
	strings.write_string(&b, Level_Headers[level])
	do_location_header(options, &b, location)
	fmt.sbprint(&b, text)

	if bc, bc_err := strings.to_cstring(&b); bc_err == nil {
		puts(bc)
	}
}

@(private="file") Level_Headers := [?]string {
	0 ..< 10 = "[DEBUG] --- ",
	10 ..< 20 = "[INFO ] --- ",
	20 ..< 30 = "[WARN ] --- ",
	30 ..< 40 = "[ERROR] --- ",
	40 ..< 50 = "[FATAL] --- ",
}

@(private="file") do_location_header :: proc(opts: log.Options, buf: ^strings.Builder, location := #caller_location) {
	if log.Location_Header_Opts & opts == nil {
		return
	}
	fmt.sbprint(buf, "[")
	file := location.file_path
	if .Short_File_Path in opts {
		last := 0
		for r, i in location.file_path {
			if r == '/' {
				last = i + 1
			}
		}
		file = location.file_path[last:]
	}

	if log.Location_File_Opts & opts != nil {
		fmt.sbprint(buf, file)
	}
	if .Line in opts {
		if log.Location_File_Opts & opts != nil {
			fmt.sbprint(buf, ":")
		}
		fmt.sbprint(buf, location.line)
	}

	if .Procedure in opts {
		if (log.Location_File_Opts | {.Line}) & opts != nil {
			fmt.sbprint(buf, ":")
		}
		fmt.sbprintf(buf, "%s()", location.procedure)
	}

	fmt.sbprint(buf, "] ")
}
