API char ToLowerCase(char a) {
    if (a >= 'A' && a <= 'Z') a += 32;
    return a;
}

API char ToUpperCase(char a) {
    if (a >= 'a' && a <= 'z') a -= 32;
    return a;
}

API bool IsWhitespace(char w) {
    bool result = w == '\n' || w == ' ' || w == '\t' || w == '\v' || w == '\r';
    return result;
}

API bool IsAlphabetic(char a) {
    bool result = (a >= 'a' && a <= 'z') || (a >= 'A' && a <= 'Z');
    return result;
}

API bool IsIdent(char a) {
    bool result = (a >= 'a' && a <= 'z') || (a >= 'A' && a <= 'Z') || a == '_';
    return result;
}

API bool IsDigit(char a) {
    bool result = a >= '0' && a <= '9';
    return result;
}

API bool IsAlphanumeric(char a) {
    bool result = IsDigit(a) || IsAlphabetic(a);
    return result;
}

API bool IsSymbol(char w) {
    bool result = (w >= '!' && w <= '/') || (w >= ':' && w <= '@') || (w >= '[' && w <= '`') || (w >= '{' && w <= '~');
    return result;
}

API bool IsNonWord(char w) {
    if (w == '_') return false;
    bool result = IsSymbol(w) || IsWhitespace(w);
    return result;
}

API bool IsWord(char w) {
    bool result = !IsNonWord(w);
    return result;
}

API bool IsBrace(char c) {
    bool result = c == '{' || c == '}';
    return result;
}

API bool IsParen(char c) {
    bool result = c == '(' || c == ')';
    return result;
}

API String Chop(String a, int64_t len) {
    len             = ClampTop(len, a.len);
    String result = {a.data, a.len - len};
    return result;
}

API String Skip(String a, int64_t len) {
    len             = ClampTop(len, a.len);
    String result = {a.data + len, a.len - len};
    return result;
}

API String GetPostfix(String a, int64_t len) {
    len                 = ClampTop(len, a.len);
    int64_t  remain_len = a.len - len;
    String result     = {a.data + remain_len, len};
    return result;
}

API String GetPrefix(String a, int64_t len) {
    len             = ClampTop(len, a.len);
    String result = {a.data, len};
    return result;
}

API char At(String a, int64_t idx) {
    if (idx < a.len) {
        return a.data[idx];
    }
    return 0;
}

API String GetSlice(String arr, int64_t first_index, int64_t one_past_last_index) {
    // Negative indexes work in python style, they return you the index counting from end of list
    if (one_past_last_index == SLICE_LAST) one_past_last_index = arr.len;
    if (one_past_last_index < 0) one_past_last_index = arr.len + one_past_last_index;

    if (first_index == SLICE_LAST) first_index = arr.len;
    if (first_index < 0) first_index = arr.len + first_index;

    String result = {arr.data, arr.len};
    if (arr.len > 0) {
        if (one_past_last_index > first_index) {
            first_index         = ClampTop(first_index, arr.len - 1);
            one_past_last_index = ClampTop(one_past_last_index, arr.len);
            result.data += first_index;
            result.len = one_past_last_index - first_index;
        } else {
            result.len = 0;
        }
    }
    return result;
}

API bool AreEqual(String a, String b, unsigned ignore_case) {
    if (a.len != b.len) return false;
    for (int64_t i = 0; i < a.len; i++) {
        char A = a.data[i];
        char B = b.data[i];
        if (ignore_case) {
            A = ToLowerCase(A);
            B = ToLowerCase(B);
        }
        if (A != B)
            return false;
    }
    return true;
}

API bool EndsWith(String a, String end, unsigned ignore_case) {
    String a_end  = GetPostfix(a, end.len);
    bool   result = AreEqual(end, a_end, ignore_case);
    return result;
}

API bool StartsWith(String a, String start, unsigned ignore_case) {
    String a_start = GetPrefix(a, start.len);
    bool   result  = AreEqual(start, a_start, ignore_case);
    return result;
}

