
use mem

global poolc: uint
global pools: [ptr x 128]

value POOL_DATA_OFFSET ((size_of(ptr) * 5.to(uint)))

fn pools_init() {
    let list = pools
    mem:clear(list, 128 * size_of(ptr))

    let i : uint = size_of(ptr)
    let c : uint = 0
    let until : uint = 16 * size_of(ptr)
    while i <= until {
        let pool = Pool.init(i)
        @ptrv(list, Pool, c) = pool
        i += size_of(ptr)
        c++
    }
    i = 16 * size_of(ptr) * 2
    while i <= 2147483648 {
        let pool = Pool.init(i)
        @ptrv(list, Pool, c) = pool
        i *= 2
        c++
    }

    poolc = c
    // print("P: %pools | %poolc\n")
}

fn pools_free() {
    let i : uint = 0
    let last = poolc
    while i < last {
        let pool = @ptrv(pools, Pool, i++)
        pool.free()
    }
}

fn get_pool_index_for_size(size: uint) uint {
    let until : uint = 16 * size_of(ptr)
    if size <= until {
        let index = size / size_of(ptr)
        if index * size_of(ptr) == size : index--
        return index
    }
    let i : uint = 16
    let isize = until * 2
    while isize < size {
        i++
        isize *= 2
    }
    return i
}

fn get_pool_for_size(size: uint) Pool {
    let until : uint = 16 * size_of(ptr)
    if size <= until {
        let index = size / size_of(ptr)
        if index * size_of(ptr) == size : index--
        return get_pool(index, size)
    }
    let i : uint = 16
    let isize = until * 2
    while isize < size {
        i++
        isize *= 2
    }
    return get_pool(i, isize)
}

fn get_pool(i: uint, size: uint) Pool {
    let pool = @ptrv(pools, ?Pool, i)
    if isset(pool) : return pool
    pool = Pool.init(size)
    @ptrv(pools, Pool, i) = pool
    return pool
}

fn reset_pools(pools: ptr, poolc: uint) {
    let i : uint = 0
    let last = poolc
    while i < last {
        let pool = @ptrv(pools, Pool, i++)
        pool.reset()
    }
}

fn log_pools() {
    println("-----------------")
    let i : uint = 0
    let last = poolc
    while i < last {
        if i > 0 : print(",")
        let pool = @ptrv(pools, Pool, i++)
        let x : String = pool.blockc
        print(x)
    }
    print("\n")
}

struct Pool {
    blockc: uint
    first: ptr
    block: ptr
    item: ptr (null.@cast(.))
    last: ptr (null.@cast(.))
    isize: uint (0)

    + static fn init(size: uint) Pool {

        let p = Pool {
            isize: size + 8
            first: null.@cast(.)
            block: null.@cast(.)
            blockc: 1
        }

        return p
    }

    fn free() {
        mem:free(this)
    }

    static fn create_block(size: uint, isize: uint) ptr {
        let max: uint = 65400 / isize
        if max > 240 : max = 240
        if size > max : size = max
        if size == 0 : size = 1
        let alloc_size : uint = size * isize + POOL_DATA_OFFSET
        let b = mem:alloc(alloc_size)
        // Settings
        @ptrv(b, ?ptr, 0) = null // next block
        @ptrv(b, uint, 1) = size // count
        @ptrv(b, uint, 2) = 0 // items used
        @ptrv(b, uint, 3) = 0 // usage percent
        @ptrv(b, uint, 4) = isize // item size
        // @ptrv(b, uint, 5) = -1 >> (64 - size) // Unused marks

        // Set indexes
        let data = b + POOL_DATA_OFFSET
        let base_offset = POOL_DATA_OFFSET + 8
        let i : uint = 0
        while i < size {
            let offset = i * isize
            let item : ptr = data + offset
            @ptrv(item, u64) = 0
            @ptrv(item, u16, 3) = (offset + base_offset).@cast(u16)
            i++
        }
        // Track memory usage
        gc.mem_usage_thread += alloc_size
        let before = atomic(mem_usage_shared + alloc_size)
        let usage = before + alloc_size
        if usage > mem_usage_peak : mem_usage_peak = usage
        //
        return b
    }

    fn get_vtable() ptr $hot {
        let item = this.get()
        let data = (@offset(item, u8, -8)).@cast(GcData)
        data.set_has_vtable()
        gc.blanks.add_ptr(item)
        return item
    }

