Buffer :: struct {
    data: *u8;
    len:   int;
    cap:   int;

    lines: Lines;
}

Lines :: struct {
    data: *Range;
    len:   int;
    cap:   int;
}

Range :: struct {
    min: int;
    max: int;
}

LineInfo :: struct {
    number: int;
    range: Range;
}

GetRangeSize :: proc(range: Range): int {
    result := range.max - range.min;
    return result;
}

ClampInt :: proc(val: int, min: int, max: int): int {
    result := val;
    if (val < min) result = min;
    if (val > max) result = max;
    return result;
}

MinInt :: proc(a: int, b: int): int {
    result := a;
    if (a > b) result = b;
    return result;
}

MaxInt :: proc(a: int, b: int): int {
    result := b;
    if (a > b) result = a;
    return result;
}

AdjustUTF8PosUnsafe :: proc(buffer: *Buffer, pos: int, direction: int = 1): int {
    for pos >= 0 && pos < buffer.len && IsUTF8ContinuationByte(buffer.data[pos]) {
        pos += direction;
    }
    return pos;
}

AdjustUTF8Pos :: proc(buffer: *Buffer, pos: int, direction: int = 1): int {
    assert(direction == 1 || direction == -1);
    pos = AdjustUTF8PosUnsafe(buffer, pos, direction);
    pos = ClampInt(pos, 0, buffer.len - 1);
    if (buffer.data) assert(!IsUTF8ContinuationByte(buffer.data[pos]));
    return pos;
}

AdjustRange :: proc(buffer: *Buffer, range: *Range) {
    range.min = AdjustUTF8Pos(buffer, range.min, direction = -1);
    range.max = AdjustUTF8Pos(buffer, range.max, direction = +1);
}

ReplaceText :: proc(buffer: *Buffer, replace: Range, with: String) {
    AdjustRange(buffer, &replace);

    new_buffer_len  := buffer.len + :int(with.len) - GetRangeSize(replace);
    new_buffer_size := new_buffer_len + 1; // possible addition of a null terminator
    if new_buffer_size > buffer.cap {
        new_buffer_cap := MaxInt(4096, new_buffer_size * 2);
        new_buffer     := malloc(:usize(new_buffer_cap));
        if (buffer.data) {
            memcpy(new_buffer, buffer.data, :usize(buffer.len));
            free(buffer.data);
        }
        buffer.data = new_buffer;
        buffer.cap  = new_buffer_cap;
    }

    old_text_range: Range = replace;
    new_text_range: Range = {old_text_range.min, old_text_range.min + :int(with.len)};
    right_range:    Range = {old_text_range.max, buffer.len};

    memmove(&buffer.data[new_text_range.max], &buffer.data[right_range.min], :usize(GetRangeSize(right_range)));
    memmove(&buffer.data[new_text_range.min], with.str, :usize(GetRangeSize(new_text_range)));

    buffer.len = new_buffer_len;
    if buffer.len && buffer.data[buffer.len - 1] != 0 {
        buffer.data[buffer.len] = 0;
        buffer.len += 1;
        assert(buffer.len <= buffer.cap);
    }

    UpdateBufferLines(buffer);
}

AddLine :: proc(lines: *Lines, line: Range) {
    if lines.len + 1 > lines.cap {
        new_cap   := MaxInt(16, lines.cap * 2);
        lines.data = realloc(lines.data, :usize(new_cap) * sizeof(:Range));
        lines.cap  = new_cap;
    }
    lines.data[lines.len] = line;
    lines.len            += 1;
}

UpdateBufferLines :: proc(buffer: *Buffer) {
    buffer.lines.len = 0;

    line: Range = {0, 0};
    for i := 0; i < buffer.len; i += 1 {
        if buffer.data[i] == '\n' {
            AddLine(&buffer.lines, line);
            line.min = i + 1;
            line.max   = i + 1;
        } else {
            line.max += 1;
        }
    }

    line.min = AdjustUTF8Pos(buffer, line.min);
    line.max = AdjustUTF8Pos(buffer, line.max);
    AddLine(&buffer.lines, line);
}

