package mem_virtual

import "core:mem"

Growing_Arena :: struct {
	curr_block:     ^Memory_Block,
	total_used:     uint,
	total_reserved: uint,
	
	minimum_block_size: uint,
	temp_count: int,
}

DEFAULT_MINIMUM_BLOCK_SIZE :: 1<<20 // 1 MiB should be enough

growing_arena_alloc :: proc(arena: ^Growing_Arena, min_size: int, alignment: int) -> (data: []byte, err: Allocator_Error) {
	align_forward_offset :: proc "contextless" (arena: ^Growing_Arena, alignment: int) -> uint #no_bounds_check {
		alignment_offset := uint(0)
		ptr := uintptr(arena.curr_block.base[arena.curr_block.used:])
		mask := uintptr(alignment-1)
		if ptr & mask != 0 {
			alignment_offset = uint(alignment) - uint(ptr & mask)
		}
		return alignment_offset
	}
	
	assert(mem.is_power_of_two(uintptr(alignment)))

	size := uint(0)
	if arena.curr_block != nil {
		size = uint(min_size) + align_forward_offset(arena, alignment)
	}
	
	if arena.curr_block == nil || arena.curr_block.used + size > arena.curr_block.reserved {
		size = uint(mem.align_forward_int(min_size, alignment))
		arena.minimum_block_size = max(DEFAULT_MINIMUM_BLOCK_SIZE, arena.minimum_block_size)
		
		block_size := max(size, arena.minimum_block_size)
		
		new_block := memory_block_alloc(block_size, block_size, {}) or_return
		new_block.prev = arena.curr_block
		arena.curr_block = new_block
		arena.total_reserved += new_block.reserved	
	}	
	
	
	data, err = alloc_from_memory_block(arena.curr_block, int(size), alignment)
	if err == nil {
		arena.total_used += size
	}
	return
}

growing_arena_free_last_memory_block :: proc(arena: ^Growing_Arena) {
	free_block := arena.curr_block
	arena.curr_block = free_block.prev
	memory_block_dealloc(free_block)
}

growing_arena_free_all :: proc(arena: ^Growing_Arena) {
	for arena.curr_block != nil {
		growing_arena_free_last_memory_block(arena)
	}
	arena.total_used = 0
	arena.total_reserved = 0
}

growing_arena_destroy :: proc(arena: ^Growing_Arena) {
	growing_arena_free_all(arena)
}

growing_arena_bootstrap_new_by_offset :: proc($T: typeid, offset_to_arena: uintptr, minimum_block_size := DEFAULT_MINIMUM_BLOCK_SIZE) -> (ptr: ^T, err: Allocator_Error) {
	bootstrap: Growing_Arena
	bootstrap.minimum_block_size = minimum_block_size
	
	data := growing_arena_alloc(&bootstrap, size_of(T), align_of(T)) or_return
	
	ptr = (^T)(raw_data(data))
	
	(^Growing_Arena)(uintptr(ptr) + offset_to_arena)^ = bootstrap
	
	return
}

growing_arena_bootstrap_new_by_name :: proc($T: typeid, $field_name: string, minimum_block_size := DEFAULT_MINIMUM_BLOCK_SIZE) -> (ptr: ^T, err: Allocator_Error) { 
	return growing_arena_bootstrap_new_by_offset(T, offset_of_by_string(T, field_name), minimum_block_size)
}
growing_arena_bootstrap_new :: proc{
	growing_arena_bootstrap_new_by_offset, 
	growing_arena_bootstrap_new_by_name,
}

growing_arena_allocator :: proc(arena: ^Growing_Arena) -> mem.Allocator {
	return mem.Allocator{growing_arena_allocator_proc, arena}
}

growing_arena_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: Allocator_Error) {
	arena := (^Growing_Arena)(allocator_data)
		
	switch mode {
	case .Alloc:
		return growing_arena_alloc(arena, size, alignment)
	case .Free:
		err = .Mode_Not_Implemented
		return
	case .Free_All:
		growing_arena_free_all(arena)
		return
	case .Resize:
		return mem.default_resize_bytes_align(mem.byte_slice(old_memory, old_size), size, alignment, growing_arena_allocator(arena), location)
		
	case .Query_Features, .Query_Info:
		err = .Mode_Not_Implemented
		return	
	}	
	
	err = .Mode_Not_Implemented
	return 
}

Growing_Arena_Temp :: struct {
	arena: ^Growing_Arena,
	block: ^Memory_Block,
	used:  uint,
}

growing_arena_temp_begin :: proc(arena: ^Growing_Arena) -> (temp: Growing_Arena_Temp) {
	temp.arena = arena
	temp.block = arena.curr_block
	if arena.curr_block != nil {
		temp.used = arena.curr_block.used
	}
	arena.temp_count += 1
	return
}

growing_arena_temp_end :: proc(temp: Growing_Arena_Temp, loc := #caller_location) {
	assert(temp.arena != nil, "nil arena", loc)
	arena := temp.arena
	
	for arena.curr_block != temp.block {
		growing_arena_free_last_memory_block(arena)
	}
	
	if block := arena.curr_block; block != nil {
		assert(block.used >= temp.used, "out of order use of growing_arena_temp_end", loc)
		amount_to_zero := min(block.used-temp.used, block.reserved-block.used)
		mem.zero_slice(block.base[temp.used:][:amount_to_zero])
		block.used = temp.used
	}
	
	assert(arena.temp_count > 0, "double-use of growing_arena_temp_end", loc)
	arena.temp_count -= 1
}

growing_arena_check_temp :: proc(arena: ^Growing_Arena, loc := #caller_location) {
	assert(arena.temp_count == 0, "Growing_Arena_Temp not been ended", loc)
}