API String Trim(String string) {
    if (string.len == 0)
        return string;

    int64_t whitespace_begin = 0;
    for (; whitespace_begin < string.len; whitespace_begin++) {
        if (!IsWhitespace(string.data[whitespace_begin])) {
            break;
        }
    }

    int64_t whitespace_end = string.len;
    for (; whitespace_end != whitespace_begin; whitespace_end--) {
        if (!IsWhitespace(string.data[whitespace_end - 1])) {
            break;
        }
    }

    if (whitespace_begin == whitespace_end) {
        string.len = 0;
    } else {
        string = GetSlice(string, whitespace_begin, whitespace_end);
    }

    return string;
}

API String TrimEnd(String string) {
    int64_t whitespace_end = string.len;
    for (; whitespace_end != 0; whitespace_end--) {
        if (!IsWhitespace(string.data[whitespace_end - 1])) {
            break;
        }
    }

    String result = GetPrefix(string, whitespace_end);
    return result;
}

API String Copy(Allocator allocator, String string) {
    char *copy = (char *)AllocSize(allocator, sizeof(char) * (string.len + 1));
    memcpy(copy, string.data, string.len);
    copy[string.len] = 0;
    String result    = {copy, string.len};
    return result;
}

API String Copy(Allocator allocator, char *string) {
    return Copy(allocator, {string, (int64_t)strlen(string)});
}

API void NormalizePathInPlace(String s) {
    for (int64_t i = 0; i < s.len; i++) {
        if (s.data[i] == '\\')
            s.data[i] = '/';
    }
}

API String NormalizePath(Allocator allocator, String s) {
    String copy = Copy(allocator, s);
    NormalizePathInPlace(copy);
    return copy;
}

API bool Seek(String string, String find, int64_t *index_out, SeekFlag flags) {
    bool ignore_case = flags & SeekFlag_IgnoreCase ? true : false;
    bool result = false;
    if (flags & SeekFlag_MatchFindLast) {
        for (int64_t i = string.len; i != 0; i--) {
            int64_t index = i - 1;
            String substring = GetSlice(string, index, index + find.len);
            if (AreEqual(substring, find, ignore_case)) {
                bool ok_boundary = true;
                if (flags & SeekFlag_WordBoundary) {
                    String left = GetSlice(string, i - 1, i);
                    String right = GetSlice(string, i + find.len, i + find.len + 1);
                    if (left.len != 0 && !IsNonWord(left.data[0])) {
                        ok_boundary = false;
                    }
                    if (right.len != 0 && !IsNonWord(right.data[0])) {
                        ok_boundary = false;
                    }
                }
                if (ok_boundary) {
                    if (index_out) {
                        *index_out = index;
                    }
                    result = true;
                    break;
                }
            }
        }
    } else {
        for (int64_t i = 0; i < string.len; i++) {
            String substring = GetSlice(string, i, i + find.len);
            if (AreEqual(substring, find, ignore_case)) {
                bool ok_boundary = true;
                if (flags & SeekFlag_WordBoundary) {
                    String left = GetSlice(string, i - 1, i);
                    String right = GetSlice(string, i + find.len, i + find.len + 1);
                    if (left.len != 0 && !IsNonWord(left.data[0])) {
                        ok_boundary = false;
                    }
                    if (right.len != 0 && !IsNonWord(right.data[0])) {
                        ok_boundary = false;
                    }
                }
                if (ok_boundary) {
                    if (index_out) {
                        *index_out = i;
                    }
                    result = true;
                    break;
                }
            }
        }
    }

    return result;
}

API String CutLastSlash(String *s) {
    String result = *s;
    Seek(*s, "/", &s->len, SeekFlag_MatchFindLast);
    result = Skip(result, s->len);
    return result;
}

API String ChopLastSlash(String s) {
    String result = s;
    Seek(s, "/", &result.len, SeekFlag_MatchFindLast);
    return result;
}

API String ChopLastPeriod(String s) {
    String result = s;
    Seek(s, ".", &result.len, SeekFlag_MatchFindLast);
    return result;
}

API String SkipToLastSlash(String s) {
    int64_t pos;
    String  result = s;
    if (Seek(s, "/", &pos, SeekFlag_MatchFindLast)) {
        result = Skip(result, pos + 1);
    }
    return result;
}