AllocStringFromBuffer :: proc(buffer: *Buffer, range: Range, null_terminate: bool = false): String {
    AdjustRange(buffer, &range);
    size := GetRangeSize(range);

    alloc_size := :usize(size);
    if (null_terminate) alloc_size += 1;
    data := malloc(alloc_size);
    memcpy(data, &buffer.data[range.min], :usize(size));

    result: String = {data, size};
    if (null_terminate) result.str[result.len] = 0;
    return result;
}

GetStringFromBuffer :: proc(buffer: *Buffer, range: Range): String {
    AdjustRange(buffer, &range);
    size := GetRangeSize(range);
    result: String = {:*char(&buffer.data[range.min]), size};
    return result;
}

AddText :: proc(buffer: *Buffer, text: String) {
    end_of_buffer: Range = {buffer.len - 1, buffer.len - 1};
    ReplaceText(buffer, end_of_buffer, text);
}

FindLineOfPos :: proc(buffer: *Buffer, pos: int): LineInfo {
    for i := 0; i < buffer.lines.len; i += 1 {
        line := buffer.lines.data[i];
        if pos >= line.min && pos <= line.max {
            return {i, line};
        }
    }
    return {};
}

IsLineValid :: proc(buffer: *Buffer, line: int): bool {
    result := line >= 0 && line < buffer.lines.len;
    return result;
}

GetLine :: proc(buffer: *Buffer, line: int): LineInfo {
    line = ClampInt(line, 0, buffer.lines.len - 1);
    range := buffer.lines.data[line];
    return {line, range};
}

IsPosInBounds :: proc(buffer: *Buffer, pos: int): bool {
    result := buffer.len != 0 && pos >= 0 && pos < buffer.len;
    return result;
}

GetChar :: proc(buffer: *Buffer, pos: int): u8 {
    result: u8;
    if IsPosInBounds(buffer, pos) {
        result = buffer.data[pos];
    }
    return result;
}

GetUTF32 :: proc(buffer: *Buffer, pos: int, codepoint_size: *int = nil): u32 {
    if !IsPosInBounds(buffer, pos) return 0;
    p := &buffer.data[pos];
    max := buffer.len - pos;
    utf32 := UTF8ToUTF32(:*uchar(p), max);
    assert(utf32.error == 0);
    if (utf32.error != 0) return 0;
    if (codepoint_size) codepoint_size[0] = utf32.advance;
    return utf32.out_str;
}

IsWhitespace :: proc(w: u8): bool {
    result := w == '\n' || w == ' ' || w == '\t' || w == '\v' || w == '\r';
    return result;
}

SeekOnWordBoundary :: proc(buffer: *Buffer, pos: int, direction: int = GO_FORWARD): int {
    assert(direction == GO_FORWARD || direction == GO_BACKWARD);
    check_pos: int;
    if direction == GO_FORWARD {
        check_pos = AdjustUTF8Pos(buffer, pos + 1, direction);
        pos = check_pos;
        // this difference here because the Backward for loop is not inclusive.
        // It doesn't move an inch forward after "pos"
        //  - - - - pos - -
        // It goes backward on first Advance
    } else {
        check_pos = AdjustUTF8Pos(buffer, pos - 1, direction);
    }

    cursor_char            := GetChar(buffer, check_pos);
    standing_on_whitespace := IsWhitespace(cursor_char);
    seek_whitespace        := standing_on_whitespace == false;
    seek_word              := standing_on_whitespace;

    end := buffer.len - 1;
    if (direction == GO_BACKWARD) end = 0;

    result   := end;
    iter     := Iterate(buffer, pos, end, direction);
    prev_pos := iter.pos;
    for IsValid(iter); Advance(&iter) {
        if seek_word && !IsWhitespace(:u8(iter.item)) {
            result = prev_pos;
            break;
        }
        if seek_whitespace && IsWhitespace(:u8(iter.item)) {
            if direction == GO_FORWARD {
                result = iter.pos;
            } else {
                result = prev_pos;
            }
            break;
        }
        prev_pos = iter.pos;
    }

    return result;
}