    fn get() ptr $hot {

        let isize = this.isize
        if this.block == null {
            let block = Pool.create_block(this.blockc * 2, isize)
            this.first = block
            this.set_block(block)
        }

        let item = this.item
        let last = this.last
        while true {

            if item == last {
                this.set_next_block(this.block)
                item = this.item
                last = this.last
                continue
            }

            let data = item.@cast(GcData)
            if data.is_not_new() {
                item += isize
                continue
            }

            this.item = item + isize

            #if TEST
            let mark = data.block_offset & 3
            if mark != 0 : panic("Mark not 0 at alloc")
            if data.co_count != 0 : panic("co_count not 0 at alloc")
            if data.flags != 0 {
                print("Flags: ")
                data.flags.print(10)
                print("\n")
                panic("Flags not 0 at alloc")
            }
            #end

            // Increase mem usage
            gc.mem_new += isize

            return item + 8
        }

        panic("Failed to allocate memory")
    }

    fn set_next_block(block: ptr) {
        while true {
            let next = @ptrv(block, ?ptr, 0)

            if isset(next) {
                if @ptrv(next, uint, 3) > 95 {
                    block = next
                    continue
                }

                this.set_block(next)
            } else {
                // Create new block
                let blockc = ++this.blockc
                let new = get_unused_block(this.isize) !? Pool.create_block(blockc * blockc, this.isize)
                // Set next
                @ptrv(block, ptr, 0) = new
                this.set_block(new)
            }
            break
        }

        // GC check
        collect_if_threshold_reached()
    }

    fn set_block(block: ?ptr) {
        this.block = block.@cast(ptr)
        if isset(block) {
            this.item = block + POOL_DATA_OFFSET
            this.last = block + POOL_DATA_OFFSET + @ptrv(block, uint, 1) * this.isize
        }
    }

    fn reset() {
        let block = this.first
        if block == null : return

        let current = this.block
        let isize = this.isize

        let reached_current = gc.did_shutdown

        let first_non_empty : ?ptr = null
        let last_non_empty : ?ptr = null
        let first_empty : ?ptr = null
        let last_empty : ?ptr = null

        while block != null {
            let b = block
            let next = @ptrv(b, ptr, 0)
            let total = @ptrv(b, uint, 1)
            let used = @ptrv(b, uint, 2)

            @ptrv(b, ?ptr, 0) = null // clear next

            // Validate used amount
            #if TEST
            let base = b + POOL_DATA_OFFSET
            let end = base + total * isize
            while base < end {
                let data = base.@cast(GcData)
                if data.is_new() {
                    let mark = data.block_offset & 3
                    if mark != 0 : panic("GC error: item with state 'new' contains 'mark' bits")
                    if data.co_count != 0 : panic("GC error: item with state 'new' has a co_count > 0")
                    if data.flags != 0 {
                        print("Flags: ")
                        data.flags.print(10)
                        print("\n")
                        panic("GC error: item with state 'new' contains 'flag' bits")
                    }
                }
                base += isize
            }
            #end


            //
            if used == 0 {
                // Empty blocks
                if reached_current == false {
                    @ptrv(b, uint, 3) = 0 // Set percent used
                    if isset(last_empty) {
                        @ptrv(last_empty, ptr, 0) = b
                        last_empty = b
                    } else {
                        first_empty = b
                        last_empty = b
                    }
                } else {
                    let alloc_size = total * isize + POOL_DATA_OFFSET
                    gc.mem_usage_thread -= alloc_size
                    atomic(mem_usage_shared - alloc_size)
                    mem:free(b)
                    this.blockc--
                }
            } else {
                // Update usage percent
                @ptrv(b, uint, 3) = used * 100 / total
                //
                if isset(last_non_empty) {
                    @ptrv(last_non_empty, ptr, 0) = b
                    last_non_empty = b
                } else {
                    first_non_empty = b
                    last_non_empty = b
                }
            }
            if b == current : reached_current = true
            block = next
        }

        let first : ?ptr = first_non_empty
        if isset(last_non_empty) {
            @ptrv(last_non_empty, ?ptr, 0) = first_empty
        } else {
            first = first_empty
        }

        this.first = first.@cast(ptr)
        this.set_block(first)
    }
}
