
use mem
use gc

+ class ByteBuffer {
    ~ data: gc:GcPtr
    ~ size: uint
    ~ length: uint (0)

    + static fn new(start_size: uint (128)) ByteBuffer {
        if start_size < 10 {
            start_size = 10
        }
        let res = ByteBuffer {
            data: gc:alloc(start_size)
            size: start_size
        }
        return res
    }

    + fn clone() SELF {
        let size = this.size
        let data = gc:alloc(size)
        mem:copy(this.data, data, size)

        return SELF {
            data: data
            size: size
            length: this.length
        }
    }

    + fn clear() {
        this.length = 0
    }
    + fn clear_until(index: uint) {
        let len = this.length
        if index >= len {
            this.clear()
            return
        }
        this.clear_part(0, index)
    }
    + fn clear_part(index: uint, len: uint) {
        if len == 0 || index >= this.length : return
        let data = this.data.offset(index)
        // Limits
        let total = this.length
        let max = total - index
        if len > max : len = max
        let rem = max - len
        // Copy bytes if needed
        if rem > 0 : mem:copy(@offset(data, u8, len), data, rem)
        this.length = index + rem
    }

    // Read
    + fn get(index: uint) u8 $offset {
        if index >= this.length : return 0
        return @ptrv(this.data, u8, index)
    }
    + fn set(index: uint, v: u8) $offset_assign {
        if index >= this.length : return
        @ptrv(this.data, u8, index) = v
    }

    // Appending
    + fn append(buffer: ByteBuffer, start_index: uint (0)) {
        if start_index >= buffer.length : return
        this.append_from_ptr(buffer.data.offset(start_index), buffer.length - start_index)
    }

    + fn append_from_ptr(data: ptr, length: uint) {
        if length == 0 : return
        let bytes : uint = this.length
        let new_bytes : uint = bytes + length
        if(new_bytes > this.size) : this.minimum_size(new_bytes)
        mem:copy(data, @offset(this.data, u8, bytes), length)
        this.length = new_bytes
    }

    + fn append_byte(byte: u8) {
        let bytes : uint = this.length
        let new_bytes : uint = bytes + 1
        if new_bytes > this.size : this.minimum_size(new_bytes)
        @ptrv(this.data, u8, bytes) = byte
        this.length = new_bytes
    }

    + fn append_str(str: String) {
        this.append_from_ptr(str.data, str.bytes)
    }
    + fn append_uint(value: uint) {
        let len = this.length
        let new_bytes : uint = len + size_of(uint)
        if new_bytes > this.size : this.minimum_size(new_bytes)
        let ilen = value.to_base_to_ptr(10, this.data.offset(len))
        this.length += ilen
    }
    + fn append_int(value: int) {
        let len = this.length
        let new_bytes : uint = len + size_of(uint)
        if new_bytes > this.size : this.minimum_size(new_bytes)
        let ilen = value.to_base_to_ptr(10, this.data.offset(len))
        this.length += ilen
    }

    // Compare
    + fn equals_str(str: String) bool $eq {
    	let str_len = str.bytes
    	let len = this.length
    	if str_len != len {
    		return false
    	}
    	return mem:equal(this.data, str.data, len)
    }
    + fn starts_with(str: String, offset: uint) bool {
        let len = str.bytes
        if offset + len > this.length : return false
        return mem:equal(this.data.offset(offset), str.data, len)
    }

    // Find
    + fn index_of(byte: u8, start_index: uint (0)) uint !not_found {
        let len = this.length
        if start_index >= len : throw not_found
        let data : ptr = this.data
        let res = mem:find_char(data + start_index, byte, len - start_index) ! throw not_found
        return res + start_index
    }

    + fn index_where_byte_is_not(byte: u8, start_index: uint (0)) uint !not_found {
        let index = start_index
        let len = this.length
        let data : ptr = this.data
        while index < len {
            let ch = @ptrv(data, u8, index)
            if ch != byte : return index
            index++
        }
        throw not_found
    }

    /////////////////////////
    // String functions
    /////////////////////////

    + fn to_string() String $to {
        if this.length == 0 : return ""
        let str = String.make_empty(this.length)
        mem:copy(this.data, str.data, this.length)
        return str
    }

    + fn part(start_index: uint, length: uint) String {
        let len = this.length
        if length == 0 || start_index >= len : return ""
        let end = start_index + length
        let real_len = end > len ? len - start_index : length
        return String.make_from_ptr(@offset(this.data, u8, start_index), real_len)
    }

    + fn str_ref(offset: uint, length: uint) ByteBufferStrRef {
        let len = this.length
        if length == 0 || offset >= len {
            return ByteBufferStrRef.new(this, 0, 0)
        }
        let end = offset + length
        let real_len = end > len ? len - offset : length
        return ByteBufferStrRef.new(this, offset, real_len)
    }

    // 
    + fn minimum_free_space(length: uint) {
        if(this.length + length > this.size) : this.minimum_size(this.length + length)
    }
    + fn minimum_size(minimum_size: uint) {

        let size : uint = this.size
        let new_size : uint = size
        while new_size < minimum_size {
            new_size *= 2
        }
        if(new_size != size){
            let data = this.data
            let adr = gc:alloc(new_size)
            mem:copy(data, adr, this.length)
            this.size = new_size
            this.data = adr
        }
    }

    + fn reduce_size(size: uint) {
        if size >= this.size : return
        let len = this.length
        if len > size : size = len
        let data = this.data
        let adr = gc:alloc(size)
        mem:copy(data, adr, len)
        this.data = adr
        this.size = size
    }
}