API String SkipToLastPeriod(String s) {
    int64_t pos;
    String  result = s;
    if (Seek(s, ".", &pos, SeekFlag_MatchFindLast)) {
        result = Skip(result, pos + 1);
    }
    return result;
}

API String CutPrefix(String *string, int64_t len) {
    String result = GetPrefix(*string, len);
    *string       = Skip(*string, len);
    return result;
}

API String CutPostfix(String *string, int64_t len) {
    String result = GetPostfix(*string, len);
    *string       = Chop(*string, len);
    return result;
}

API String FormatV(Allocator allocator, const char *data, va_list args1) {
    va_list args2;
    va_copy(args2, args1);
    int64_t len = stbsp_vsnprintf(0, 0, data, args2);
    va_end(args2);

    char *result = (char *)AllocSize(allocator, sizeof(char) * (len + 1));
    stbsp_vsnprintf(result, (int)(len + 1), data, args1);
    String res = {result, len};
    return res;
}

API String Format(Allocator allocator, const char *data, ...) {
    STRING_FORMAT(allocator, data, result);
    return result;
}

API Array<String> Split(Allocator allocator, String string, String delimiter) {
    Array<String> result = {allocator};
    int64_t       index  = 0;
    while (Seek(string, delimiter, &index)) {
        String before_match = {string.data, index};
        Add(&result, before_match);
        string = Skip(string, index + delimiter.len);
    }
    Add(&result, string);
    return result;
}

API String Merge(Allocator allocator, Array<String> list, String separator) {
    int64_t char_count = 0;
    For(list) char_count += it.len;
    if (char_count == 0) return {};
    int64_t node_count = list.len;

    int64_t base_size = (char_count + 1);
    int64_t sep_size  = (node_count - 1) * separator.len;
    int64_t size      = base_size + sep_size;
    char   *buff      = (char *)AllocSize(allocator, sizeof(char) * (size + 1));
    String  string    = {buff, 0};
    For(list) {
        Assert(string.len + it.len <= size);
        memcpy(string.data + string.len, it.data, it.len);
        string.len += it.len;
        if (!IsLast(list, it)) {
            memcpy(string.data + string.len, separator.data, separator.len);
            string.len += separator.len;
        }
    }
    Assert(string.len == size - 1);
    string.data[size] = 0;
    return string;
}

API Int GetSize(Array<String> array) {
    Int result = 0;
    For (array) result += it.len;
    return result;
}

API bool Chop(String *string, String ending) {
    if (EndsWith(*string, ending)) {
        *string = Chop(*string, ending.len);
        return true;
    }
    return false;
}

API String SkipNumberEx(String *string) {
    String col = {string->data, 0};
    for (int64_t i = 0; i < string->len; i += 1) {
        if (IsDigit(string->data[i])) {
            col.len += 1;
        } else {
            break;
        }
    }
    *string = Skip(*string, col.len);
    return col;
}

API Int SkipNumber(String *string) {
    String col = SkipNumberEx(string);
    if (col.len == 0) return -1;
    Int result = strtoll(col.data, NULL, 10);
    return result;
}

API String SkipUntil(String *string, String str) {
    String begin = *string;
    begin.len      = 0;
    for (; string->len; begin.len += 1) {
        String match = GetPrefix(*string, str.len);
        if (StartsWith(match, str)) break;
        *string = Skip(*string, 1);
    }
    return begin;
}

API String SkipWhitespace(String *string) {
    String begin = {string->data, 0};
    for (Int i = 0; i < string->len; i += 1) {
        if (!IsWhitespace(string->data[i])) break;
        *string = Skip(*string, 1);
        begin.len += 1;
    }
    return begin;
}


API String ChopNumberEx(String *string) {
    String col = {};
    for (int64_t i = string->len - 1; i >= 0; i -= 1) {
        if (IsDigit(string->data[i])) {
            col.data = string->data + i;
            col.len += 1;
        } else {
            break;
        }
    }
    *string = Chop(*string, col.len);
    return col;
}

API Int ChopNumber(String *string) {
    Scratch  scratch;
    String col = ChopNumberEx(string);
    if (col.len == 0) return -1;
    Int result = strtoll(col.data, NULL, 10) - 1;
    return result;
}
