#+build !freestanding

package log

import "base:runtime"
import "core:fmt"
import "core:sync"

Log_Allocator_Format :: enum {
	Bytes, // Actual number of bytes.
	Human, // Bytes in human units like bytes, kibibytes, etc. as appropriate.
}


// Log_Allocator is an allocator which calls `context.logger` on each of its allocations operations.
// The format can be changed by setting the `size_fmt: Log_Allocator_Format` field to either `Bytes` or `Human`.
Log_Allocator :: struct {
	allocator: runtime.Allocator,
	level:     Level,
	prefix:    string,
	lock:      sync.Mutex,
	size_fmt:  Log_Allocator_Format,
}

log_allocator_init :: proc(la: ^Log_Allocator, level: Level, size_fmt := Log_Allocator_Format.Bytes,
                           allocator := context.allocator, prefix := "") {
	la.allocator = allocator
	la.level    = level
	la.prefix   = prefix
	la.lock     = {}
	la.size_fmt = size_fmt
}


log_allocator :: proc(la: ^Log_Allocator) -> runtime.Allocator {
	return runtime.Allocator{
		procedure = log_allocator_proc,
		data = la,
	}
}

log_allocator_proc :: proc(allocator_data: rawptr, mode: runtime.Allocator_Mode,
                           size, alignment: int,
                           old_memory: rawptr, old_size: int, location := #caller_location) -> ([]byte, runtime.Allocator_Error)  {
	la := (^Log_Allocator)(allocator_data)

	if context.logger.procedure == nil || la.level < context.logger.lowest_level {
		return la.allocator.procedure(la.allocator.data, mode, size, alignment, old_memory, old_size, location)
	}

	padding := " " if la.prefix != "" else ""

	buf: [256]byte = ---

	sync.lock(&la.lock)
	switch mode {
	case .Alloc:
		format: string
		switch la.size_fmt {
		case .Bytes: format = "%s%s>>> ALLOCATOR(mode=.Alloc, size=%d, alignment=%d)"
		case .Human: format = "%s%s>>> ALLOCATOR(mode=.Alloc, size=%m, alignment=%d)"
		}
		str := fmt.bprintf(buf[:], format, la.prefix, padding, size, alignment)
		context.logger.procedure(context.logger.data, la.level, str, context.logger.options, location)

	case .Alloc_Non_Zeroed:
		format: string
		switch la.size_fmt {
		case .Bytes: format = "%s%s>>> ALLOCATOR(mode=.Alloc_Non_Zeroed, size=%d, alignment=%d)"
		case .Human: format = "%s%s>>> ALLOCATOR(mode=.Alloc_Non_Zeroed, size=%m, alignment=%d)"
		}
		str := fmt.bprintf(buf[:], format, la.prefix, padding, size, alignment)
		context.logger.procedure(context.logger.data, la.level, str, context.logger.options, location)

	case .Free:
		if old_size != 0 {
			format: string
			switch la.size_fmt {
			case .Bytes: format = "%s%s<<< ALLOCATOR(mode=.Free, ptr=%p, size=%d)"
			case .Human: format = "%s%s<<< ALLOCATOR(mode=.Free, ptr=%p, size=%m)"
			}
			str := fmt.bprintf(buf[:], format, la.prefix, padding, old_memory, old_size)
			context.logger.procedure(context.logger.data, la.level, str, context.logger.options, location)
		} else {
			str := fmt.bprintf(buf[:], "%s%s<<< ALLOCATOR(mode=.Free, ptr=%p)", la.prefix, padding, old_memory)
			context.logger.procedure(context.logger.data, la.level, str, context.logger.options, location)
		}

	case .Free_All:
		str := fmt.bprintf(buf[:], "%s%s<<< ALLOCATOR(mode=.Free_All)", la.prefix, padding)
		context.logger.procedure(context.logger.data, la.level, str, context.logger.options, location)

	case .Resize:
		format: string
		switch la.size_fmt {
		case .Bytes: format = "%s%s>>> ALLOCATOR(mode=.Resize, ptr=%p, old_size=%d, size=%d, alignment=%d)"
		case .Human: format = "%s%s>>> ALLOCATOR(mode=.Resize, ptr=%p, old_size=%m, size=%m, alignment=%d)"
		}
		str := fmt.bprintf(buf[:], format, la.prefix, padding, old_memory, old_size, size, alignment)
		context.logger.procedure(context.logger.data, la.level, str, context.logger.options, location)

	case .Resize_Non_Zeroed:
		format: string
		switch la.size_fmt {
		case .Bytes: format = "%s%s>>> ALLOCATOR(mode=.Resize_Non_Zeroed, ptr=%p, old_size=%d, size=%d, alignment=%d)"
		case .Human: format = "%s%s>>> ALLOCATOR(mode=.Resize_Non_Zeroed, ptr=%p, old_size=%m, size=%m, alignment=%d)"
		}
		str := fmt.bprintf(buf[:], format, la.prefix, padding, old_memory, old_size, size, alignment)
		context.logger.procedure(context.logger.data, la.level, str, context.logger.options, location)

	case .Query_Features:
		str := fmt.bprintf(buf[:], "%s%sALLOCATOR(mode=.Query_Features)", la.prefix, padding)
		context.logger.procedure(context.logger.data, la.level, str, context.logger.options, location)

	case .Query_Info:
		str := fmt.bprintf(buf[:], "%s%sALLOCATOR(mode=.Query_Info)", la.prefix, padding)
		context.logger.procedure(context.logger.data, la.level, str, context.logger.options, location)
	}
	sync.unlock(&la.lock)

	data, err := la.allocator.procedure(la.allocator.data, mode, size, alignment, old_memory, old_size, location)
	if err != nil {
		sync.lock(&la.lock)
		str := fmt.bprintf(buf[:], "%s%sALLOCATOR ERROR=%v", la.prefix, padding, err)
		context.logger.procedure(context.logger.data, la.level, str, context.logger.options, location)
		sync.unlock(&la.lock)
	}
	return data, err
}
